Configuring Next.js with Nginx and PM2 for Production Deployment

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 (npm in this case)
  • args: The arguments passed to npm (start runs npm start)
  • cwd: The working directory where PM2 should run the command
  • instances: Set to max to run one process per CPU core
  • exec_mode: Set to cluster so 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:

  1. Check that PM2 is running your application: pm2 list
  2. Check that Nginx is running: sudo systemctl status nginx
  3. Verify Nginx configuration is valid: sudo nginx -t
  4. Check Nginx error logs: sudo tail -f /var/log/nginx/error.log
  5. Check your application logs: pm2 logs nextjs-app
  6. Verify port 3000 is listening: netstat -tlnp | grep 3000
  7. 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.