816 words
4 minutes

Dockerize an Astro Static Site: Build dist, Serve It with Caddy

2026-08-01
DevOps
Astro
/
Docker
/
DevOps
/
Deployment

An Astro static site does not need Node.js at runtime. Its production artifact is the generated dist/ directory, so a container can build the site with Node in one stage and serve only that output in the final stage. That boundary keeps build dependencies out of the web-server image and makes a failed deployment easier to diagnose.

This guide targets the primary question behind “Dockerize an Astro static site”: how to build dist reproducibly, serve it from a small web-server image, and verify that the container is returning the generated site.

Use a two-stage Dockerfile#

Astro’s deployment guide identifies astro build (or the package-script equivalent) as the build command and dist as the publish directory. Docker multi-stage builds let the final image copy just that artifact instead of carrying the package manager, source files, and node_modules into production.

For a pnpm project, add this Dockerfile at the repository root:

FROM node:22-slim AS build
WORKDIR /app
# Copy dependency metadata first so this layer can be cached.
COPY package.json pnpm-lock.yaml ./
RUN npm install --global [email protected]
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
FROM caddy:2-alpine
COPY --from=build /app/dist /usr/share/caddy
EXPOSE 80

The order is deliberate. A content or component change does not invalidate the dependency-install layer as long as package.json and pnpm-lock.yaml remain unchanged. The final caddy stage receives only /app/dist; it has no project source or Node package tree to execute.

Use the Node and pnpm versions your repository supports rather than copying these numbers blindly. In this site, the package manifest currently declares pnpm 9.14.4 and Astro 5.12.8, so the build-stage command should match that lockfile contract.

Keep the Docker build context small#

A .dockerignore controls what Docker sends to the builder. At minimum, exclude local dependencies and output that the container will regenerate:

node_modules
.astro
dist
.git

Add project-specific generated folders as needed. Do not add a blanket .env* rule without first checking whether your static build requires non-secret, build-time public values. Secrets should not be baked into a static site image: values exposed to client-side JavaScript become public in the generated output.

Docker’s documentation recommends multi-stage builds to separate build-time and runtime dependencies, and .dockerignore keeps unnecessary local files out of the build context. Those are separate safeguards: the first keeps the final image focused, while the second reduces what is available during the build.

Build and verify the container locally#

From the repository root, build the final stage and map its HTTP port:

Terminal window
docker build -t astro-static-site .
docker run --rm --publish 8080:80 astro-static-site

In another terminal, confirm that the web server is returning the generated HTML:

Terminal window
curl --head http://localhost:8080/

Then open http://localhost:8080/ and test one non-homepage route. If the homepage works but a page with a trailing slash returns 404, inspect the corresponding path in dist/ before changing the web-server configuration. With Astro’s usual static output, a route such as /about/ should result in a matching generated directory and index.html.

For a production check, run the same build command outside Docker first. This is especially useful for content collections, image transforms, and search-index generation because it tells you whether the failure belongs to Astro or to the container build.

Terminal window
pnpm build
test -f dist/index.html

If your site has language-specific output, test one of those paths too. Build an Astro Multilingual Blog Without Migrating Existing URLs includes concrete output checks for /en/ routes.

Do not use this pattern for on-demand routes#

This Dockerfile is for a static Astro deployment. It is not a substitute for an adapter when the site uses on-demand rendering, server endpoints, sessions, or other server-side behavior. Astro’s deployment documentation calls for the appropriate adapter in that case; the runtime image must start the adapter’s server instead of serving a fixed dist/ directory.

That distinction also narrows debugging:

SymptomCheck first
pnpm build failsDependencies, content schema, or Astro configuration
Docker build fails at pnpm installLockfile, package-manager version, or network access to the registry
Container starts but returns 404The copied dist/ path and the server’s document root
A dynamic endpoint is missingWhether the project needs an Astro adapter rather than static serving

The practical rule is simple: build the static artifact once, copy that artifact into the final image, and verify the generated paths before treating the issue as a hosting problem.

FAQ#

Q: Do I need Node.js in the final Docker image for a static Astro site?#

A: No. A static Astro build produces files in dist/, which a web server can serve directly. Keep Node.js in the build stage unless your project uses on-demand rendering through an adapter.

Q: Why copy package files before the rest of the Astro project?#

A: Docker caches layers. Copying package.json and the lockfile before application files allows the dependency-install layer to be reused when only content or source files change.

Q: Can I use Nginx instead of Caddy?#

A: Yes. The relevant boundary is the same: copy the completed dist/ directory into a static web-server image and ensure that the image’s document root matches the copied location. Use the server image and configuration your platform supports.

References:

Astro: Deploy your Astro site

Docker: Multi-stage builds

Docker: Building best practices

Caddy Docker image

Dockerize an Astro Static Site: Build dist, Serve It with Caddy
https://laplusda.com/en/posts/dockerize-astro-static-site-caddy/
Author
Zero
Published at
2026-08-01
License
CC BY-NC-SA 4.0
Was this article useful?

Report a typo or broken link, or suggest a related topic.