| |

Angular 112 🅰️ Deployment — CI/CD and Caching

⏱️ Estimated read: 15 min | |

A CI/CD pipeline for Angular automates the path from a code commit to a production deployment. It runs linting, tests, and builds in a controlled environment, then publishes the output to a hosting platform. Caching, in two distinct senses, is central to making this pipeline both fast and correct: build caching accelerates repeated CI runs, while asset caching and cache busting ensure users receive updated code without sacrificing browser performance.

The distinction matters because the two types of caching serve opposite goals. Build caching is an internal optimization that makes the pipeline faster by reusing previous work. Asset caching is an external strategy that makes the application faster for users by letting browsers store files locally. A correct deployment strategy must do both: cache aggressively during the build, and manage browser caching precisely so that new versions are delivered when they exist and old versions remain cached when they do not.

This chapter covers setting up a GitHub Actions CI/CD pipeline for Angular, the Angular CLI’s disk cache and its CI environment configuration, output hashing for cache busting, Nginx cache headers for static assets, and Docker layer caching for containerized deployments.

Key point: The Angular CLI has a disk cache that is disabled in CI by default; enabling it with cli.cache.environment set to all speeds up repeated pipeline runs. Output hashing appends content-based hashes to filenames so browsers fetch new assets only when content changes.


Why CI/CD and caching are inseparable

The build speed problem. An Angular build from scratch can take several minutes on a cold CI runner. Installing node_modules, compiling TypeScript, bundling with esbuild or webpack, and optimizing for production all consume time. When a pipeline runs on every pull request and every push, those minutes accumulate into hours of developer waiting time. Caching dependencies and build artifacts reduces that wait dramatically.

The cache correctness problem. A stale cache can produce incorrect results. If a CI runner restores an npm cache that is out of sync with package-lock.json, it may install older dependencies than specified. If a build cache contains artifacts from a different branch, the output may not reflect the current code. Correct caching requires cache keys that include the files that determine what is cached, such as package-lock.json for dependencies and source file hashes for build output.

The asset freshness problem. When you deploy a new version of an Angular application, users who have the old version cached in their browsers will continue to use it until the cache expires. If the HTML references main.js without a hash, the browser may serve the old main.js even after a new one is deployed. Output hashing solves this by embedding a content hash in the filename, so a changed file gets a new name and the browser fetches it.

The Angular CLI cache problem. Angular CLI maintains a disk cache at .angular/cache that stores build state between runs. By default, this cache is only enabled on local machines, not in CI. In a CI environment, every run starts from a cold cache unless you explicitly configure it otherwise. The cli.cache.environment setting controls this behavior .

The Docker layer caching problem. When building a Docker image for an Angular application, each RUN and COPY instruction creates a layer. Docker caches layers and reuses them when the instruction and its inputs are unchanged. Copying package.json before source code allows the dependency installation layer to be cached even when source files change, significantly reducing image build times .


a. Setting up a GitHub Actions CI/CD pipeline

A GitHub Actions workflow for Angular typically includes checkout, Node.js setup, dependency installation, linting, testing, building, and deployment. The workflow is triggered on pushes to the main branch and on pull requests.

The basic structure uses actions/checkout to pull the code and actions/setup-node to configure the Node.js version . Using a version matrix allows testing against multiple Node.js versions, though most Angular projects pin a single version in CI.

name: Angular CI/CD Pipeline

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

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [ '20.x' ]
    steps:
      - uses: actions/checkout@v4
      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
      - run: npm ci
      - run: npm run lint
      - run: npm run test -- --watch=false --browsers=ChromeHeadless
      - run: npm run build

The npm ci command is preferred over npm install in CI because it strictly adheres to package-lock.json and fails if the lock file and package.json are out of sync, guaranteeing reproducible builds .

A common pitfall with Angular tests in CI is the browser environment. CI runners have no display, so Chrome must run in headless mode. The --browsers=ChromeHeadless flag instructs Karma to use headless Chrome . On some Linux CI runners, Chrome’s sandbox requires privileges that are not available, so a custom launcher with --no-sandbox is necessary .

