Deploying a Next.js application to production requires more than just running npm start. You need a reverse proxy to handle static assets efficiently, a process manager to keep your application running, and a strategy for zero-downtime deployments. This tutorial shows you how to configure Nginx and PM2 to deploy a Next.js application in production.
The Problem
Running a Node.js application directly on your server has several issues. Node.js doesn’t restart itself if it crashes. If you try to run multiple instances of your application on the same port, they’ll conflict. Static assets like JavaScript, CSS, and images should be served directly by a web server for better performance, not handled by your application server. These problems are solved by combining Nginx and PM2.
Nginx acts as a reverse proxy sitting in front of your Next.js application. It serves static assets directly from disk, handles SSL/TLS termination, and forwards everything else to Node. PM2 manages your Node.js processes, keeps them running if they crash, enables clustering across CPU cores, and allows zero-downtime deployments.
Prerequisites
- A Linux server (Ubuntu 20.04 or later recommended)
- Node.js 20+ and npm installed
- A built Next.js application ready for production
- Sudo or root access to the server
- A domain name pointing to your server (for SSL certificates)
Step 1: Install Node.js and PM2
First, ensure Node.js is installed on your server. Install PM2 globally:
npm install -g pm2
Verify the installation:
pm2 --version
Step 2: Prepare Your Next.js Application
Build your Next.js application for production:
npm run build
This creates a .next directory containing the optimized build output. Copy your entire project to your server. For this guide, we’ll assume it’s located at /var/www/nextjs-app.
Step 3: Create a PM2 Ecosystem Configuration File
PM2 uses an ecosystem configuration file to define how your application should run. Create the file at /var/www/nextjs-app/ecosystem.config.js:
module.exports = {
apps: [{
name: 'nextjs-app',
script: 'npm',
args: 'start',
cwd: '/var/www/nextjs-app',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: 3000
},
error_file: '/var/log/pm2/nextjs-app-error.log',
out_file: '/var/log/pm2/nextjs-app-out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z'
}]
}
Here’s what each setting does:
- name: The name of your application for PM2 management
- script: The command to run (
npmin this case) - args: The arguments passed to npm (
startrunsnpm start) - cwd: The working directory where PM2 should run the command
- instances: Set to
maxto run one process per CPU core - exec_mode: Set to
clusterso multiple instances share the same port via Node.js clustering - env: Environment variables available to your application
- error_file and out_file: Log locations for debugging
- log_date_format: Timestamp format for log entries
Step 4: Start Your Application with PM2
Start your application using the ecosystem file:
cd /var/www/nextjs-app
pm2 start ecosystem.config.js
Verify that your application is running:
pm2 list
You should see your Next.js application running with multiple instances (one per CPU core). Check that it’s listening on port 3000:
netstat -tlnp | grep 3000
Save your PM2 configuration so it automatically restarts on server reboot:
pm2 save
pm2 startup
The pm2 startup command will output a command you need to run with sudo. Copy and run it:
sudo env PATH=$PATH:/usr/bin /usr/local/lib/node_modules/pm2/bin/pm2 startup systemd -u yourusername --hp /home/yourusername
Step 5: Install and Configure Nginx
Install Nginx:
sudo apt install nginx
Create a Nginx configuration file for your Next.js application at /etc/nginx/sites-available/nextjs-app:
# Next.js Application Server Configuration
server {
server_name yourdomain.com www.yourdomain.com;
# Serve Next.js static assets directly with Nginx
location /_next/static/ {
alias /var/www/nextjs-app/.next/static/;
expires 1y;
add_header Cache-Control "public, immutable";
add_header X-Served-By "nginx";
}
# Serve public folder assets (favicon, images, etc.)
location ~* \.(ico|png|jpg|jpeg|gif|svg|webp|woff|woff2)$ {
root /var/www/nextjs-app/public;
expires 1y;
add_header Cache-Control "public, immutable";
add_header X-Served-By "nginx";
}
# Everything else proxied to Next.js application
location / {
proxy_pass http://127.0.0.1:3000;
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;
}
listen 80;
}
The key aspects of this configuration are:
- Static assets (
/_next/static/): Served directly by Nginx with a one-year cache expiration. These files have content hashes in their names, so browsers will cache them indefinitely. - Public folder assets: Images, fonts, and favicons are served from your public directory with the same long-lived cache.
- Everything else: Proxied to your Next.js application running on
127.0.0.1:3000. - Proxy headers: Essential headers that tell your Node.js application about the original client and protocol.
Enable the site:
sudo ln -s /etc/nginx/sites-available/nextjs-app /etc/nginx/sites-enabled/
Test the Nginx configuration:
sudo nginx -t
If there are no errors, restart Nginx:
sudo systemctl restart nginx
Step 6: Configure SSL with Let’s Encrypt
Security requires HTTPS. Install Certbot and the Nginx plugin:
sudo apt install certbot python3-certbot-nginx
Generate and install an SSL certificate:
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
Follow the prompts to agree to the terms of service. Certbot will automatically modify your Nginx configuration to use SSL and redirect HTTP traffic to HTTPS. Verify the redirect works:
curl -I http://yourdomain.com
You should see a 301 redirect to HTTPS. Certbot also sets up automatic certificate renewal. Verify the renewal timer is active:
sudo systemctl status certbot.timer
Step 7: Verify Your Deployment
Visit your domain in a browser. Your Next.js application should load successfully through Nginx. Check the browser console to verify static assets are being served by Nginx (look for the X-Served-By: nginx header).
You can also verify headers with curl:
curl -I https://yourdomain.com/_next/static/somefile.js
The response should include X-Served-By: nginx and the long cache expiration headers.
Zero-Downtime Deployments
When you need to deploy a new version of your application, use PM2’s reload command to replace processes one at a time without downtime:
pm2 reload ecosystem.config.js
This command gracefully shuts down one process at a time, starts a new one with fresh code, and waits for it to become healthy before moving to the next. Load balancing among the cluster continues during this time.
For a full restart (needed in some cases):
pm2 restart ecosystem.config.js
Monitor your application during deployments:
pm2 monit
Monitoring and Maintenance
Check application status:
pm2 status
View real-time logs:
pm2 logs nextjs-app
View logs from a specific instance:
pm2 logs nextjs-app --lines 100 --stream
If your application crashes, PM2 automatically restarts it. Monitor for crashes:
pm2 report
Nginx logs are located at /var/log/nginx/access.log and /var/log/nginx/error.log. Monitor them for issues:
tail -f /var/log/nginx/error.log
Increasing Performance
Gzip compression: Add this to your Nginx configuration inside the server block to compress responses:
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
gzip_min_length 1000;
Increase file descriptor limits: Edit /etc/security/limits.conf and add:
www-data soft nofile 65535
www-data hard nofile 65535
This allows Nginx to handle more concurrent connections.
Load balancing across multiple servers: If your application grows, modify the proxy_pass directive in your Nginx configuration to split traffic across multiple backend servers:
upstream nextjs_backend {
server 127.0.0.1:3000;
server 127.0.0.1:3001;
server 127.0.0.1:3002;
}
location / {
proxy_pass http://nextjs_backend;
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;
}
Troubleshooting
If your application isn’t accessible:
- Check that PM2 is running your application:
pm2 list - Check that Nginx is running:
sudo systemctl status nginx - Verify Nginx configuration is valid:
sudo nginx -t - Check Nginx error logs:
sudo tail -f /var/log/nginx/error.log - Check your application logs:
pm2 logs nextjs-app - Verify port 3000 is listening:
netstat -tlnp | grep 3000 - Verify Nginx is listening on port 80/443:
sudo netstat -tlnp | grep nginx
If static assets return 404 errors, verify that the paths in your Nginx configuration match the actual file paths on your server.
Conclusion
You now have a production-ready deployment of your Next.js application with Nginx serving static content and PM2 managing your Node.js processes. This setup provides high availability, zero-downtime deployments, and efficient static asset delivery. As your application scales, you can expand this architecture by adding multiple backend servers and implementing advanced load balancing strategies.