VPS & deployment
Next.js on a Hostinger VPS: From a Bare Server to Production HTTPS
Deploy Next.js on a Hostinger VPS with Node 24, standalone output, PM2, Nginx and Let's Encrypt, plus a one-command redeploy and the gotchas that cost hours.
By Mr. Ahmed · 20+ years in tech
Updated · first published · 12 min read
The short answer
- 01To deploy Next.js on a Hostinger VPS, build with
output: 'standalone', run.next/standalone/server.jsunder PM2 on 127.0.0.1:3000, and put Nginx with a Let's Encrypt certificate in front of it. - 02The entry KVM 1 plan (1 vCPU, 4 GB RAM, 50 GB NVMe) handles a small-to-medium Next.js site, and RAM is the spec that matters because
next buildis memory-hungry. - 03Standalone output leaves out
publicand.next/static, so copy both into.next/standaloneafter every build or styles and images go missing. - 04Variables prefixed
NEXT_PUBLIC_are inlined at build time; changing one requires a rebuild, not just a PM2 restart. - 05Only a fully static export (
output: 'export') can live on shared hosting. Server components, route handlers and ISR need a long-running Node process, which means a VPS.
Does Next.js really need a VPS?
If your app renders anything on the server (server components, route handlers, ISR), it is a Node process that has to stay up between requests. Shared hosting is designed around PHP: a script starts, answers one request and exits. Nothing supervises a node process there, you cannot bind your own port, and SSH is often missing. That mismatch is the entire reason this walkthrough begins with a VPS.
There is one exception. An app built with output: 'export' and no server features is just HTML and assets, and any shared plan will serve it. If that is you, the React/Vite shared hosting guide covers the upload and the rewrite rules. Everyone else, read on.
For the server itself, the entry KVM 1 is enough for a small-to-medium site: 1 vCPU, 4 GB of RAM and 50 GB of NVMe. Memory is what I look at first for Next.js, because the build step eats it, and 4 GB at this price tier is unusually generous.
Groundwork before the first command
Get these four things sorted up front. Each one is a common reason a later step fails.
- A VPS on Ubuntu 24.04 LTS or 26.04 LTS. You choose the OS template when you create the server.
- An A record for your domain pointing at the server's IP. See pointing a domain at your VPS.
- The app in a Git repository the server can clone from.
- Your SSH public key added during provisioning, rather than relying on a root password.
Step 1: a non-root user to own the app
Running the app as root is the mistake I see most in copy-paste tutorials. Make an unprivileged deploy user that owns the code and the process, and keep root for installing packages. If you want the full hardening pass, the new VPS security checklist goes further.
ssh root@YOUR_SERVER_IP
adduser --disabled-password --gecos "" deploy
usermod -aG sudo deploy
# Copy your SSH key across so you can log in as deploy
rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy/Step 2: Node.js 24 LTS from NodeSource
As of September 2026, Node 24 (codename Krypton) is the Active LTS line and the sensible default for a new server. Node 22 stays in maintenance until April 2027, so existing apps on it are fine, but I would not start a fresh box there.
Skip Ubuntu's own nodejs package; it lags well behind. NodeSource gives you the major version you asked for.
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
sudo apt-get install -y nodejs
node -v # v24.x.x
npm -vStep 3: a swap file, cheap insurance for the build
On a mid-sized app, next build can climb well past 1 GB of memory. With 4 GB you usually have headroom, but a swap file costs nothing and turns an out-of-memory kill into a build that is merely slow. On a smaller server, treat it as mandatory.
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
free -h # swap should now be listedStep 4: build with standalone output
Turning on output: 'standalone' is the most valuable single change for a VPS deploy. Next.js traces which files each route actually needs and copies only those, along with a small server.js, into .next/standalone. You ship a fraction of node_modules, so deploys are quicker and the disk stays lean.
The trap: public and .next/static are not included, because Next.js expects a CDN to serve them. Copy both into the standalone folder yourself and server.js will serve them.
One more ordering rule. Anything prefixed NEXT_PUBLIC_ is written into the browser bundle at build time, not read when the server starts. Put .env.production in place before you run the build, and rebuild whenever one of those values changes.
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
};
export default nextConfig;sudo mkdir -p /var/www && sudo chown deploy:deploy /var/www
cd /var/www
git clone https://github.com/you/your-app.git myapp
cd myapp
npm ci
npm run build
# The bit everyone forgets:
cp -r public .next/standalone/
cp -r .next/static .next/standalone/.next/Step 5: PM2 keeps the process alive
The app has to come back after a crash and after a reboot. PM2 does both. Start the standalone server.js rather than next start; it boots faster and does not need the full dependency tree.
Bind to 127.0.0.1, never 0.0.0.0. Only Nginx on the same machine should talk to Node. Listen on every interface and anyone can skip your proxy by hitting port 3000 directly.
sudo npm install -g pm2
cd /var/www/myapp
PORT=3000 HOSTNAME=127.0.0.1 pm2 start .next/standalone/server.js --name myapp
pm2 save
pm2 startup systemd
# PM2 prints a sudo command. Run it, then run 'pm2 save' again.pm2 status
pm2 logs myapp --lines 100
pm2 restart myapp
pm2 monitStep 6: Nginx as the reverse proxy, then the firewall
The Next.js self-hosting docs recommend a reverse proxy instead of exposing Node, and the reasoning holds up: Nginx soaks up malformed requests, slow-client attacks, oversized bodies and rate limiting, so Node spends its CPU rendering pages.
The /_next/static/ block is free performance. Those files are content-hashed and never change, so Nginx can serve them from disk with a year-long cache header and Node never sees the request.
sudo apt-get install -y nginx
sudo nano /etc/nginx/sites-available/myappserver {
listen 80;
server_name example.com www.example.com;
# Immutable, content-hashed assets: let Nginx serve them directly.
location /_next/static/ {
alias /var/www/myapp/.next/static/;
expires 365d;
add_header Cache-Control "public, max-age=31536000, immutable";
access_log off;
}
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_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;
proxy_cache_bypass $http_upgrade;
}
}sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginxsudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw --force enable
sudo ufw statusStep 7: free HTTPS from Let's Encrypt
Certbot rewrites the Nginx config, installs the certificate and schedules renewal in one go. Renewal runs on a systemd timer, so once the dry run passes you can forget about it.
sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
# Prove auto-renewal works
sudo certbot renew --dry-runVerify it worked, and what to check when it did not
Check from the inside out: the Node process, then Nginx, then the public URL. Whichever layer fails first is where the problem lives.
- 502 Bad Gateway: Nginx is up but Node is not. Check
pm2 logs myappand confirm the port inproxy_passmatchesPORT. - Page loads but has no styles or images:
publicor.next/staticwas not copied into.next/standaloneafter the last build. - Certbot fails the challenge: the A record has not propagated yet, or port 80 is blocked. Check DNS, then
sudo ufw status. - `ufw allow 'Nginx Full'` errors: Nginx is not installed yet, so the profile does not exist.
- A changed `NEXT_PUBLIC_` value has no effect: it was baked in at build time. Rebuild, copy the assets, restart.
pm2 status # myapp should be 'online'
sudo ss -tlnp | grep 3000 # want 127.0.0.1:3000, not 0.0.0.0:3000
curl -I http://127.0.0.1:3000 # Node answers directly
curl -I https://example.com # the full chain, with TLS
sudo tail -n 50 /var/log/nginx/error.logStep 8: make every redeploy one command
Repeating the build-copy-restart sequence by hand is how a Friday deploy goes wrong. Save it as a script. It also slots straight into push-to-deploy with GitHub Actions.
#!/usr/bin/env bash
set -euo pipefail
cd /var/www/myapp
git pull origin main
npm ci
npm run build
# Standalone output does not include these: copy them every build.
cp -r public .next/standalone/
cp -r .next/static .next/standalone/.next/
pm2 restart myapp --update-env
pm2 save
echo "Deployed at $(date -Iseconds)"Failure modes that show up weeks later
None of these break on day one. All of them have cost people real time.
- ISR pages and the data cache live on the instance's local disk. Perfect on one server; on two, each serves the other's stale pages. Scaling out means a shared cache handler.
next/imageoptimizes at runtime withsharp, which can use a lot of memory on glibc-based Linux. If resident memory creeps up over days, look here first.- Dynamically rendered pages are sent with
private, no-cache, no-store. Do not add an Nginx cache that overrides it, or one visitor may receive another's page. - Copy
publicand.next/staticon every build, not just the first. A stale copy means this week's HTML is wearing last week's CSS. - The intro VPS rate is not the long-run rate. Note the renewal figure before your term ends; the renewal price guide explains how it works.
What it costs once it is running
One KVM 1 server will run several Next.js apps side by side: another PM2 process on port 3001, another server block, another certificate. That is the real advantage over per-service platform billing. The second app costs nothing extra. I cover the layout in hosting multiple sites on one VPS.
If you are buying the server now, the links here apply coupon WEBHOST2026 automatically, and longer terms carry the lower monthly rate (KVM 1 lists at $4.99/mo on 48 months against $6.99/mo on 12, before the coupon).
Questions people ask
Which Hostinger VPS plan do I need for Next.js?
KVM 1 is enough for a small-to-medium Next.js site. Its 4 GB of RAM lets next build finish, and a 2 GB swap file covers the occasional spike. Move up when builds are too slow on one vCPU or you run several heavy apps.
Can I host Next.js on Hostinger shared hosting?
Only a fully static export. If the app uses server components, route handlers, ISR or server rendering, it needs a Node process that stays running, and shared hosting cannot provide that.
Why does my deployed Next.js site have no CSS?
The standalone build does not include public or .next/static. Copy both into .next/standalone after every build, then restart the PM2 process.
Should PM2 run next start or server.js?
Run .next/standalone/server.js. It starts faster and does not need the full node_modules tree, which is the point of standalone output.
Independent site, not Hostinger. Buy through my links and Hostinger pays me a referral fee; your price is the same or lower. How that works.