How to deploy Angular with Docker
Deploying Angular applications to production requires a consistent and reproducible environment to avoid the “it works on my machine” syndrome. As the creator of CoreUI, with over 25 years of experience in software development, I’ve containerized hundreds of enterprise-grade Angular projects to ensure seamless scaling. The most efficient and modern solution is using a multi-stage Docker build, which separates the build environment from the production runtime for maximum security and minimal image size. This approach leverages Node.js for compilation and Nginx for serving static assets, providing a high-performance production setup.
Use a multi-stage Dockerfile to build the Angular application in a temporary container and then serve the resulting static files using a lightweight Nginx image.
Section 1: The Multi-Stage Dockerfile
The multi-stage build is the industry standard for frontend applications because it keeps your production image small by excluding development dependencies like Node.js and the node_modules folder.
# Stage 1: Build the Angular application
FROM node:20-alpine AS build-stage
# Set the working directory
WORKDIR /app
# Copy package files and install dependencies
COPY package*.json ./
RUN npm install
# Copy the rest of the application code
COPY . .
# Build the project for production
RUN npm run build -- --configuration=production
# Stage 2: Serve the application with Nginx
FROM nginx:stable-alpine
# Remove default nginx static assets
RUN rm -rf /usr/share/nginx/html/*
# Copy the build output from the first stage
# Angular 17+: output is in dist/my-angular-app/browser
# Angular 16 and earlier: output is in dist/my-angular-app
COPY --from=build-stage /app/dist/my-angular-app/browser /usr/share/nginx/html
# Copy custom nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Expose port 80
EXPOSE 80
# Start Nginx
CMD ["nginx", "-g", "daemon off;"]
This Dockerfile uses node:20-alpine for the build process and nginx:stable-alpine for the final image. This ensures that the final container only contains the compiled files and the web server, significantly reducing the attack surface and download time.
Section 2: Custom Nginx Configuration for Angular
Angular is a Single Page Application (SPA), which means the web server must be configured to redirect all navigation requests back to index.html. This allows the Angular Router to handle the URL correctly.
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Gzip compression for better performance
gzip on;
gzip_http_version 1.1;
gzip_disable 'msie6';
gzip_vary on;
gzip_proxied any;
gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;
location / {
try_files $uri $uri/ /index.html;
}
# Cache control for static assets
location ~* \.(?:ico|css|js|gif|jpe?g|png)$ {
expires 30d;
add_header Pragma public;
add_header Cache-Control 'public';
}
# Error pages
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
This configuration ensures that users can refresh the page or share deep links without getting a 404 error from Nginx. We also include Gzip compression to speed up the delivery of your assets.
Section 3: Optimizing with .dockerignore
A .dockerignore file is crucial to prevent unnecessary files from being sent to the Docker daemon. This speeds up the build process and keeps your image clean.
# Ignore local node_modules
node_modules
# Ignore build outputs
dist
.angular
# Ignore git metadata
.git
.gitignore
# Ignore environment specific files
.env
.editorconfig
README.md
# Ignore Docker related files
Dockerfile
docker-compose.yml
# Ignore OS files
.DS_Store
Thumbs.db
By ignoring node_modules, we ensure that npm install runs inside the container, matching the container’s architecture rather than your local machine’s, which is a common source of build failures.
Section 4: Building the Docker Image
Once your Dockerfile and configuration are ready, you can build your image using the docker build command. Tagging your image helps in versioning and organization.
# Build the image with a specific tag
docker build -t my-angular-app:v1 .
# List your images to verify
docker images
# Optional: Build for multiple architectures
# docker buildx build --platform linux/amd64,linux/arm64 -t my-angular-app:v1 .
The build process will execute each step defined in the Dockerfile. Because we used a multi-stage build, you will notice that the final image size is much smaller than the sum of the project files and dependencies.
Section 5: Running the Container
After building the image, you can run it as a container. Mapping the host port to the container port allows you to access the application from your browser.
# Run the container in detached mode
docker run -d -p 8080:80 --name angular-prod-container my-angular-app:v1
# Check the running containers
docker ps
# Access logs if needed
# docker logs angular-prod-container
# Stop the container
# docker stop angular-prod-container
You can now navigate to http://localhost:8080 to see your Angular application running inside a Docker container. This setup is identical to what will be deployed in a production cluster.
Section 6: Local Development with Docker Compose
While the Dockerfile is for production, using Docker Compose can simplify local testing and integration with other services like databases or APIs.
version: '3.8'
services:
angular-app:
build:
context: .
dockerfile: Dockerfile
ports:
- '8080:80'
restart: always
environment:
- NODE_ENV=production
networks:
- app-network
networks:
app-network:
driver: bridge
Using Docker Compose allows you to spin up the entire environment with a single command: docker-compose up -d. This is especially useful for testing the full production build locally before pushing to a registry.
Section 7: Handling Environment Variables
Angular applications are compiled into static assets — they cannot read process.env at runtime like a Node.js server can. There are two common approaches: build-time replacement via environment.ts files, or a runtime config.json fetched at startup.
Build-time (simple, one image per environment):
// src/environments/environment.prod.ts
export const environment = {
production: true,
apiUrl: 'https://api.coreui.io/v1'
}
Pass the configuration into the Angular CLI build: npm run build -- --configuration=production. The correct environment.ts file is swapped in at compile time.
Runtime config.json (one image, any environment):
// app-config.service.ts — fetch before app bootstrap
export class AppConfigService {
private config: Record<string, string> = {}
load(): Promise<void> {
return fetch('/assets/config.json')
.then(r => r.json())
.then(data => { this.config = data })
}
get(key: string): string {
return this.config[key]
}
}
Serve a different config.json from Nginx per environment (or mount it via a Kubernetes ConfigMap) without rebuilding the image. This is the preferred approach for containerized deployments where you want a single image promoted through staging to production.
Best Practice Note:
This Docker configuration is exactly what we recommend for deploying our Angular Dashboard Template.
For high-traffic applications, consider adding a health check to your Dockerfile to ensure that your orchestration tool (like Kubernetes) can detect if the Nginx server stops responding.
Additionally, always use a specific version for your base images (e.g., node:20 instead of node:latest) to ensure build reproducibility across different environments. To take this further with orchestration, see how to deploy Angular on Kubernetes.



