Angular Dockerfile: Production Build Served by Nginx
This Angular Dockerfile builds your app on node:24-alpine and serves the compiled browser bundle from nginxinc/nginx-unprivileged:1.27-alpine. Nginx runs as a non-root user on port 8080, and no Node runtime ships in the final image. It suits client-side Angular apps without server-side rendering.
- CI status
- Build passing
- Image size (uncompressed)
- 48.5 MB
- Build time
- 18 s
- Runtime image
nginxinc/nginx-unprivileged:1.27-alpine- Port
- 8080
- Last built
- 2026-10-15
# syntax=docker/dockerfile:1
FROM node:24-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginxinc/nginx-unprivileged:1.27-alpine
COPY --from=build /app/dist/app/browser /usr/share/nginx/html
EXPOSE 8080.git
node_modules/
dist/
.angular/
.env*
Dockerfile
.dockerignoreBefore you build
- Name the project app so the build lands in dist/app/browser, or change the COPY path to dist/<your-project>/browser.
- Commit package-lock.json so npm ci can install exact versions.
- Use client-side rendering only, because SSR needs a Node server in the image.
- Add an Nginx config with a try_files fallback so deep links into the app don't return 404.
How this Dockerfile works
FROM node:24-alpine AS buildNode is only needed to run the Angular CLI build. The stage and its node_modules are discarded afterwards.
COPY package.json package-lock.json ./ RUN npm ciInstalls exact dependency versions from the lockfile. Copying only the manifests first keeps this slow step cached across code changes.
RUN npm run buildRuns ng build, which uses the production configuration by default and writes optimized, hashed bundles to the dist folder.
FROM nginxinc/nginx-unprivileged:1.27-alpineAn official Nginx variant preconfigured to run as a non-root user. Because of that, it listens on port 8080 rather than 80.
COPY --from=build /app/dist/app/browser /usr/share/nginx/htmlRecent Angular versions put the deployable files in a browser subfolder. Copying that folder makes it Nginx's document root.
EXPOSE 8080Documents the port the unprivileged image listens on. Map it to any host port when you run the container.
Build and run
docker build -t angular-app .
docker run --rm -p 8080:8080 angular-appBuild from the workspace root, then open http://localhost:8080. Environment-specific API URLs are compiled into the bundle, so select them with Angular build configurations.
Other Angular Dockerfile variants
Serving the static build from Nginx is the simplest and fastest way to host client-side Angular. Apps using server-side rendering need a Node runtime variant that starts the SSR server instead. Verified variants appear in the table below.
| Variant | Runtime image | Image size | CI |
|---|---|---|---|
| Static build served by unprivileged Nginx | nginxinc/nginx-unprivileged:1.27-alpine | 48.5 MB | Passing |
Troubleshooting
- COPY failed: /app/dist/app/browser: not found
- Your project has a different name. Check angular.json for the output path and change the COPY source to dist/<your-project>/browser.
- Refreshing a route like /settings returns 404 Not Found
- Nginx looks for a real file at that path. Add a server block with try_files $uri $uri/ /index.html so Angular's router handles the URL.
- Changes don't appear after deploying a new image
- Browsers may cache index.html. Serve index.html with a no-cache header and let the hashed bundle files be cached long-term.
FAQ
- Why not serve the app with ng serve?
- ng serve is a development server with live reload and unoptimized builds. Nginx serves the production bundle faster, with less memory and no Node runtime in the image.
- How do I set the API URL per environment?
- Use Angular build configurations for compile-time values, or load a small JSON config file at startup that you mount into the Nginx document root.
- Does this work with Angular SSR?
- No. Server-side rendering needs Node to run the server bundle. This file serves only the static browser output.