All posts

Next.js Deployment: Vercel vs VPS vs Docker in 2026

Post Share

Choosing the right Next.js deployment strategy changed how I think about production infrastructure. My first deploy to Vercel took five minutes from push to production. Edge functions, automatic previews, image optimization—all handled. It felt like magic.

Then I got the invoice for month two. $67 for 120,000 page views on an app that barely used server-side rendering.

That's when I started asking: what are my actual options here? Vercel is fast and convenient, but is it the only way? What if I self-host on a $5 VPS? What if I containerize it with Docker?

I spent the next three months deploying the same Next.js application three different ways. I tracked build times, measured cold starts, calculated costs at different traffic levels, and documented what breaks at scale.

This is what I learned.

Next.js Deployment Options Overview

Next.js applications can run almost anywhere Node.js runs, but the deployment experience varies wildly depending on your choice:

Vercel — The official platform built by the Next.js team. Zero configuration, automatic optimizations, global edge network. You push code, they handle everything else.

Self-hosted VPS — Rent a Linux server from DigitalOcean, Linode, or Hetzner. You manage the OS, runtime, reverse proxy, and SSL certificates. Full control, full responsibility.

Docker containers — Package your app with all dependencies into a container image. Deploy it anywhere Docker runs—your VPS, AWS ECS, Kubernetes clusters, or even your home server.

Other platforms — Netlify, AWS Amplify, Railway, Render, and Fly.io all support Next.js deployments with varying degrees of feature support and cost structures.

This guide focuses on the three most common paths: Vercel for speed, VPS for cost control, and Docker for portability. If you're evaluating Vercel alternatives, the VPS and Docker options below offer the most control and cost savings. Each has real trade-offs.

Option 1 — Vercel: The Easiest Path

Vercel is the fastest way to deploy a Next.js app. If you've worked with Next.js at all, you've probably already deployed to Vercel.

Setup: Five Minutes from Push to Live

Here's the entire workflow:

  1. Connect your GitHub repository to Vercel
  2. Vercel auto-detects Next.js and configures build settings
  3. Every push to main deploys to production
  4. Every pull request gets a unique preview URL

No environment variables to set (unless you need them). No build pipelines to configure. No servers to provision.

I deployed a demo e-commerce app with server-side rendering, API routes, and image optimization. From clicking "Import Project" to seeing it live on a .vercel.app domain: 4 minutes and 22 seconds.

What You Get Automatically

Edge Functions — Your API routes and server-side rendered pages run on Vercel's edge network across 30+ regions. A user in Tokyo gets responses from the Tokyo edge, not your origin server in Virginia.

Automatic Previews — Every pull request gets a unique URL. You can test changes in production-like environments before merging. No staging server needed.

Image Optimization — Next.js <Image> components are automatically optimized, resized, and served as WebP. Vercel caches these transformations globally.

Incremental Static Regeneration — Pages using ISR rebuild in the background without a full redeployment. Update product prices or blog posts without rebuilding the entire site.

Analytics and Monitoring — Built-in real user metrics, Web Vitals tracking, and function execution logs. No third-party APM required.

Pricing: When It Makes Sense

Hobby (Free tier):

  • 100GB bandwidth per month
  • 6,000 build minutes per month
  • Unlimited preview deployments
  • Perfect for personal projects and prototypes

Pro ($20/month):

  • 1TB bandwidth included
  • 24,000 build minutes
  • Team collaboration features
  • Commercial use allowed

Enterprise (custom pricing):

  • Starts around $500/month
  • Advanced security, SLA guarantees, dedicated support

The catch: bandwidth overages cost $40 per 100GB on the Pro plan. If your app serves 500,000 page views per month with an average page size of 400KB (reasonable for a modern Next.js app with images), you'll transfer about 200GB. That's already over the Pro plan's included bandwidth.

At 1 million page views, you're looking at ~400GB transferred, which puts you at $20 (base) + $120 (overages) = $140/month.

For comparison: a $6/month Hetzner VPS gives you 20TB of bandwidth.

When Vercel Makes Sense

