Docker

Docker packages your app and its dependencies into an image that can run on any machine with Docker installed. You don't need Node installed on the server.

Before starting, read Before You Deploy. You don't need to complete the standalone output setup separately — the Dockerfile handles it.

Create the Dockerfile

Create web/Dockerfile:

# ---- Build stage ----
FROM node:22-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .

# Build-time public values. Anything starting with NEXT_PUBLIC_ is baked into
# the browser bundle, so it must be present during `next build`. Add one
# ARG + ENV pair per variable your project uses.
ARG NEXT_PUBLIC_API_BASE_URL
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL

RUN npm run build

# ---- Runtime stage ----
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
ENV HOSTNAME=0.0.0.0

RUN addgroup -S nodejs && adduser -S nextjs -G nodejs

COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/public ./public

USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]

Also create web/.dockerignore to keep secrets and build artifacts out of the image:

.env* files are excluded on purpose. Build-time NEXT_PUBLIC_* values come in via --build-arg. Runtime secrets come in via --env-file when you run the container. Baking a .env file into an image layer exposes its contents to anyone who can pull the image.

Build and run

Two reasons for the split:

  • NEXT_PUBLIC_* values must exist during next build. Next.js bakes them into the JavaScript bundle at compile time. Passing them only at docker run is too late.

  • Server-side secrets must not live in the image. Image layers travel with the image to any registry it's pushed to. The --env-file flag reads from the host at run time, keeping secrets out of the image entirely.

If your platform has a managed secret store (AWS Secrets Manager, Azure Key Vault, Kubernetes Secrets, etc.), prefer that over --env-file for runtime values.

Reverse proxy

Docker still needs a reverse proxy in front of it for TLS. Two options:

External proxy: Run nginx on the host and point it at http://127.0.0.1:3000 (the container's exposed port). Use the same nginx config from the Linux guide.

Sidecar in Docker Compose: Add a Caddy service alongside the app. Caddy fetches and renews a Let's Encrypt certificate automatically.

docker-compose.yml:

Caddyfile:

Verify

Open your site in a browser. It should load over HTTPS without a port number.

Last updated

Was this helpful?