
Nextjs Deployment
- 1.5k installs
- 311 repo stars
- Updated June 22, 2026
- giuseppe-trisciuoglio/developer-kit
nextjs-deployment is an agent skill that deploys Next.js applications using Docker standalone builds, GitHub Actions CI/CD, and OpenTelemetry monitoring.
About
The nextjs-deployment skill provides production deployment patterns for Next.js including Docker multi-stage builds, GitHub Actions pipelines, environment variable handling, and observability setup. It recommends standalone output for containers, documents NEXT_PUBLIC versus server-only secrets, and covers instrumentation.ts with OpenTelemetry via @vercel/otel. Dockerfile examples use node:20-alpine deps, builder, and runner stages with non-root nextjs user and HTTP health checks on /api/health. GitHub Actions workflows build and push to ghcr.io with GIT_HASH and NEXT_SERVER_ACTIONS_ENCRYPTION_KEY build args. The skill warns that multi-server Server Actions require a consistent encryption key or actions fail with Failed to find Server Action errors. Reference files cover docker-patterns, github-actions, monitoring, and deployment-platform guides. Use when developers dockerize Next.js, configure CI/CD, or add production health and tracing.
- Standalone output mode with multi-stage Docker builds and non-root container user.
- GitHub Actions workflow pushing to ghcr.io with build cache and encryption key generation.
- Health check route pattern and OpenTelemetry instrumentation.ts setup.
- Documents NEXT_PUBLIC_, server-only, and runtime environment variable rules.
- Critical warning on NEXT_SERVER_ACTIONS_ENCRYPTION_KEY for multi-server deployments.
Nextjs Deployment by the numbers
- 1,519 all-time installs (skills.sh)
- +57 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #135 of 1,453 DevOps & CI/CD skills by installs in the Skillselion catalog
- Security screen: HIGH risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
nextjs-deployment capabilities & compatibility
- Capabilities
- multi stage docker standalone builds · github actions container publish workflow · environment variable validation patterns · health check api route template · opentelemetry instrumentation setup
- Works with
- github · docker · kubernetes
- Use cases
- devops · ci cd · orchestration
- Runs
- Local or remote
What nextjs-deployment says it does
Deploy Next.js applications to production with Docker, CI/CD pipelines, and comprehensive monitoring.
Without this key, Server Actions fail with "Failed to find Server Action" errors in multi-server deployments.
npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill nextjs-deploymentAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.5k |
|---|---|
| repo stars | ★ 311 |
| Security audit | 2 / 3 scanners passed |
| Last updated | June 22, 2026 |
| Repository | giuseppe-trisciuoglio/developer-kit ↗ |
How do I dockerize Next.js, set up CI/CD, configure env vars, and add health checks for production?
Deploy Next.js apps with Docker standalone builds, GitHub Actions CI/CD, health checks, and OpenTelemetry monitoring.
Who is it for?
Developers shipping Next.js to containers or automated pipelines who need standalone output and observability.
Skip if: Skip for static export-only sites without server runtime or non-Next.js frameworks.
When should I use this skill?
User asks to deploy Next.js, dockerize, set up GitHub Actions, or configure production monitoring.
What you get
Dockerfile, GitHub Actions workflow, env validation, health endpoint, and instrumentation patterns ready for production.
- vercel.json configuration
- AWS deployment guide snippets
- Security header and rewrite rules
Files
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
1. 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 nextConfig2. 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"]3. 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 }}4. 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(', ')}`)
}
}5. 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)
}6. 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',
})
}7. 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_KEYWithout 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, useNEXT_PUBLIC_only for public values, setNEXT_SERVER_ACTIONS_ENCRYPTION_KEY - Performance: Use
output: 'standalone', enable CDN for static assets, usenext/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/apiConstraints 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_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')
References
- [references/docker-patterns.md](references/docker-patterns.md) - Advanced Docker configurations, multi-arch builds, optimization
- [references/github-actions.md](references/github-actions.md) - Complete CI/CD workflows, testing, security scanning
- [references/monitoring.md](references/monitoring.md) - OpenTelemetry, logging, alerting, dashboards
- [references/deployment-platforms.md](references/deployment-platforms.md) - Platform-specific guides (Vercel, AWS, GCP, Azure)
Deployment Platforms
Platform-specific guides for deploying Next.js applications.
Vercel (Platform Native)
Configuration
// vercel.json
{
"version": 2,
"buildCommand": "next build",
"devCommand": "next dev",
"installCommand": "npm install",
"framework": "nextjs",
"regions": ["iad1"],
"env": {
"NEXT_TELEMETRY_DISABLED": "1"
},
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "X-Frame-Options",
"value": "DENY"
},
{
"key": "X-Content-Type-Options",
"value": "nosniff"
}
]
}
],
"rewrites": [
{
"source": "/api/(.*)",
"destination": "/api/$1"
}
],
"redirects": [
{
"source": "/old-path",
"destination": "/new-path",
"permanent": true
}
]
}Environment Variables
# Set via Vercel Dashboard or CLI
vercel env add DATABASE_URL production
vercel env add NEXT_PUBLIC_API_URL productionPreview Deployments
Automatic for all pull requests. Configure in project settings:
- Environment Variables > Preview > Add variables for preview environments
AWS
Elastic Container Service (ECS)
# ecs-task-definition.json
{
"family": "nextjs-app",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "512",
"memory": "1024",
"containerDefinitions": [
{
"name": "nextjs",
"image": "myapp:latest",
"portMappings": [
{
"containerPort": 3000,
"protocol": "tcp"
}
],
"environment": [
{ "name": "NODE_ENV", "value": "production" },
{ "name": "PORT", "value": "3000" }
],
"secrets": [
{
"name": "DATABASE_URL",
"valueFrom": "arn:aws:secretsmanager:region:account:secret:db-url"
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/nextjs-app",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "ecs"
}
},
"healthCheck": {
"command": ["CMD-SHELL", "curl -f http://localhost:3000/api/health || exit 1"],
"interval": 30,
"timeout": 5,
"retries": 3,
"startPeriod": 60
}
}
]
}Application Load Balancer
# alb.yml
Resources:
LoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Name: nextjs-alb
Scheme: internet-facing
Type: application
Subnets:
- !Ref PublicSubnet1
- !Ref PublicSubnet2
SecurityGroups:
- !Ref ALBSecurityGroup
TargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
Name: nextjs-tg
Port: 3000
Protocol: HTTP
VpcId: !Ref VPC
TargetType: ip
HealthCheckPath: /api/health
HealthCheckIntervalSeconds: 30
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 2
UnhealthyThresholdCount: 3
Listener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
LoadBalancerArn: !Ref LoadBalancer
Port: 443
Protocol: HTTPS
Certificates:
- CertificateArn: !Ref SSLCertificate
DefaultActions:
- Type: forward
TargetGroupArn: !Ref TargetGroupS3 Static Export
# s3-cloudfront.yml
Resources:
StaticBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub "${AWS::StackName}-static"
PublicAccessBlockConfiguration:
BlockPublicAcls: false
BlockPublicPolicy: false
IgnorePublicAcls: false
RestrictPublicBuckets: false
WebsiteConfiguration:
IndexDocument: index.html
ErrorDocument: 404.html
CloudFrontDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Enabled: true
DefaultRootObject: index.html
Origins:
- DomainName: !GetAtt StaticBucket.RegionalDomainName
Id: S3Origin
S3OriginConfig:
OriginAccessIdentity: ""
OriginAccessControlId: !GetAtt OriginAccessControl.Id
DefaultCacheBehavior:
TargetOriginId: S3Origin
ViewerProtocolPolicy: redirect-to-https
AllowedMethods: [GET, HEAD]
CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6 # Managed-CachingOptimized
CustomErrorResponses:
- ErrorCode: 404
ResponseCode: 200
ResponsePagePath: /404.htmlBuild and deploy:
# Build static export
NEXT_PUBLIC_API_URL=https://api.example.com npm run build
# Sync to S3
aws s3 sync dist/ s3://my-bucket --delete
# Invalidate CloudFront
aws cloudfront create-invalidation --distribution-id XYZ --paths "/*"Google Cloud Platform
Cloud Run
# cloudbuild.yaml
steps:
- name: 'gcr.io/cloud-builders/docker'
args:
- 'build'
- '--build-arg=GIT_HASH=$SHORT_SHA'
- '--tag=gcr.io/$PROJECT_ID/nextjs-app:$SHORT_SHA'
- '--tag=gcr.io/$PROJECT_ID/nextjs-app:latest'
- '.'
- name: 'gcr.io/cloud-builders/docker'
args: ['push', 'gcr.io/$PROJECT_ID/nextjs-app:$SHORT_SHA']
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
entrypoint: gcloud
args:
- 'run'
- 'deploy'
- 'nextjs-app'
- '--image=gcr.io/$PROJECT_ID/nextjs-app:$SHORT_SHA'
- '--region=us-central1'
- '--platform=managed'
- '--allow-unauthenticated'
- '--set-env-vars=NODE_ENV=production'
- '--set-secrets=DATABASE_URL=db-url:latest'
images:
- 'gcr.io/$PROJECT_ID/nextjs-app:$SHORT_SHA'
- 'gcr.io/$PROJECT_ID/nextjs-app:latest'# Deploy manually
gcloud run deploy nextjs-app \
--source . \
--region=us-central1 \
--allow-unauthenticated \
--set-env-vars="NODE_ENV=production"App Engine
# app.yaml
runtime: nodejs20
instance_class: F2
automatic_scaling:
min_instances: 1
max_instances: 10
target_cpu_utilization: 0.6
env_variables:
NODE_ENV: 'production'
NEXT_TELEMETRY_DISABLED: '1'
handlers:
- url: /static
static_dir: .next/static
- url: /_next/static
static_dir: .next/static
- url: /.*
script: auto
health_check:
enable_health_check: true
check_interval_sec: 30
timeout_sec: 5
unhealthy_threshold: 3
healthy_threshold: 1Microsoft Azure
Container Instances
# Create container
az container create \
--resource-group myResourceGroup \
--name nextjs-app \
--image myregistry.azurecr.io/nextjs:latest \
--cpu 1 \
--memory 2 \
--ports 3000 \
--environment-variables 'NODE_ENV=production' \
--secrets 'database-url=secret-value' \
--secrets-mount-path /secretsApp Service
# docker-compose.azure.yml
version: '3.8'
services:
app:
image: myregistry.azurecr.io/nextjs:latest
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=${DATABASE_URL}DigitalOcean
App Platform
# .do/app.yaml
name: nextjs-app
services:
- name: web
source_dir: /
github:
repo: username/repo
branch: main
deploy_on_push: true
build_command: npm run build
run_command: npm start
environment_slug: node-js
instance_count: 2
instance_size_slug: basic-xs
envs:
- key: NODE_ENV
value: production
- key: DATABASE_URL
value: ${db.DATABASE_URL}
type: SECRET
health_check:
http_path: /api/health
timeout_seconds: 10
port: 3000
success_threshold: 1
failure_threshold: 3
static_sites:
- name: static
source_dir: .next/static
output_dir: /Railway
# Install CLI
npm i -g @railway/cli
# Login
railway login
# Link project
railway link
# Deploy
railway up
# Set environment variables
railway variables set DATABASE_URL="postgresql://..."
railway variables set NEXT_PUBLIC_API_URL="https://api.example.com"Render
# render.yaml
services:
- type: web
name: nextjs-app
env: docker
dockerfilePath: ./Dockerfile
envVars:
- key: NODE_ENV
value: production
- key: DATABASE_URL
fromDatabase:
name: postgres
property: connectionString
- key: NEXT_PUBLIC_API_URL
value: https://api.example.com
healthCheckPath: /api/health
autoDeploy: true
databases:
- name: postgres
databaseName: nextjs
user: nextjsFly.io
# Install flyctl
curl -L https://fly.io/install.sh | sh
# Launch app
fly launch --dockerfile Dockerfile
# Set secrets
fly secrets set DATABASE_URL="postgresql://..."
fly secrets set NEXT_SERVER_ACTIONS_ENCRYPTION_KEY="..."
# Deploy
fly deploy
# Scale
fly scale count 3
fly scale vm shared-cpu-1x --memory 1024# fly.toml (auto-generated)
app = "nextjs-app"
primary_region = "iad"
[build]
dockerfile = "Dockerfile"
[env]
PORT = "3000"
NODE_ENV = "production"
[http_service]
internal_port = 3000
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 1
processes = ["app"]
[http_service.concurrency]
type = "requests"
hard_limit = 1000
soft_limit = 200
[[vm]]
cpu_kind = "shared"
cpus = 1
memory_mb = 1024
[checks]
[checks.health]
port = 3000
type = "http"
interval = "15s"
timeout = "5s"
grace_period = "30s"
method = "GET"
path = "/api/health"Netlify
# netlify.toml
[build]
command = "npm run build"
publish = "dist"
[build.environment]
NODE_VERSION = "20"
NEXT_TELEMETRY_DISABLED = "1"
[[plugins]]
package = "@netlify/plugin-nextjs"
[[redirects]]
from = "/api/*"
to = "/.netlify/functions/:splat"
status = 200
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"
X-Content-Type-Options = "nosniff"Comparison Table
| Platform | Best For | Standalone | Serverless | Price Model |
|---|---|---|---|---|
| Vercel | Next.js native | Partial | Yes | Usage-based |
| AWS ECS | Enterprise | Yes | No | Resource-based |
| GCP Cloud Run | Container scaling | Yes | Yes | Request-based |
| Azure App Service | .NET/Azure shops | Yes | No | Instance-based |
| DigitalOcean | Simplicity | Yes | No | Fixed |
| Railway | Developer UX | Yes | No | Usage-based |
| Render | Full-stack apps | Yes | No | Fixed |
| Fly.io | Edge deployment | Yes | No | Resource-based |
| Netlify | JAMstack | Partial | Yes | Usage-based |
Platform-Specific Notes
Vercel
- Native Next.js optimizations
- Automatic preview deployments
- Edge Functions for middleware
- Limits: 4.5MB serverless function size
AWS
- Full control over infrastructure
- ECS Fargate for containerized apps
- Lambda for serverless (with
@sls-next) - CloudFront for global CDN
GCP
- Cloud Run: Pay per request
- Automatic scaling to zero
- Built-in Cloud Monitoring
- Cloud CDN integration
Azure
- App Service: Simple deployment
- Container Instances: Short-lived tasks
- AKS: Kubernetes orchestration
- Static Web Apps: JAMstack
Docker Patterns for Next.js
Advanced Docker configurations for production Next.js deployments.
Multi-Architecture Builds
Build for multiple architectures (AMD64 and ARM64):
# syntax=docker/dockerfile:1
FROM --platform=$BUILDPLATFORM 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 . .
ARG NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production
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
CMD ["node", "server.js"]Build command:
docker buildx build \
--platform linux/amd64,linux/arm64 \
--tag myapp:latest \
--push .Development Dockerfile
For local development with hot reload:
# Dockerfile.dev
FROM node:20-alpine
WORKDIR /app
RUN apk add --no-cache libc6-compat
# Install dependencies first (cached layer)
COPY package.json package-lock.json* ./
RUN npm install
# Copy source code
COPY . .
ENV NODE_ENV=development
ENV PORT=3000
ENV NEXT_TELEMETRY_DISABLED=1
EXPOSE 3000
CMD ["npm", "run", "dev"]# docker-compose.dev.yml
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "3000:3000"
volumes:
- .:/app
- /app/node_modules
- /app/.next
environment:
- NODE_ENV=developmentProduction Optimization
Minimal Production Image
# syntax=docker/dockerfile:1
FROM node:20-alpine AS base
FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci --only=production
FROM base AS builder
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
RUN apk add --no-cache dumb-init
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=3000
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
USER node
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "server.js"]Image Size Optimization
Compare base images:
| Image | Size | Use Case |
|---|---|---|
node:20-alpine | ~180MB | Recommended for production |
node:20-slim | ~250MB | Good compatibility |
node:20 | ~1GB | Development only |
gcr.io/distroless/nodejs20-debian11 | ~120MB | Minimal, no shell |
Docker Compose Configurations
Full Stack Setup
# docker-compose.yml
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
args:
- NEXT_PUBLIC_API_URL=http://localhost:3000/api
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://postgres:password@db:5432/myapp
- REDIS_URL=redis://redis:6379
- NEXT_SERVER_ACTIONS_ENCRYPTION_KEY=${NEXT_SERVER_ACTIONS_ENCRYPTION_KEY}
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/api/health')"]
interval: 30s
timeout: 5s
retries: 3
db:
image: postgres:16-alpine
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
- POSTGRES_DB=myapp
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 5
volumes:
postgres_data:
redis_data:Kubernetes Deployment
# k8s-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nextjs-app
spec:
replicas: 3
selector:
matchLabels:
app: nextjs-app
template:
metadata:
labels:
app: nextjs-app
spec:
containers:
- name: app
image: myapp:latest
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: "production"
- name: NEXT_TELEMETRY_DISABLED
value: "1"
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: database-url
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: nextjs-service
spec:
selector:
app: nextjs-app
ports:
- port: 80
targetPort: 3000
type: ClusterIPSecurity Hardening
Non-Root User Setup
FROM node:20-alpine AS runner
WORKDIR /app
# Create non-root user
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
# Set proper permissions
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
# Read-only root filesystem
USER nextjs
# Security options
EXPOSE 3000
CMD ["node", "server.js"]Docker Security Scanning
# Scan image for vulnerabilities
docker scan myapp:latest
# Or use Trivy
trivy image myapp:latest
# In CI/CD
- name: Scan Docker image
uses: aquasecurity/trivy-action@master
with:
image-ref: 'myapp:latest'
format: 'sarif'
output: 'trivy-results.sarif'Caching Strategies
Layer Caching
# Dependencies layer (cached if package.json unchanged)
FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci
# Build layer
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run buildBuildKit Cache Mounts
# syntax=docker/dockerfile:1
FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json* ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN --mount=type=cache,target=/app/.next/cache \
npm run buildNGINX Reverse Proxy
# nginx/Dockerfile
FROM nginx:alpine
COPY nginx.conf /etc/nginx/nginx.conf
COPY default.conf /etc/nginx/conf.d/default.conf# nginx/default.conf
upstream nextjs {
server app:3000;
}
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://nextjs;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
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;
}
location /_next/static {
proxy_pass http://nextjs;
proxy_cache_valid 365d;
add_header Cache-Control "public, immutable";
}
}GitHub Actions for Next.js
Complete CI/CD workflows for Next.js applications.
Basic CI Workflow
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run lint
type-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run type-check
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run test
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run buildDocker Build and Push
# .github/workflows/docker.yml
name: Docker Build
on:
push:
branches: [main]
tags: ['v*']
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=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha
- name: Generate Server Actions Key
id: key
run: echo "key=$(openssl rand -base64 32)" >> $GITHUB_OUTPUT
- name: Build and push
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
platforms: linux/amd64,linux/arm64
build-args: |
GIT_HASH=${{ github.sha }}
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY=${{ steps.key.outputs.key }}Deploy to AWS ECS
# .github/workflows/deploy-aws.yml
name: Deploy to AWS ECS
on:
push:
branches: [main]
env:
AWS_REGION: us-east-1
ECR_REPOSITORY: my-nextjs-app
ECS_SERVICE: my-app-service
ECS_CLUSTER: my-app-cluster
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ${{ env.AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build, tag, and push image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
run: |
docker build \
--build-arg GIT_HASH=${{ github.sha }} \
--build-arg NEXT_SERVER_ACTIONS_ENCRYPTION_KEY=$(openssl rand -base64 32) \
-t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG \
-t $ECR_REGISTRY/$ECR_REPOSITORY:latest \
.
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest
- name: Download task definition
run: |
aws ecs describe-task-definition \
--task-definition my-app-task \
--query taskDefinition > task-definition.json
- name: Fill in image ID
id: task-def
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: task-definition.json
container-name: my-app
image: ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ github.sha }}
- name: Deploy ECS task definition
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
service: ${{ env.ECS_SERVICE }}
cluster: ${{ env.ECS_CLUSTER }}
wait-for-service-stability: trueDeploy to Vercel
# .github/workflows/deploy-vercel.yml
name: Deploy to Vercel
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Vercel CLI
run: npm install --global vercel@latest
- name: Pull Vercel Environment
run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
- name: Build
run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
- name: Deploy
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}Security Scanning
# .github/workflows/security.yml
name: Security Scan
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: '0 0 * * 0' # Weekly
jobs:
dependency-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm audit --audit-level=high
codeql:
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
steps:
- uses: actions/checkout@v4
- uses: github/codeql-action/init@v3
with:
languages: javascript
- uses: github/codeql-action/analyze@v3
trivy:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Run Trivy scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: 'myapp:${{ github.sha }}'
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload results
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-results.sarif'Performance Budget
# .github/workflows/performance.yml
name: Performance Budget
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Audit URLs
uses: treosh/lighthouse-ci-action@v11
with:
urls: |
https://staging.example.com/
https://staging.example.com/dashboard
budgetPath: ./budget.json
uploadArtifacts: true
- name: Assert Budget
run: |
if [ -f "lighthouse-results.json" ]; then
node scripts/check-lighthouse-budget.js
fi// budget.json
[
{
"path": "/*",
"resourceSizes": [
{ "resourceType": "document", "budget": 50 },
{ "resourceType": "script", "budget": 300 },
{ "resourceType": "stylesheet", "budget": 100 },
{ "resourceType": "image", "budget": 500 }
],
"resourceCounts": [
{ "resourceType": "third-party", "budget": 10 }
]
}
]Reusable Workflows
# .github/workflows/reusable-build.yml
name: Reusable Build
on:
workflow_call:
inputs:
node-version:
required: false
type: string
default: '20'
environment:
required: true
type: string
secrets:
API_TOKEN:
required: true
jobs:
build:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run build
env:
API_TOKEN: ${{ secrets.API_TOKEN }}# .github/workflows/production.yml
name: Production Deploy
on:
push:
branches: [main]
jobs:
build:
uses: ./.github/workflows/reusable-build.yml
with:
environment: production
secrets:
API_TOKEN: ${{ secrets.PROD_API_TOKEN }}Environment Protection
# .github/workflows/deploy-with-approval.yml
name: Deploy with Approval
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: echo "Building..."
deploy-staging:
needs: build
runs-on: ubuntu-latest
environment: staging
steps:
- run: echo "Deploying to staging..."
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment: production # Requires approval
steps:
- run: echo "Deploying to production..."Configure protection rules in GitHub Settings > Environments > production:
- Required reviewers: 1+
- Deployment branches: main only
- Wait timer: optional
Monitoring and Logging for Next.js
Comprehensive observability patterns for Next.js applications.
OpenTelemetry Setup
Basic Configuration
// 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 { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http'
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics'
import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-node'
import { resourceFromAttributes } from '@opentelemetry/resources'
import {
ATTR_SERVICE_NAME,
ATTR_SERVICE_VERSION,
ATTR_DEPLOYMENT_ENVIRONMENT
} 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',
[ATTR_DEPLOYMENT_ENVIRONMENT]: process.env.NODE_ENV || 'development',
}),
spanProcessor: new SimpleSpanProcessor(
new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
headers: {
'x-api-key': process.env.OTEL_API_KEY || '',
},
})
),
metricReader: new PeriodicExportingMetricReader({
exportIntervalMillis: 60000,
exporter: new OTLPMetricExporter({
url: process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT,
headers: {
'x-api-key': process.env.OTEL_API_KEY || '',
},
}),
}),
})
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))
})Vercel OTel (Simplified)
// instrumentation.ts
import { registerOTel } from '@vercel/otel'
export function register() {
registerOTel({
serviceName: process.env.OTEL_SERVICE_NAME || 'next-app',
})
}Custom Span Creation
// src/lib/tracing.ts
import { trace, Span, context, SpanStatusCode } from '@opentelemetry/api'
const tracer = trace.getTracer('next-app')
export async function withSpan<T>(
name: string,
fn: (span: Span) => Promise<T>,
attributes?: Record<string, string | number | boolean>
): Promise<T> {
return tracer.startActiveSpan(name, async (span) => {
if (attributes) {
Object.entries(attributes).forEach(([key, value]) => {
span.setAttribute(key, value)
})
}
try {
const result = await fn(span)
span.setStatus({ code: SpanStatusCode.OK })
return result
} catch (error) {
span.recordException(error as Error)
span.setStatus({
code: SpanStatusCode.ERROR,
message: (error as Error).message,
})
throw error
} finally {
span.end()
}
})
}
// Usage in API routes
import { withSpan } from '@/lib/tracing'
export async function GET() {
return withSpan(
'fetch-users',
async (span) => {
const users = await db.user.findMany()
span.setAttribute('user.count', users.length)
return NextResponse.json(users)
},
{ 'db.table': 'users' }
)
}Structured Logging
JSON Logger
// src/lib/logger.ts
type LogLevel = 'debug' | 'info' | 'warn' | 'error'
interface LogContext {
requestId?: string
userId?: string
path?: string
method?: string
[key: string]: unknown
}
interface LogEntry {
level: LogLevel
message: string
timestamp: string
service: string
version: string
context?: LogContext
error?: {
message: string
stack?: string
code?: string
}
}
class Logger {
private service: string
private version: string
constructor() {
this.service = process.env.OTEL_SERVICE_NAME || 'next-app'
this.version = process.env.npm_package_version || '1.0.0'
}
private log(level: LogLevel, message: string, context?: LogContext, error?: Error) {
const entry: LogEntry = {
level,
message,
timestamp: new Date().toISOString(),
service: this.service,
version: this.version,
...(context && { context }),
...(error && {
error: {
message: error.message,
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined,
code: (error as { code?: string }).code,
},
}),
}
if (process.env.NODE_ENV === 'production') {
console.log(JSON.stringify(entry))
} else {
const color = this.getColor(level)
console.log(
`${color}[${level.toUpperCase()}]${'\x1b[0m'} ${message}`,
context || '',
error || ''
)
}
}
private getColor(level: LogLevel): string {
const colors: Record<LogLevel, string> = {
debug: '\x1b[36m', // Cyan
info: '\x1b[32m', // Green
warn: '\x1b[33m', // Yellow
error: '\x1b[31m', // Red
}
return colors[level]
}
debug(message: string, context?: LogContext) {
if (process.env.LOG_LEVEL === 'debug') {
this.log('debug', message, context)
}
}
info(message: string, context?: LogContext) {
this.log('info', message, context)
}
warn(message: string, context?: LogContext) {
this.log('warn', message, context)
}
error(message: string, error?: Error, context?: LogContext) {
this.log('error', message, context, error)
}
}
export const logger = new Logger()
// Request-scoped logger
export function createRequestLogger(requestId: string, context?: Omit<LogContext, 'requestId'>) {
return {
debug: (message: string, extra?: Record<string, unknown>) =>
logger.debug(message, { requestId, ...context, ...extra }),
info: (message: string, extra?: Record<string, unknown>) =>
logger.info(message, { requestId, ...context, ...extra }),
warn: (message: string, extra?: Record<string, unknown>) =>
logger.warn(message, { requestId, ...context, ...extra }),
error: (message: string, error?: Error, extra?: Record<string, unknown>) =>
logger.error(message, error, { requestId, ...context, ...extra }),
}
}Middleware for Request Logging
// src/middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { createRequestLogger } from '@/lib/logger'
export function middleware(request: NextRequest) {
const requestId = crypto.randomUUID()
const start = Date.now()
const logger = createRequestLogger(requestId, {
path: request.nextUrl.pathname,
method: request.method,
userAgent: request.headers.get('user-agent'),
})
logger.info('Request started')
const response = NextResponse.next({
request: {
headers: new Headers(request.headers),
},
})
response.headers.set('x-request-id', requestId)
const duration = Date.now() - start
logger.info('Request completed', { duration: `${duration}ms` })
return response
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
}Error Tracking
Sentry Integration
// sentry.client.config.ts
import * as Sentry from '@sentry/nextjs'
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
environment: process.env.NODE_ENV,
release: process.env.npm_package_version,
// Adjust sampling rates
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
integrations: [
Sentry.replayIntegration({
maskAllText: false,
blockAllMedia: false,
}),
],
})// sentry.server.config.ts
import * as Sentry from '@sentry/nextjs'
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
release: process.env.npm_package_version,
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
})// src/lib/errors.ts
import * as Sentry from '@sentry/nextjs'
import { logger } from './logger'
export class AppError extends Error {
constructor(
message: string,
public code: string,
public statusCode: number = 500,
public context?: Record<string, unknown>
) {
super(message)
this.name = 'AppError'
}
}
export function handleError(error: Error, context?: Record<string, unknown>) {
// Log locally
logger.error(error.message, error, context)
// Send to Sentry
Sentry.withScope((scope) => {
if (context) {
Object.entries(context).forEach(([key, value]) => {
scope.setExtra(key, value)
})
}
Sentry.captureException(error)
})
}Health Checks
Basic Health Endpoint
// src/app/api/health/route.ts
import { NextResponse } from 'next/server'
export const dynamic = 'force-dynamic'
interface HealthCheck {
name: string
check: () => Promise<{ status: 'ok' | 'error'; details?: unknown }>
}
const checks: HealthCheck[] = [
{
name: 'memory',
check: async () => {
const used = process.memoryUsage()
const threshold = 1024 * 1024 * 1024 // 1GB
return {
status: used.heapUsed < threshold ? 'ok' : 'error',
details: {
heapUsed: `${Math.round(used.heapUsed / 1024 / 1024)}MB`,
heapTotal: `${Math.round(used.heapTotal / 1024 / 1024)}MB`,
rss: `${Math.round(used.rss / 1024 / 1024)}MB`,
},
}
},
},
{
name: 'uptime',
check: async () => ({
status: 'ok',
details: { uptime: `${Math.round(process.uptime())}s` },
}),
},
]
// Add database check if needed
// checks.push({
// name: 'database',
// check: async () => {
// try {
// await db.$queryRaw`SELECT 1`
// return { status: 'ok' }
// } catch {
// return { status: 'error', details: 'Database connection failed' }
// }
// },
// })
export async function GET() {
const results = await Promise.all(
checks.map(async ({ name, check }) => ({
name,
...(await check()),
}))
)
const isHealthy = results.every((r) => r.status === 'ok')
return NextResponse.json(
{
status: isHealthy ? 'healthy' : 'unhealthy',
timestamp: new Date().toISOString(),
version: process.env.npm_package_version,
buildId: process.env.GIT_HASH,
checks: results,
},
{ status: isHealthy ? 200 : 503 }
)
}Readiness/Liveness Probes
// src/app/api/health/ready/route.ts
import { NextResponse } from 'next/server'
export const dynamic = 'force-dynamic'
export async function GET() {
// Check critical dependencies
// const dbReady = await checkDatabase()
// const cacheReady = await checkCache()
const ready = true // dbReady && cacheReady
return NextResponse.json(
{ status: ready ? 'ready' : 'not ready' },
{ status: ready ? 200 : 503 }
)
}// src/app/api/health/live/route.ts
import { NextResponse } from 'next/server'
export const dynamic = 'force-dynamic'
export async function GET() {
// Simple liveness check - process is running
return NextResponse.json({ status: 'alive' })
}Metrics Collection
Custom Metrics
// src/lib/metrics.ts
import { metrics, ValueType } from '@opentelemetry/api'
const meter = metrics.getMeter('next-app')
// Counters
export const requestCounter = meter.createCounter('http.requests.total', {
description: 'Total HTTP requests',
valueType: ValueType.INT,
})
export const errorCounter = meter.createCounter('http.errors.total', {
description: 'Total HTTP errors',
valueType: ValueType.INT,
})
// Histograms
export const requestDuration = meter.createHistogram('http.request.duration', {
description: 'HTTP request duration in milliseconds',
valueType: ValueType.DOUBLE,
unit: 'ms',
})
// UpDownCounter
export const activeConnections = meter.createUpDownCounter('http.connections.active', {
description: 'Number of active connections',
valueType: ValueType.INT,
})
// Usage
export function recordRequest(method: string, route: string, status: number, duration: number) {
const attributes = { method, route, status: status.toString() }
requestCounter.add(1, attributes)
requestDuration.record(duration, attributes)
if (status >= 400) {
errorCounter.add(1, { ...attributes, error_type: status >= 500 ? 'server' : 'client' })
}
}Metrics Middleware
// src/middleware.ts (updated)
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { recordRequest } from '@/lib/metrics'
export function middleware(request: NextRequest) {
const start = Date.now()
const response = NextResponse.next()
const duration = Date.now() - start
recordRequest(
request.method,
request.nextUrl.pathname,
response.status,
duration
)
return response
}Dashboard Queries
Grafana/Prometheus Examples
# Request rate
sum(rate(http_requests_total[5m])) by (route)
# Error rate
sum(rate(http_errors_total[5m])) by (error_type)
# P95 latency
histogram_quantile(0.95, sum(rate(http_request_duration_bucket[5m])) by (le, route))
# Memory usage
process_resident_memory_bytes{service="next-app"}Logging Queries (Loki)
# Error logs
{service="next-app"} |= "error"
# Slow requests
{service="next-app"} |= "duration" | json | duration > "1000ms"
# Requests by user
{service="next-app"} | json | userId != ""Related skills
Forks & variants (1)
Nextjs Deployment has 1 known copy in the catalog totaling 21 installs. They canonicalize to this original listing.
- giuseppe-trisciuoglio - 21 installs
How it compares
Use nextjs-deployment for platform config; use nextjs-code-review when the task is auditing App Router code before deploy.
FAQ
What Next.js output mode does nextjs-deployment recommend for Docker?
Standalone output with multi-stage Docker builds, non-root user, and HTTP health checks on /api/health.
When should I use nextjs-deployment?
When configuring Docker, CI/CD, environment variables, health checks, or OpenTelemetry for Next.js production.
Is nextjs-deployment safe to install?
Review the Security Audits panel on this page before installing in production.