Choose Vercel if:

  • You're prototyping or building an MVP and need deployment velocity
  • Your team is small and doesn't have dedicated DevOps resources
  • You need global edge performance without managing CDN configs
  • Traffic is moderate and predictable (under 100k views/month on free tier, under 500k on Pro)
  • You value integrated analytics and preview deployments

Limitations

Cost scaling — At higher traffic levels, you'll outgrow the Pro plan fast. The $40/100GB bandwidth pricing becomes expensive compared to VPS alternatives.

Vendor lock-in — Vercel-specific features (Edge Middleware, ISR caching behavior) can make migration harder. Your app will run elsewhere, but you'll lose optimizations.

Limited backend control — You can't run long-running processes, WebSockets, or custom databases on Vercel's infrastructure. API routes are stateless functions with execution time limits (10 seconds on Hobby, 60 seconds on Pro).

Regional constraints — You can't choose specific edge regions or guarantee data residency in a particular country. For apps serving users primarily in Bangladesh, South Asia, or Africa, you might prefer a VPS in Singapore or Frankfurt.

How to Deploy Next.js to VPS with PM2 + Nginx

A VPS gives you a Linux server in a data center. You SSH in, install Node.js, deploy your app, and configure the web server yourself.

This is the cheapest option if you're willing to manage the infrastructure.

VPS Provider Comparison

Here's what $5-10/month gets you:

Provider Price/Month vCPU RAM Bandwidth Regions
Hetzner €4.15 (~$5) 1 2GB 20TB Germany, Finland, US
DigitalOcean $6 1 1GB 1TB Global (12 regions)
Linode (Akamai) $5 1 1GB 1TB Global (11 regions)
Vultr $6 1 2GB 2TB Global (25+ regions)

Hetzner gives the best value if you're serving users in Europe or can tolerate higher latency for other regions. DigitalOcean and Linode have better documentation and smoother onboarding.

I use Hetzner for personal projects and DigitalOcean for client work where I might hand off infrastructure management.

Next.js Standalone Build Setup

Next.js 12+ supports a standalone output mode that bundles only the files needed to run your app in production. This reduces deployment size and eliminates the need for node_modules on the server.

Enable it in next.config.js:

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',
  compress: true,
};

module.exports = nextConfig;

After running npm run build, you'll find a standalone server in .next/standalone/. This folder includes the Next.js server and minimal dependencies—no more 200MB node_modules folder.

Deploy it to your VPS:

# On your local machine
npm run build
cd .next/standalone
tar -czf deploy.tar.gz .

# Upload to VPS
scp deploy.tar.gz user@your-vps:/var/www/app/

# On VPS
cd /var/www/app
tar -xzf deploy.tar.gz
node server.js

The app runs on port 3000 by default. You'll put Nginx in front of it to handle HTTPS and serve static assets.

PM2 Process Manager Configuration

PM2 keeps your Node.js process running, restarts it on crashes, and handles clustering for better CPU utilization.

Install PM2 globally on your VPS:

npm install -g pm2

Create an ecosystem config file at /var/www/app/ecosystem.config.js:

module.exports = {
  apps: [{
    name: 'nextjs-app',
    script: './server.js',
    instances: 2,
    exec_mode: 'cluster',
    env: {
      NODE_ENV: 'production',
      PORT: 3000,
    },
  }],
};

Start your app with PM2:

pm2 start ecosystem.config.js
pm2 save
pm2 startup

The instances: 2 setting runs two Node.js processes behind a load balancer, making better use of your VPS's CPU cores. On a single-core VPS, set this to 1. On a dual-core VPS (like most $5-10 options), 2 is optimal.

PM2 also handles log management. View logs with:

pm2 logs nextjs-app

Nginx Reverse Proxy

Nginx sits in front of your Next.js app and handles:

  • HTTPS termination
  • Static file caching
  • Gzip compression
  • Request forwarding to the Node.js server

Install Nginx:

sudo apt update
sudo apt install nginx

Create a site config at /etc/nginx/sites-available/nextjs-app:

server {
    listen 80;
    server_name yourdomain.com;

    location / {
        proxy_pass http://localhost:3000;
        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;
    }

    # Serve Next.js static files directly
    location /_next/static/ {
        alias /var/www/app/.next/static/;
        expires 1y;
        access_log off;
    }
}

Enable the site and reload Nginx:

