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
Section titled “Create the Dockerfile”Create web/Dockerfile:
# ---- Build stage ----FROM node:22-alpine AS builderWORKDIR /appCOPY package.json package-lock.json ./RUN npm ciCOPY . .
# 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_URLENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
RUN npm run build
# ---- Runtime stage ----FROM node:22-alpine AS runnerWORKDIR /appENV NODE_ENV=productionENV PORT=3000ENV 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/staticCOPY --from=builder --chown=nextjs:nodejs /app/public ./public
USER nextjsEXPOSE 3000CMD ["node", "server.js"]Also create web/.dockerignore to keep secrets and build artifacts out of the image:
node_modules.next.env.env.*e2etest-resultsplaywright-reportcoverage.husky.git.vscode.idea*.log.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
Section titled “Build and run”# Build — pass one --build-arg per NEXT_PUBLIC_* variable.# These are baked into the browser bundle and must be set here, not at run time.docker build \ --build-arg NEXT_PUBLIC_API_BASE_URL="https://api.example.com" \ -t my-app:latest ./web
# Run — server-side secrets come in at run time via --env-file.docker run -d \ --name my-app \ -p 3000:3000 \ --env-file ./web/.env.production \ --restart unless-stopped \ my-app:latestTwo reasons for the split:
NEXT_PUBLIC_*values must exist duringnext build. Next.js bakes them into the JavaScript bundle at compile time. Passing them only atdocker runis 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-fileflag 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
Section titled “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:
services: web: build: context: ./web args: NEXT_PUBLIC_API_BASE_URL: "https://api.example.com" env_file: ./web/.env.production restart: unless-stopped caddy: image: caddy:2-alpine ports: - "80:80" - "443:443" volumes: - ./Caddyfile:/etc/caddy/Caddyfile - caddy_data:/data restart: unless-stoppedvolumes: caddy_data:Caddyfile:
app.your-domain.com { reverse_proxy web:3000}Verify
Section titled “Verify”docker ps # "my-app" should show as Updocker logs -f my-app # tail the app logsOpen your site in a browser. It should load over HTTPS without a port number.