Angular 112 🅰️ Deployment — CI/CD and Caching
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 :
| Value | Effect |
|---|---|
all | Hashes all files (bundles, media, CSS) |
media | Hashes only media files (images, fonts) |
bundles | Hashes only JavaScript and CSS bundles |
none | Disables 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
| Step | Command | Purpose |
|---|---|---|
| Checkout | actions/checkout | Pull source code |
| Node setup | actions/setup-node | Configure runtime with npm cache |
| Install | npm ci | Reproducible dependency install |
| Lint | npm run lint | Enforce code style |
| Test | npm test -- --watch=false --browsers=ChromeHeadless | Run unit tests |
| Build | npm run build | Production build |
| Archive | actions/upload-artifact | Save build output |
| Deploy | Platform-specific | Publish to hosting |
Angular CLI Cache Configuration
| Setting | Location | Values |
|---|---|---|
cli.cache.environment | angular.json | local, ci, all |
cli.cache.path | angular.json | Path string (default .angular/cache) |
Output Hashing Options
| Value | Hashes |
|---|---|
all | Bundles, media, CSS |
media | Images, fonts only |
bundles | JavaScript and CSS only |
none | Nothing |
Nginx Cache Headers
| Location | Expires | Cache-Control |
|---|---|---|
Hashed assets (.js, .css, .woff2) | 1 year | public, immutable |
index.html | -1 | no-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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Tests fail in CI | Browser not headless | --browsers=ChromeHeadless |
| Chrome sandbox error | Linux CI lacks privileges | Custom launcher with --no-sandbox |
| Build cache not used | CI env detected, cache disabled | ng config cli.cache.environment all |
| Assets not hashed | Assets copied as-is | Use custom cache-busting for assets |
| Users get old version | index.html cached | Cache-Control: no-store on index.html |
| Docker build slow | Dependencies installed before source copy | Copy package*.json first |
| Pipeline takes too long | No npm cache configured | cache: '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
| Item | Value |
|---|---|
| CI/CD trigger | Push to main, pull requests |
| Dependency install | npm ci (reproducible) |
| Test command | npm test -- --watch=false --browsers=ChromeHeadless |
| Angular CLI cache | .angular/cache, disabled in CI by default |
| Cache environment setting | cli.cache.environment: local, ci, all |
| Output hashing | outputHashing in angular.json: all, media, bundles, none |
| Index.html caching | Disabled (no-store, no-cache) |
| Asset caching | Hashed assets: 1 year, public, immutable |
| Docker caching | Copy package*.json first |
| Karma headless launcher | ChromeHeadlessNoSandbox with --no-sandbox |
Key takeaways:
- Use
npm ciinstead ofnpm installin CI. It strictly followspackage-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.environmenttoallto enable it and speed up repeated pipeline runs . - Headless Chrome is mandatory for CI tests. The
--browsers=ChromeHeadlessflag prevents Karma from attempting to launch a browser with a display . - Output hashing ensures cache correctness. When
outputHashingisall, changed files get new filenames and browsers fetch the updated version instead of serving a stale cached copy . index.htmlmust 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
immutabledirective tells browsers not to revalidate files whose URLs are content-addressed . - Docker layer caching is maximized by copying
package*.jsonbefore source code. Dependency installation is cached as long as dependencies don’t change . - Assets in the
assetsfolder are not hashed. Angular copies them as-is, even withoutputHashing: 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.
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!