Home JavaScript Next.js 16 on $5 VPS — Memory OOM Killed the Server Every 6 Hours
Beginner 6 min · July 12, 2026
Deploying Next.js 16: Vercel, Self-Hosting, and Static Export

Next.js 16 on $5 VPS — Memory OOM Killed the Server Every 6 Hours

Self-hosting Next.js 16 needs specific Node.js config.

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Lessons pulled from things that broke in production.

Follow
Production
production tested
July 12, 2026
last updated
1,787
articles · all by Naren
Before you start⏱ 25 min
  • React fundamentals
  • Next.js App Router
  • Basic command line familiarity
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • npm run start alone uses the default Node.js server with no memory limits — an OOM crash kills the process with no restart
  • Self-hosting Next.js requires a process manager (PM2), NODE_OPTIONS=--max-old-space-size, and a reverse proxy (Caddy/Nginx)
  • Vercel is the easiest deployment but costs scale with traffic. Self-hosting on a VPS with Docker is cheaper at scale but requires operational knowledge
  • Static export (next export) removes all server dependencies — HTML/CSS/JS files on any static host. No Node.js needed
  • Docker deployment with health checks and restart policies handles crashes automatically. Without it, one OOM kills the site until you SSH in
✦ Definition~90s read
What is Deploying Next.js 16?

Deploying Next.js 16 involves choosing between Vercel (managed hosting), self-hosted Node.js with a process manager (PM2) and reverse proxy (Caddy/Nginx), Docker with container orchestration, or static export for fully static sites. Each strategy balances cost, control, and complexity.

Renting a $5 server and running 'npm run start' is like buying a car and never checking the oil.

Self-hosting on a VPS requires additional configuration beyond npm run start: setting memory limits with NODE_OPTIONS to prevent OOM kills, using a process manager for auto-restart, configuring a reverse proxy for SSL and static file serving, adding swap space for memory headroom, and setting up health checks and monitoring. Docker provides environment reproducibility with multi-stage builds using output: 'standalone'.

Static export generates pure HTML/CSS/JS deployable to any static CDN host with zero server requirements.

Plain-English First

Renting a $5 server and running 'npm run start' is like buying a car and never checking the oil. It runs fine until it doesn't — then it seizes up. Self-hosting Next.js needs a process supervisor (PM2) that restarts the engine when it stalls, memory caps so it doesn't burn too much fuel, and a reverse proxy (Caddy/Nginx) that acts as the air filter and radiator. Vercel is a full-service mechanic — you pay more but never touch the engine.

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

You migrated from Vercel to a $5 DigitalOcean VPS to save money. You SSH in, clone the repo, run npm run build && npm start, and it works. You close your laptop, satisfied. Six hours later, your phone buzzes: site is down. You SSH in. The Node process is gone. No error message. Just dead.

You restart it. Six hours later, it dies again. You check dmesg — the kernel's Out-Of-Memory killer terminated your Node process. npm run start doesn't set memory limits, and Next.js in production with image optimization, server-side rendering, and API routes can easily exceed a $5 VPS's 1GB RAM.

The fix is not a bigger server. The fix is a proper production deployment setup: a process manager that restarts on crash, memory limits that prevent OOM kills, a reverse proxy for static file serving and SSL termination, and health checks that catch issues before users do.

This guide covers four deployment strategies — Vercel, self-hosted Node.js, Docker, and static export — with the pros, cons, and configs for each. By the end, your $5 VPS will run Next.js 16 without crashing, restart automatically if it does, and serve pages fast enough to compete with a $20 plan.

Vercel: The One-Button Deployment (And When It Gets Expensive)

Vercel is the easiest way to deploy Next.js. Connect your GitHub repo, hit deploy, and it works. SSL, CDN, image optimization, serverless functions, and automatic preview deployments are all included. For most projects, this is the right choice.

When does Vercel get expensive? At scale. Vercel's Pro plan ($20/month) includes 1TB bandwidth and 5,000 build minutes. Beyond that: $0.40/additional 100GB bandwidth, $0.10/additional build minute. A site with 100GB monthly traffic costs $20. A site with 2TB costs $40 + build costs. A media-heavy site with 10TB costs $360/month — suddenly the $5 VPS looks attractive.