// karma.conf.js
customLaunchers: {
  ChromeHeadlessNoSandbox: {
    base: 'ChromeHeadless',
    flags: ['--no-sandbox', '--disable-gpu']
  }
}

b. Angular CLI disk cache and CI configuration

Angular CLI saves cacheable operations to disk at .angular/cache by default . On subsequent builds, the build system restores the previous state and reuses operations that have not changed. This reduces build time, especially for incremental changes.

By default, the disk cache is only enabled for local development. In a CI environment, the cache is disabled unless explicitly configured. The cli.cache.environment setting accepts three values: local (default), ci, and all .

To enable disk cache in CI, set the environment to all in angular.json:

{
  "cli": {
    "cache": {
      "environment": "all"
    }
  }
}

The Angular CLI detects the CI environment by checking the CI environment variable. Most CI providers, including GitHub Actions, set this variable automatically .

In a CI pipeline, the .angular/cache directory should be included in the cache configuration so it persists between runs. For GitHub Actions, the built-in npm cache handles node_modules, but the Angular cache requires separate configuration or inclusion in the actions/cache step.

c. Output hashing for asset cache busting

Angular’s build process can append content-based hashes to output filenames using the outputHashing option. When enabled, a file like main.js becomes main.abc123def.js, where the hash changes whenever the file content changes .

The outputHashing option accepts four values :

ValueEffect
allHashes all files (bundles, media, CSS)
mediaHashes only media files (images, fonts)
bundlesHashes only JavaScript and CSS bundles
noneDisables hashing

For production builds, all is the recommended setting. It ensures that every asset gets a new filename when its content changes, so browsers will fetch the new version rather than serving a stale cached copy .

To configure this in angular.json:

{
  "configurations": {
    "production": {
      "outputHashing": "all"
    }
  }
}

A critical detail: Angular’s assets configuration copies files as-is and does not hash them, even when outputHashing is set to all . Files referenced in component templates and processed by the build system receive hashes, but assets copied directly from the assets folder do not. For these files, you need either a custom cache-busting strategy or a versioned asset path .

The index.html file should never be cached aggressively. If index.html is cached and references the old main.js, the cache-busting benefit is lost. Browsers should always fetch the latest index.html so they receive the new asset references .

d. Nginx cache headers for static assets

When serving an Angular application through Nginx, cache headers control how long browsers and CDNs store assets. The strategy is to cache hashed assets aggressively and index.html not at all.

A production Nginx configuration for Angular :

location / {
    try_files $uri $uri/ /index.html;
}

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

location = /index.html {
    expires -1;
    add_header Cache-Control "no-store, no-cache, must-revalidate";
}

The try_files directive ensures that client-side routes fall back to index.html, which is mandatory for Angular’s router to handle deep links .

The asset location block sets expires 1y and Cache-Control: public, immutable. The immutable directive tells browsers that the content of a file at a given URL will never change, so they should not revalidate it. This is safe because output hashing ensures that changed content gets a new URL .

The index.html location block disables caching with no-store, no-cache, must-revalidate. This ensures that every navigation fetches the latest index.html and receives references to the current asset filenames .

e. Docker layer caching for Angular images

A Dockerfile for an Angular application should separate dependency installation from source code copying to maximize layer cache reuse. Copying package.json and package-lock.json first, then running npm ci, creates a layer that only invalidates when dependencies change. Copying source code afterward keeps source changes from invalidating the dependency layer .

A multi-stage Dockerfile for a static Angular application :

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

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

The builder stage uses a Node.js image with the full toolchain. The production stage uses Nginx to serve only the built static files, resulting in a much smaller final image. The COPY --from=builder instruction extracts only the built output, discarding node_modules and source code.

For containerized Angular builds in CI, the Docker build cache itself can be persisted using BuildKit or registry-based caching. This is separate from the npm dependency cache and the Angular CLI disk cache, but it follows the same principle: cache layers that have not changed.


Complete Example Session

# ============================================
# PART 1: GITHUB ACTIONS WORKFLOW STRUCTURE
# ============================================
name: Angular CI/CD
on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
# ============================================
# PART 2: CHECKOUT AND NODE SETUP
# ============================================
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20.x'
          cache: 'npm'
