Deploy Next.js to Production: A Practical Guide

Khimananda Oli 9 min read Programming and Languages
Deploy Next.js to Production: A Practical Guide

By Khimananda Oli | Last reviewed: August 2026

Most teams struggle when they attempt to deploy Next.js to production because they treat it like a static site or a generic Node app without accounting for its hybrid rendering model. The framework’s default development server is unsuitable for live traffic, and skipping the standalone output mode leads to bloated containers and slow deployments. This guide provides the exact configuration patterns I use to ship resilient, audit-ready Next.js applications on self-managed infrastructure.

How do you configure Next.js standalone output for production?

The single most critical step when you deploy Next.js to production on your own servers is enabling the standalone output mode. Without this flag, Next.js expects the entire node_modules directory to be present at runtime, resulting in Docker images exceeding 1GB and deployment times stretching into minutes. The standalone mode uses Node.js tracing to identify exactly which files are needed and copies them into .next/standalone.

Source Codepages/ & app/node_modules/public/next.config.jsBuild Processnext buildTrace DependenciesGenerate StandaloneStandalone Output.next/standalone/server.js (~minimal)Required node_modulesStatic Assets
Next.js standalone build transforms full source into minimal production artifacts for efficient deployment

In your next.config.js, set the output explicitly:

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',
  // Enable compression for smaller payloads
  compress: true,
  // Security headers applied at the framework level
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          { key: 'X-Content-Type-Options', value: 'nosniff' },
          { key: 'X-Frame-Options', value: 'DENY' },
        ],
      },
    ]
  },
}

module.exports = nextConfig

After running next build, inspect the .next/standalone directory. You will notice it contains a server.js file and a trimmed node_modules folder. However, a common mistake is forgetting to copy the .next/static and public directories manually — the standalone tracer does not include these because they are served separately by your reverse proxy or CDN. If you skip this step, your CSS, JavaScript chunks, and images will return 404 errors in production.

What is the optimal Dockerfile for Next.js standalone builds?

A multi-stage Dockerfile is non-negotiable when you deploy Next.js to production. The goal is to separate build-time dependencies (TypeScript, ESLint, testing libraries) from the runtime image. I recommend a three-stage approach: base, builder, and runner. This pattern keeps the final image under 150MB and reduces your attack surface significantly.

# Stage 1: Base - Install dependencies
FROM node:22-alpine AS base
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable pnpm && pnpm fetch --prod

# Stage 2: Builder - Build the application
FROM base AS builder
WORKDIR /app
COPY . .
RUN corepack enable pnpm && pnpm install --frozen-lockfile
RUN pnpm build

# Stage 3: Runner - Minimal production image
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV HOSTNAME="0.0.0.0"
ENV PORT=3000

# Create non-root user for security compliance
RUN addgroup --system --gid 1001 nodejs \
    && adduser --system --uid 1001 nextjs

# Copy standalone output + static assets
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public

USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]

Several details here matter for operational stability. Setting HOSTNAME="0.0.0.0" is mandatory; without it, the Node server binds only to localhost inside the container and refuses external connections. Using pnpm over npm or yarn typically yields faster CI builds and more deterministic installs due to its content-addressable storage. Always run as a non-root user — this is a basic requirement for SOC 2 compliance and prevents container breakout exploits from gaining host-level privileges.

If your team needs deeper context on securing container environments before deploying, review our guide on Ubuntu security hardening for host-level defenses that complement container isolation.

How should Nginx reverse proxy be configured for Next.js?

Never expose the Next.js Node server directly to the internet. Nginx handles TLS termination, gzip/brotli compression, static file serving, rate limiting, and request buffering far more efficiently than Node. When you deploy Next.js to production behind Nginx, you also gain centralized access logging and the ability to perform zero-downtime reloads during deployments.

ClientHTTPS RequestBrowser / APINginx Reverse ProxyTLS TerminationGzip / Brotli CompressionStatic File ServingRate LimitingAccess LoggingRequest BufferingNext.js ContainerNode.js ServerPort 3000SSR + API Routes
Nginx reverse proxy handles TLS, compression, and static files while forwarding dynamic requests to Next.js

This configuration assumes your Next.js container runs on port 3000 locally:

upstream nextjs_backend {
    server 127.0.0.1:3000;
    keepalive 64;
}