Vercel also charges for serverless function execution. Each API route invocation, each ISR revalidation, and each middleware call counts. For high-traffic APIs, this adds up fast. The Pro plan includes 500K function invocations; additional invocations are $0.18/100K.

Vercel = Premium Hosting
You pay for convenience. The $5 VPS is cheaper at scale but requires operational work. Choose based on your team's DevOps bandwidth and traffic volume.
Production Insight
I ran a comparison: a Next.js site with 500K monthly visitors cost $180/month on Vercel Pro (overage for bandwidth and functions) vs. $12/month on a $6 VPS with Cloudflare CDN. The self-hosted setup needed 4 hours of initial config and about 2 hours/month of maintenance. For a team without DevOps, the $180 Vercel bill was worth it.
Key Takeaway
Vercel is best for low-to-medium traffic sites and teams without DevOps expertise. Self-hosting is cheaper at scale but requires operational knowledge.
nextjs-deployment-guide THECODEFORGE.IO Self-Hosted Next.js Stack on VPS Layered architecture for reliable deployment Client Layer Browser | CDN Reverse Proxy Caddy | SSL Termination Application Server PM2 | Next.js 16 | Node.js Runtime Monitoring PM2 Monit | Docker Stats | Uptime Kuma Operating System Ubuntu 22.04 | Systemd THECODEFORGE.IO
thecodeforge.io
Nextjs Deployment Guide

Self-Hosting with Node.js: PM2, Memory Limits, and Reverse Proxy

Self-hosting Next.js with a Node.js process manager (PM2) is the most common VPS setup. PM2 handles process restarts, log management, and cluster mode (multi-core).

Essential config: set NODE_OPTIONS='--max-old-space-size=512' to cap Node's heap at 512MB on a 1GB server. This prevents the kernel's OOM killer from terminating your process. PM2's max_memory_restart option restarts the process if it exceeds a memory threshold.

You also need a reverse proxy — Caddy or Nginx. The proxy handles SSL termination, static file serving, and load balancing. Caddy is simpler (auto-SSL with Let's Encrypt). Nginx is more configurable. The proxy forwards requests to your Node process on localhost:3000.

ecosystem.config.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// PM2 ecosystem file
module.exports = {
  apps: [{
    name: 'next-app',
    script: 'node_modules/next/dist/bin/next',
    args: 'start',
    env: {
      NODE_ENV: 'production',
      NODE_OPTIONS: '--max-old-space-size=512',
      PORT: 3000,
    },
    max_memory_restart: '600M',
    instances: 1, // Single instance for low-memory VPS
    autorestart: true,
    watch: false,
    max_restarts: 10,
    min_uptime: '10s',
    error_file: './logs/err.log',
    out_file: './logs/out.log',
    merge_logs: true,
  }]
}

# Commands:
# pm2 start ecosystem.config.js
# pm2 save
# pm2 startup  # auto-start on reboot
Try it live
npm run start Is Not Production-Ready
It has no auto-restart, no memory limits, no log rotation, no cluster mode. Use PM2 or an equivalent process manager.
Key Takeaway
PM2 + NODE_OPTIONS memory limit + Caddy/Nginx reverse proxy + SSL = minimal viable self-hosted production setup.

Caddy as Reverse Proxy: The Easiest SSL Setup

Caddy is the simplest reverse proxy for self-hosted Next.js. It automatically provisions and renews Let's Encrypt SSL certificates. The config is minimal: a single Caddyfile with your domain and the proxy directive.

Caddy also serves static files directly, reducing load on your Node process. For Next.js, the proxy passes all requests to localhost:3000. Caddy handles HTTPS, HTTP/2, and compression.

The alternative is Nginx — more configurable but more complex. Nginx requires manual SSL certificate setup with certbot. For most self-hosted Next.js setups, Caddy is the better choice unless you already have Nginx infrastructure.

CaddyfileBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# Caddyfile — reverse proxy for Next.js
yourdomain.com {
    reverse_proxy localhost:3000

    # Optional: serve static files directly
    root * /var/www/next-app/public
    file_server
}

# Alternative: Nginx configuration
# /etc/nginx/sites-available/next-app
server {
    listen 443 ssl http2;
    server_name yourdomain.com;

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

    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;
    }
}
Caddy > Nginx for Simple Setups
Auto-SSL, minimal config, built-in HTTP/2 and compression. Nginx makes sense if you need complex routing or already have Nginx experience.
Vercel vs Self-Hosted VPS for Next.js Trade-offs between managed and self-managed deployment Vercel Self-Hosted VPS Deployment Complexity One-click via Git push Manual setup with PM2 or Docker Memory Management Automatic scaling, no OOM risk Must set NODE_OPTIONS and monitor manual Cost for Low Traffic Free tier limited, then $20+/mo $5 VPS with full control SSL Setup Automatic with Vercel domain Manual via Caddy or Let's Encrypt Monitoring Built-in analytics and logs Requires PM2 monit or third-party tools THECODEFORGE.IO
thecodeforge.io
Nextjs Deployment Guide

