Docker
Docker is the recommended way to deploy a Stadium 8 app. It packages your application and everything it needs into an image that runs on any machine with Docker installed. The server doesn’t need Node.js, and the build happens inside Docker.
Read Before You Deploy first. It covers the environment variables, reverse proxy and cookie settings that apply to every deployment.
The Dockerfile that ships with your project
Section titled “The Dockerfile that ships with your project”Your project already has a Dockerfile and a .dockerignore at the top of the repository, so you don’t need to create them. Three things to know about them:
- You build from the project root, not from inside
web/. The build only copiesweb/into the image, so nothing else in your repository ships. .dockerignorekeeps secrets and clutter out of the image. It excludes every.env*file except.env.example, plusnode_modules, build output, tests, and the workflow folders (.claude/,documentation/,generated-docs/).- The image runs as a non-root user and contains only the standalone build, so it stays small.
Add your NEXT_PUBLIC_ variables
Section titled “Add your NEXT_PUBLIC_ variables”If your project uses any NEXT_PUBLIC_* variables, tell the Dockerfile about them. Open the root Dockerfile, find the commented block in the builder stage, directly above RUN npm run build, and uncomment one ARG and ENV pair per variable:
ARG NEXT_PUBLIC_API_BASE_URLENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URLAdd a pair for each further NEXT_PUBLIC_* variable in web/.env.example.
These values are built into the browser code during the build, so you supply them on the docker build command. Docker builds the image in isolation and can’t read your web/.env.local, which is why these values arrive as build arguments instead.
Build and run
Section titled “Build and run”Run these from the top of your project:
# Build: pass one --build-arg per NEXT_PUBLIC_* variable you uncommented.# The trailing "." is the project root, which is the build context.docker build \ --build-arg NEXT_PUBLIC_API_BASE_URL="https://api.example.com" \ -t my-app:latest .
# 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:latestThe values are split between build time and run time for two reasons:
NEXT_PUBLIC_*values must exist during the build. Next.js builds them into the JavaScript. Supplying them only atdocker runis too late, because the app is already built without them.- Server-side secrets must not live in the image. Image layers travel with the image to any registry it’s pushed to.
--env-filereads the file from the host at run time, so secrets stay outside the image.
If your platform has a managed secret store, such as AWS Secrets Manager, Azure Key Vault, Kubernetes Secrets or Docker Swarm secrets, use it for the run-time values instead of --env-file.
Reverse proxy
Section titled “Reverse proxy”Docker still needs a reverse proxy in front of it for HTTPS. There are two common set-ups.
An external proxy: run nginx or Caddy on the host and point it at http://127.0.0.1:3000, the container’s published port. You can use the nginx configuration from the Linux guide.
A proxy alongside the app in Docker Compose: add a Caddy service next to the app. Caddy fetches and renews a Let’s Encrypt certificate for you.
docker-compose.yml, at the top of your project:
services: web: build: context: . args: # One arg per NEXT_PUBLIC_* variable, as with docker build. NEXT_PUBLIC_API_BASE_URL: "https://api.example.com" env_file: ./web/.env.local # server-side values, loaded at run time 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.example.com { reverse_proxy web:3000}Verify
Section titled “Verify”docker ps # "my-app" should show as Updocker logs -f my-app # follow the app's logsOpen your site in a browser. It should load over HTTPS without a port number.
If your project uses cookie-based sign-in, read Cookie domains before you go to production.