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.confneedstry_files $uri /index.htmlfor SPA routing- CI pipeline: lint → test → build → deploy (separate staging and production)
- Use
APP_INITIALIZERfor runtime config that can change without rebuilding - Always add security headers in your web server (CSP, X-Frame-Options, etc.)