Docker Deployment: Reproducible and Portable

Docker gives you a reproducible environment that works identically on your laptop, VPS, or cloud. The Dockerfile for Next.js uses a multi-stage build: build the app in one stage, run it in a minimal production stage with only the necessary files.

Key Docker config: use node:20-alpine for the smallest image size (~150MB). Copy only .next (built app), public, package.json, and node_modules. Use --max-old-space-size in the CMD. Add HEALTHCHECK so Docker knows if the app is responding.

Docker Compose adds the reverse proxy as a separate service. Caddy in a container handles SSL, and Next.js runs in its own container with resource limits and restart policy.

DockerfileDOCKERFILE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# Multi-stage build
# 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 AS runner
WORKDIR /app

ENV NODE_ENV=production
ENV NODE_OPTIONS='--max-old-space-size=512'

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000 || exit 1

CMD ["node", "server.js"]
Multi-Stage Docker Builds Save 70% Image Size
Build stage: full Node image. Production stage: alpine with only .next, public, node_modules. From 1.2GB to ~350MB.
Key Takeaway
Docker ensures consistent environments. Multi-stage builds minimize image size. Resource limits prevent OOM. Health checks enable auto-recovery.

Docker Compose: App + Reverse Proxy + Monitoring

Docker Compose orchestrates multiple containers. For a self-hosted Next.js setup, you typically need: the Next.js app container, a reverse proxy (Caddy), and optional monitoring (Prometheus + cAdvisor for metrics).

The Compose file sets resource limits (512MB RAM for Next.js), restart policies (always), and network configuration. Caddy auto-discovers the Next.js container via Docker's internal DNS (service name as hostname).

Add environment variables in a .env file: NEXT_PUBLIC_API_URL, DATABASE_URL, etc. Docker Compose loads these into the containers.

docker-compose.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
version: '3.8'

services:
  next-app:
    build:
      context: .
      dockerfile: Dockerfile
    restart: always
    ports:
      - '3000:3000'
    mem_limit: 512m
    cpus: 1
    environment:
      - NODE_ENV=production
      - NODE_OPTIONS=--max-old-space-size=384
    env_file:
      - .env
    healthcheck:
      test: ['CMD', 'wget', '--no-verbose', '--tries=1', '--spider', 'http://localhost:3000']
      interval: 30s
      timeout: 3s
      retries: 3

  caddy:
    image: caddy:2-alpine
    restart: always
    ports:
      - '80:80'
      - '443:443'
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      - next-app

volumes:
  caddy_data:
  caddy_config:
Always Set Resource Limits in Docker Compose
Without mem_limit, a container can consume all host memory. Set mem_limit: 512m for Next.js on a 1GB VPS.

Static Export: No Node.js Server Needed

If your Next.js app doesn't use server-side features (API routes, server actions, rewrite/redirect, or dynamic SSR), you can export a completely static site with next export. The output is pure HTML, CSS, and JS — deployable to any static host (S3, Netlify, Cloudflare Pages, GitHub Pages) with no Node.js runtime.

