Skip to content

Docker

We recommend Docker as the primary deployment method for Stadium 8 apps. It packages your application and its dependencies into a self-contained image that runs on any machine with Docker installed. No Node.js setup on the server is required. The build happens entirely inside the container.

For the three things that apply to every deployment path, see Before You Deploy.

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:

node_modules
.next
.env
.env.*
e2e
test-results
playwright-report
coverage
.husky
.git
.vscode
.idea
*.log

.env* files are excluded on purpose. Build-time NEXT_PUBLIC_* values come in via --build-arg. Server-side values come in via --env-file, which reads web/.env.local from the host when you run the container. Baking a .env file into an image layer exposes its contents to anyone who can pull the image.

Terminal window
# 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 values come in at run time via --env-file, which reads
# web/.env.local from the host. The port is published to loopback only, so your
# reverse proxy can reach it but the internet can't.
docker run -d \
--name my-app \
-p 127.0.0.1:3000:3000 \
--env-file ./web/.env.local \
--restart unless-stopped \
my-app:latest

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. Docker builds the image in isolation, so it can’t read your web/.env.local the way the Windows and Linux flows do. --env-file reads that same file from the host at run time instead, keeping secrets out of the image (image layers travel with the image to any registry it’s pushed to).

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.

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.local # server-side values only; safe to load at runtime
restart: unless-stopped
caddy:
image: caddy:2-alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
restart: unless-stopped
volumes:
caddy_data:

Caddyfile:

app.your-domain.com {
reverse_proxy web:3000
}
Terminal window
docker ps # "my-app" should show as Up
docker logs -f my-app # tail the app logs

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

If your project uses cookie-based authentication, read the cookie domains section before going to production.