Docker Deployment Guide¶
Complete guide for building and deploying Next.js applications as Docker containers to the Kubernetes cluster.
Prerequisites:
- Next.js application created and tested locally
- Docker installed on host (Proxmox)
- Access to internal container registry (10.89.97.201:30500)
- Kubernetes cluster access
See Also: - Production App Deployment - Complete deployment workflow - Production Deployment - Kubernetes deployment patterns - App Conventions - Application structure standards
Next.js Configuration¶
Required for Docker deployment:
Update next.config.ts (or next.config.js) to enable standalone output:
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
output: 'standalone', // Required for Docker deployment
}
export default nextConfig
Why: Standalone mode creates a minimal production build with only required dependencies, significantly reducing Docker image size and startup time.
Dockerfile Structure¶
Standard multi-stage Dockerfile for Next.js + Supabase apps:
FROM node:20-alpine AS base
# Dependencies stage
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci
# Builder stage
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Build args for environment variables (CRITICAL for Supabase apps)
ARG NEXT_PUBLIC_SUPABASE_URL
ARG NEXT_PUBLIC_SUPABASE_ANON_KEY
ENV NEXT_PUBLIC_SUPABASE_URL=${NEXT_PUBLIC_SUPABASE_URL}
ENV NEXT_PUBLIC_SUPABASE_ANON_KEY=${NEXT_PUBLIC_SUPABASE_ANON_KEY}
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
# Runner stage
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 standalone build
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"
# IMPORTANT: Always use JSON array syntax with quotes
CMD ["node", "server.js"]
Critical Dockerfile Requirements¶
1. Build-Time Environment Variables¶
Problem: Next.js apps with Supabase need environment variables during the build step for pre-rendering pages (SSG/ISR).
Solution: Use ARG and ENV directives in the builder stage:
ARG NEXT_PUBLIC_SUPABASE_URL
ARG NEXT_PUBLIC_SUPABASE_ANON_KEY
ENV NEXT_PUBLIC_SUPABASE_URL=${NEXT_PUBLIC_SUPABASE_URL}
ENV NEXT_PUBLIC_SUPABASE_ANON_KEY=${NEXT_PUBLIC_SUPABASE_ANON_KEY}
Build command:
docker build \
--build-arg NEXT_PUBLIC_SUPABASE_URL=http://10.89.97.214:8000 \
--build-arg NEXT_PUBLIC_SUPABASE_ANON_KEY="your-anon-key" \
-t app-name:v1.0.0 .
2. CMD Syntax - JSON Array Format¶
CRITICAL: Always use JSON array syntax with proper quotes:
# ✅ Correct
CMD ["node", "server.js"]
# ❌ Wrong - will fail with "/bin/sh: [node,: not found"
CMD [node, server.js]
Why: Without quotes, Docker interprets the array as shell syntax, causing parsing errors.
3. .dockerignore¶
Required to prevent permission errors and reduce image size:
node_modules
.next
.git
.gitignore
.github
.env
.env*.local
supabase/.branches
supabase/.temp
.supabase
README.md
Multi-Schema Supabase Configuration¶
CRITICAL for production deployments:
When deploying to k8s with the shared Supabase instance, you must configure clients to use the correct schema:
// lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
db: { schema: "your_app_schema" } // ← CRITICAL for multi-schema setups!
}
)
}
// lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export async function createClient() {
const cookieStore = await cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
db: { schema: "your_app_schema" }, // ← CRITICAL for multi-schema setups!
cookies: {
get(name: string) {
return cookieStore.get(name)?.value
},
},
}
)
}
Schema names:
- home_portal - Home Portal
- money_tracker - Money Tracker
- rms - Recipe Management System
What happens without this: Client defaults to the first schema in PostgREST config (usually home_portal), causing 404 errors when querying tables.
See Supabase Multi-App Architecture for complete details.
Docker Build and Push¶
Standard workflow:
# Build with proper args
docker build \
--build-arg NEXT_PUBLIC_SUPABASE_URL=http://10.89.97.214:8000 \
--build-arg NEXT_PUBLIC_SUPABASE_ANON_KEY="$(kubectl get secret -n supabase supabase-secrets -o jsonpath='{.data.ANON_KEY}' | base64 -d)" \
-t app-name:v1.0.0 .
# Tag for registry
docker tag app-name:v1.0.0 10.89.97.201:30500/app-name:v1.0.0
docker tag app-name:v1.0.0 10.89.97.201:30500/app-name:latest
# Push to registry
docker push 10.89.97.201:30500/app-name:v1.0.0
docker push 10.89.97.201:30500/app-name:latest
Insecure Registry Configuration¶
Required for internal registry access:
The k8s container registry (10.89.97.201:30500) runs without TLS. Docker must be configured to allow insecure registries.
Host Docker configuration (/etc/docker/daemon.json):
Apply changes:
Common Docker Issues¶
BuildKit / Buildx Issues¶
Issue: npm ci fails with "spawn sh EACCES" during Docker build
npm error Error: spawn sh EACCES
npm error at ChildProcess._handle.onexit (node:internal/child_process:285:19)
npm error errno: -13,
npm error code: 'EACCES',
npm error syscall: 'spawn sh',
Root Cause: Corrupted or misconfigured Docker buildx builder. This can happen after system upgrades (apt dist-upgrade) that update Docker, containerd, or buildx packages.
Diagnosis:
# Check buildx builder status
docker buildx ls
# If you see "v0.0.0+unknown" for the default builder, it's corrupted:
# NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
# default* docker
# \_ default \_ default running v0.0.0+unknown linux/amd64...
Solution: Create a fresh buildx builder:
# Create new builder with proper BuildKit
docker buildx create --name fresh-builder --use
# Bootstrap it (downloads BuildKit image)
docker buildx inspect --bootstrap
# Verify it shows a proper version (e.g., v0.26.2)
docker buildx ls
# NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
# fresh-builder* docker-container
# \_ fresh-builder0 \_ unix:///var/run/docker.sock running v0.26.2 linux/amd64...
Permanent Fix for Deploy Scripts:
Update deploy scripts to explicitly use the working builder:
# Instead of:
docker build -t app:v1.0.0 .
# Use:
docker buildx build --builder fresh-builder --load -t app:v1.0.0 .
Why this happens:
- System upgrades can leave the default Docker builder in a broken state
- The default docker driver doesn't properly initialize BuildKit after upgrades
- Using docker-container driver (via docker buildx create) runs BuildKit in a container, bypassing host issues
Prevention:
- After major system upgrades, verify docker buildx ls shows valid BuildKit versions
- Consider using the docker-container driver by default for more isolation
Issue: Build fails with missing Supabase URL/key
Solution: Pass build args (see Build-Time Environment Variables above)Issue: Container starts then immediately crashes
Solution: Fix CMD syntax to use JSON array with quotes:CMD ["node", "server.js"]
Issue: Permission denied errors during build
Solution: Addnode_modules, .next, and .git to .dockerignore
Issue: Docker push fails - HTTPS to HTTP error
Solution: Configure insecure registry in Docker daemon.json (see above)See Troubleshooting for more issues and solutions.
Automated Deployment Scripts¶
For complete deployment automation including build, push, and Kubernetes deployment, see:
- Production App Deployment - Complete workflow
/root/tower-fleet/scripts/deploy-app.sh- Automated deployment script/root/tower-fleet/scripts/rebuild-app.sh- Rebuild and redeploy script
See Also¶
- Production App Deployment - Kubernetes deployment workflow
- Ingress Configuration - Exposing apps via Ingress
- Supabase Multi-App Architecture - Schema isolation
- Troubleshooting - Common issues and solutions