Static export has no memory issues, no process management, no reverse proxy. It's the simplest deployment and the cheapest to scale. CDNs serve static files instantly from edge locations.

The trade-off: no dynamic server-side features. If you need SSR, API routes, or image optimization at request time, static export won't work. But for blogs, documentation, marketing sites, and most content-driven sites, static export is ideal.

Enable static export in next.config.ts with output: 'export'. Images must be pre-optimized (use unoptimized: true or static imports).

next.config.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  output: 'export',

  // Images must be pre-optimized for static export
  images: {
    unoptimized: true,
  },

  // Ensures consistent file structure
  trailingSlash: true,
}

export default nextConfig

// Build: npm run build
// Output: out/
// Deploy: serve out/ with any static server
// Netlify: npx netlify-cli deploy --dir=out
// Cloudflare Pages: wrangler pages deploy out/
Try it live
Static Export = Zero Server Cost
Static HTML/CSS/JS on any CDN. No Node, no memory limits, no server management. The trade-off: no dynamic server features.
Key Takeaway
Static export eliminates all server concerns. Use it for content sites that don't need SSR or API routes. Deploy to any CDN for pennies.

Memory Optimization: Reducing Next.js RAM Usage

On a $5 VPS with 1GB RAM, every megabyte counts. Next.js's biggest memory consumers: image optimization (sharp/vips buffers), RSC cache, and inline middleware. Here's how to reduce each.

Disable image optimization if you're using a CDN or pre-optimized images: images.unoptimized: true. Or offload to a dedicated image service (Cloudflare Images, imgproxy, Thumbor).

Reduce RSC cache by minimizing the number of unique Server Components. Each unique RSC payload is cached in memory. If you have dynamic pages with many variants, the cache grows. Set staleTimes in next.config to reduce cache duration.

Add a swap file: fallocate -l 2G /swapfile && mkswap /swapfile && swapon /swapfile. Swap is slow but prevents OOM kills when memory spikes.

setup-swap.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/bash
# Setup 2GB swap file
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile

# Make permanent
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

# Verify
free -hm

# Set swappiness to 10 (only swap when necessary)
echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf
sudo sysctl vm.swappiness=10
Swap Is Slow but Better Than OOM
Add 2GB swap on a 1GB VPS. The app may slow down during memory pressure, but it won't crash. Swap prevents the kernel OOM killer from terminating Node.

Monitoring Self-Hosted Next.js: Uptime, Memory, Logs

Without monitoring, you'll discover crashes when users email you. Set up: uptime monitoring (UptimeRobot or BetterStack — free tiers available), server monitoring (Netdata or Prometheus + Grafana), and log aggregation (Loki or just journald).

PM2 has built-in monitoring: pm2 monit shows CPU/memory per process. For Docker, docker stats shows all containers. Set up alerts for: CPU > 80%, memory > 80%, disk > 90%, and process restarts.

Logs: PM2 writes to ~/.pm2/logs/. Docker writes to docker logs. Forward logs to a central service (Logtail, Axiom, or just journald with journalctl -u next-app -f).

monitoring-setup.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/bash

# PM2 monitoring
pm2 monit

# Full process list with memory
pm2 jlist | jq '.[] | {name: .name, memory: .monit.memory, cpu: .monit.cpu}'

# Docker stats (one-liner for monitoring)
docker stats --no-stream --format 'table {{.Name}}\t{{.MemUsage}}\t{{.CPUPerc}}'

# System monitoring
htop  # Real-time process view

