| |

Angular 111 🅰️ Deployment — Static and SSR

⏱️ Estimated read: 15 min | |

Angular applications can be deployed in two fundamentally different ways depending on your rendering strategy. A static client-side rendered (CSR) application produces a folder of HTML, CSS, and JavaScript files that any web server or CDN can host. A server-side rendered (SSR) application requires a running Node.js process to generate HTML on each request, which introduces additional deployment considerations around server infrastructure and configuration.

The choice between these approaches determines your hosting requirements, cost structure, and operational complexity. CSR deployment is simpler and cheaper because static files can be served from object storage or a CDN with no compute costs. SSR deployment is more complex because it requires a Node.js runtime, but it delivers faster initial page loads, better SEO for dynamic content, and the ability to fetch data before the page reaches the browser.

This chapter covers both deployment paths: building and serving static Angular output, deploying SSR applications to Node.js environments, configuring web servers for Angular’s routing behavior, and containerizing SSR applications with Docker.

Key point: Static Angular deployment serves pre-built files with a fallback to index.html for client-side routing. SSR deployment runs a Node.js server that renders pages on demand, requiring a platform that supports long-running processes.


Why deployment strategy differs by rendering mode

The static file problem. A client-side rendered Angular application is, at build time, a collection of static assets: index.html, JavaScript bundles, CSS files, and assets like images and fonts. All routing and rendering happens in the browser after the JavaScript loads. Any web server can serve these files, and because there is no server-side computation, the hosting cost is minimal. The only requirement is that the server returns index.html for any route the application defines, so that deep links and page refreshes work correctly .

The SSR runtime problem. A server-side rendered Angular application is not just a collection of static files. The build produces two outputs: a browser/ folder containing client-side assets and a server/ folder containing a compiled Node.js application that renders HTML on each request . Deployment requires a platform that can run a persistent Node.js process, not just serve files. This rules out pure static hosts like AWS S3 or GitHub Pages for the SSR portion, though those platforms can still serve the client assets if combined with a serverless function .

The fallback routing problem. Both CSR and SSR applications share a common requirement: the web server must handle routes that do not correspond to actual files. When a user navigates directly to https://example.com/users/42, the server receives a request for /users/42, which is not a file on disk. A default web server configuration returns a 404. Angular’s router expects to receive index.html (CSR) or the rendered page (SSR) and take over from there. Configuring this fallback is one of the most common deployment pitfalls .

The prerendering middle ground. Angular’s hybrid rendering model allows specific routes to be prerendered at build time while others use client or server rendering. Prerendered routes are generated as static HTML files and can be served like CSR content, combining the performance of static hosting with the SEO benefits of server rendering for content that does not change per request . This means a single application can have a mix of static and dynamic deployment requirements.


a. Building for production

The build command differs depending on your rendering mode. For a standard CSR application, ng build produces the production output by default . The output goes to dist/<project-name>/ unless configured otherwise. For Angular 17 and later, the browser assets are placed in a browser/ subfolder within the output directory .

For an SSR application, the build also produces a server/ folder alongside browser/. The server/ folder contains a compiled Node.js entry point (typically server.mjs) and supporting chunks that implement the rendering logic . The exact output structure depends on the Angular version and the builder configuration. Angular 19 uses @angular/build:application for the server target, which generates an engine manifest that the server uses at runtime .

A critical detail for deployment is the output path configuration. If you customize outputPath in angular.json, ensure that both the browser and server targets point to the expected locations. Deployment scripts and server configurations reference these paths, so inconsistencies here cause runtime failures.

b. Deploying static (CSR) applications

Static deployment is conceptually simple: copy the contents of the browser/ folder (or dist/<app>/ for older Angular versions) to a web server or object storage bucket. The server must be configured to serve index.html for any route that does not match a static file .

For Nginx, this configuration uses try_files:

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

This tells Nginx to first check for the requested file, then the directory, then fall back to index.html. The =404 at the end is a safeguard: if index.html does not exist, return a real 404 rather than an infinite loop .

