05 — Engineering notes
Shipping a static site as a container
A two-stage build with Node for the site and Caddy for the runtime, plus the deployment details that make it worth doing.
A static site does not need a container to exist. It needs a container when you want the deployment to stop depending on what happens to be installed on the host.
The pattern I use for this site is two stages: build with Node, serve with Caddy.
FROM node:24-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM caddy:2-alpine
COPY --from=build /app/dist /usr/share/caddy
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget -q --spider http://127.0.0.1/ || exit 1
Why two stages
The build needs Node, a package manager, and the full source tree. The runtime needs none of that — it needs an HTTP server and the built files.
Splitting the stages means:
- The final image has no Node runtime and no
node_modules. - The build tools never ship to the server.
- Rebuilding is reproducible from the lockfile.
The result is a small image that behaves the same on my machine, on a VPS, and in a CI runner.
Why Caddy
Caddy gives me a static file server with sane defaults and automatic TLS when it is exposed directly. In front of a reverse proxy it is still useful: small image, simple configuration, and no extra runtime.
For a single-page static site, the alternative is putting Nginx in the image and writing a config file. That also works. Caddy just needs less configuration to do the same job, and I already use it as the proxy layer.
Running it
podman build -t portfolio .
podman run -d --name portfolio \
-p 8080:80 \
--restart unless-stopped \
portfolio
Two details matter more than the command:
- Pin the port mapping at the proxy layer. The container serves HTTP on 80; TLS and hostnames belong to whatever sits in front.
- Set a restart policy. A container that does not come back after a host reboot is not deployed.
A healthcheck is worth the line it costs. It turns “the process is running” into “the site answers”, which is the question you actually care about.
What this does not solve
Containerising a static site does not give you zero-downtime deploys, rollbacks, or monitoring. Those are separate decisions:
- Rollbacks need the previous image to still exist.
- Zero downtime needs two instances behind a proxy, or a brief restart window you accept.
- Monitoring needs something outside the container to check it.
The container makes the artifact consistent. Everything else is still operations.