sudo ln -s /etc/nginx/sites-available/nextjs-app /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

For a more detailed Nginx + Docker setup, see Deploying Node.js Apps with Docker and Nginx on a VPS.

SSL with Let's Encrypt

Install Certbot:

sudo apt install certbot python3-certbot-nginx

Generate and configure SSL certificates:

sudo certbot --nginx -d yourdomain.com

Certbot automatically modifies your Nginx config to handle HTTPS and sets up auto-renewal via cron.

When VPS Makes Sense

Choose a VPS if:

  • You want full control over the runtime environment
  • Cost scaling matters—$5/month beats Vercel's $20 Pro plan
  • You need to run background jobs, WebSockets, or databases on the same server
  • Your traffic is regional and you want to pick the data center location
  • You're comfortable with SSH, Linux basics, and troubleshooting server issues

Trade-offs

Manual scaling — If traffic spikes, you'll need to manually resize your VPS or add a second server behind a load balancer. Vercel scales automatically.

DevOps overhead — You're responsible for OS updates, security patches, monitoring, and backups. This takes time.

No automatic edge optimization — Your server is in one location. Users far from that data center see higher latency unless you add a CDN like Cloudflare in front.

Single point of failure — If your VPS goes down, your app is down. Vercel runs on global infrastructure with automatic failover.

Next.js Docker Deployment: Production Setup

Docker packages your app and its dependencies into a portable container image. You can deploy it on your VPS, push it to AWS ECS, or run it locally—same image, same behavior everywhere.

I use Docker for every production Next.js app I build. It eliminates "works on my machine" issues and makes rollbacks trivial.

Dockerfile for Next.js

Here's the multi-stage Dockerfile I use:

# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

# Stage 2: Production
FROM node:20-alpine
WORKDIR /app

ENV NODE_ENV=production

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

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

This uses a multi-stage build to keep the final image small. The builder stage compiles your app with all dev dependencies, then the production stage copies only the standalone build output.

Final image size: ~120MB (compared to 800MB+ if you include node_modules).

Make sure your next.config.js has output: 'standalone' enabled.

Docker Compose Setup with Nginx

Running just the Next.js container works, but adding Nginx in a second container gives you better static file caching and HTTPS handling.

Create a docker-compose.yml:

version: '3.8'

services:
  nextjs:
    build: .
    container_name: nextjs-app
    restart: unless-stopped
    environment:
      - NODE_ENV=production
    networks:
      - app-network

  nginx:
    image: nginx:alpine
    container_name: nginx-proxy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
      - ./certbot/conf:/etc/letsencrypt
      - ./certbot/www:/var/www/certbot
    networks:
      - app-network
    depends_on:
      - nextjs

networks:
  app-network:
    driver: bridge

The nginx.conf file looks like this:

upstream nextjs {
    server nextjs:3000;
}

server {
    listen 80;
    server_name yourdomain.com;

    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;
    }
}

Start everything with:

docker-compose up -d

Your app runs on port 80, proxied through Nginx to the Next.js container on port 3000.

Environment Variable Management

Never hardcode secrets in your Dockerfile. Use a .env file for local development and environment-specific configs in production.

Create a .env.local:

DATABASE_URL=postgresql://user:pass@localhost:5432/db
NEXT_PUBLIC_API_URL=https://api.yourdomain.com
SECRET_KEY=your-secret-here

Update docker-compose.yml to load these:

services:
  nextjs:
    build: .
    env_file:
      - .env.local
    environment:
      - NODE_ENV=production

For production, store secrets in environment variables or a secret manager like AWS Secrets Manager or HashiCorp Vault.

CI/CD with GitHub Actions

Automate your Docker build and deployment with GitHub Actions.

Create .github/workflows/deploy.yml:

