Skip to main content

Command Palette

Search for a command to run...

Deploying a Next.js App on a Linux VPS (The Complete Guide)

Updated
10 min read
Deploying a Next.js App on a Linux VPS (The Complete Guide)

Linux for Everyday Use | Part 7 of the Linux → Deploy Series


This is what everything in this series has been building toward.

By the end of this blog, your Next.js app will be live on the internet with a real domain, HTTPS, and automatic restarts if the server reboots or your app crashes. No Vercel. No Netlify. Your server, your rules.

Here's the full stack you're deploying:

Browser → nginx (port 443, HTTPS) → Next.js via PM2 (port 3000)
                     ↑
              Certbot (SSL cert)

Prerequisites

Before you start, make sure you have:

  • A VPS running Ubuntu 22.04 (DigitalOcean, Hetzner, Vultr, AWS EC2 — any works)

  • A domain name pointing to your server's IP (A record set, propagated)

  • SSH access to the server (ideally with key auth from Blog 6)

  • Your Next.js app on GitHub

If you don't have a domain yet, you can complete steps 1–5 using your server's IP address and add the domain + SSL later.


Step 1 — Connect to Your Server

ssh aakib@your-server-ip

Once connected, update the package list and upgrade existing packages:

sudo apt update && sudo apt upgrade -y

The -y flag auto-confirms prompts. This can take a few minutes on a fresh server.


Step 2 — Install Node.js via nvm

Don't install Node from apt directly — the version in Ubuntu's default repos is almost always outdated. Use nvm (Node Version Manager) instead. It lets you install any Node version and switch between them.

# Download and run the nvm install script
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash

After it finishes, either restart your terminal session or run:

export NVM_DIR="$HOME/.nvm"
[ -s "\(NVM_DIR/nvm.sh" ] && \. "\)NVM_DIR/nvm.sh"

Verify nvm is available:

nvm --version

Install the latest LTS version of Node:

nvm install --lts

Verify:

node -v
# v22.x.x (or whatever the current LTS is)

npm -v

Step 3 — Clone Your Repository

Navigate to where you want to keep your app. The /var/www/ directory is a common convention for web apps:

sudo mkdir -p /var/www
sudo chown \(USER:\)USER /var/www
cd /var/www

Clone your repo:

git clone https://github.com/yourusername/your-nextjs-app.git
cd your-nextjs-app

If your repo is private, you'll need to authenticate. The cleanest way is to generate a deploy key:

# On the server, generate a key specifically for this repo
ssh-keygen -t ed25519 -C "deploy-key" -f ~/.ssh/deploy_key -N ""

# Print the public key
cat ~/.ssh/deploy_key.pub

Copy that public key and add it as a Deploy Key in your GitHub repo: Settings → Deploy keys → Add deploy key. Then clone with SSH:

git clone git@github.com:yourusername/your-nextjs-app.git

Step 4 — Set Environment Variables

Your Next.js app almost certainly needs environment variables — API keys, database URLs, secrets. These should never be committed to Git.

Create the .env.production file directly on the server:

nano /var/www/your-nextjs-app/.env.production

Add your variables:

DATABASE_URL=postgresql://user:password@localhost:5432/mydb
NEXTAUTH_SECRET=your-secret-here
NEXTAUTH_URL=https://your-domain.com
NEXT_PUBLIC_API_URL=https://your-domain.com/api

Save with Ctrl+O, exit with Ctrl+X.

Lock down the permissions:

chmod 600 .env.production

Only your user can read or write it. Good.

Important note on NEXT_PUBLIC_ variables: These are embedded into the client-side bundle at build time. If you change them, you need to rebuild the app. Regular (non-public) variables are read at runtime by the server.


Step 5 — Install Dependencies and Build

cd /var/www/your-nextjs-app

npm install

npm run build

npm run build compiles your Next.js app into the .next/ directory — static assets, server-side code, everything optimized for production.

This can take 1–3 minutes. A successful build ends with something like:

Route (app)                              Size     First Load JS
┌ ○ /                                    5.2 kB         87.4 kB
├ ○ /about                               1.1 kB         83.3 kB
└ ○ /api/health                          0 B                0 B

○  (Static)   prerendered as static content

Test that the app starts:

npm start

Visit http://your-server-ip:3000 in your browser. If you see your app, the build is good. Press Ctrl+C to stop it — you'll use PM2 to run it properly next.


Step 6 — Install PM2 and Keep the App Alive

PM2 is a production process manager for Node.js. It:

  • Keeps your app running after you close the terminal

  • Automatically restarts it if it crashes

  • Starts it automatically on server reboot

  • Manages logs

Install PM2 globally:

npm install -g pm2

Start your Next.js app with PM2:

cd /var/www/your-nextjs-app
pm2 start npm --name "nextjs-app" -- start

This tells PM2 to run npm start and name the process nextjs-app.

Check it's running:

pm2 status
┌────┬──────────────┬─────────┬──────┬───────────┬──────────┐
│ id │ name         │ status  │ cpu  │ mem       │ watching │
├────┼──────────────┼─────────┼──────┼───────────┼──────────┤
│ 0  │ nextjs-app   │ online  │ 0%   │ 87.3 MB   │ disabled │
└────┴──────────────┴─────────┴──────┴───────────┴──────────┘

Make PM2 start on reboot

pm2 startup