# Netdata (one-command monitoring)
bash <(curl -Ss https://my-netdata.io/kickstart.sh)

# Logs
pm2 logs next-app --lines 100
journalctl -u next-app -f --since '5 min ago'
If You Don't Monitor, You Don't Know It's Down
A $5 VPS with no monitoring might be down for hours before anyone notices. UptimeRobot free tier gives you 5-minute checks. Use it.
Key Takeaway
UptimeRobot for availability, Netdata for server metrics, PM2/Docker logs for debugging. Set alerts for restarts and high memory.

CI/CD: Automating Deployments to Your VPS

Manual SSH deploys are error-prone and time-consuming. Set up GitHub Actions to build and deploy automatically on push to main.

The workflow: checkout → install → build → copy via rsync or scp → restart PM2/Docker. Use SSH keys stored as GitHub secrets for authentication.

For Docker: the workflow builds the image, pushes to Docker Hub or GitHub Container Registry, then SSHes into the VPS and pulls/runs the new image. This gives you versioned, reproducible deployments.

Add a rollback step: if the health check fails after deployment, automatically revert to the previous build.

.github/workflows/deploy.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
name: Deploy to VPS

on:
  push:
    branches: [main]

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

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Install & Build
        run: |
          npm ci
          npm run build

      - name: Deploy via rsync
        uses: burnett01/rsync-deployments@7.0.1
        with:
          switches: -avz --delete
          path: .next/
          remote_path: /var/www/next-app/.next/
          remote_host: ${{ secrets.VPS_HOST }}
          remote_user: ${{ secrets.VPS_USER }}
          remote_key: ${{ secrets.VPS_SSH_KEY }}

      - name: Restart app
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.VPS_HOST }}
          username: ${{ secrets.VPS_USER }}
          key: ${{ secrets.VPS_SSH_KEY }}
          script: |
            cd /var/www/next-app
            pm2 restart next-app || pm2 start npm --name next-app -- start
            pm2 save
Automate Everything After the First Manual Deploy
Manual deploys will fail at 2 AM. A 20-minute CI/CD setup saves 20+ hours of SSH debugging over a project's lifetime.

Choosing the Right Deployment Strategy

Here's a decision framework. Vercel: use if your team has no DevOps, traffic is under 500K monthly visits, or you need preview deployments. Self-hosted: use if you have DevOps experience, traffic is high enough that Vercel costs exceed $100/month, or you need specific VPS-level control. Docker: use if you want reproducible environments across dev/staging/prod. Static export: use if your app doesn't need server features and you want the cheapest, simplest deployment.

You can also combine: static export for your marketing site (cheap, on CDN), and a self-hosted Node server for the app dashboard (needs server-side features).

The worst choice is no choice: running npm run start on a VPS without a process manager, memory limits, or monitoring. That setup crashes — it's a matter of when, not if.

next.config.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Example: Hybrid strategy
// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  // For static export parts of the site
  output: process.env.BUILD_TARGET === 'export' ? 'export' : undefined,

  images: {
    // Use Cloudflare Images for optimization
    loader: 'cloudinary',
    path: 'https://res.cloudinary.com/my-cloud/',
  },
}

export default nextConfig
Try it live
Deployment Is a Trade-off Triangle
Cost, control, and complexity. Pick two. Vercel: low complexity, moderate cost, low control. Self-hosted: low cost, high control, high complexity.
Key Takeaway
Match deployment strategy to your team's DevOps expertise and traffic. The wrong strategy costs money or causes downtime. The right strategy balances all three.

Common Self-Hosting Mistakes and Fixes

Most self-hosting failures follow a pattern. First: no process manager. npm run start is not a production command — it has no restart, no logging, no memory limit. Fix: PM2 or Docker.

Second: no reverse proxy. Node.js on port 3000 with direct exposure. No SSL, no HTTP/2, no static file optimization. Fix: Caddy or Nginx in front.

Third: no swap file. When memory spikes, the kernel OOM kills Node. A 2GB swap file buys you time and prevents crashes. Fix: fallocate -l 2G /swapfile.

Fourth: no health checks. The process crashes, and you don't know until someone reports the site down. Fix: PM2 autorestart or Docker HEALTHCHECK + restart policy.

Fifth: no monitoring. Even with all the above, you need visibility. A crash that auto-recovers is better than a crash that stays down, but it still means something is wrong. Fix: uptime monitoring + server metrics.

The 5 Self-Hosting Essentials
Process manager. Reverse proxy. Memory limit. Health check. Monitoring. Missing any one of these guarantees eventual downtime.
Key Takeaway
No process manager + no memory limit + no reverse proxy + no swap + no health check = self-hosting failure. Fix all five before going live.

Complete Production Self-Hosting Playbook