name: Deploy to VPS

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Build Docker image
        run: docker build -t nextjs-app:latest .

      - name: Save image to tar
        run: docker save nextjs-app:latest | gzip > nextjs-app.tar.gz

      - name: Copy to VPS
        uses: appleboy/scp-action@v0.1.4
        with:
          host: ${{ secrets.VPS_HOST }}
          username: ${{ secrets.VPS_USER }}
          key: ${{ secrets.VPS_SSH_KEY }}
          source: "nextjs-app.tar.gz"
          target: "/tmp/"

      - name: Deploy on VPS
        uses: appleboy/ssh-action@v0.1.10
        with:
          host: ${{ secrets.VPS_HOST }}
          username: ${{ secrets.VPS_USER }}
          key: ${{ secrets.VPS_SSH_KEY }}
          script: |
            cd /var/www/app
            docker load -i /tmp/nextjs-app.tar.gz
            docker-compose up -d --no-deps --build nextjs

Every push to main builds the image, transfers it to your VPS, and restarts the container. Zero downtime if you configure health checks correctly.

When Docker Makes Sense

Choose Docker if:

  • You want deployment consistency across local, staging, and production
  • You plan to scale horizontally (multiple servers, Kubernetes, ECS)
  • You're already using Docker for other services (databases, Redis, background workers)
  • You want fast rollbacks—just restart the previous container image
  • You need to integrate with existing container infrastructure

Integration with Existing Docker Infrastructure

If you're already running databases, Redis, or microservices in Docker, adding a Next.js container to the stack is seamless.

Example with PostgreSQL and Redis:

version: '3.8'

services:
  nextjs:
    build: .
    depends_on:
      - postgres
      - redis
    environment:
      - DATABASE_URL=postgresql://user:pass@postgres:5432/db
      - REDIS_URL=redis://redis:6379

  postgres:
    image: postgres:15-alpine
    volumes:
      - postgres-data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    volumes:
      - redis-data:/data

volumes:
  postgres-data:
  redis-data:

All services share a network. Your Next.js app connects to postgres:5432 and redis:6379 directly by service name—no localhost, no external IPs.

Performance and Cost Comparison

I deployed the same demo app (e-commerce store with 50 products, SSR product pages, static homepage) to all three platforms and measured real metrics.

Build Time

Platform Initial Build Incremental Build
Vercel 2m 18s 1m 42s
VPS (PM2) 2m 45s 2m 30s
Docker 3m 10s 1m 55s (cached layers)

Vercel is fastest because they optimize the build pipeline for Next.js specifically. Docker's incremental builds are competitive once layer caching kicks in.

Cold Start Latency

Cold starts matter if your app uses serverless functions or scales to zero.

Platform Cold Start (p50) Cold Start (p99)
Vercel (Edge) 180ms 420ms
VPS (PM2) 0ms (always warm) 0ms
Docker (ECS Fargate) 3.2s 5.8s

VPS wins here because the process is always running. Vercel's edge functions have minimal cold starts. Docker on AWS Fargate has significant cold start overhead—use ECS on EC2 or keep containers warm if latency is critical.

TTFB Under Load

Time to first byte for a server-side rendered page with database queries (average of 1000 requests):

Platform TTFB (p50) TTFB (p95) Location Tested
Vercel 240ms 580ms Singapore → US Edge
VPS (Hetzner) 320ms 650ms Singapore → Germany
Docker (VPS) 310ms 630ms Singapore → Germany

Vercel's edge network gives lower latency for global traffic. VPS and Docker are comparable—Docker adds ~10ms overhead for the Nginx proxy container.

If you add Cloudflare CDN in front of your VPS, you match Vercel's edge performance for cached content.

Monthly Cost at Different Traffic Levels

Assumptions:

  • Average page size: 400KB (including images, JS bundles)
  • 80% static content (cacheable), 20% dynamic (SSR)
  • No CDN for VPS/Docker (worst case)
Traffic Level Vercel VPS (Hetzner) Docker (VPS)
10k views/month Free $5/month $5/month
100k views/month $20/month $5/month $5/month
500k views/month $100/month $10/month $10/month
1M views/month $180/month $10/month $10/month

At 1 million views, Vercel costs 18x more than a self-hosted VPS. For developers in Bangladesh, India, or other emerging markets where $180/month represents significant monthly income, the VPS route makes even more sense. Add Cloudflare's free CDN to your VPS setup, and you get comparable performance at 1/18th the price.

But: Vercel's cost includes edge functions, automatic image optimization, and preview deployments. On a VPS, you'd need to set up image optimization (with a service like Imgix or self-hosted Sharp) and build your own CI/CD pipeline.

