Deployment Overview
Estimated Time: 3 hours | Difficulty: Intermediate | Prerequisites: Build process, Git
Copy
┌─────────────────────────────────────────────────────────────────────────┐
│ Deployment Pipeline │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Code │───►│ Build │───►│ Test │───►│ Deploy │ │
│ │ Push │ │ │ │ │ │ │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Trigger │ │ Install │ │ Unit │ │ Preview │ │
│ │ CI │ │ Deps │ │ Tests │ │ Stage │ │
│ │ │ │ Lint │ │ E2E │ │ Prod │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ Environments: │
│ • Development → localhost:4200 │
│ • Preview → pr-123.preview.example.com │
│ • Staging → staging.example.com │
│ • Production → www.example.com │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Build Optimization
Production Build
Copy
# Standard production build
ng build --configuration production
# With additional optimizations
ng build --configuration production \
--output-hashing=all \
--source-map=false
# Analyze bundle size
ng build --configuration production --stats-json
npx webpack-bundle-analyzer dist/my-app/stats.json
Environment Configuration
Copy
// environments/environment.ts
export const environment = {
production: false,
apiUrl: 'http://localhost:3000/api',
features: {
analytics: false,
newDashboard: true
}
};
// environments/environment.production.ts
export const environment = {
production: true,
apiUrl: 'https://api.example.com',
features: {
analytics: true,
newDashboard: true
}
};
// environments/environment.staging.ts
export const environment = {
production: true,
apiUrl: 'https://staging-api.example.com',
features: {
analytics: true,
newDashboard: true
}
};
Copy
// angular.json
{
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kb",
"maximumError": "8kb"
}
],
"outputHashing": "all",
"optimization": true,
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.production.ts"
}
]
},
"staging": {
"budgets": [
{ "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }
],
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.staging.ts"
}
],
"optimization": true,
"sourceMap": true
}
}
}
GitHub Actions
Complete CI/CD Pipeline
Copy
# .github/workflows/ci-cd.yml
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
NODE_VERSION: '20.x'
ANGULAR_CLI_VERSION: '17'
jobs:
# Job 1: Lint and Test
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Run unit tests
run: npm run test:ci
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
# Job 2: Build
build:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build:prod
env:
CI: true
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
retention-days: 7
# Job 3: E2E Tests
e2e:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Download build artifact
uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: Run E2E tests
uses: cypress-io/github-action@v6
with:
start: npx http-server dist/my-app/browser -p 4200
wait-on: 'http://localhost:4200'
browser: chrome
- name: Upload E2E screenshots
uses: actions/upload-artifact@v4
if: failure()
with:
name: cypress-screenshots
path: cypress/screenshots
# Job 4: Deploy Preview (PRs)
deploy-preview:
runs-on: ubuntu-latest
needs: build
if: github.event_name == 'pull_request'
steps:
- name: Download build artifact
uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: Deploy to Vercel Preview
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
working-directory: dist/my-app/browser
- name: Comment PR with preview URL
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '🚀 Preview deployed to: ${{ steps.deploy.outputs.preview-url }}'
})
# Job 5: Deploy Staging
deploy-staging:
runs-on: ubuntu-latest
needs: [build, e2e]
if: github.ref == 'refs/heads/develop'
environment: staging
steps:
- name: Download build artifact
uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: Deploy to AWS S3
uses: jakejarvis/s3-sync-action@master
with:
args: --delete
env:
AWS_S3_BUCKET: ${{ secrets.STAGING_S3_BUCKET }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
SOURCE_DIR: 'dist/my-app/browser'
- name: Invalidate CloudFront
uses: chetan/invalidate-cloudfront-action@v2
env:
DISTRIBUTION: ${{ secrets.STAGING_CF_DISTRIBUTION }}
PATHS: '/*'
AWS_REGION: 'us-east-1'
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
# Job 6: Deploy Production
deploy-production:
runs-on: ubuntu-latest
needs: [build, e2e]
if: github.ref == 'refs/heads/main'
environment: production
steps:
- name: Download build artifact
uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: Deploy to AWS S3
uses: jakejarvis/s3-sync-action@master
with:
args: --delete
env:
AWS_S3_BUCKET: ${{ secrets.PROD_S3_BUCKET }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
SOURCE_DIR: 'dist/my-app/browser'
- name: Invalidate CloudFront
uses: chetan/invalidate-cloudfront-action@v2
env:
DISTRIBUTION: ${{ secrets.PROD_CF_DISTRIBUTION }}
PATHS: '/*'
AWS_REGION: 'us-east-1'
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Notify Slack
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
text: 'Production deployment completed!'
fields: repo,message,commit,author
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
Package.json Scripts
Copy
{
"scripts": {
"start": "ng serve",
"build": "ng build",
"build:prod": "ng build --configuration production",
"build:staging": "ng build --configuration staging",
"test": "ng test",
"test:ci": "ng test --no-watch --no-progress --browsers=ChromeHeadless --code-coverage",
"lint": "ng lint",
"e2e": "ng e2e",
"analyze": "ng build --configuration production --stats-json && npx webpack-bundle-analyzer dist/my-app/stats.json"
}
}
Docker Deployment
Dockerfile
Copy
# Build stage
FROM node:20-alpine AS build
WORKDIR /app
# Install dependencies
COPY package*.json ./
RUN npm ci
# Copy source and build
COPY . .
RUN npm run build:prod
# Production stage
FROM nginx:alpine AS production
WORKDIR /usr/share/nginx/html
# Remove default nginx website
RUN rm -rf ./*
# Copy built assets
COPY --from=build /app/dist/my-app/browser .
# Copy nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Add healthcheck
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:80/health || exit 1
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Nginx Configuration
Copy
# nginx.conf
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com;" always;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/json application/xml;
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Don't cache index.html
location = /index.html {
expires -1;
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
# Angular routing - try file, then directory, then fallback to index.html
location / {
try_files $uri $uri/ /index.html;
}
# Health check endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
# API proxy (if needed)
location /api/ {
proxy_pass http://api-server:3000/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
Docker Compose
Copy
# docker-compose.yml
version: '3.8'
services:
web:
build:
context: .
dockerfile: Dockerfile
ports:
- "80:80"
environment:
- NODE_ENV=production
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost/health"]
interval: 30s
timeout: 10s
retries: 3
networks:
- app-network
depends_on:
- api
api:
image: my-api:latest
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/myapp
networks:
- app-network
depends_on:
- db
db:
image: postgres:15-alpine
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=myapp
networks:
- app-network
volumes:
postgres-data:
networks:
app-network:
driver: bridge
Cloud Platform Deployments
Vercel
Copy
// vercel.json
{
"buildCommand": "npm run build:prod",
"outputDirectory": "dist/my-app/browser",
"framework": null,
"rewrites": [
{ "source": "/(.*)", "destination": "/index.html" }
],
"headers": [
{
"source": "/assets/(.*)",
"headers": [
{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
]
}
]
}
Netlify
Copy
# netlify.toml
[build]
command = "npm run build:prod"
publish = "dist/my-app/browser"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
[[headers]]
for = "/assets/*"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"
X-XSS-Protection = "1; mode=block"
X-Content-Type-Options = "nosniff"
Firebase Hosting
Copy
// 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": "max-age=31536000"
}
]
}
]
}
}
Copy
# Deploy to Firebase
npm install -g firebase-tools
firebase login
firebase init hosting
npm run build:prod
firebase deploy
Monitoring & Logging
Copy
// error-handler.service.ts
import * as Sentry from '@sentry/angular';
@Injectable({ providedIn: 'root' })
export class ErrorHandlerService implements ErrorHandler {
handleError(error: Error): void {
console.error('Application error:', error);
// Send to Sentry
Sentry.captureException(error);
// Log to backend
this.logError(error);
}
private http = inject(HttpClient);
private logError(error: Error): void {
const errorLog = {
message: error.message,
stack: error.stack,
timestamp: new Date().toISOString(),
url: window.location.href,
userAgent: navigator.userAgent
};
this.http.post('/api/logs/error', errorLog).subscribe();
}
}
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
{ provide: ErrorHandler, useClass: ErrorHandlerService },
Sentry.createErrorHandler({ showDialog: false })
]
};
Deployment Checklist
Copy
┌─────────────────────────────────────────────────────────────────────────┐
│ Pre-Deployment Checklist │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Build & Tests: │
│ □ All tests passing │
│ □ No linting errors │
│ □ Build succeeds without errors │
│ □ Bundle size within budget │
│ │
│ Configuration: │
│ □ Environment variables set │
│ □ API URLs configured for production │
│ □ Feature flags updated │
│ □ Analytics/tracking enabled │
│ │
│ Security: │
│ □ HTTPS enabled │
│ □ Security headers configured │
│ □ CSP policy in place │
│ □ Sensitive data removed from bundles │
│ │
│ Performance: │
│ □ Source maps disabled (or uploaded to error tracking) │
│ □ Assets optimized and compressed │
│ □ Caching headers configured │
│ □ CDN configured │
│ │
│ Monitoring: │
│ □ Error tracking enabled │
│ □ Performance monitoring enabled │
│ □ Health checks configured │
│ □ Alerts set up │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Next: Micro-frontends
Build scalable applications with micro-frontend architecture