For object storage like AWS S3, the equivalent configuration is setting the error document to index.html. When S3 cannot find the requested key, it returns index.html with a 200 status, allowing the Angular router to handle the route .

Platforms like Azure Static Web Apps, Firebase Hosting, and Scalingo automate this configuration through their Angular-specific deployment integrations . The ng deploy command works with platform-specific builders, such as @angular/fire:deploy for Firebase, to handle the build and upload process .

c. Deploying SSR applications

SSR deployment requires a Node.js runtime. The build output includes a server.mjs (or equivalent) file that starts an Express server. The server listens on a port, typically provided by the environment through the PORT variable .

A minimal production server configuration uses the Angular CommonEngine to render requests:

const commonEngine = new CommonEngine();
server.get('*', (req, res, next) => {
    commonEngine.render({
        bootstrap: AppServerModule,
        documentFilePath: indexHtml,
        url: `${req.protocol}://${req.headers.host}${req.originalUrl}`,
        publicPath: browserDistFolder,
        providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }],
    }).then(html => res.send(html)).catch(next);
});

Angular CLI scaffolds this server automatically when SSR is enabled. The deployment task is to run this server on a platform that supports persistent Node.js processes .

Managed platforms like Cloudways Velocity, Scalingo, and Firebase App Hosting provide Node.js environments with automatic builds and deployments from Git repositories . These platforms handle the server process management, SSL, and proxy configuration. For self-managed deployments, the Procfile or start script must invoke the server entry point, and the environment must provide Node.js and the application dependencies .

d. Configuring a reverse proxy for SSR

In production, SSR applications are typically placed behind a reverse proxy like Nginx. The proxy serves the client-side static assets directly and forwards rendering requests to the Node.js server.

A Docker Compose configuration with separate Nginx and Angular containers illustrates the pattern :

services:
  angular:
    build: .
    ports:
      - '4000:4000'
  nginx:
    image: nginx
    ports:
      - '80:80'
      - '443:443'
    depends_on:
      - angular

The Nginx configuration proxies all requests to the Angular server:

upstream docker-angular {
    server angular:4000;
}

