nextjs-deployment
by giuseppe-trisciuoglio
Provides comprehensive patterns for deploying Next.js applications to production. Use when configuring Docker containers, setting up GitHub Actions CI/CD pipelines, managing environment variables, implementing preview deployments, or setting up monitoring and logging for Next.js applications. Covers standalone output, multi-stage Docker builds, health checks, OpenTelemetry instrumentation, and production best practices.
安装
安装命令
git clone https://github.com/giuseppe-trisciuoglio/developer-kit/tree/main/plugins/developer-kit-typescript/skills/nextjs-deployment文档
Next.js Deployment
Deploy Next.js applications to production with Docker, CI/CD pipelines, and comprehensive monitoring.
Overview
This skill provides patterns for:
- Docker configuration with multi-stage builds
- GitHub Actions CI/CD pipelines
- Environment variables management (build-time and runtime)
- Preview deployments
- Monitoring with OpenTelemetry
- Logging and health checks
- Production optimization
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
Enable standalone output for optimized Docker deployments:
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
output: 'standalone',
poweredByHeader: false,
generateBuildId: async () => {
// Use git hash for consistent builds across servers
return process.env.GIT_HASH || process.env.GITHUB_SHA || 'build'
},
}
export default nextConfig
Create Multi-Stage Dockerfile
Build optimized Docker image with minimal footprint:
# syntax=docker/dockerfile:1
FROM node:20-alpine AS base
# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies
COPY package.json package-lock.json* pnpm-lock.yaml* yarn.lock* ./
RUN \
if [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
elif [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
else echo "Lockfile not found." && exit 1; \
fi
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Set build-time environment variables
ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production
# Generate build ID from git (set during build)
ARG GIT_HASH
ENV GIT_HASH=${GIT_HASH}
# Server Actions encryption key (CRITICAL for multi-server deployments)
ARG NEXT_SERVER_ACTIONS_ENCRYPTION_KEY
ENV NEXT_SERVER_ACTIONS_ENCRYPTION_KEY=${NEXT_SERVER_ACTIONS_ENCRYPTION_KEY}
RUN \
if [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
elif [ -f yarn.lock ]; then yarn build; \
elif [ -f package-lock.json ]; then npm run build; \
else npm run build; \
fi
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Copy standalone output
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
# Copy public files if they exist
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
USER nextjs
EXPOSE 3000
# Health check
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 CI/CD
Create automated build and deployment pipeline:
# .github/workflows/deploy.yml
name: Build and Deploy
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha,prefix={{branch}}-
type=raw,value=latest,enable={{is_default_branch}}
- name: Generate Server Actions Key
id: generate-key
run: |
KEY=$(openssl rand -base64 32)
echo "key=$KEY" >> $GITHUB_OUTPUT
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
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 }}
deploy-staging:
needs: build
if: github.ref == 'refs/heads/develop'
runs-on: ubuntu-latest
environment:
name: staging
url: https://staging.example.com
steps:
- name: Deploy to staging
run: |
echo "Deploying to staging..."
# Add your deployment commands here
# e.g., kubectl, helm, or platform-specific CLI
deploy-production:
needs: build
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment:
name: production
url: https://example.com
steps:
- name: Deploy to production
run: |
echo "Deploying to production..."
# Add your deployment commands here
Manage Environment Variables
Build-Time Variables (next.config.ts)
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
env: {
// These are inlined at build time
APP_VERSION: process.env.npm_package_version || '1.0.0',
BUILD_DATE: new Date().toISOString(),
},
// Public runtime config (available on server and client)
publicRuntimeConfig: {
apiUrl: process.env.NEXT_PUBLIC_API_URL,
featureFlags: {
newDashboard: process.env.NEXT_PUBLIC_FF_NEW_DASHBOARD === 'true',
},
},
}
export default nextConfig
Runtime Environment Variables
For runtime variables with Docker, use a single image across environments:
// src/lib/env.ts
export function getEnv() {
return {
// Server-only (read at request time)
databaseUrl: process.env.DATABASE_URL!,
apiKey: process.env.API_KEY!,
// Public (must be prefixed with NEXT_PUBLIC_ at build time)
publicApiUrl: process.env.NEXT_PUBLIC_API_URL!,
}
}
// Validate required environment variables
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(', ')}`)
}
}
Environment Variable Files
# .env.local (development - never commit)
DATABASE_URL=postgresql://localhost:5432/mydb
API_KEY=dev-key
NEXT_PUBLIC_API_URL=http://localhost:3000/api
# .env.production (production defaults)
NEXT_PUBLIC_API_URL=https://api.example.com
# .env.example (template for developers)
DATABASE_URL=
API_KEY=
NEXT_PUBLIC_API_URL=
Implement Health Checks
Create a health check endpoint for load balancers and orchestrators:
// 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',
buildId: process.env.GIT_HASH || 'unknown',
uptime: process.uptime(),
checks: {
memory: checkMemory(),
// Add database, cache, etc. checks here
},
}
const isHealthy = Object.values(checks.checks).every((check) => check.status === 'ok')
return NextResponse.json(checks, {
status: isHealthy ? 200 : 503
})
}
function checkMemory() {
const used = process.memoryUsage()
const threshold = 1024 * 1024 * 1024 // 1GB
return {
status: used.heapUsed < threshold ? 'ok' : 'warning',
heapUsed: `${Math.round(used.heapUsed / 1024 / 1024)}MB`,
heapTotal: `${Math.round(used.heapTotal / 1024 / 1024)}MB`,
}
}
Set Up OpenTelemetry Monitoring
Add observability with OpenTelemetry:
// instrumentation.ts
import { registerOTel } from '@vercel/otel'
export function register() {
registerOTel({
serviceName: process.env.OTEL_SERVICE_NAME || 'next-app',
serviceVersion: process.env.npm_package_version,
})
}
// instrumentation.node.ts
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http'
import { NodeSDK } from '@opentelemetry/sdk-node'
import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-node'
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics'
import { resourceFromAttributes } from '@opentelemetry/resources'
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions'
const sdk = new NodeSDK({
resource: resourceFromAttributes({
[ATTR_SERVICE_NAME]: process.env.OTEL_SERVICE_NAME || 'next-app',
[ATTR_SERVICE_VERSION]: process.env.npm_package_version || '1.0.0',
}),
spanProcessor: new SimpleSpanProcessor(
new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
})
),
metricReader: new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({
url: process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT,
}),
}),
})
sdk.start()
// Graceful shutdown
process.on('SIGTERM', () => {
sdk.shutdown()
.then(() => console.log('OpenTelemetry terminated'))
.catch((err) => console.error('OpenTelemetry termination error', err))
.finally(() => process.exit(0))
})
// src/lib/logger.ts
interface LogEntry {
level: string
message: string
timestamp: string
requestId?: string
[key: string]: unknown
}
export function createLogger(requestId?: string) {
const base = {
timestamp: new Date().toISOString(),
...(requestId && { requestId }),
}
return {
info: (message: string, meta?: Record<string, unknown>) => {
log({ level: 'info', message, ...base, ...meta })
},
warn: (message: string, meta?: Record<string, unknown>) => {
log({ level: 'warn', message, ...base, ...meta })
},
error: (message: string, error?: Error, meta?: Record<string, unknown>) => {
log({
level: 'error',
message,
error: error?.message,
stack: error?.stack,
...base,
...meta
})
},
}
}
function log(entry: LogEntry) {
// In production, send to structured logging service
// In development, pretty print
if (process.env.NODE_ENV === 'production') {
console.log(JSON.stringify(entry))
} else {
console.log(`[${entry.level.toUpperCase()}] ${entry.message}`, entry)
}
}
Configure Preview Deployments
Set up preview environments for pull requests:
# .github/workflows/preview.yml
name: Preview Deployment
on:
pull_request:
types: [opened, synchronize, closed]
jobs:
deploy-preview:
if: github.event.action != 'closed'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
env:
NEXT_PUBLIC_API_URL: https://staging-api.example.com
NEXT_PUBLIC_PREVIEW: 'true'
- name: Deploy to Preview
run: |
# Example: Deploy to Vercel, Netlify, or your platform
# npx vercel --token=${{ secrets.VERCEL_TOKEN }} --prebuilt
echo "Deploying preview for PR #${{ github.event.number }}"
cleanup-preview:
if: github.event.action == 'closed'
runs-on: ubuntu-latest
steps:
- name: Cleanup Preview
run: |
echo "Cleaning up preview for PR #${{ github.event.number }}"
Handle Server Actions Encryption
CRITICAL: For multi-server deployments, set a consistent encryption key:
# Generate a key locally
openssl rand -base64 32
# Set in GitHub Actions (Secret)
# NEXT_SERVER_ACTIONS_ENCRYPTION_KEY
# Or generate in workflow (see GitHub Actions example above)
# In Dockerfile - pass as build arg
ARG NEXT_SERVER_ACTIONS_ENCRYPTION_KEY
ENV NEXT_SERVER_ACTIONS_ENCRYPTION_KEY=${NEXT_SERVER_ACTIONS_ENCRYPTION_KEY}
Without this key, Server Actions will fail with "Failed to find Server Action" errors in multi-server deployments.
Best Practices
Docker Best Practices
- Use multi-stage builds to minimize final image size
- Enable standalone output for production deployments
- Set proper permissions with non-root user
- Include health checks for orchestration platforms
- Cache dependencies in CI/CD for faster builds
Security Checklist
- Never commit
.env.localor secrets - Use
NEXT_PUBLIC_prefix only for truly public values - Set
NEXT_SERVER_ACTIONS_ENCRYPTION_KEYfor multi-server deployments - Disable telemetry in production with
NEXT_TELEMETRY_DISABLED=1 - Remove
poweredByHeaderin production
Performance Optimization
- Use
output: 'standalone'for minimal Docker images - Enable compression at the reverse proxy level
- Configure CDN for static assets
- Use
next/imagefor optimized images
Environment Management
- Use same Docker image across all environments
- Inject runtime configuration via environment variables
- Use different tags for different environments (
latest,staging) - Implement feature flags for gradual rollouts
Examples
Example 1: Complete Production Setup
Input: Set up production deployment with Docker and GitHub Actions
Output:
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
output: 'standalone',
poweredByHeader: false,
compress: true,
generateBuildId: async () => process.env.GIT_HASH || 'build',
env: {
APP_VERSION: process.env.npm_package_version,
},
}
export default nextConfig
# Dockerfile
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
ENV NODE_ENV=production
ARG GIT_HASH
ENV GIT_HASH=${GIT_HASH}
ARG NEXT_SERVER_ACTIONS_ENCRYPTION_KEY
ENV NEXT_SERVER_ACTIONS_ENCRYPTION_KEY=${NEXT_SERVER_ACTIONS_ENCRYPTION_KEY}
RUN npm run build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
RUN addgroup --system --gid 1001 nodejs
RUN 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"]
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
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: key
run: echo "key=$(openssl rand -base64 32)" >> $GITHUB_OUTPUT
- uses: docker/build-push-action@v5
with:
push: true
tags: ${{ steps.meta.outputs.tags }}
build-args: |
GIT_HASH=${{ github.sha }}
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY=${{ steps.key.outputs.key }}
Example 2: Runtime Environment Variables
Input: Configure different API URLs for staging and production
Output:
// src/lib/env.ts
const envSchema = {
server: {
DATABASE_URL: process.env.DATABASE_URL!,
API_SECRET: process.env.API_SECRET!,
},
public: {
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL!,
NEXT_PUBLIC_APP_NAME: process.env.NEXT_PUBLIC_APP_NAME || 'MyApp',
},
}
export function getServerEnv() {
return envSchema.server
}
export function getPublicEnv() {
return envSchema.public
}
// Use in Server Components
import { getServerEnv } from '@/lib/env'
async function fetchData() {
const env = getServerEnv()
// Use env.DATABASE_URL
}
// Use in Client Components
import { getPublicEnv } from '@/lib/env'
function ApiClient() {
const env = getPublicEnv()
// Use env.NEXT_PUBLIC_API_URL
}
# docker-compose.yml for local development
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://db:5432/myapp
- NEXT_PUBLIC_API_URL=http://localhost:3000/api
Example 3: OpenTelemetry Integration
Input: Add distributed tracing to Next.js application
Output:
// instrumentation.ts
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
await import('./instrumentation.node')
}
}
// instrumentation.node.ts
import { NodeSDK } from '@opentelemetry/sdk-node'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
import { resourceFromAttributes } from '@opentelemetry/resources'
import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions'
const sdk = new NodeSDK({
resource: resourceFromAttributes({
[ATTR_SERVICE_NAME]: process.env.OTEL_SERVICE_NAME || 'next-app',
}),
spanProcessor: new SimpleSpanProcessor(
new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
})
),
})
sdk.start()
// src/app/api/users/route.ts
import { trace } from '@opentelemetry/api'
export async function GET() {
const tracer = trace.getTracer('next-app')
return tracer.startActiveSpan('fetch-users', async (span) => {
try {
const users = await db.user.findMany()
span.setAttribute('user.count', users.length)
return NextResponse.json(users)
} catch (error) {
span.recordException(error as Error)
throw error
} finally {
span.end()
}
})
}
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' - Health checks need explicit route handler
- OpenTelemetry requires instrumentation.ts at project root
Warnings
- Never use
NEXT_PUBLIC_prefix for sensitive values - Always set
NEXT_SERVER_ACTIONS_ENCRYPTION_KEYfor multi-server deployments - Without health checks, orchestrators may send traffic to unhealthy instances
- Runtime env vars don't work with static export (
output: 'export') - Cache build artifacts in CI/CD to speed up builds
References
Consult these files for detailed patterns:
- 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)
相关 Skills
by daymade
Configures and customizes Claude Code statuslines with multi-line layouts, cost tracking via ccusage, git status indicators, and customizable colors. Activates for statusline setup, installation, configuration, customization, color changes, cost display, git status integration, or troubleshooting statusline issues.
by daymade
Diagnoses and fixes conflicts between Tailscale and proxy/VPN tools (Shadowrocket, Clash, Surge) on macOS. Covers five conflict layers - (1) route hijacking, (2) HTTP proxy env var interception, (3) system proxy bypass, (4) SSH ProxyCommand double tunneling, and (5) VM/container runtime proxy propagation (OrbStack/Docker). Includes SOP for remote development via SSH tunnels with proxy-safe Makefile patterns. Use when Tailscale ping works but SSH/HTTP times out, when browser returns 503 but curl works, when git push fails with "failed to begin relaying via HTTP", when Docker pull times out behind TUN/VPN, when setting up Tailscale SSH to WSL instances, or when bootstrapping remote dev environments over Tailscale.
by levnikolaevich
Orchestrates full decomposition (scope → Epics → Stories → RICE prioritization) by delegating ln-210 → ln-220 → ln-230. Sequential processing. Epic 0 for Infrastructure.