# ============================================
# PART 3: DEPENDENCY INSTALLATION
# ============================================
      - run: npm ci
      # npm ci installs exactly what's in package-lock.json
# ============================================
# PART 4: LINTING
# ============================================
      - run: npm run lint
      # Fails pipeline on style violations
# ============================================
# PART 5: UNIT TESTS IN HEADLESS MODE
# ============================================
      - run: npm test -- --watch=false --browsers=ChromeHeadlessNoSandbox
      # karma.conf.js needs ChromeHeadlessNoSandbox custom launcher
# ============================================
# PART 6: PRODUCTION BUILD
# ============================================
      - run: npm run build -- --configuration=production
      # Output: dist/my-app/browser/
# ============================================
# PART 7: ARCHIVE BUILD OUTPUT
# ============================================
      - uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/my-app/browser
# ============================================
# PART 8: NGINX CACHE CONFIGURATION
# ============================================
# In nginx.conf:
location ~* \.(js|css|woff2)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}
location = /index.html {
    expires -1;
    add_header Cache-Control "no-store";
}
// ============================================
// PART 9: OUTPUT HASHING CONFIG
// ============================================
// angular.json production configuration
{
  "configurations": {
    "production": {
      "outputHashing": "all"
    }
  }
}
# ============================================
# PART 10: DOCKER MULTI-STAGE BUILD
# ============================================
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build -- --configuration=production

FROM nginx:alpine
COPY --from=builder /app/dist/my-app/browser /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf

These ten parts cover the CI/CD pipeline structure, dependency caching, headless testing, production builds, artifact archiving, Nginx cache headers, output hashing, and Docker layer caching. The pipeline is modular: each step can be extended or replaced without changing the others.


Quick Reference

CI/CD Pipeline Steps

StepCommandPurpose
Checkoutactions/checkoutPull source code
Node setupactions/setup-nodeConfigure runtime with npm cache
Installnpm ciReproducible dependency install
Lintnpm run lintEnforce code style
Testnpm test -- --watch=false --browsers=ChromeHeadlessRun unit tests
Buildnpm run buildProduction build
Archiveactions/upload-artifactSave build output
DeployPlatform-specificPublish to hosting

Angular CLI Cache Configuration

SettingLocationValues
cli.cache.environmentangular.jsonlocal, ci, all
cli.cache.pathangular.jsonPath string (default .angular/cache)

Output Hashing Options

ValueHashes
allBundles, media, CSS
mediaImages, fonts only
bundlesJavaScript and CSS only
noneNothing

Nginx Cache Headers

LocationExpiresCache-Control
Hashed assets (.js, .css, .woff2)1 yearpublic, immutable
index.html-1no-store, no-cache, must-revalidate

Best Practices

✅ Do This:

- uses: actions/setup-node@v4
  with:
    cache: 'npm'                              # Enable npm cache
- run: npm ci                                 # Reproducible install
- run: npm test -- --browsers=ChromeHeadless  # Headless tests
// angular.json
"outputHashing": "all"                        // Cache busting for all assets
location = /index.html {
    expires -1;                               # Never cache index.html
}

❌ Don’t Do This:

- run: npm install                            # ❌ Non-deterministic
- run: npm test                               # ❌ Opens browser; fails in CI
"outputHashing": "none"                       // ❌ Browsers serve stale assets
location = /index.html {
    expires 1y;                               // ❌ Users never get updates
}

Common Pitfalls

PitfallWhy It HappensFix
Tests fail in CIBrowser not headless--browsers=ChromeHeadless
Chrome sandbox errorLinux CI lacks privilegesCustom launcher with --no-sandbox
Build cache not usedCI env detected, cache disabledng config cli.cache.environment all
Assets not hashedAssets copied as-isUse custom cache-busting for assets
Users get old versionindex.html cachedCache-Control: no-store on index.html
Docker build slowDependencies installed before source copyCopy package*.json first
Pipeline takes too longNo npm cache configuredcache: 'npm' in setup-node

Real-World Examples

1. GitHub Actions with npm Cache