server {
    listen 80;
    location / {
        proxy_pass http://docker-angular;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

This separation allows Nginx to handle SSL termination, static asset caching, and compression while the Node.js server focuses on rendering .

e. Dockerizing an SSR application

A Dockerfile for an Angular SSR application follows a multi-step pattern: install dependencies, build the application, and run the server. The exact commands depend on the Angular version and project configuration.

A working Dockerfile from a deployed project :

FROM node:20.12.2
RUN mkdir /usr/src/app
WORKDIR /usr/src/app
COPY package*.json /usr/src/app/
RUN npm install
RUN npm install -g @angular/cli@17.3.8
COPY ./ /usr/src/app
EXPOSE 4000
RUN npm run build
CMD ["npm", "run", "serve:ssr:my-app"]

A common pitfall is the package.json script name. Angular CLI generates scripts like serve:ssr:<project-name>, and the Docker CMD must reference the correct script. Another pitfall is the port: Angular SSR servers default to port 4000, not 4200, which is the development server port. The Docker EXPOSE directive and the platform’s port configuration must match the port the server actually binds to .

For Angular 19 projects using the @angular/build:application builder, the deployment must include the engine-manifest.json file that the server uses to locate the rendering engine. Omitting this file from the deployment causes runtime errors .


Complete Example Session

# ============================================
# PART 1: BUILD A STATIC ANGULAR APP
# ============================================
# Production build for CSR deployment.

ng build --configuration=production
# Output: dist/my-app/browser/
# ============================================
# PART 2: BUILD AN SSR ANGULAR APP
# ============================================
# SSR build produces browser and server folders.

ng build
# Output:
# dist/my-app/browser/   ← client assets
# dist/my-app/server/    ← Node.js server
# ============================================
# PART 3: CONFIGURE NGINX FOR STATIC CSR
# ============================================
# Fallback to index.html for client routing.

# /etc/nginx/sites-available/my-app
server {
    listen 80;
    root /var/www/my-app/browser;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html =404;
    }
}
# ============================================
# PART 4: DEPLOY STATIC FILES TO S3
# ============================================
# Sync build output to S3 bucket.

aws s3 sync dist/my-app/browser/ s3://my-bucket/ --delete
# Configure bucket for static website hosting
# Set error document to index.html
# ============================================
# PART 5: RUN SSR SERVER LOCALLY
# ============================================
# Start the production SSR server.

node dist/my-app/server/server.mjs
# Server listens on http://localhost:4000
# ============================================
# PART 6: VERIFY SSR OUTPUT
# ============================================
# Check that content is in the initial HTML.

curl http://localhost:4000 | grep "app-root"
# HTML should contain rendered content, not empty shell
# ============================================
# PART 7: DOCKERFILE FOR SSR
# ============================================
# Multi-stage build for production.

cat > Dockerfile << 'EOF'
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
RUN npm ci --omit=dev
EXPOSE 4000
CMD ["node", "dist/my-app/server/server.mjs"]
EOF
# ============================================
# PART 8: DOCKER COMPOSE WITH NGINX
# ============================================
# Reverse proxy configuration.

cat > docker-compose.yml << 'EOF'
services:
  app:
    build: .
    ports:
      - "4000:4000"
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - app
EOF
# ============================================
# PART 9: NGINX CONFIG FOR SSR
# ============================================
# Proxy all requests to the Angular server.

# nginx.conf
upstream angular {
    server app:4000;
}

server {
    listen 80;
    location / {
        proxy_pass http://angular;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}
# ============================================
# PART 10: DEPLOY WITH FIREBASE
# ============================================
# Firebase Hosting with SSR support.

ng add @angular/fire
firebase deploy
# Firebase builds and deploys both browser and server

These ten parts cover the complete deployment lifecycle: building for CSR and SSR, configuring Nginx for static fallback, uploading to object storage, running and verifying the SSR server, containerizing with Docker, and deploying through Firebase’s integrated workflow.


Quick Reference

Build Output Structure

Rendering ModeBrowser OutputServer Output
CSR (static)dist/<app>/browser/None
SSRdist/<app>/browser/dist/<app>/server/
Prerender (SSG)dist/<app>/browser/ with HTML filesOptional server for fallback

Static Hosting Options

PlatformConfiguration
Nginxtry_files $uri $uri/ /index.html
Apache.htaccess rewrite to index.html
S3Error document = index.html
Azure Static Web AppsAngular build preset; output dist/<app>/browser
Firebase Hostingfirebase deploy after ng add @angular/fire

SSR Hosting Options

PlatformRequirements
Self-managed VPSNode.js runtime, Nginx reverse proxy
DockerNode.js image, port 4000 exposed
Cloudways VelocityManaged Node.js; output dir browser/
ScalingoMulti buildpack: Node.js + Nginx
Firebase App HostingPaid plan; Node.js runtime
VercelCustom api/index.js and vercel.json

Common Server Configuration Files

FilePurpose
nginx.confReverse proxy for SSR or static serving
DockerfileContainer build for SSR
docker-compose.ymlMulti-container orchestration
ProcfilePlatform start command (Scalingo, Heroku)
vercel.jsonVercel routing configuration

Best Practices

✅ Do This:

try_files $uri $uri/ /index.html =404;    # Nginx fallback for CSR
proxy_pass http://angular:4000;           # Nginx proxy for SSR
COPY --from=builder /app/dist ./dist;     # Multi-stage Docker build
EXPOSE 4000;                              # Match SSR server port
npm ci --omit=dev;                        # Production dependencies only

❌ Don’t Do This:

try_files $uri $uri/ /index.html;         # ❌ Infinite loop if index.html missing
CMD ["ng", "serve"];                      # ❌ Development server in production
EXPOSE 4200;                              # ❌ Wrong port for SSR server
npm install;                              # ❌ Non-deterministic installs
COPY . .;                                 # ❌ Copies node_modules and dist

Common Pitfalls

PitfallWhy It HappensFix
404 on page refreshServer has no fallback routeConfigure try_files or error document
SSR server crashes on startupMissing engine-manifest.jsonInclude all server output files in deployment
Docker container exits immediatelyWrong entry point or script nameVerify CMD references existing script
Port conflictSSR server defaults to 4000, not 4200Match EXPOSE and proxy port to server port
Blank page after SSR deployBrowser assets not deployed alongside serverDeploy both browser/ and server/ folders
API calls fail from browserCORS not configured on API serverConfigure server CORS headers
Node version mismatchPlatform uses older Node.jsSpecify engines in package.json

Real-World Examples

1. Nginx Fallback for CSR

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

2. S3 Static Website Configuration

aws s3 website s3://my-bucket --index-document index.html --error-document index.html

3. Docker Compose with Nginx Proxy

services:
  app:
    build: .
    expose: ["4000"]
  nginx:
    image: nginx:alpine
    ports: ["80:80"]
    volumes: ["./nginx.conf:/etc/nginx/conf.d/default.conf"]

4. SSR Server Start Command

node dist/my-app/server/server.mjs

5. Firebase Deploy

ng add @angular/fire
firebase deploy

6. Scalingo Buildpacks

# .buildpacks
https://github.com/Scalingo/nodejs-buildpack
https://github.com/Scalingo/nginx-buildpack

7. Vercel API Entry Point

export default async (req, res) => {
  const { reqHandler } = await import('../dist/app/server/server.mjs');
  return reqHandler(req, res);
};

8. Prerender Configuration

// app.routes.server.ts
export const serverRoutes: ServerRoute[] = [
  { path: '', renderMode: RenderMode.Prerender },
  { path: '**', renderMode: RenderMode.Server },
];

9. Docker Multi-Stage Build

FROM node:20-alpine AS builder
RUN npm ci && npm run build

FROM node:20-alpine
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/app/server/server.mjs"]

10. Plesk Node.js Deployment

# Copy dist/ contents to document root
# Configure Node.js app in Plesk to run server.mjs

Visual

Static (CSR) Deployment Flow

┌──────────────────────────────────────────────────────────────┐
│  STATIC CSR DEPLOYMENT                                       │
│                                                              │
│  ng build --configuration=production                         │
│       │                                                      │
│       ▼                                                      │
│  dist/my-app/browser/                                        │
│  ├── index.html                                              │
│  ├── main.js                                                 │
│  ├── polyfills.js                                            │
│  └── styles.css                                              │
│       │                                                      │
│       ▼                                                      │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  WEB SERVER / CDN / OBJECT STORAGE                     │  │
│  │  Nginx, S3, Azure Static Web Apps, Firebase            │  │
│  └────────────────────────────────────────────────────────┘  │
│       │                                                      │
│       ▼                                                      │
│  Request /users/42                                           │
│       │                                                      │
│       ▼                                                      │
│  Server checks for file → not found                          │
│       │                                                      │
│       ▼                                                      │
│  Returns index.html (fallback)                               │
│       │                                                      │
│       ▼                                                      │
│  Angular router handles /users/42 client-side                │
└──────────────────────────────────────────────────────────────┘

SSR Deployment Flow

┌──────────────────────────────────────────────────────────────┐
│  SSR DEPLOYMENT                                              │
│                                                              │
│  ng build                                                    │
│       │                                                      │
│       ▼                                                      │
│  dist/my-app/                                                │
│  ├── browser/          ← static assets                       │
│  └── server/           ← Node.js rendering server            │
│      ├── server.mjs                                          │
│      └── engine-manifest.json                                │
│       │                                                      │
│       ▼                                                      │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  NODE.JS SERVER (port 4000)                            │  │
│  │  Express + Angular CommonEngine                        │  │
│  └────────────────────────────────────────────────────────┘  │
│       │                                                      │
│       ▼                                                      │
│  Request /users/42                                           │
│       │                                                      │
│       ▼                                                      │
│  Server renders Angular app for that route                   │
│       │                                                      │
│       ▼                                                      │
│  Returns fully rendered HTML                                 │
│       │                                                      │
│       ▼                                                      │
│  Browser hydrates → interactive                              │
└──────────────────────────────────────────────────────────────┘

Nginx Configuration Comparison

┌──────────────────────────────────────────────────────────────┐
│  STATIC CSR                    SSR                           │
│                                                              │
│  server {                      upstream angular {            │
│    listen 80;                    server app:4000;            │
│    root /var/www;              }                             │
│    index index.html;                                         │
│                                 server {                      │
│    location / {                  listen 80;                  │
│      try_files $uri $uri/        location / {                │
│        /index.html =404;           proxy_pass                │
│    }                                 http://angular;         │
│  }                               }                            │
│                                }                             │
│                                                              │
│  Serves files directly.        Proxies to Node.js server.    │
│  No compute cost.              Compute cost per request.     │
└──────────────────────────────────────────────────────────────┘

Docker Architecture for SSR

┌──────────────────────────────────────────────────────────────┐
│  DOCKER COMPOSE: NGINX + ANGULAR SSR                         │
│                                                              │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  NGINX CONTAINER                                       │  │
│  │  Port 80/443 exposed to host                           │  │
│  │  SSL termination, static caching                       │  │
│  └────────────────────────────────────────────────────────┘  │
│                          │                                   │
│                          │ proxy_pass                        │
│                          ▼                                   │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  ANGULAR SSR CONTAINER                                 │  │
│  │  Port 4000 (internal)                                  │  │
│  │  Node.js + server.mjs                                  │  │
│  └────────────────────────────────────────────────────────┘  │
│                                                              │
│  Dockerfile: node:20-alpine, npm ci, npm run build           │
│  CMD: node dist/app/server/server.mjs                        │
│  EXPOSE: 4000 (must match server port)                       │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
CSR build outputdist/<app>/browser/
SSR build outputbrowser/ + server/ folders
SSR server entryserver/server.mjs or similar
SSR default port4000
Static server requirementFallback to index.html for routing
SSR server requirementNode.js runtime, reverse proxy optional
Nginx CSR fallbacktry_files $uri $uri/ /index.html =404
Nginx SSR proxyproxy_pass http://app:4000
Docker base imagenode:20-alpine
Managed SSR platformsCloudways, Scalingo, Firebase App Hosting
Static platformsS3, Azure Static Web Apps, Firebase Hosting

Key takeaways:

  • Static and SSR deployments differ fundamentally. CSR produces files that any server can host; SSR produces a Node.js application that must run continuously.
  • The index.html fallback is mandatory for CSR. Without it, deep links and page refreshes return 404 because the server cannot find a file for the requested route .
  • SSR requires deploying both browser/ and server/ folders. The browser folder contains client assets for hydration; the server folder contains the rendering logic.
  • The SSR server listens on port 4000 by default. Docker EXPOSE and proxy configurations must match this port, not the development port 4200 .
  • Nginx acts as a reverse proxy for SSR. It handles SSL, caching, and static assets while forwarding rendering requests to the Node.js process .
  • Docker multi-stage builds keep production images small. Build in one stage, copy only the output and production dependencies to the final image.
  • Managed platforms simplify SSR deployment. Cloudways, Scalingo, and Firebase App Hosting handle Node.js process management, though each has specific configuration requirements .
  • Missing engine manifest files cause SSR failures. Angular 19 deployments must include engine-manifest.json from the server output .

Remember: Angular deployment is not a single process but a decision tree. Static CSR applications are simple to deploy: build, copy files, configure a fallback route, and serve. SSR applications add a Node.js runtime requirement, which means choosing a platform that supports persistent processes, configuring a reverse proxy or platform-specific routing, and ensuring that all server build outputs are deployed together. The Nginx try_files directive for CSR and the proxy_pass configuration for SSR are the two most important server configurations to understand. Docker provides a portable way to package SSR applications, and managed platforms abstract the infrastructure but introduce their own configuration requirements. The correct deployment strategy depends on whether your application needs server-side rendering, and once that decision is made, the deployment path becomes a series of well-understood configuration steps.


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!