Next.js Deployment
Deploy Next.js applications to production with Docker, CI/CD pipelines, and comprehensive monitoring.
Overview
This skill provides patterns and code examples for deploying Next.js applications to production environments. It covers containerization with Docker, CI/CD automation with GitHub Actions, environment configuration, health checks, and production monitoring. Use standalone output mode for container deployments, multi-stage Docker builds for optimized images, and OpenTelemetry for observability.
When to Use
Activate when user requests involve:
-
"Deploy Next.js", "Dockerize Next.js", "containerize"
-
"GitHub Actions", "CI/CD pipeline", "automated deployment"
-
"Environment variables", "runtime config", "NEXT_PUBLIC"
-
"Preview deployment", "staging environment"
-
"Monitoring", "OpenTelemetry", "tracing", "logging"
-
"Health checks", "readiness", "liveness"
-
"Production build", "standalone output"
-
"Server Actions encryption key", "NEXT_SERVER_ACTIONS_ENCRYPTION_KEY"
Quick Reference
Output Modes
Mode Use Case Command
standalone
Docker/container deployment output: 'standalone'
export
Static site (no server) output: 'export'
(default) Node.js server deployment next start
Environment Variable Types
Prefix Availability Use Case
NEXT_PUBLIC_
Build-time + Browser Public API keys, feature flags
(no prefix) Server-only Database URLs, secrets
Runtime Server-only Different values per environment
Key Files
File Purpose
Dockerfile
Multi-stage container build
.github/workflows/deploy.yml
CI/CD pipeline
next.config.ts
Build configuration
instrumentation.ts
OpenTelemetry setup
src/app/api/health/route.ts
Health check endpoint
Instructions
- Configure Standalone Output
// next.config.ts import type { NextConfig } from 'next'
const nextConfig: NextConfig = { output: 'standalone', poweredByHeader: false, generateBuildId: async () => process.env.GIT_HASH || 'build', }
export default nextConfig
- Create Dockerfile
See references/docker-patterns.md for complete multi-stage builds, multi-arch support, and optimization.
syntax=docker/dockerfile:1
FROM node:20-alpine AS base
FROM base AS deps RUN apk add --no-cache libc6-compat WORKDIR /app COPY package.json package-lock.json* ./ RUN npm ci
FROM base AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . ENV NEXT_TELEMETRY_DISABLED=1 NODE_ENV=production ARG GIT_HASH NEXT_SERVER_ACTIONS_ENCRYPTION_KEY ENV GIT_HASH=${GIT_HASH} NEXT_SERVER_ACTIONS_ENCRYPTION_KEY=${NEXT_SERVER_ACTIONS_ENCRYPTION_KEY} RUN npm run build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 PORT=3000 HOSTNAME="0.0.0.0"
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
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
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3
CMD node -e "require('http').get('http://localhost:3000/api/health', (r) => r.statusCode === 200 ? process.exit(0) : process.exit(1))"
CMD ["node", "server.js"]
- Set Up GitHub Actions
See references/github-actions.md for complete workflows with testing, security scanning, and deployment strategies.
.github/workflows/deploy.yml
name: Build and Deploy on: push: branches: [main, develop]
env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }}
jobs: build: runs-on: ubuntu-latest permissions: contents: read packages: write steps: - uses: actions/checkout@v4 - uses: docker/setup-buildx-action@v3 - uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - id: generate-key run: echo "key=$(openssl rand -base64 32)" >> $GITHUB_OUTPUT - uses: docker/build-push-action@v5 with: context: . push: true tags: ${{ steps.meta.outputs.tags }} cache-from: type=gha cache-to: type=gha,mode=max build-args: | GIT_HASH=${{ github.sha }} NEXT_SERVER_ACTIONS_ENCRYPTION_KEY=${{ steps.generate-key.outputs.key }}
- Configure Environment Variables
// src/lib/env.ts export function getEnv() { return { databaseUrl: process.env.DATABASE_URL!, apiKey: process.env.API_KEY!, publicApiUrl: process.env.NEXT_PUBLIC_API_URL!, } }
export function validateEnv() {
const required = ['DATABASE_URL', 'API_KEY', 'NEXT_PUBLIC_API_URL']
const missing = required.filter((key) => !process.env[key])
if (missing.length > 0) {
throw new Error(Missing required environment variables: ${missing.join(', ')})
}
}
- Implement Health Checks
// src/app/api/health/route.ts import { NextResponse } from 'next/server'
export const dynamic = 'force-dynamic'
export async function GET() { const checks = { status: 'healthy', timestamp: new Date().toISOString(), version: process.env.npm_package_version || 'unknown', uptime: process.uptime(), } return NextResponse.json(checks) }
- Set Up Monitoring
See references/monitoring.md for OpenTelemetry configuration, logging, alerting, and dashboards.
// instrumentation.ts import { registerOTel } from '@vercel/otel'
export function register() { registerOTel({ serviceName: process.env.OTEL_SERVICE_NAME || 'next-app', }) }
- Handle Server Actions Encryption
CRITICAL: Generate and set consistent encryption key for multi-server deployments:
Generate key
openssl rand -base64 32
Set in GitHub Actions Secrets as NEXT_SERVER_ACTIONS_ENCRYPTION_KEY
Without this key, Server Actions fail with "Failed to find Server Action" errors in multi-server deployments.
Best Practices
-
Docker: Use multi-stage builds, enable standalone output, set non-root user, include health checks
-
Security: Never commit .env.local , use NEXT_PUBLIC_ only for public values, set NEXT_SERVER_ACTIONS_ENCRYPTION_KEY
-
Performance: Use output: 'standalone' , enable CDN for static assets, use next/image
-
Environment: Use same Docker image across environments, inject runtime config via env vars
Examples
// next.config.ts const nextConfig = { output: 'standalone', poweredByHeader: false, compress: true, generateBuildId: async () => process.env.GIT_HASH || 'build', } export default nextConfig
docker-compose.yml
version: '3.8' services: app: build: . ports: - "3000:3000" environment: - DATABASE_URL=postgresql://db:5432/myapp - NEXT_PUBLIC_API_URL=http://localhost:3000/api
Constraints and Warnings
Constraints
-
Standalone output requires Node.js 18+
-
Server Actions encryption key must be consistent across all instances
-
Runtime environment variables only work with output: 'standalone'
-
OpenTelemetry requires instrumentation.ts at project root
Warnings
-
Never use NEXT_PUBLIC_ prefix for sensitive values
-
Always set NEXT_SERVER_ACTIONS_ENCRYPTION_KEY for multi-server deployments
-
Without health checks, orchestrators may send traffic to unhealthy instances
-
Runtime env vars don't work with static export (output: 'export' )
References
-
references/docker-patterns.md - Advanced Docker configurations, multi-arch builds, optimization
-
references/github-actions.md - Complete CI/CD workflows, testing, security scanning
-
references/monitoring.md - OpenTelemetry, logging, alerting, dashboards
-
references/deployment-platforms.md - Platform-specific guides (Vercel, AWS, GCP, Azure)