AngularJSBuild & Deployment

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

Bash
# 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

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:

Bash
src/environments/
├── environment.ts          # Development defaults
├── environment.prod.ts     # Production overrides
└── environment.staging.ts  # Staging overrides (custom)

TS
// src/environments/environment.ts
export const environment = {
  production: false,
  apiUrl: 'http://localhost:3000/api',
  featureFlags: {
    newDashboard: true,
    betaFeatures: true,
  },
};

TS
// src/environments/environment.prod.ts
export const environment = {
  production: true,
  apiUrl: 'https://api.myapp.com',
  featureFlags: {
    newDashboard: true,
    betaFeatures: false,
  },
};

JSON
// 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"
        }
      ]
    }
  }
}

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:

TS
// 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:

JSON
{
  "budgets": [
    {
      "type": "initial",
      "maximumWarning": "500kB",
      "maximumError": "1MB"
    },
    {
      "type": "anyComponentStyle",
      "maximumWarning": "2kB",
      "maximumError": "4kB"
    },
    {
      "type": "anyScript",
      "maximumWarning": "100kB",
      "maximumError": "200kB"
    }
  ]
}
Warning
If the build fails due to budget errors, first analyze what's large with ng build --stats-json + webpack-bundle-analyzer before raising the budget limits.
Deploying to Firebase Hosting
  1. ng add @angular/fire

  2. firebase login

  3. firebase init hosting

  4. ng build

  5. firebase deploy

Bash
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

JSON
// 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

Bash
npm install -g vercel
ng build
vercel deploy dist/my-app/browser

JSON
// vercel.json (for SPA routing)
{
  "rewrites": [
    { "source": "/((?!api).*)", "destination": "/index.html" }
  ],
  "buildCommand": "ng build",
  "outputDirectory": "dist/my-app/browser"
}
Deploying to Nginx

Text
# 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

Text
# 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;"]

Bash
# Build and run Docker image
docker build -t my-angular-app .
docker run -p 8080:80 my-angular-app
CI/CD with GitHub Actions

YAML
# .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: live
Environment Variables in CI

Inject environment-specific values at build time using environment files or build-time substitution:

TS
// 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
};

JSON
// angular.json — use define to inject at build time
{
  "configurations": {
    "production": {
      "define": {
        "process.env.API_URL": "\"https://api.myapp.com\""
      }
    }
  }
}
Tip
Never embed secrets (API keys, tokens) in Angular's environment files — they end up in the JavaScript bundle and are visible to anyone. Use a backend proxy for sensitive operations.