Docs
/
Angular
Chapter 24

24 — Deployment & CI/CD

Build Configurations

Environment Files

// src/environments/environment.ts
export const environment = {
  production: false,
  apiUrl: 'http://localhost:3000/api',
  sentryDsn: '',
};

// src/environments/environment.prod.ts
export const environment = {
  production: true,
  apiUrl: 'https://api.myapp.com',
  sentryDsn: 'https://xxx@sentry.io/123',
};

// src/environments/environment.staging.ts
export const environment = {
  production: false,
  apiUrl: 'https://staging-api.myapp.com',
  sentryDsn: 'https://xxx@sentry.io/456',
};

angular.json — Build Configs

"configurations": {
  "production": {
    "budgets": [
      { "type": "initial", "maximumWarning": "500kB", "maximumError": "1MB" },
      { "type": "anyComponentStyle", "maximumWarning": "4kB" }
    ],
    "outputHashing": "all",
    "optimization": true,
    "sourceMap": false
  },
  "staging": {
    "optimization": true,
    "sourceMap": true,
    "fileReplacements": [
      { "replace": "src/environments/environment.ts", "with": "src/environments/environment.staging.ts" }
    ]
  }
}
ng build --configuration=production
ng build --configuration=staging

Docker

Multi-Stage Dockerfile (SPA — No SSR)

# Stage 1: Build
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build -- --configuration=production

# Stage 2: Serve with Nginx
FROM nginx:alpine
COPY --from=build /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;"]

nginx.conf (SPA Routing)

server {
    listen 80;
    root /usr/share/nginx/html;
    index index.html;

    # SPA fallback — all routes serve index.html
    location / {
        try_files $uri $uri/ /index.html;
    }

    # Cache static assets
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";
    add_header X-XSS-Protection "1; mode=block";
    add_header Referrer-Policy "strict-origin-when-cross-origin";
}

SSR Dockerfile

FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build -- --configuration=production

FROM node:20-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/package*.json ./
RUN npm ci --omit=dev
EXPOSE 4000
CMD ["node", "dist/my-app/server/server.mjs"]

GitHub Actions CI/CD

# .github/workflows/ci.yml
name: CI/CD

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - run: npm ci
      - run: npm run lint
      - run: npm test -- --no-watch --code-coverage
      - run: npm run build -- --configuration=production

      # Upload coverage
      - uses: codecov/codecov-action@v4
        with:
          file: coverage/lcov.info

      # Check bundle size
      - name: Bundle size check
        run: |
          MAIN_SIZE=$(stat -f%z dist/my-app/browser/main-*.js 2>/dev/null || stat -c%s dist/my-app/browser/main-*.js)
          echo "Main bundle: $((MAIN_SIZE / 1024))KB"
          if [ "$MAIN_SIZE" -gt 512000 ]; then
            echo "❌ Main bundle exceeds 500KB"
            exit 1
          fi

  deploy-staging:
    needs: build-and-test
    if: github.ref == 'refs/heads/develop'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20, cache: npm }
      - run: npm ci
      - run: npm run build -- --configuration=staging
      # Deploy to staging (Vercel, Firebase, AWS, etc.)

  deploy-production:
    needs: build-and-test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20, cache: npm }
      - run: npm ci
      - run: npm run build -- --configuration=production
      # Deploy to production

Deploy Targets

Vercel

npm install -g vercel
vercel

Vercel auto-detects Angular and supports SSR out of the box.

Firebase Hosting

ng add @angular/fire
firebase init hosting
ng build --configuration=production
firebase deploy

AWS S3 + CloudFront (SPA)

# Build
ng build --configuration=production

# Sync to S3
aws s3 sync dist/my-app/browser s3://my-bucket --delete

# Invalidate CloudFront cache
aws cloudfront create-invalidation --distribution-id XXXX --paths "/*"

Azure Static Web Apps

# .github/workflows/azure-deploy.yml
- name: Deploy
  uses: Azure/static-web-apps-deploy@v1
  with:
    azure_static_web_apps_api_token: ${{ secrets.AZURE_TOKEN }}
    app_location: "/"
    output_location: "dist/my-app/browser"

Runtime Configuration (Without Rebuild)

// Load config at app startup (from a JSON file or API)
export function initializeApp(http: HttpClient): () => Observable<AppConfig> {
  return () => http.get<AppConfig>('/assets/config.json').pipe(
    tap(config => (window as any).__APP_CONFIG__ = config),
  );
}

// app.config.ts
providers: [
  {
    provide: APP_INITIALIZER,
    useFactory: initializeApp,
    deps: [HttpClient],
    multi: true,
  },
]
// assets/config.json (can be modified per environment without rebuilding)
{
  "apiUrl": "https://api.myapp.com",
  "featureFlags": { "newDashboard": true }
}

Key Takeaways

  • Use environment files for build-time config, JSON config for runtime config
  • Set bundle budgets in angular.json — CI fails if bundles are too large
  • Use multi-stage Docker builds — build image stays small
  • nginx.conf needs try_files $uri /index.html for SPA routing
  • CI pipeline: lint → test → build → deploy (separate staging and production)
  • Use APP_INITIALIZER for runtime config that can change without rebuilding
  • Always add security headers in your web server (CSP, X-Frame-Options, etc.)