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 buildProduction 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/browserThe 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.shInstall 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.