Docker Deployment
Deploy with Docker and Docker Compose.
Overview
Docker deployment packages your Next.js app into a container for consistent deployment anywhere.
Prerequisites
- Docker 24+
- Production server (VPS, cloud instance)
Dockerfile
Create a Dockerfile in the project root. The shared base pins Node.js 22.23.4
for dependency installation, the build, and the running app. When updating Node.js,
select a patched image that satisfies package.json and rebuild the image:
FROM node:22.23.4-alpine3.24 AS base
# Install dependencies
FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --legacy-peer-deps
# Build the app
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Next.js reads these public settings during the build.
COPY .env.build .env.production
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
# Production image
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
CMD ["node", "server.js"]Add output: 'standalone' to next.config.mjs before building this image.
Create .dockerignore beside the Dockerfile so server secrets and local build
output stay outside the build context. Only .env.build, containing public
values, is included:
node_modules
.next
.git
tmp
.env*
!.env.buildDocker Compose
For easier deployment with environment variables, use Compose. The healthcheck
uses Node.js's built-in fetch, so it needs no additional tools in Alpine:
version: '3.8'
services:
web:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
env_file:
- .env.build
- .env.runtime
restart: unless-stopped
healthcheck:
test:
- CMD
- node
- -e
- >-
fetch('http://127.0.0.1:3000/api/health', { signal: AbortSignal.timeout(5000) })
.then((response) => process.exit(response.ok ? 0 : 1))
.catch(() => process.exit(1))
interval: 30s
timeout: 10s
retries: 3Deployment Steps
Save Public Build Settings
Use the values you've saved in Hosted Environment Variables.
Put every NEXT_PUBLIC_* entry in .env.build at the project root. The example
includes Stripe live billing; omit its Stripe entries for a Polar-only app.
Polar's selection, offer mappings and credentials are server-only and belong in
.env.runtime; follow hosted Polar setup
and the Polar environment reference.
Keep the shared app and Supabase public values below for either provider.
# App origin and Supabase Auth
NEXT_PUBLIC_APP_URL=https://yourdomain.com
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_...
# Stripe only: live settings shared by lifetime and subscription billing
NEXT_PUBLIC_STRIPE_MODE=live
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
# Lifetime tiers: Individual and Team
NEXT_PUBLIC_STRIPE_INDIVIDUAL_PRODUCT_ID=prod_replace_individual
NEXT_PUBLIC_STRIPE_INDIVIDUAL_PRICE_ID=price_replace_individual
NEXT_PUBLIC_STRIPE_TEAM_PRODUCT_ID=prod_replace_team
NEXT_PUBLIC_STRIPE_TEAM_PRICE_ID=price_replace_team
# Subscription tiers: Starter, Pro, and Business (monthly/yearly)
NEXT_PUBLIC_STRIPE_STARTER_PRODUCT_ID=prod_replace_starter
NEXT_PUBLIC_STRIPE_STARTER_PRICE_ID_MONTHLY=price_replace_starter_monthly
NEXT_PUBLIC_STRIPE_STARTER_PRICE_ID_YEARLY=price_replace_starter_yearly
NEXT_PUBLIC_STRIPE_PRO_PRODUCT_ID=prod_replace_pro
NEXT_PUBLIC_STRIPE_PRO_PRICE_ID_MONTHLY=price_replace_pro_monthly
NEXT_PUBLIC_STRIPE_PRO_PRICE_ID_YEARLY=price_replace_pro_yearly
NEXT_PUBLIC_STRIPE_BUSINESS_PRODUCT_ID=prod_replace_business
NEXT_PUBLIC_STRIPE_BUSINESS_PRICE_ID_MONTHLY=price_replace_business_monthly
NEXT_PUBLIC_STRIPE_BUSINESS_PRICE_ID_YEARLY=price_replace_business_yearlyFor Stripe live billing, configure only the offers and cadences you sell. Missing Price IDs leave those choices unavailable. Shared pricing is presentation only; the server validates supplied mappings when the corresponding offer is used.
For Stripe hosted test payments, use NEXT_PUBLIC_STRIPE_MODE=test, a pk_test_...
key, and your matching test catalog instead. The hosting target does not select
Stripe's mode. Next.js compiles public values into the bundle,
so rebuild the image whenever .env.build changes.
Save Server Runtime Settings
Put the remaining unprefixed settings from the hosted reference in .env.runtime.
Use credentials from the same Supabase project as .env.build. The Stripe
entries below apply only when using Stripe and must match its public mode.
For Polar, omit those entries and save the server-only Polar settings linked
above, using sandbox for the first hosted check. Keep retained Stripe settings
if Stripe still handles existing billing or outstanding deliveries.
# Supabase database and server access
SUPABASE_SECRET_KEY=sb_secret_...
DATABASE_URL=postgresql://postgres:[password]@db.xxx.supabase.co:5432/postgres
# Stripe only: server access and this deployment's webhook
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...For Stripe test payments, use sk_test_... and the test webhook's signing secret.
For Polar, configure this origin's
hosted webhook, not a local
listener's secret. Configure real application email for its
manual hosted checks.
Keep server secrets out of .env.build. The template ignores .env* files in
Git, and .dockerignore excludes .env.runtime from the image build. Compose
loads these files into the running container.
Restrict access to the runtime file:
chmod 600 .env.runtimeVerify Deployment
docker compose ps
curl http://localhost:3000/api/healthNginx Reverse Proxy
Put Nginx in front for SSL and caching:
server {
listen 80;
server_name yourdomain.com;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}Enable the site:
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginxSSL with Let's Encrypt
sudo certbot --nginx -d yourdomain.comUpdating the App
Pull latest code and rebuild:
git pull
docker compose up -d --buildFor zero-downtime updates, use rolling deployments or container orchestration.
Troubleshooting
Container won't start
Check logs:
docker compose logs webPort already in use
sudo lsof -i :3000Environment variables not loaded
Validate the Compose configuration without printing expanded secret values:
docker compose config --quietConfirm both environment files exist at the project root. If a public mode, key, or catalog ID changed, rebuild the image; restarting the container keeps the public values from its previous build.
Related
- VPS Deployment - Server setup guide
- Environment Variables - Production config
Was this page helpful?