PM2 prints a command — copy it and run it. It looks like:

sudo env PATH=$PATH:/home/aakib/.nvm/versions/node/v22.0.0/bin \
  /home/aakib/.nvm/versions/node/v22.0.0/lib/node_modules/pm2/bin/pm2 \
  startup systemd -u aakib --hp /home/aakib

Run the exact command it gives you (yours will have your paths). Then save the current PM2 process list:

pm2 save

Now if your server reboots, PM2 restarts automatically and brings your Next.js app back up.

Useful PM2 commands

pm2 logs nextjs-app          # View live logs
pm2 logs nextjs-app --lines 50  # Last 50 lines
pm2 restart nextjs-app       # Restart the app
pm2 stop nextjs-app          # Stop the app
pm2 delete nextjs-app        # Remove from PM2
pm2 monit                    # Interactive monitoring dashboard

Step 7 — Configure nginx

Install nginx if you haven't already (from Blog 6):

sudo apt install nginx

Create your site config:

sudo nano /etc/nginx/sites-available/your-domain.com
server {
    listen 80;
    server_name your-domain.com www.your-domain.com;

    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_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 it:

sudo ln -s /etc/nginx/sites-available/your-domain.com /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx

Visit http://your-domain.com — your app should load over HTTP.


Step 8 — Add HTTPS with Certbot (Free SSL)

Certbot obtains a free SSL certificate from Let's Encrypt and automatically configures nginx to use it.

sudo apt install certbot python3-certbot-nginx

Run Certbot:

sudo certbot --nginx -d your-domain.com -d www.your-domain.com

Certbot will:

  1. Verify you own the domain (by serving a challenge file via nginx)

  2. Issue an SSL certificate

  3. Automatically update your nginx config to redirect HTTP → HTTPS and serve on port 443

When it asks about HTTP → HTTPS redirect, choose option 2 (Redirect). This forces all traffic to HTTPS.

Test by visiting https://your-domain.com. Your browser should show the padlock.

Auto-renewal

Let's Encrypt certs expire every 90 days. Certbot installs a cron job or systemd timer to renew automatically. Test it with:

sudo certbot renew --dry-run

If it completes without errors, your cert will renew automatically forever.


Step 9 — Deploying Updates

When you push changes to GitHub, here's how to deploy them to the server:

cd /var/www/your-nextjs-app

# Pull latest code
git pull origin main

# Install any new dependencies
npm install

# Rebuild
npm run build

# Restart the app (zero-downtime via PM2)
pm2 restart nextjs-app

That's 4 commands. You can put them in a shell script for convenience:

nano ~/deploy.sh
#!/bin/bash
set -e

cd /var/www/your-nextjs-app
git pull origin main
npm install
npm run build
pm2 restart nextjs-app
echo "Deployed successfully."
chmod +x ~/deploy.sh

Now deploying is just:

~/deploy.sh

Troubleshooting Common Issues

App starts but shows a 502 Bad Gateway

nginx can't reach your app on port 3000. Check:

pm2 status          # Is the app running?
pm2 logs nextjs-app # Any crash logs?
ss -tlnp            # Is port 3000 actually listening?

Environment variables not loading

Make sure your .env.production file is in the project root (same level as package.json). If you added new variables after building, you need to rebuild:

npm run build && pm2 restart nextjs-app

Build fails with out-of-memory errors

On a 1 GB RAM server, Next.js builds can run out of memory. Add a swap file:

sudo fallocate -l 1G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile

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

This gives your server 1 GB of swap space — slower than RAM, but it prevents OOM kills during builds.

Domain not working after connecting to server

DNS propagation takes up to 48 hours but usually resolves in 15–60 minutes. Check whether your domain is pointing to the right IP:

# On your local machine
nslookup your-domain.com
# or
dig your-domain.com

If the IP matches your server, nginx should be reachable.


The Full Picture

Here's what you've built:

User types https://your-domain.com
    ↓
DNS resolves to your server IP
    ↓
nginx receives request on port 443 (HTTPS)
SSL cert verified by browser (Certbot / Let's Encrypt)
    ↓
nginx proxies request to localhost:3000
    ↓
PM2-managed Next.js process handles the request
    ↓
Response goes back through nginx to the user

Your server survives:

  • Terminal disconnect — PM2 keeps the process alive

  • App crash — PM2 auto-restarts

  • Server reboot — PM2 startup script brings everything back


Quick Reference

Command What it does
nvm install --lts Install latest LTS Node
npm run build Build Next.js for production
pm2 start npm --name "app" -- start Start app with PM2
pm2 startup && pm2 save Persist PM2 across reboots
pm2 logs app View live app logs
pm2 restart app Restart app
sudo nginx -t Test nginx config
sudo systemctl reload nginx Apply nginx config
sudo certbot --nginx -d domain.com Issue SSL cert + configure nginx
sudo certbot renew --dry-run Test cert auto-renewal

What's Next

You have a production-grade Next.js deployment. But there's more to learn:

  • CI/CD — automate the ~/deploy.sh to run on every push to main using GitHub Actions

  • Database on the server — PostgreSQL setup, connection pooling, migrations

  • Docker — containerize your app so it runs identically everywhere

  • Monitoring — Uptime checks, alerting when your app goes down

Each of these is a series in itself. But you now have the Linux foundation to approach all of them.