Skip to content
savemores

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 · 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.js under 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 build is memory-hungry.
  • 03Standalone output leaves out public and .next/static, so copy both into .next/standalone after 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.

Connect as root from your machine, then create the user and give it your key:
bash
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.

Log back in as deploy, then:
bash
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
sudo apt-get install -y nodejs

node -v   # v24.x.x
npm -v

Step 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.

A 2 GB swap file that persists across reboots:
bash
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 listed

Step 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.

next.config.ts
ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  output: "standalone",
};

export default nextConfig;
Clone into /var/www, install and build:
bash
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.

Start the app and hook PM2 into systemd:
bash
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.
Commands you will use daily:
bash
pm2 status
pm2 logs myapp --lines 100
pm2 restart myapp
pm2 monit

Step 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.

bash
sudo apt-get install -y nginx
sudo nano /etc/nginx/sites-available/myapp
/etc/nginx/sites-available/myapp
nginx
server {
    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;
    }
}
Enable the site, test, reload. Always run nginx -t first; it catches typos while the old config is still serving:
bash
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 nginx
Now close everything except SSH, HTTP and HTTPS. The 'Nginx Full' profile only exists once Nginx is installed:
bash
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw --force enable
sudo ufw status

Step 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.

bash
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-run

Verify 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 myapp and confirm the port in proxy_pass matches PORT.
  • Page loads but has no styles or images: public or .next/static was not copied into .next/standalone after 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.
bash
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.log

Step 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.

/var/www/myapp/deploy.sh (then chmod +x deploy.sh)
bash
#!/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/image optimizes at runtime with sharp, 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 public and .next/static on 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.