Angular 111 🅰️ Deployment — Static and SSR
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 Mode | Browser Output | Server Output |
|---|---|---|
| CSR (static) | dist/<app>/browser/ | None |
| SSR | dist/<app>/browser/ | dist/<app>/server/ |
| Prerender (SSG) | dist/<app>/browser/ with HTML files | Optional server for fallback |
Static Hosting Options
| Platform | Configuration |
|---|---|
| Nginx | try_files $uri $uri/ /index.html |
| Apache | .htaccess rewrite to index.html |
| S3 | Error document = index.html |
| Azure Static Web Apps | Angular build preset; output dist/<app>/browser |
| Firebase Hosting | firebase deploy after ng add @angular/fire |
SSR Hosting Options
| Platform | Requirements |
|---|---|
| Self-managed VPS | Node.js runtime, Nginx reverse proxy |
| Docker | Node.js image, port 4000 exposed |
| Cloudways Velocity | Managed Node.js; output dir browser/ |
| Scalingo | Multi buildpack: Node.js + Nginx |
| Firebase App Hosting | Paid plan; Node.js runtime |
| Vercel | Custom api/index.js and vercel.json |
Common Server Configuration Files
| File | Purpose |
|---|---|
nginx.conf | Reverse proxy for SSR or static serving |
Dockerfile | Container build for SSR |
docker-compose.yml | Multi-container orchestration |
Procfile | Platform start command (Scalingo, Heroku) |
vercel.json | Vercel 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| 404 on page refresh | Server has no fallback route | Configure try_files or error document |
| SSR server crashes on startup | Missing engine-manifest.json | Include all server output files in deployment |
| Docker container exits immediately | Wrong entry point or script name | Verify CMD references existing script |
| Port conflict | SSR server defaults to 4000, not 4200 | Match EXPOSE and proxy port to server port |
| Blank page after SSR deploy | Browser assets not deployed alongside server | Deploy both browser/ and server/ folders |
| API calls fail from browser | CORS not configured on API server | Configure server CORS headers |
| Node version mismatch | Platform uses older Node.js | Specify 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
| Item | Value |
|---|---|
| CSR build output | dist/<app>/browser/ |
| SSR build output | browser/ + server/ folders |
| SSR server entry | server/server.mjs or similar |
| SSR default port | 4000 |
| Static server requirement | Fallback to index.html for routing |
| SSR server requirement | Node.js runtime, reverse proxy optional |
| Nginx CSR fallback | try_files $uri $uri/ /index.html =404 |
| Nginx SSR proxy | proxy_pass http://app:4000 |
| Docker base image | node:20-alpine |
| Managed SSR platforms | Cloudways, Scalingo, Firebase App Hosting |
| Static platforms | S3, 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.htmlfallback 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/andserver/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
EXPOSEand 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.jsonfrom 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.
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!