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
# 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.git
node_modules/
dist/
.astro/
.env*
Dockerfile
.dockerignoreBefore you build
- Keep Astro's default static output so the build writes HTML to dist/.
- Commit package-lock.json so npm ci can install dependencies.
- Add a custom Nginx config if you need SPA fallbacks, redirects or long-lived cache headers.
How this Dockerfile works
FROM node:24-alpine AS buildNode is needed only to run the Astro build. The whole stage, including node_modules, is discarded afterwards.
COPY package.json package-lock.json ./ RUN npm ciInstalling from the lockfile before copying the source caches dependencies across content and component edits.
RUN npm run buildAstro renders every page to static HTML, CSS and JavaScript in dist/, which is all the web server needs.
FROM nginxinc/nginx-unprivileged:1.27-alpineThis 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.
COPY --from=build /app/dist /usr/share/nginx/htmlPlaces the built site in Nginx's default document root, so the stock configuration serves it without changes.
EXPOSE 8080Documents the port the unprivileged image listens on. Publish it, or map it to 80 on the host, when running the container.
Build and run
docker build -t astro-app .
docker run --rm -p 8080:8080 astro-appOpen 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.
| Variant | Runtime image | Image size | CI |
|---|---|---|---|
| Static build served by unprivileged Nginx | nginxinc/nginx-unprivileged:1.27-alpine | 48.2 MB | Passing |
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.