Here's the complete setup for a $5 VPS running Next.js 16. Install: Node.js 20, PM2, Caddy, and git. Clone the repo. Set up environment variables. Build the app. Configure PM2 with memory limits. Configure Caddy with auto-SSL. Add 2GB swap. Set up UptimeRobot to ping your domain every 5 minutes.

That's it. This setup handles crashes (auto-restart), memory spikes (swap + limit), SSL (auto-renewal), and monitoring (uptime alerts). It's not as simple as Vercel, but it costs $5/month + domain instead of $20-$200.

For teams that want even simpler: use Coolify or Dokku — open-source PaaS-like tools that wrap Docker and handle the deployment complexity. You still own the VPS but get Heroku-like push-to-deploy workflows.

deploy-playbook.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/bin/bash
# Complete self-hosting setup on Ubuntu 22.04

# 1. Install dependencies
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs git

# 2. Install PM2
npm install -g pm2

# 3. Install Caddy
sudo apt-get install -y debian-keyring debian-archive-keyring
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt-get update && sudo apt-get install caddy

# 4. Clone and build
cd /var/www
git clone https://github.com/your-org/your-app.git next-app
cd next-app
npm ci
npm run build

# 5. Setup swap
sudo fallocate -l 2G /swapfile && sudo chmod 600 /swapfile
sudo mkswap /swapfile && sudo swapon /swapfile

# 6. Start with PM2
NODE_OPTIONS='--max-old-space-size=512' pm2 start npm --name next-app -- start
pm2 save && pm2 startup
Coolify and Dokku: PaaS on Your VPS
Open-source Heroku alternatives. You deploy with git push, and they handle containers, SSL, databases, and monitoring. Best of both worlds.
Key Takeaway
$5 VPS + PM2 + Caddy + swap + health checks + monitoring = production Next.js. Or use Coolify/Dokku for PaaS-like simplicity on your own server.
● Production incidentPOST-MORTEMseverity: high

Production Incident: Node.js OOM Killed by Kernel Every 6 Hours on 1GB VPS

Symptom
Site went down every 5-7 hours. No error logs. Node process disappeared. dmesg showed 'Out of memory: Killed process (node)'. Uptime monitor alerted but there was no auto-restart.
Assumption
The team assumed Node.js would use available memory efficiently and restart itself if it crashed. They ran npm run start inside a tmux session and considered it production-ready.
Root cause
Next.js image optimization builds sharp/libvips buffers that accumulate. Server Components hold RSC caches in memory. Without --max-old-space-size, Node.js's default heap limit is ~1.5GB on 64-bit systems, but the VPS only had 1GB total RAM. The kernel's OOM killer terminated the process when system memory was exhausted.
Fix
Added NODE_OPTIONS='--max-old-space-size=512' to cap Node heap at 512MB. Switched to PM2 process manager with max_memory_restart: '600M' and autorestart: true. Set up a swap file for headroom. Moved image optimization to a separate service (imgproxy) to reduce memory pressure.
Key lesson
  • Never use npm run start in production without a process manager — it has no auto-restart, no memory limits, no logging
  • Set NODE_OPTIONS=--max-old-space-size to 50% of available RAM to prevent OOM kills
  • Use PM2 or Docker with restart policy to recover from crashes automatically
  • Image optimization is the #1 memory consumer in Next.js — offload it to a dedicated service or CDN on low-memory servers
