Deploy Next.js on Linux for Production Using PM2 (2026)
Introduction
If you've ever deployed a Next.js app by running npm run start in a terminal and then closed the SSH tab — yes, I've seen it, and yes, it's brought down production — this guide is for you.
Deploying Next.js properly on a Linux server takes about ten more minutes than the naive approach, and it saves you from 3 AM alerts, crashed apps, and that sinking feeling when you realize your server restarted and nothing came back up. In this guide, I'll walk you through the exact setup I use for deploying Next.js applications to production Linux servers: a Node.js environment managed with NVM, a build-ready project structure, PM2 as the process manager, and Nginx as the reverse proxy in front.
This isn't a "hello world" tutorial. We'll talk about real things — environment files, startup behavior, log management, Nginx tuning, and exactly what goes wrong if you skip any of it.
- Introduction
- Understanding the Production Architecture
- Prerequisites: Preparing the Linux Server
- Step 2: Clone and Build Your Next.js Application
- Step 3: Manage Environment Variables Properly
- Step 4: Run and Manage the App with PM2
- Step 5: Configure Nginx as a Reverse Proxy
- Step 6: Enforce HTTPS with Certbot
- Common Deployment Mistakes
- Frequently Asked Questions (FAQ)
Understanding the Production Architecture
Before touching the terminal, understand what you're building. A production Next.js deployment is not just running a Node.js process. It's a layered stack where each component has a specific job, and they all depend on each other being configured correctly.
- Nginx: The public-facing gateway. It handles HTTPS termination, routes incoming traffic to your Next.js server, and acts as a buffer against direct internet exposure. Your app never talks to the outside world directly.
- PM2: The process manager. It keeps your Next.js app alive, restarts it on crash, persists it across server reboots, and gives you centralized log management. Think of it as Systemd, but built specifically for Node.js workloads.
- Next.js (Node.js): The application server itself, running on an internal port (typically
3000), only reachable through Nginx from outside the server. - NVM: Node Version Manager. It keeps your Node.js version isolated and predictable — no conflicts between projects or system packages.
Prerequisites: Preparing the Linux Server
Start with a clean, updated Ubuntu server. Never skip system updates before deploying anything. Outdated packages are a common vector for vulnerabilities that aren't flashy but are very real.
Update and Upgrade System
sudo apt update && sudo apt upgrade -y
Install Required System Packages
sudo apt install nginx curl git build-essential -y
Start and Enable Nginx
sudo systemctl start nginx
sudo systemctl enable nginx
sudo systemctl status nginx
Configure Firewall (UFW)
sudo ufw allow ssh
sudo ufw allow 'Nginx Full'
sudo ufw enable
Step 2: Clone and Build Your Next.js Application
Keep your web applications organized under a consistent directory structure. I personally use /home/$USER/webapps/ — it keeps things clean, predictable, and makes permission management straightforward.
# Create and secure your app directory
sudo mkdir -p /home/$USER/webapps/nextjs-app.com
sudo chown -R $USER:$USER /home/$USER/webapps
sudo chmod -R 755 /home/$USER/webapps
# Clone your repository
git clone https://github.com/your-username/your-nextjs-app.git /home/$USER/webapps/nextjs-app.com
# Navigate into the project
cd /home/$USER/webapps/nextjs-app.com
# Install dependencies (production-safe)
npm install
# Build the production bundle
npm run build
The npm run build step compiles your Next.js application into an optimized production bundle inside the .next/ directory. This must complete without errors before you can run the app. If it fails, do not proceed — fix the build errors first.
Step 3: Manage Environment Variables Properly
Hardcoding API keys, database URLs, or secret tokens directly into your source code is one of the most common and dangerous mistakes I see in early-stage deployments. Even if your repository is private, leaked keys cost real money and create real security incidents.
Create a .env.production (or .env.local) file inside your project directory:
nano /home/$USER/webapps/nextjs-app.com/.env.local
Add your production values:
NODE_ENV=production
NEXT_PUBLIC_APP_NAME="My Production App"
NEXT_PUBLIC_API_URL=https://api.yourdomain.com
# Database
DATABASE_URL=mysql://user:password@your-db-host:3306/yourdb
# Authentication
NEXTAUTH_SECRET=your_very_secure_random_secret
NEXTAUTH_URL=https://yourdomain.com
# Third-party Integrations
STRIPE_SECRET_KEY=sk_live_your_key_here
SENDGRID_API_KEY=SG.your_key_here
Lock down the file permissions immediately:
chmod 600 /home/$USER/webapps/nextjs-app.com/.env.local
chown $USER:$USER /home/$USER/webapps/nextjs-app.com/.env.local
NEXT_PUBLIC_ is embedded into the client-side JavaScript bundle during build time and is visible to end users. Never prefix secrets, API keys, or database credentials with NEXT_PUBLIC_.
Step 4: Run and Manage the App with PM2
PM2 is the industry standard for running Node.js applications in production. It handles process persistence, automatic restarts on crash, log rotation, and most importantly — it survives server reboots by hooking into the system's startup mechanism.
# Install PM2 globally
npm install -g pm2
# Start your Next.js app with PM2
pm2 start npm --name "nextjs-app" -- start
# Or use an ecosystem config file (recommended for teams)
# pm2 start ecosystem.config.js
Using a PM2 ecosystem config file gives you fine-grained control over your process. Create one inside your project:
nano /home/$USER/webapps/nextjs-app.com/ecosystem.config.js
module.exports = {
apps: [
{
name: 'nextjs-app',
script: 'node_modules/.bin/next',
args: 'start',
cwd: '/home/ubuntu/webapps/nextjs-app.com',
instances: 'max', // Use all available CPU cores
exec_mode: 'cluster', // Enable cluster mode for load balancing
watch: false,
env: {
NODE_ENV: 'production',
PORT: 3000,
},
},
],
};
# Start using the ecosystem file
pm2 start ecosystem.config.js
# Save the PM2 process list to survive reboots
pm2 save
# Register PM2 with the system's startup mechanism
pm2 startup
PM2 will output a command after pm2 startup — copy and run it. It registers PM2 as a system service so your application automatically starts when the server boots.
Useful PM2 Commands
pm2 list # View all running processes
pm2 logs nextjs-app # Stream live application logs
pm2 restart nextjs-app # Restart the application
pm2 stop nextjs-app # Stop the application
pm2 monit # Real-time CPU and memory dashboard
Step 5: Configure Nginx as a Reverse Proxy
Your Next.js app is now running on port 3000 internally. The job of Nginx is to sit in front of it, accept traffic on ports 80 and 443, and forward requests to your app.
sudo nano /etc/nginx/sites-available/nextjs-app.conf
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
client_max_body_size 50M;
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;
}
}
# Enable the configuration
sudo ln -s /etc/nginx/sites-available/nextjs-app.conf /etc/nginx/sites-enabled/
# Test the config before restarting (never skip this)
sudo nginx -t
# Restart Nginx
sudo systemctl restart nginx
Step 6: Enforce HTTPS with Certbot
Serving your Next.js app over plain HTTP is not acceptable in 2026. Modern browsers flag it, search engines penalize it, and it exposes your users' data. Certbot handles the entire SSL certificate process for free using Let's Encrypt.
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
Follow the prompts. Certbot will automatically modify your Nginx config to include SSL certificate paths and set up HTTP-to-HTTPS redirection. Certificates auto-renew via a cron job installed by Certbot — you don't need to manage this manually.
Common Deployment Mistakes
After reviewing dozens of Next.js deployments, these are the mistakes I see repeatedly. Most are easy to avoid once you know what to look for.
| Mistake | What Happens | Fix |
|---|---|---|
Running npm run start directly in SSH |
App dies the moment you close the terminal or the connection drops | Use PM2 to manage the process — it persists independently of your SSH session |
Skipping npm run build |
App runs in slow development mode with no optimizations; hot reload runs in production | Always run npm run build before starting with PM2 on the production server |
Forgetting pm2 save after startup |
PM2 loses all process entries after a server reboot, app goes offline | Run pm2 save and pm2 startup after every major change |
Using NEXT_PUBLIC_ on secret keys |
API keys get bundled into client JavaScript and exposed to anyone who opens DevTools | Server-side secrets must never use the NEXT_PUBLIC_ prefix |
Not running nginx -t before restart |
A config syntax error brings down Nginx — everything goes offline | Always validate: sudo nginx -t before sudo systemctl restart nginx |
Using system Node.js from apt |
Outdated Node.js version causes build failures or incompatibility with newer packages | Install Node.js via NVM for version control and isolation |
| Leaving port 3000 open in UFW | App is directly accessible without Nginx — bypasses SSL, headers, and access control | Block port 3000 publicly; only Nginx should proxy to it internally |
Frequently Asked Questions (FAQ)
Why use PM2 instead of Systemd for Next.js?
Both work, and Systemd is perfectly valid for Node.js. The reason I reach for PM2 is the developer experience — built-in cluster mode across CPU cores, pm2 monit for real-time process stats, log aggregation with pm2 logs, and zero-downtime restarts with pm2 reload. For teams where multiple developers manage deployments, PM2's commands are more intuitive than writing and debugging Systemd unit files. That said, if you want tighter OS-level integration and no additional dependencies, Systemd is a solid choice too.
How do I deploy code updates without downtime?
This is where PM2's cluster mode earns its keep. After pulling new code and rebuilding, use pm2 reload instead of pm2 restart. The reload command performs a rolling restart — it brings up new instances of your app before shutting down old ones, so there's no gap in availability.
cd /home/ubuntu/webapps/nextjs-app.com
git pull origin main
npm install
npm run build
pm2 reload nextjs-app
Can I run multiple Next.js apps on the same Linux server?
Yes, absolutely. Assign each app a different internal port (e.g., 3000, 3001, 3002) in their respective PM2 configs. Then create a separate Nginx server block for each domain, pointing to the correct port. Nginx handles routing by domain name — it's designed exactly for this use case.
My app works locally but shows a 502 Bad Gateway after deploying — what now?
A 502 means Nginx is running, but it can't reach your Next.js app on port 3000. The most common causes: PM2 process isn't running (pm2 list to check), the app is bound to the wrong port, or it crashed at startup. Check live logs immediately with pm2 logs nextjs-app — the stack trace will tell you exactly what failed.
Do I need to run npm run build every time I update my code?
Yes, for every change that affects your application code — pages, components, API routes, styles. Next.js is a compiled framework; the .next/ build artifact must be regenerated to reflect your changes. Only configuration files like .env.local that are read at runtime can sometimes be updated without a full rebuild, depending on how your app accesses them.