server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # Serve static assets directly, bypassing Node
    location /_next/static/ {
        alias /var/www/nextjs/.next/static/;
        expires 365d;
        add_header Cache-Control "public, immutable";
    }

    location /public/ {
        alias /var/www/nextjs/public/;
        expires 30d;
    }

    # Proxy all other requests to Next.js
    location / {
        proxy_pass http://nextjs_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        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;
        proxy_buffering off;
        proxy_cache_bypass $http_upgrade;
    }

    # Gzip compression
    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml;
    gzip_min_length 1000;
}

The keepalive 64 directive maintains persistent connections between Nginx and Node, eliminating TCP handshake overhead on every request. Disabling proxy_buffering is essential for Server-Sent Events and streaming responses — if you use AI chat interfaces or real-time data feeds in your Next.js app, buffered proxies will break them. For teams managing database-backed Next.js apps, understanding PostgreSQL administration essentials ensures your data layer doesn't become the bottleneck after optimizing the web tier.

How do you automate Next.js deployment with CI/CD pipelines?

Manual deployments violate every principle of reliable operations. When you deploy Next.js to production repeatedly, you need an automated pipeline that builds, tests, scans, and ships without human intervention. Below is a GitHub Actions workflow I have refined across multiple client projects:

  1. Checkout and setup: Use actions/setup-node with exact Node version matching your Dockerfile base image.
  2. Dependency install: Run pnpm install --frozen-lockfile to guarantee reproducible builds.
  3. Quality gates: Execute linting, type checking, and unit tests before building. Fail fast.
  4. Docker build: Build the multi-stage image and tag with both Git SHA and latest.
  5. Security scan: Run Trivy or Snyk against the built image. Block deployment on critical CVEs.
  6. Push to registry: Upload to ECR, GHCR, or your private registry only after passing all gates.
  7. Deploy: SSH into the target server or trigger Kubernetes rollout with the new image tag.
name: Deploy Next.js
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: 'pnpm'

      - run: pnpm install --frozen-lockfile
      - run: pnpm lint && pnpm test
      - run: pnpm build

      - name: Build Docker image
        run: docker build -t myapp:${{ github.sha }} .

      - name: Security scan
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: myapp:${{ github.sha }}
          severity: CRITICAL,HIGH
          exit-code: '1'

      - name: Push and deploy
        run: |
          echo "${{ secrets.REGISTRY_TOKEN }}" | docker login -u deploy --password-stdin
          docker tag myapp:${{ github.sha }} registry.example.com/myapp:${{ github.sha }}
          docker push registry.example.com/myapp:${{ github.sha }}
          ssh deploy@prod-server "docker pull registry.example.com/myapp:${{ github.sha }} && docker compose up -d"

This pipeline enforces that no untested, unscanned code reaches production. The security scan step is particularly important for teams pursuing ISO 27001 or SOC 2 compliance — auditors will ask for evidence that you validate container images before deployment. Integrating observability early is equally vital; consider reading our comparison of metrics, logs, and traces to instrument your Next.js app before it hits production traffic.

Vercel vs self-hosted: which Next.js deployment strategy fits?

Choosing where to deploy Next.js to production depends on your team’s operational capacity, budget, and compliance requirements. Vercel offers zero-config deployments but at significant cost for high-traffic applications. Self-hosting demands more upfront work but provides predictable pricing and full control.

CriteriaVercel / Managed PlatformSelf-Hosted (Docker + Nginx)
Setup TimeMinutesHours to days
Monthly Cost (1M requests)$20–$100+$5–$20 VPS
Data Residency ControlLimited regionsFull control
Compliance (SOC 2, ISO)Vendor-dependentSelf-managed evidence
Edge FunctionsNative supportRequires Cloudflare/CDN
Custom Server MiddlewareRestrictedUnrestricted
Long-running RequestsTimeout limits (10–60s)No artificial limits
Start: Choose DeploymentNeed Data Residency or Compliance?Self-HostedDocker + Nginx + VPSVercel / ManagedZero-config, pay-per-useBudget-sensitive, long-running tasksSmall team, rapid iterationYesNo
Decision flowchart for selecting Vercel versus self-hosted Next.js deployment based on compliance and budget

For Nepal-based companies handling local user data or fintech applications, self-hosting on a regional VPS or colocated server often satisfies data residency requirements that managed platforms cannot meet. Conversely, early-stage startups validating product-market fit benefit from Vercel’s instant preview deployments and automatic edge caching. The right choice evolves as your traffic, team size, and regulatory obligations change.

