Angular – Build and Deploy to Production

August 14, 20263 min readUpdated 8/21/2026

An Angular application compiles to static files. Once you have them, deploying is a matter of serving a directory correctly — and the "correctly" hides two things that catch everyone.

ng build

ng build

Production is the default configuration. It compiles ahead of time, tree-shakes, minifies, and adds a content hash to every file name.

Read the output. It lists each chunk with its raw and transferred size, and the transferred figure — after compression — is the one that matters to a user on a phone.

Budgets

"budgets": [
  {
    "type": "initial",
    "maximumWarning": "800kB",
    "maximumError": "1MB"
  },
  {
    "type": "anyComponentStyle",
    "maximumWarning": "4kB",
    "maximumError": "8kB"
  }
],

A budget fails the build when a bundle grows past the threshold. This is the single most useful line of configuration in the file: without it, bundle size grows a little with every merge and nobody notices until it is a project of its own.

Set them slightly above where you are now, and treat a breach as a question rather than a number to raise.

Environments

export const environment = {
  production: true,
  apiBaseUrl: 'http://localhost:8085',

Angular has no .env mechanism. The CLI swaps this file for another via fileReplacements in angular.json, at build time — so the value is baked into the bundle and there is nothing to read at runtime.

⚠️ Everything in that file ships to the browser. Only the publishable Stripe key belongs there; the secret key stays on the server, where it is the thing that actually authorises a charge. "It is in the environment file" is not a security boundary — anyone can read it in DevTools.

If you need configuration that varies after the build — one artefact promoted through staging to production — the file-replacement approach cannot help. Fetch a small config JSON at startup instead.

The two deployment gotchas

1. The SPA rewrite

The router owns /menu, but the server has no file called menu. Typing that URL or refreshing on it gives a 404 unless the host is told to serve index.html for anything that is not a real file.

Every host spells this differently — try_files $uri $uri/ /index.html; in nginx, a rewrite rule on S3/CloudFront, rewrites in a Netlify or Vercel config. It is the single most common Angular deployment problem, and the symptom is always the same: the app works until you refresh.

2. Cache headers

Hashed files (main-A1B2C3.js) can be cached for a year — the name changes when the content does. index.html must never be cached, because it is the file that points at the new hashes.

Get this backwards and users get a stale index.html requesting chunks that no longer exist, which presents as a blank page after every deploy, only for some people, and clears if they hard-refresh.

Serving it

# any static host works
npx http-server dist/pizza-angular-frontend/browser

The output is static files. There is no Node process to run and nothing to keep alive — object storage behind a CDN is a perfectly good production deployment for an Angular SPA, and usually the cheapest one.

Continuous deployment

- run: npm ci
- run: npm test
- run: npx playwright test
- run: npx ng build
- run: ./scripts/deploy.sh

Install with npm ci, not npm install — it installs exactly the lockfile and fails if package.json and the lock disagree, which is what you want on a build machine.

Run the tests before the build, so a failure costs seconds rather than minutes.

What is next

The last lesson: the questions Angular interviews actually ask.