In this article, we will learn how to containerize an Angular app for production. Our goal is to use the same build for all environments: test, staging, and production, and make sure it is easy to configure, secure, and fast.
Requirements
Single Build for All Environments: We want to build our Angular app once and use that same build for every environment. Normally, Angular encourages you to create different environment files like
environment.ts
,environment.staging.ts
, andenvironment.prod.ts
, which requires creating different builds for different environments. We want to avoid that and use system environment variables instead.Easy Reverse-Proxy Configuration: Our backend APIs do not support CORS. We prefer to keep everything on a single origin.
Secure and Good Performance: We want to use a web server that is easy to configure, secure, and offers good performance. Using the development server (
ng serve
) is not recommended for production due to security and performance issues.Redirects: For deep links in our Angular app, we want to redirect to the Angular app's
index.html
file.
Using Caddy
Caddy is a web server that is easy to configure and offers good performance. It allows us to set up reverse proxies easily with a Caddyfile
and environment variables.
Most guides out there use Nginx as the server component, but I found it very tedious to create a configuration that lets me configure reverse proxy URLs through environment variables.
Example Caddy Configuration
To configure Caddy for our Angular app, we create a Caddyfile
that specifies how to serve our app and set up reverse proxies:
# Caddyfile
{
# Global options can be specified here if needed
# (currently none are needed)
}
# Angular app server configuration block:
:80 {
root * ./www
file_server
handle_path /api/* {
reverse_proxy {$API_URL} {
header_up Host {upstream_hostport}
}
}
handle_path /telemetry/* {
reverse_proxy {$TELEMETRY_COLLECTOR_URL} {
header_up Host {upstream_hostport}
header_up X-API-KEY {$TELEMETRY_API_KEY}
}
}
# Rewrite for deep links if the file does not exist.
# Without this rule, deep links to Angular routes will return
# a 404 error (unless you use Angular's HashLocationStrategy).
@notFound {
not path /api/*
not path /telemetry/*
not file {path}
}
rewrite @notFound /index.html
}
This Caddy configuration file does the following:
Serve the Angular App: The
root * ./www
directive tells Caddy to serve files from the./www
directory. This is where we will copy the Angular build output.Reverse Proxy Configuration: The
handle_path
directives set up reverse proxies for the API and telemetry endpoints. We use placeholders like{$API_URL}
and{$TELEMETRY_COLLECTOR_URL}
to specify the URLs. We can set these placeholders using environment variables.Rewrite for Deep Links: The
rewrite
directive handles deep links in the Angular app. If the requested file does not exist, Caddy will rewrite the request toindex.html
. This is necessary for Angular's HTML5 routing mode.
Example Dockerfile
Next, we need to create a Dockerfile that will build our Angular app and configure Caddy to serve it.
In this example, we have an Angular 18 app set up.
Important: The output path of the Angular build depends on your Angular version and the project name! In my case, it is dist/my-containerized-angular-app-example/browser
. This is important because we need to copy the Angular build output to the correct location in the final image. Be sure to check where your Angular build outputs the files (by running ng build
manually) and adjust the Dockerfile accordingly.
Here is an example Dockerfile:
# Dockerfile
# Stage 1: Build the Angular app
FROM node:20-alpine as build-angular
# Set the working directory for the build
WORKDIR /build
# Copy package.json and package-lock.json to leverage Docker cache for npm install
COPY package*.json ./
# Install the dependencies
RUN npm install
# Copy the source code for the Angular app to the build directory
COPY . .
# Install the dependencies and build the Angular app
RUN npm install
RUN npm run build
# Stage 2: Create the final image
FROM caddy:latest
# Set the working directory for the final container
WORKDIR /app
# Copy the Angular build output to the final location
COPY --from=build-angular /build/dist/my-containerized-angular-app-example/browser ./www
# Copy the Caddyfile to the final location
COPY Caddyfile /etc/caddy/Caddyfile
# Expose the port for Caddy
EXPOSE 80
# Command to start Caddy
CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile"]
The Dockerfile has two stages:
Build the Angular app: In this stage, we use the
node:20-alpine
image to build the Angular app. We copy the source code to the build directory, install the dependencies, and build the Angular app usingnpm install
andnpm run build
.Create the final image: In this stage, we use the
caddy:latest
image as the base image. We set the working directory for the final container to/app
. We copy the Angular build output from the previous stage to the final location in the container (./www
). We also copy theCaddyfile
to the final location in the container (/etc/caddy/Caddyfile
). We expose port 80 for Caddy, and we specify the command to start Caddy.
Example Docker Ignore File
To keep our Docker image clean and small, we create a .dockerignore
file to exclude unnecessary files from the build context:
# .dockerignore
# Ignore node_modules
node_modules
# Ignore build outputs
dist
build
# Ignore environment files (secrets)
*.env
.env.*
# Ignore local Angular cache
.angular/cache
Example Docker Compose File
We can use Docker Compose to manage our Docker containers. Here is an example docker-compose.yml
file that defines our Angular app service:
services:
my-containerized-angular-app-example:
# Build the Docker image using the Dockerfile in the current directory
build:
context: .
# Map port 80 in the container to port 3000 on the host machine
ports:
- "3000:80"
# Load environment variables from the .env file
env_file:
- .env
Example Environment File
Finally, we need to create an .env
file to store our environment variables. Here is an example .env
file:
# .env
API_URL=https://api.example.com
TELEMETRY_COLLECTOR_URL=https://telemetry.example.com
TELEMETRY_API_KEY=your-telemetry-api-key
Building and Running the Docker Container
To build and run the Docker container, we can simply run:
docker compose up
Then our app will be accessible at http://localhost:3000
.
If we want to deploy the container to our staging or production environment, we simply change the API_URL
and TELEMETRY_COLLECTOR_URL
environment variables in the .env
file. No rebuild is necessary!
Working Example
For a complete working example, you can visit the GitHub repository: github.com/rensjaspers/containerized-angular-app-example. This repository includes all the files and configurations discussed in this article.
I hope this article helps you containerize your Angular app for production. If you have any questions or feedback, feel free to leave a comment below. Happy coding!
Top comments (2)
Hi Rens Jaspers,
Top, very nice and helpful !
Thanks for sharing.
Thanks, João!