Table Of Content
- Prerequisites
- Step 1 — Getting the Source Code onto the Server
- Step 2 — Configuring Environment Variables
- Step 3 — Installing Dependencies and Building
- Step 4 — Running the App With PM2
- Step 5 — Configuring Nginx as a Reverse Proxy
- Step 6 — Enabling HTTPS With Let's Encrypt
- Step 7 — Updating the App When You Push New Code
- Conclusion
How To Deploy a Next.js App on an Ubuntu VPS with PM2 and Nginx
Published on September 6, 2026

Introduction
Vercel is the fastest way to deploy Next.js, but it is not always the right fit: you may need full control over the server, want the app next to its database, or simply already have a VPS. In that case, deploying to an Ubuntu VPS yourself is a sensible choice and not hard at all.
In this guide we will:
- Get the Next.js source code onto the server and build it for production.
- Use PM2 to run the app in the background and restart it automatically after a reboot.
- Configure Nginx as a reverse proxy that forwards traffic from ports 80/443 to the Next.js app.
- Enable HTTPS with Let's Encrypt.
Prerequisites
- A VPS running Ubuntu 20.04 or 22.04 with a
sudouser. - Node.js 18 or newer installed. See How To Install Node.js on Ubuntu.
- Nginx installed and allowed through the firewall. See How To Install Nginx on Ubuntu.
- A domain name pointing at the VPS (
your_domainin the examples). - Your Next.js source code in a Git repository (GitHub, GitLab, etc.).
Step 1 — Getting the Source Code onto the Server
Install Git if it is not there yet:
sudo apt update
sudo apt install gitCreate a directory for the app and clone the repository. Replace the URL with your own:
sudo mkdir -p /var/www
sudo chown -R $USER:$USER /var/www
cd /var/www
git clone https://github.com/your_user/your_nextjs_app.git
cd your_nextjs_appIf the repository is private, generate an SSH key on the server and add it to your Git account:
ssh-keygen -t ed25519 -C "deploy@your_domain"
cat ~/.ssh/id_ed25519.pubStep 2 — Configuring Environment Variables
Production apps usually need environment variables (database connection strings, API keys, etc.). Create a .env.production or .env.local file in the project directory:
nano .env.localDATABASE_URL=postgresql://sammy:password@localhost:5432/myapp
NEXT_PUBLIC_SITE_URL=https://your_domainNote: Variables prefixed with NEXT_PUBLIC_ are inlined into the client bundle at build time. Never put secrets in them.
Step 3 — Installing Dependencies and Building
Install dependencies. Use npm ci rather than npm install so the exact versions from package-lock.json are used:
npm ciBuild the app for production:
npm run buildOutput
▲ Next.js 14.2.3
Creating an optimized production build ...
✓ Compiled successfully
✓ Linting and checking validity of types
✓ Collecting page data
✓ Generating static pages (12/12)
✓ Finalizing page optimization
Route (app) Size First Load JS
┌ ○ / 5.2 kB 92 kB
├ ○ /about 1.1 kB 88 kB
└ ● /blogs/[slug] 3.4 kB 90 kBIf the VPS has little RAM (1 GB or less), the build may get killed for running out of memory. You can add a temporary swap file:
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfileRun the app once to make sure the build works:
npm run startOutput
▲ Next.js 14.2.3
- Local: http://localhost:3000
✓ Ready in 320msOpen a second terminal and check:
curl -I http://localhost:3000If you get HTTP/1.1 200 OK, press CTRL + C in the first terminal to stop the app.
Step 4 — Running the App With PM2
If you close your SSH session, the npm run start process dies with it. PM2 is a process manager for Node.js that keeps the app running in the background, restarts it when it crashes and brings it back after a server reboot.
Install PM2 globally:
sudo npm install -g pm2Start the app through PM2. The -- separates PM2's own arguments from those passed to npm:
pm2 start npm --name "nextjs-app" -- startOutput
[PM2] Starting /usr/bin/npm in fork_mode (1 instance)
[PM2] Done.
┌────┬───────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┐
│ id │ name │ mode │ ↺ │ status │ cpu │ mem │ user │
├────┼───────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┤
│ 0 │ nextjs-app │ fork │ 0 │ online │ 0% │ 58mb │ hiusvu │
└────┴───────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┘Instead of a long command, you can create an ecosystem.config.js in the project directory for more detailed configuration:
module.exports = {
apps: [
{
name: "nextjs-app",
script: "npm",
args: "start",
cwd: "/var/www/your_nextjs_app",
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: "500M",
env: {
NODE_ENV: "production",
PORT: 3000,
},
},
],
};And start it with:
pm2 start ecosystem.config.jsUseful PM2 commands:
pm2 list # List processes
pm2 logs nextjs-app # Tail the logs
pm2 restart nextjs-app
pm2 stop nextjs-app
pm2 delete nextjs-app
pm2 monit # Monitor CPU / RAMTo have PM2 bring your apps back after a reboot, run:
pm2 startupPM2 prints a command like sudo env PATH=... pm2 startup systemd -u your_user --hp /home/your_user. Copy and run it exactly as shown. Finally, save the current process list:
pm2 saveStep 5 — Configuring Nginx as a Reverse Proxy
The app is listening on port 3000, but you do not want visitors typing :3000. Nginx will listen on port 80, accept requests for your_domain and forward them to port 3000.
Create a new server block:
sudo nano /etc/nginx/sites-available/your_domainserver {
listen 80;
listen [::]:80;
server_name your_domain www.your_domain;
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;
}
# Cache Next.js static assets for a long time
location /_next/static/ {
proxy_pass http://127.0.0.1:3000;
proxy_cache_valid 60m;
add_header Cache-Control "public, max-age=31536000, immutable";
}
}The Upgrade and Connection headers allow WebSockets to work (needed for hot reload in development and for some realtime libraries). The X-Forwarded-* headers let Next.js see the visitor's real IP address and protocol.
Enable the server block, test the syntax and reload Nginx:
sudo ln -s /etc/nginx/sites-available/your_domain /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginxVisit http://your_domain and you should see your Next.js app.
Step 6 — Enabling HTTPS With Let's Encrypt
Install Certbot and obtain a certificate. Certbot edits the server block above to add the SSL configuration and redirect HTTP to HTTPS:
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
sudo ufw allow 'Nginx Full'
sudo certbot --nginx -d your_domain -d www.your_domainEach step is covered in detail in How To Secure Nginx with Let's Encrypt on Ubuntu.
Step 7 — Updating the App When You Push New Code
Every time you push new code, updating the server means: pull, install dependencies, rebuild and restart PM2. Create a deploy.sh script in the project directory to do all of that in one command:
nano deploy.sh#!/bin/bash
set -e
cd /var/www/your_nextjs_app
echo ">>> Pulling latest code..."
git pull origin main
echo ">>> Installing dependencies..."
npm ci
echo ">>> Building..."
npm run build
echo ">>> Restarting PM2..."
pm2 restart nextjs-app
echo ">>> Done!"Make it executable and run it:
chmod +x deploy.sh
./deploy.shWith pm2 restart the app is briefly unavailable for a few seconds. If you need zero downtime, run PM2 in cluster mode with several instances and use pm2 reload:
pm2 start ecosystem.config.js -i 2
pm2 reload nextjs-appAs a further step, you can trigger this script from GitHub Actions to deploy automatically on every push to main.
Conclusion
You have deployed a Next.js app on an Ubuntu VPS: built for production, kept alive by PM2 with boot persistence, served through an Nginx reverse proxy and secured with HTTPS. This setup is enough for most personal and small-business projects. As the app grows, consider packaging it with Docker or running several instances behind a load balancer.
