Build and Deployment in Angular
Building and deploying Angular applications involves understanding the Angular CLI build system, optimizing production bundles, configuring environments, and choosing the right deployment strategy.
Angular 17+ uses esbuild (via the @angular/build builder) as the default build system,
replacing the older webpack-based builder for dramatically faster builds.
Build Commands
# Development build (fast, includes source maps) ng build # Production build (optimized, minified) ng build --configuration=production # Serve production build locally ng serve --configuration=production # Build and analyze bundle size ng build --stats-json npx webpack-bundle-analyzer dist/my-app/browser/stats.json
Build Configurations in angular.json
{
"projects": {
"my-app": {
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/my-app",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "tsconfig.app.json",
"assets": ["src/favicon.ico", "src/assets"],
"styles": ["src/styles.scss"],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kB",
"maximumError": "1MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kB",
"maximumError": "4kB"
}
],
"outputHashing": "all",
"optimization": true,
"sourceMap": false
},
"development": {
"optimization": false,
"sourceMap": true,
"namedChunks": true
}
}
}
}
}
}
}Environment Configuration
Angular uses environment files to manage configuration per build target:
src/environments/ ├── environment.ts # Development defaults ├── environment.prod.ts # Production overrides └── environment.staging.ts # Staging overrides (custom)
// src/environments/environment.ts
export const environment = {
production: false,
apiUrl: 'http://localhost:3000/api',
featureFlags: {
newDashboard: true,
betaFeatures: true,
},
};// src/environments/environment.prod.ts
export const environment = {
production: true,
apiUrl: 'https://api.myapp.com',
featureFlags: {
newDashboard: true,
betaFeatures: false,
},
};// angular.json — file replacements for production
{
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
]
},
"staging": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.staging.ts"
}
]
}
}
}// Using environment in a service
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from '../environments/environment';
@Injectable({ providedIn: 'root' })
export class ApiService {
private baseUrl = environment.apiUrl;
constructor(private http: HttpClient) {}
getUsers() {
return this.http.get(`${this.baseUrl}/users`);
}
}Bundle Optimization
Angular's production build automatically applies several optimizations:
Optimization | Description | Default |
|---|---|---|
Tree shaking | Removes unused code | On in production |
Minification | Minifies JS and CSS | On in production |
Code splitting | Lazy-loaded routes become separate chunks | Automatic |
Output hashing | Adds content hash to filenames for cache busting | On in production |
Ahead-of-Time (AOT) | Compiles templates at build time | Always on |
Differential loading | Separate bundles for modern/legacy browsers | Removed in v17+ |
Lazy Loading Routes (Code Splitting)
Lazy loading splits your app into smaller chunks loaded on demand:
// app.routes.ts
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: '',
loadComponent: () =>
import('./pages/home/home.component').then((m) => m.HomeComponent),
},
{
path: 'dashboard',
loadComponent: () =>
import('./pages/dashboard/dashboard.component').then((m) => m.DashboardComponent),
},
{
path: 'admin',
loadChildren: () =>
import('./pages/admin/admin.routes').then((m) => m.adminRoutes),
},
];After building, you'll see separate chunks in the dist folder:
dist/my-app/browser/ ├── main-HASH.js (~150kB) — app shell ├── chunk-HASH.js (~45kB) — dashboard lazy chunk ├── chunk-HASH.js (~80kB) — admin lazy chunk └── polyfills-HASH.js (~35kB)
Bundle Size Budgets
Configure budgets in angular.json to fail the build when bundles exceed size limits:
{
"budgets": [
{
"type": "initial",
"maximumWarning": "500kB",
"maximumError": "1MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kB",
"maximumError": "4kB"
},
{
"type": "anyScript",
"maximumWarning": "100kB",
"maximumError": "200kB"
}
]
} ng build --stats-json + webpack-bundle-analyzer before raising the budget limits.Deploying to Firebase Hosting
ng add @angular/fire
firebase login
firebase init hosting
ng build
firebase deploy
ng add @angular/fire # Or manually: npm install -g firebase-tools firebase login firebase init hosting # Public directory: dist/my-app/browser # Single page app: Yes # Overwrite index.html: No ng build firebase deploy --only hosting
// firebase.json
{
"hosting": {
"public": "dist/my-app/browser",
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
],
"headers": [
{
"source": "**/*.@(js|css)",
"headers": [
{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
]
},
{
"source": "/index.html",
"headers": [
{ "key": "Cache-Control", "value": "no-cache" }
]
}
]
}
}Deploying to Vercel
npm install -g vercel ng build vercel deploy dist/my-app/browser
// vercel.json (for SPA routing)
{
"rewrites": [
{ "source": "/((?!api).*)", "destination": "/index.html" }
],
"buildCommand": "ng build",
"outputDirectory": "dist/my-app/browser"
}Deploying to Nginx
# nginx.conf
server {
listen 80;
server_name myapp.com;
root /var/www/myapp;
index index.html;
# Serve static assets with long cache
location ~* .(js|css|png|jpg|gif|ico|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# SPA fallback — all routes serve index.html
location / {
try_files $uri $uri/ /index.html;
}
}Docker Deployment
# Dockerfile # Stage 1: Build FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build # Stage 2: Serve with Nginx FROM nginx:alpine COPY --from=builder /app/dist/my-app/browser /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]
# Build and run Docker image docker build -t my-angular-app . docker run -p 8080:80 my-angular-app
CI/CD with GitHub Actions
# .github/workflows/deploy.yml
name: Build and Deploy
on:
push:
branches: [main]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: ng test --watch=false --browsers=ChromeHeadless
- name: Build production
run: ng build --configuration=production
- name: Deploy to Firebase
uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: ${{ secrets.GITHUB_TOKEN }}
firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }}
channelId: liveEnvironment Variables in CI
Inject environment-specific values at build time using environment files or build-time substitution:
// src/environments/environment.prod.ts
// Values injected from CI environment variables via a build script:
export const environment = {
production: true,
apiUrl: process.env['API_URL'] || 'https://api.myapp.com',
// Note: process.env is replaced by esbuild's define at build time
};// angular.json — use define to inject at build time
{
"configurations": {
"production": {
"define": {
"process.env.API_URL": "\"https://api.myapp.com\""
}
}
}
}