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:
- Connect your GitHub repository to Vercel
- Vercel auto-detects Next.js and configures build settings
- Every push to
maindeploys to production - 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
.envfiles 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_connectionslimits 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