Astro Dockerfile: Static Build Served by Unprivileged Nginx

This Astro Dockerfile builds your static site on node:24-alpine and serves the generated dist folder 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's ideal for blogs, docs and marketing sites built with Astro's default static output.

CI status
Build passing
Image size (uncompressed)
48.2 MB
Build time
11 s
Runtime image
nginxinc/nginx-unprivileged:1.27-alpine
Port
8080
Last built
2026-10-15
Dockerfile
# 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 /usr/share/nginx/html
EXPOSE 8080
.dockerignore
.git
node_modules/
dist/
.astro/
.env*
Dockerfile
.dockerignore

Before you build

How this Dockerfile works

  1. FROM node:24-alpine AS build

    Node is needed only to run the Astro build. The whole stage, including node_modules, is discarded afterwards.

  2. COPY package.json package-lock.json ./ RUN npm ci

    Installing from the lockfile before copying the source caches dependencies across content and component edits.

  3. RUN npm run build

    Astro renders every page to static HTML, CSS and JavaScript in dist/, which is all the web server needs.

  4. FROM nginxinc/nginx-unprivileged:1.27-alpine

    This official Nginx variant is preconfigured to run as a non-root user and to listen on port 8080, because unprivileged processes can't bind to port 80.

  5. COPY --from=build /app/dist /usr/share/nginx/html

    Places the built site in Nginx's default document root, so the stock configuration serves it without changes.

  6. EXPOSE 8080

    Documents the port the unprivileged image listens on. Publish it, or map it to 80 on the host, when running the container.

Build and run

terminal
docker build -t astro-app .
docker run --rm -p 8080:8080 astro-app

Open http://localhost:8080 to browse the built site. Because the output is plain files, the same image runs unchanged on any container host or Kubernetes cluster.

Other Astro Dockerfile variants

Static output served by Nginx is the simplest and most cache-friendly way to containerize Astro. Sites using server rendering need a Node adapter variant that runs Astro's server instead. They appear in the table below once verified.

VariantRuntime imageImage sizeCI
Static build served by unprivileged Nginxnginxinc/nginx-unprivileged:1.27-alpine48.2 MBPassing

Troubleshooting

Refreshing a nested URL returns 404 Not Found
Nginx looks for a matching file or index.html in a directory. Use Astro's directory build format, or add a custom Nginx config with a try_files rule for your routes.
bind() to 0.0.0.0:80 failed: Permission denied
A custom config tries to listen on port 80 as a non-root user. Keep listen 8080 in your server block and map host ports with docker run instead.
Changes to the site don't show up after rebuilding
Make sure .dockerignore excludes your local dist folder and that the build stage really ran npm run build. Use --no-cache once to rule out stale layers.

FAQ

Why nginx-unprivileged instead of the regular nginx image?
The regular image starts its master process as root. The unprivileged variant runs entirely as a normal user, which many Kubernetes security policies require.
Does this work with Astro server rendering?
No. This file serves static files only. Pages that render on request need the Node adapter and a Node runtime in the final image.
Can I add compression and caching headers?
Yes. Copy a custom server block into the image's conf.d directory with gzip enabled and long cache lifetimes for hashed files in the _astro folder.