Next.js 16 on $5 VPS — Memory OOM Killed the Server Every 6 Hours
Self-hosting Next.js 16 needs specific Node.js config.
20+ years shipping production JavaScript and front-end systems at scale. Lessons pulled from things that broke in production.
- ✓React fundamentals
- ✓Next.js App Router
- ✓Basic command line familiarity
- 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
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.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
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.
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.
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.
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.
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.
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).
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.
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).
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.
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.
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.
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.
git push, and they handle containers, SSL, databases, and monitoring. Best of both worlds.Production Incident: Node.js OOM Killed by Kernel Every 6 Hours on 1GB VPS
dmesg showed 'Out of memory: Killed process (node)'. Uptime monitor alerted but there was no auto-restart.npm run start inside a tmux session and considered it production-ready.--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.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.- 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
dmesg | grep -i oom for OOM killer logs. Also check free -m for memory usage patterns. Add swap and reduce Node heap limit.sharp is installed. For static export, images must be pre-optimized.trailingSlash: true in next.config or configure your static host for SPA routing.dmesg | grep -i 'killed process' || journalctl -k | grep -i oomfree -m && pm2 show app-name 2>/dev/null || docker stats --no-stream| File | Command / Code | Purpose |
|---|---|---|
| ecosystem.config.js | module.exports = { | Self-Hosting with Node.js |
| Caddyfile | yourdomain.com { | Caddy as Reverse Proxy |
| Dockerfile | FROM node:20-alpine AS builder | Docker Deployment |
| docker-compose.yml | version: '3.8' | Docker Compose |
| next.config.ts | const nextConfig: NextConfig = { | Static Export |
| setup-swap.sh | sudo fallocate -l 2G /swapfile | Memory Optimization |
| monitoring-setup.sh | pm2 monit | Monitoring Self-Hosted Next.js |
| .github | name: Deploy to VPS | CI/CD |
| deploy-playbook.sh | curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - | Complete Production Self-Hosting Playbook |
Key takeaways
Interview Questions on This Topic
Explain the four deployment strategies for Next.js 16 and when you would choose each.
Frequently Asked Questions
20+ years shipping production JavaScript and front-end systems at scale. Lessons pulled from things that broke in production.
That's Next.js. Mark it forged?
6 min read · try the examples if you haven't