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-hrefor wrongroot/directory (browser/subfolder). - Users see an old version —
index.htmlcached; setno-cache. If you use the Angular service worker, it controls updates — make surengsw.jsonisn’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.