
Nextjs Deployment
- 21 installs
- 318 repo stars
- Updated June 22, 2026
- giuseppe-trisciuoglio/developer-kit-claude-code
This is a copy of nextjs-deployment by giuseppe-trisciuoglio - installs and ranking accrue to the original listing.
Helps with devops & ci/cd tasks.
About
nextjs-deployment is a Claude Code skill for devops & ci/cd. It helps solo builders move faster with AI-assisted development.
- nextjs-deployment
- DevOps & CI/CD
- AI-coding skill
Nextjs Deployment by the numbers
- 21 all-time installs (skills.sh)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit-claude-code --skill nextjs-deploymentAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 21 |
|---|---|
| repo stars | ★ 318 |
| Last updated | June 22, 2026 |
| Repository | giuseppe-trisciuoglio/developer-kit-claude-code ↗ |
What it does
Helps with devops & ci/cd tasks.
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 != ""