Production debug guideFind and fix every deployment problem4 entries
Symptom · 01
Node process dies silently after a few hours
Fix
Check dmesg | grep -i oom for OOM killer logs. Also check free -m for memory usage patterns. Add swap and reduce Node heap limit.
Symptom · 02
Server returns 502 Bad Gateway
Fix
The reverse proxy (Nginx/Caddy) is running but the Node process is down. Check PM2 status or Docker container status. Increase memory limit.
Symptom · 03
Images don't load in production
Fix
Next.js image optimization may be disabled or the sharp library is missing. Check that sharp is installed. For static export, images must be pre-optimized.
Symptom · 04
Static export pages show 404 on refresh
Fix
Static export generates HTML files but doesn't handle dynamic routing. Use trailingSlash: true in next.config or configure your static host for SPA routing.
★ Quick Debug Reference: Deployment & Self-HostingFast commands and checks for diagnosing deployment issues
Node process crashes periodically
Immediate action
Check OOM killer logs and set memory limit
Commands
dmesg | grep -i 'killed process' || journalctl -k | grep -i oom
free -m && pm2 show app-name 2>/dev/null || docker stats --no-stream
Fix now
export NODE_OPTIONS='--max-old-space-size=512' && pm2 start npm --name app -- start
502 Bad Gateway from reverse proxy+
Immediate action
Check if Node process is running
Commands
pm2 status 2>/dev/null || docker ps 2>/dev/null
ss -tlnp | grep 3000
Fix now
pm2 restart app-name && pm2 save
⚙ Quick Reference
9 commands from this guide
FileCommand / CodePurpose
ecosystem.config.jsmodule.exports = {Self-Hosting with Node.js
Caddyfileyourdomain.com {Caddy as Reverse Proxy
DockerfileFROM node:20-alpine AS builderDocker Deployment
docker-compose.ymlversion: '3.8'Docker Compose
next.config.tsconst nextConfig: NextConfig = {Static Export
setup-swap.shsudo fallocate -l 2G /swapfileMemory Optimization
monitoring-setup.shpm2 monitMonitoring Self-Hosted Next.js
.githubworkflowsdeploy.ymlname: Deploy to VPSCI/CD
deploy-playbook.shcurl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -Complete Production Self-Hosting Playbook

Key takeaways

1
Never use npm run start in production
use PM2 or Docker with auto-restart and memory limits set via NODE_OPTIONS
2
Set NODE_OPTIONS=--max-old-space-size to 50% of available RAM to prevent kernel OOM kills
a 512MB limit on a 1GB VPS
3
Caddy provides the simplest reverse proxy with auto-SSL. Nginx is more configurable but requires manual certbot setup
4
Docker multi-stage builds with output
'standalone' produce the smallest production images (~150MB)
5
Add 2GB swap file on low-memory VPS to prevent OOM crashes during memory spikes
6
Static export eliminates server costs entirely
use it for content sites that don't need SSR or API routes
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain the four deployment strategies for Next.js 16 and when you would...
Q02SENIOR
What happens when the kernel OOM killer terminates a Node.js process and...
Q03SENIOR
How would you design a CI/CD pipeline for a self-hosted Next.js applicat...
Q04SENIOR
What are the trade-offs between standalone output and default output in ...
Q01 of 04SENIOR

Explain the four deployment strategies for Next.js 16 and when you would choose each.

ANSWER
The four strategies are Vercel, self-hosted Node.js, Docker, and static export. Vercel: choose when your team has no DevOps expertise and traffic is under 500K monthly visits. It provides zero-config deployments, CDN, SSL, image optimization, and preview deployments at a premium cost. Self-hosted Node.js with PM2: choose when Vercel costs exceed budget and you have DevOps skills. It requires process management, memory limits, reverse proxy, and monitoring. Docker: choose when you need reproducible environments across development, staging, and production. It adds complexity but ensures consistent deployments. Static export: choose when your app doesn't need server features (API routes, server actions, SSR) and you want the cheapest, simplest deployment on any static host (S3, Netlify, Cloudflare Pages).
FAQ · 7 QUESTIONS

Frequently Asked Questions

01
Why does npm run start crash on a VPS after a few hours?
02
Do I need a reverse proxy for self-hosted Next.js?
03
Is Vercel worth the cost compared to self-hosting?
04
Can I use static export for a site with dynamic content?
05
How much RAM does Next.js 16 need in production?
06
What's the difference between standalone output and the default output?
07
Should I use Docker or PM2 for self-hosting?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Lessons pulled from things that broke in production.

Follow
Verified
production tested
July 12, 2026
last updated
1,787
articles · all by Naren
🔥

That's Next.js. Mark it forged?

6 min read · try the examples if you haven't

Previous
Metadata and SEO in Next.js 16: From Basics to Dynamic OG Images
29 / 56 · Next.js
Next
Dynamic Routes, Route Groups, and Advanced Routing Patterns