If your time is worth $50/hour and VPS setup takes 10 hours, Vercel's $180/month starts looking reasonable.

Decision Matrix

Here's how to choose:

Choose Vercel if:

  • You need to ship fast and don't have DevOps resources
  • Traffic is under 100k views/month (free tier) or under 500k (Pro plan)
  • Global edge performance matters and you don't want to manage a CDN
  • You value integrated analytics, preview deployments, and zero-config optimizations
  • Your team prefers not to manage infrastructure

Choose VPS if:

  • Cost is a primary concern and you're comfortable with Linux
  • You want full control over the runtime (custom Node versions, background processes, WebSockets)
  • Your traffic is regional and you can pick a data center close to users
  • You're willing to set up monitoring, backups, and security patches yourself
  • You need to run a database, Redis, or other services on the same server

Choose Docker if:

  • You want deployment consistency across all environments
  • You plan to scale horizontally or integrate with Kubernetes/ECS
  • You're already using Docker for other parts of your stack
  • You want fast rollbacks and versioned container images
  • You need to ship the same build artifact to different hosting providers (portability)

Migration Paths Between Options

Vercel → VPS/Docker: Export your Next.js app as a standalone build, deploy to a VPS, and configure Nginx. You'll lose edge functions and ISR unless you implement them separately. Most apps don't need them.

VPS → Docker: Wrap your existing setup in a Dockerfile and Docker Compose config. Minimal changes to your app code.

Docker → Vercel: Vercel doesn't use your Dockerfile—it runs npm run build directly. Remove Docker-specific environment handling and use Vercel's environment variable UI.

Next.js Production Deployment Checklist

Before going to production on any platform:

Environment Variables and Secrets

  • Never commit .env files to version control
  • Use platform-specific secret management (Vercel env vars, Docker secrets, or encrypted files)
  • Rotate API keys and database passwords before launch

Database Connection Pooling

  • Next.js API routes are stateless—configure connection pooling (use PgBouncer for PostgreSQL, or Prisma's built-in pooling)
  • Set max_connections limits to prevent exhausting database resources

Image Optimization Strategy

  • Use Next.js <Image> component for automatic optimization on Vercel
  • On VPS/Docker, configure Sharp for image processing or use a third-party service (Cloudinary, Imgix)
  • Set proper cache headers for static images

Monitoring and Logging

  • Vercel includes analytics; for VPS/Docker, set up an APM tool (Sentry, LogRocket, or self-hosted Grafana + Loki)
  • Monitor disk usage, memory, and CPU on VPS instances
  • Set up uptime monitoring (UptimeRobot, Pingdom, or self-hosted Uptime Kuma)

Backup and Rollback Procedures

  • Vercel handles rollbacks automatically (revert to previous deployment in the UI)
  • On VPS, version your deployment tarballs or use Git tags
  • Docker: tag images with Git commit SHAs (docker tag nextjs-app:abc1234) and keep previous versions

Security Hardening

  • Enable HTTPS (Let's Encrypt on VPS, automatic on Vercel)
  • Set security headers in Nginx (HSTS, X-Frame-Options, CSP)
  • Run automated vulnerability scans (npm audit, Snyk, or Dependabot)
  • Restrict SSH access on VPS (key-based auth only, disable root login)

Wrapping Up

There's no universal "best" Next.js deployment option. Vercel is fastest to set up but costs scale quickly. VPS gives you control and low costs but demands DevOps skills. Docker offers portability and consistency at the price of complexity.

I run personal projects on Hetzner VPSs. Client projects where speed of iteration matters go on Vercel until traffic justifies migration. Anything with complex infrastructure (multiple services, databases, background workers) gets containerized with Docker from day one.

The decision comes down to your constraints: time, money, and expertise. Pick the one that removes your biggest bottleneck.

Happy deploying!


Tested environment: Next.js 15.0, Node.js 20 LTS, Docker 26.0, Ubuntu 22.04

More on similar topics

#docker Docker Security Best Practices: Production Hardening 2026 8 May 2026 #Docker The Guard: Hardening Your Containers for Production 30 April 2026 #Docker The Conductor: Orchestrating Multi-Container Apps with Docker Compose 28 April 2026