Serverless bills get out of hand fast once your Next.js app handles steady API traffic and SSR rendering. I moved one of my production apps from a managed serverless platform over to a $6/month Hostinger KVM VPS, and response times dropped from 1,200ms cold starts down to a crisp 42ms because Node stays warm in memory.
Here is the exact setup I use: standalone Next.js builds, PM2 cluster mode, Nginx reverse proxy with gzip caching, Namecheap DNS records, and an edge CDN layer.

1. Server Prep and Node.js Installation
Start with a fresh Ubuntu 24.04 LTS instance. SSH in as root (or a sudo user) and update the base packages before grabbing NodeSource’s Node 20 repository:
apt update && apt upgrade -y
apt install -y curl git ufw nginx build-essential
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt install -y nodejs
node -v
npm -v Verify that Node is at v20.x.x and npm is ready. Then install PM2 globally so it can manage your processes in the background:
npm install -g pm2Set up your firewall right away so you don’t leave arbitrary ports exposed:
ufw allow OpenSSH
ufw allow 'Nginx Full'
ufw enable2. Prepare Next.js for Standalone Deployment
Default Next.js production builds drag along hundreds of megabytes of unnecessary node_modules dependencies. Setting standalone mode tells Next.js to trace imports and compile a minimal server package containing only what is strictly required to run.
Add output: 'standalone' to your next.config.js or next.config.mjs:
/** @type {import('next').NextConfig} */
const nextConfig = { output: 'standalone', poweredByHeader: false, compress: true,
}; module.exports = nextConfig; Clone your repository into /var/www/my-next-app and adjust permissions for your deploy user:
mkdir -p /var/www/my-next-app
chown -R $USER:$USER /var/www/my-next-app
cd /var/www/my-next-app
git clone https://github.com/your-username/your-repo.git .
npm install
npm run build Here is the catch with standalone mode: Next.js outputs a self-contained runtime at .next/standalone/server.js, but it leaves out the public directory and the .next/static build assets.
You must copy those folders into the standalone folder after building:
cp -r public .next/standalone/
cp -r .next/static .next/standalone/.next/3. Manage the Process with PM2
Never run npm start directly in an SSH terminal—it dies the moment your session closes. PM2 process manager keeps your application alive, recycles instances on memory leaks, and handles zero-downtime reloads.
Create an ecosystem.config.js file in your project root at /var/www/my-next-app:
module.exports = { apps: [ { name: 'nextjs-app', script: '.next/standalone/server.js', instances: 'max', exec_mode: 'cluster', env: { PORT: 3000, NODE_ENV: 'production', }, max_memory_restart: '800M', }, ],
};Start your application cluster and make sure it automatically restarts after server reboots:
pm2 start ecosystem.config.js
pm2 save
pm2 startup systemdRun the generated sudo env PATH=... startup command that PM2 prints out to register the systemd service.
4. Configure Nginx Reverse Proxy
Next.js is now listening internally on 127.0.0.1:3000. We will put Nginx in front to handle SSL termination, gzip compression, and direct static file serving from disk.
Create an Nginx configuration file at /etc/nginx/sites-available/nextjs.conf:
server { listen 80; listen [::]:80; server_name example.com www.example.com; # Proxy Next.js standalone server location / { proxy_pass http://127.0.0.1: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; } # Direct static file serving for faster cache responses location /_next/static/ { alias /var/www/my-next-app/.next/static/; expires 365d; access_log off; add_header Cache-Control "public, max-age=31536000, immutable"; }
}Enable the site config and test Nginx syntax before reloading:
ln -s /etc/nginx/sites-available/nextjs.conf /etc/nginx/sites-enabled/
nginx -t
systemctl reload nginxIf you plan to run multiple apps or subdomains from this same machine, read our guide on pointing a subdomain to a separate server with Nginx for cleaner configuration splits.
5. Point Namecheap DNS to Your VPS
Head to Namecheap and open Domain List > Manage > Advanced DNS. Add two A records targeting your Hostinger VPS public IP address:
- Type: A Record | Host:
@| Value:YOUR_SERVER_IP| TTL: Automatic - Type: A Record | Host:
www| Value:YOUR_SERVER_IP| TTL: Automatic
If you are deciding how to structure your root records, take a look at our breakdown on configuring root domains in Namecheap DNS: A Record vs CNAME.
Check if your DNS records have resolved:
dig +short example.com
dig +short www.example.com6. Generate Free Let’s Encrypt SSL Certificates
Once DNS points to your server, install Certbot to provision free SSL/TLS certificates through Nginx:
apt install -y certbot python3-certbot-nginx
certbot --nginx -d example.com -d www.example.com Certbot automatically updates your nextjs.conf file, configures HTTPS redirects, and schedules a background systemd timer to handle renewals.
7. Add CDN Caching and Avoid Redirect Loops
Putting a CDN in front of your VPS shields the origin server from traffic spikes and delivers static assets from edge locations closest to your users. When configuring CDN edge rules, set your cache headers like this:
- Static assets (
/_next/static/*): Cache for 1 year (immutable content hashes). - Images (
/_next/image*): Cache for 7 days at the edge. - HTML / Server actions: Bypass cache entirely (pass-through to origin).
The most common bug here is an infinite redirect loop caused by the CDN requesting origin content over plain HTTP while enforcing HTTPS for the user. If you hit this issue, check our fix for SSL ERR_TOO_MANY_REDIRECTS on Nginx behind a CDN.
8. Gotchas I Hit During Setup
Out of Memory Error During Build
Next.js builds are heavy on CPU and RAM. On a 1GB or 2GB VPS, running npm run build will trigger the Linux OOM killer and abruptly kill your terminal session with Killed.
Fix this by setting up a 2GB swap file before running your builds:
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' | tee -a /etc/fstabMissing 404 and Static Assets in Standalone
If your CSS files return 404 errors after running standalone mode, you forgot to copy public/ and .next/static/ into .next/standalone. Automate this inside your package.json build script so it runs automatically every deploy:
{ "scripts": { "dev": "next dev", "build": "next build && cp -r public .next/standalone/ && cp -r .next/static .next/standalone/.next/", "start": "node .next/standalone/server.js" }
}Frequently Asked Questions
How much RAM do I need for Next.js on a VPS?
A 2-core / 4GB RAM VPS handles PM2 running 2 cluster workers easily under moderate traffic. If you are building directly on the server with 2GB of RAM or less, create a 2GB swap file so your build process does not run out of memory.
Can I deploy automated updates with GitHub Actions?
Yes. Set up a workflow that connects to your VPS over SSH, pulls the main branch, runs npm run build, and triggers pm2 reload nextjs-app for a zero-downtime deploy.
Why use PM2 instead of Docker?
PM2 runs directly on the host without the overhead of container runtimes. On entry-level VPS tiers, saving 300MB+ of idle memory makes a noticeable difference. Docker is great, but PM2 is lightweight and quick to debug.
Next Steps
If you run other modern JavaScript frameworks on this server, read our guide on deploying SvelteKit on Hostinger VPS with Nginx and CDN to run them side by side with Next.js.