- uses: actions/setup-node@v4
  with:
    node-version: '20'
    cache: 'npm'

2. Firebase Deployment Step

- uses: w9jds/firebase-action@master
  with:
    args: deploy --only hosting
  env:
    FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}

3. Karma Custom Launcher

customLaunchers: {
  ChromeHeadlessNoSandbox: {
    base: 'ChromeHeadless',
    flags: ['--no-sandbox', '--disable-gpu']
  }
}

4. Enable Angular Cache in CI

{
  "cli": {
    "cache": {
      "environment": "all"
    }
  }
}

5. Nginx Hashed Asset Cache

location ~* \.(js|css|woff2)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

6. Docker Layer Caching Pattern

COPY package*.json ./
RUN npm ci
COPY . .

7. Artifact Upload and Download

# Upload
- uses: actions/upload-artifact@v4
  with:
    name: dist
    path: dist/my-app/browser

# Download in deploy job
- uses: actions/download-artifact@v4
  with:
    name: dist
    path: dist/my-app/browser

8. Production Build Command

ng build --configuration=production

9. Cache Configuration in angular.json

{
  "cli": {
    "cache": {
      "environment": "all",
      "path": ".angular/cache"
    }
  }
}

10. Index.html Cache Disable

location = /index.html {
    add_header Cache-Control "no-store, no-cache, must-revalidate";
}

Visual

CI/CD Pipeline Flow

┌──────────────────────────────────────────────────────────────┐
│  GITHUB ACTIONS PIPELINE FOR ANGULAR                         │
│                                                              │
│  Push / PR                                                   │
│       │                                                      │
│       ▼                                                      │
│  ┌─────────────┐                                             │
│  │  CHECKOUT   │  actions/checkout@v4                        │
│  └─────────────┘                                             │
│       │                                                      │
│       ▼                                                      │
│  ┌─────────────┐                                             │
│  │  NODE SETUP │  actions/setup-node@v4 with npm cache       │
│  └─────────────┘                                             │
│       │                                                      │
│       ▼                                                      │
│  ┌─────────────┐                                             │
│  │  INSTALL    │  npm ci (reproducible)                      │
│  └─────────────┘                                             │
│       │                                                      │
│       ▼                                                      │
│  ┌─────────────┐                                             │
│  │  LINT       │  npm run lint                               │
│  └─────────────┘                                             │
│       │                                                      │
│       ▼                                                      │
│  ┌─────────────┐                                             │
│  │  TEST       │  ChromeHeadless                             │
│  └─────────────┘                                             │
│       │                                                      │
│       ▼                                                      │
│  ┌─────────────┐                                             │
│  │  BUILD      │  production configuration                   │
│  └─────────────┘                                             │
│       │                                                      │
│       ▼                                                      │
│  ┌─────────────┐                                             │
│  │  ARTIFACT   │  upload dist/                               │
│  └─────────────┘                                             │
│       │                                                      │
│       ▼                                                      │
│  ┌─────────────┐                                             │
│  │  DEPLOY     │  Firebase, Azure, S3, etc.                  │
│  └─────────────┘                                             │
└──────────────────────────────────────────────────────────────┘

Angular CLI Cache Environments

┌──────────────────────────────────────────────────────────────┐
│  cli.cache.environment SETTINGS                              │
│                                                              │
│  "local" (default):                                          │
│  ├── Disk cache enabled on developer machines                │
│  ├── Disabled when CI env variable is set                    │
│  └── Safe default for most projects                          │
│                                                              │
│  "ci":                                                       │
│  ├── Disk cache enabled only in CI                           │
│  ├── Disabled locally                                        │
│  └── Useful for CI-only optimization                         │
│                                                              │
│  "all":                                                      │
│  ├── Disk cache always enabled                               │
│  ├── Works in local and CI                                   │
│  └── Recommended for pipeline speedup                        │
│                                                              │
│  Cache location: .angular/cache (configurable)               │
│  CI detection: presence of CI env variable                   │
└──────────────────────────────────────────────────────────────┘

Output Hashing and Cache Busting

