An Angular application compiles to static files. Nginx serves them, with a fallback to index.html so the Angular Router handles deep links. Here’s a production setup for current Angular versions (17+, with the application builder).

1. Build

npm ci
npx ng build --configuration production

Recent Angular versions output to dist/<project-name>/browser/ (older ones to dist/<project-name>/). Check which you have:

ls dist/*/

2. Base href

If the app is served from a subdirectory (https://example.com/portal/), build with:

npx ng build --configuration production --base-href /portal/

Otherwise the default / is right.

3. Upload

rsync -avz --delete dist/my-app/browser/ [email protected]:/var/www/angular-app/

4. Nginx

server {
    listen 80;
    server_name portal.example.com;
    root /var/www/angular-app;
    index index.html;

    # Hashed bundles (main-ABC123.js, styles-XYZ.css, chunk-*.js)
    location ~* \.(?:js|css|woff2?|png|jpe?g|svg|webp|ico)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        try_files $uri =404;
    }

    # Angular Router fallback
    location / {
        try_files $uri $uri/ /index.html;
    }

    location = /index.html {
        add_header Cache-Control "no-cache";
    }
}

Angular adds content hashes to bundle names in production builds (outputHashing: all), so long caching is safe for them. Files in public/ (or src/assets/) keep their names — if you replace them in place, give that location a shorter lifetime or version the filenames.

sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d portal.example.com

Enable gzip/Brotli compression for the JS bundles — see Nginx compression.

5. Environment configuration

Angular’s environment.ts files are compiled in — one build per environment. To deploy the same build everywhere, load configuration at startup instead:

// /var/www/angular-app/config.json (written per server)
{ "apiBase": "https://api.example.com" }

Fetch it in an app initializer before bootstrapping services. Mark config.json no-cache in Nginx so changes apply immediately.

6. Proxy the backend

location /api/ {
    proxy_pass http://127.0.0.1:8080;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

Same origin = no CORS configuration needed. Otherwise see CORS in Nginx.

Angular SSR

With server-side rendering (ng add @angular/ssr / ng new --ssr), the build also produces dist/<project>/server/server.mjs, a Node server. Run it with systemd or PM2 on 127.0.0.1:4000, proxy Nginx to it, and serve dist/<project>/browser assets directly — the same pattern as Next.js and Nuxt.

Troubleshooting

  • 404 on page refresh — missing try_files … /index.html.
  • Blank page, console shows 404 for main-*.js — wrong --base-href or wrong root/directory (browser/ subfolder).
  • Users see an old version — index.html cached; set no-cache. If you use the Angular service worker, it controls updates — make sure ngsw.json isn’t cached long.
  • Mixed content — API URLs hard-coded with http://.

Related: React with Nginx (atomic deploys and CI apply equally) and Nginx try_files.

  • Angular
  • Nginx
  • Deployment