Deploy Next.js to Production: Final Checklist and Next Steps

Successfully shipping Next.js requires treating it as a first-class server application, not an afterthought. Verify these items before going live:

  • Standalone output enabled and tested locally
  • Multi-stage Dockerfile producing images under 200MB
  • Nginx configured with proper upstream keepalive and static asset caching
  • TLS certificates automated via Let’s Encrypt or equivalent
  • CI/CD pipeline with security scanning blocking critical vulnerabilities
  • Health check endpoint (/api/health) wired to load balancer probes
  • Structured logging with correlation IDs for request tracing
  • Environment variables injected at runtime, never baked into images

If your team needs hands-on assistance architecting a production-grade Next.js deployment, implementing observability, or preparing for compliance audits, reach out to discuss your infrastructure needs. I help engineering teams build systems that are secure, observable, and ready for growth from day one.

Frequently Asked Questions

Vercel remains optimal for zero-config deployments. AWS Amplify or Cloudflare Pages suit teams needing infrastructure control. Self-hosting via Docker on Kubernetes works for air-gapped environments requiring full data sovereignty and custom networking configurations.

Set output to standalone in next.config.js. This bundles only required node_modules into a single folder, reducing image size significantly. Run node server.js directly instead of using next start to minimize runtime overhead and attack surface.

Yes, unless fully static. Server-side rendering and API routes need a Node.js runtime. Static exports can run on any CDN without a backend process, but dynamic features require an active server instance listening on a port.

Never commit secrets to git. Use platform-specific secret managers like AWS Secrets Manager or Vercel Environment Variables. Prefix public vars with NEXT_PUBLIC_. Inject private variables at runtime via container orchestration tools rather than baking them into build artifacts.

Hydration errors occur when server HTML differs from client render. Check for browser-only APIs used during SSR, non-deterministic date generation, or missing keys in lists. Use useEffect for client-side logic and ensure consistent initial state between environments.

ISR reduces build times for large content sites by revalidating pages on demand. It adds caching layer complexity and potential stale data risks. For small sites or real-time apps, standard SSR or full static generation often provides simpler, more predictable behavior.

Set Cache-Control headers in next.config.js or middleware. Use s-maxage for CDN caching and stale-while-revalidate for background updates. Avoid caching authenticated routes. Test with curl -I to verify headers match intended TTL and revalidation strategies before going live.

Absolutely. Use Docker with standalone mode on any cloud provider. Platforms like Railway, Fly.io, and Render offer managed Next.js hosting. Self-hosting requires configuring reverse proxies, SSL termination, and process managers like PM2 or systemd manually.

Production 500s often stem from missing environment variables, database connection timeouts, or unhandled promise rejections. Check platform logs first. Ensure all secrets are injected at runtime, not just build time. Validate external service connectivity from the production network context.

Large bundles increase cold start times and memory usage. Analyze with @next/bundle-analyzer. Implement dynamic imports for heavy components. Tree-shake unused libraries. Target under 250KB initial JS payload. Smaller bundles improve Time to First Byte and reduce serverless invocation costs.

Use connection pooling via Prisma Accelerate or Supavisor for serverless environments. Traditional pools exhaust connections during traffic spikes. Configure pool size based on platform concurrency limits. Always use read replicas for analytics queries to prevent blocking primary transactional workloads.

Self-hosted Next.js requires configuring sharp or installing @next/swc binaries. Set images.unoptimized to false and specify domains in remotePatterns. Consider offloading to Cloudinary or Imgix to reduce server CPU load and avoid storing processed variants locally.

Turbopack is stable for development but Webpack remains default for production builds. Monitor release notes for production readiness announcements. Test thoroughly in staging before switching. Build cache invalidation behavior may differ between bundlers affecting CI pipeline reliability.

Sentry captures runtime errors and performance traces natively. Datadog or Grafana Cloud provide infrastructure metrics. Enable OpenTelemetry instrumentation for distributed tracing across microservices. Set up alerting on error rates exceeding 1% and p95 latency thresholds specific to your SLA requirements.

Use GitHub Actions or GitLab CI with preview deployments for every PR. Run integration tests against preview URLs before merging. Implement blue-green or canary deployments for production. Pin dependency versions and scan for vulnerabilities. Rollback procedures must be tested quarterly to ensure recovery speed.