┌──────────────────────────────────────────────────────────────┐
│  OUTPUT HASHING WORKFLOW                                     │
│                                                              │
│  WITHOUT HASHING:                                            │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  index.html references: main.js, styles.css            │  │
│  │  Browser caches: main.js (stale)                       │  │
│  │  Deploy: new main.js, same filename                    │  │
│  │  Browser: serves old main.js from cache ✗              │  │
│  └────────────────────────────────────────────────────────┘  │
│                                                              │
│  WITH HASHING:                                               │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  index.html references: main.abc123.js                 │  │
│  │  Browser caches: main.abc123.js                        │  │
│  │  Deploy: main.def456.js (content changed)              │  │
│  │  New index.html references main.def456.js              │  │
│  │  Browser: fetches new file ✓                           │  │
│  └────────────────────────────────────────────────────────┘  │
│                                                              │
│  index.html must not be cached so it always points          │
│  to the current hashed filenames.                           │
└──────────────────────────────────────────────────────────────┘

Docker Layer Caching

┌──────────────────────────────────────────────────────────────┐
│  DOCKER LAYER CACHING FOR ANGULAR BUILDS                     │
│                                                              │
│  BAD: Source copied before dependencies                     │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  COPY . .                    ← invalidates on any change│  │
│  │  RUN npm ci                  ← re-runs every build      │  │
│  │  RUN npm run build           ← re-runs every build      │  │
│  └────────────────────────────────────────────────────────┘  │
│                                                              │
│  GOOD: Dependencies first, source after                     │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  COPY package*.json ./       ← cached unless deps change│  │
│  │  RUN npm ci                  ← cached if deps unchanged │  │
│  │  COPY . .                    ← only this layer rebuilds  │  │
│  │  RUN npm run build           ← runs with cached deps     │  │
│  └────────────────────────────────────────────────────────┘  │
│                                                              │
│  Source changes only invalidate the COPY and build layers.  │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
CI/CD triggerPush to main, pull requests
Dependency installnpm ci (reproducible)
Test commandnpm test -- --watch=false --browsers=ChromeHeadless
Angular CLI cache.angular/cache, disabled in CI by default
Cache environment settingcli.cache.environment: local, ci, all
Output hashingoutputHashing in angular.json: all, media, bundles, none
Index.html cachingDisabled (no-store, no-cache)
Asset cachingHashed assets: 1 year, public, immutable
Docker cachingCopy package*.json first
Karma headless launcherChromeHeadlessNoSandbox with --no-sandbox

Key takeaways:

  • Use npm ci instead of npm install in CI. It strictly follows package-lock.json, guaranteeing reproducible builds and failing fast if dependencies are out of sync .
  • Angular CLI disk cache is disabled in CI by default. Set cli.cache.environment to all to enable it and speed up repeated pipeline runs .
  • Headless Chrome is mandatory for CI tests. The --browsers=ChromeHeadless flag prevents Karma from attempting to launch a browser with a display .
  • Output hashing ensures cache correctness. When outputHashing is all, changed files get new filenames and browsers fetch the updated version instead of serving a stale cached copy .
  • index.html must never be cached. It is the entry point that references the current hashed asset filenames; caching it would point users to old assets .
  • Hashed assets should be cached aggressively. The immutable directive tells browsers not to revalidate files whose URLs are content-addressed .
  • Docker layer caching is maximized by copying package*.json before source code. Dependency installation is cached as long as dependencies don’t change .
  • Assets in the assets folder are not hashed. Angular copies them as-is, even with outputHashing: all. For cache busting of these files, use a custom versioning strategy .

Remember: CI/CD for Angular is a sequence of verifiable steps: install, lint, test, build, deploy. Each step should fail fast if something is wrong, preventing broken code from reaching production. Caching operates at three levels: the pipeline level (npm and Angular CLI caches), the build level (Docker layers), and the browser level (output hashing and cache headers). Correct caching requires that cache keys include the inputs that determine the cached output—package-lock.json for dependencies, source hashes for build artifacts, and content hashes for browser assets. When configured properly, the pipeline runs quickly, the deployment is reproducible, and users receive updates immediately when they exist while enjoying the performance benefits of local caching when they don’t.


Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!