Deploy Next.js on a VPS: Git, Nginx, PM2 & Production Setup (2026 Guide)

DevOps

Deploy Next.js on a VPS: Git, Nginx, PM2 & Production Setup (2026 Guide)

Learn how to deploy a Next.js application to a VPS from Git clone to production. Configure Node.js, Nginx, PM2, environment variables, SSL, and automatic deployments.

Syed Minhaj Haider•September 7, 2026
Next.jsVPSDeploymentPM2NginxNode.jsDevOpsLinux

Deploying a Next.js application to a VPS gives you significantly more control over your production environment than relying entirely on managed hosting platforms.

You control the server, Node.js runtime, process manager, reverse proxy, domains, SSL, environment variables, and deployment workflow.

But that control also means there are more pieces to configure.

This guide walks through a practical production deployment of a Next.js application on a VPS, starting with cloning the project from Git and ending with a running application behind Nginx with PM2 managing the Node.js process.

The exact commands may vary depending on your VPS provider and operating system, but the overall workflow applies to most Ubuntu-based VPS environments.

Table of Contents#

  • What We Are Building
  • Prerequisites
  • 1. Connect to Your VPS
  • 2. Update the Server
  • 3. Install Node.js
  • 4. Clone the Next.js Application
  • 5. Install Dependencies
  • 6. Configure Environment Variables
  • 7. Build the Next.js Application
  • 8. Install PM2
  • 9. Create a PM2 Ecosystem File
  • 10. Start Next.js with PM2
  • 11. Make PM2 Start After Reboots
  • 12. Configure Nginx as a Reverse Proxy
  • 13. Create the Nginx Virtual Host
  • 14. Enable the Virtual Host
  • 15. Configure DNS
  • 16. Add HTTPS / SSL
  • 17. Updating the Application
  • 18. A Better PM2 Deployment Workflow
  • 19. Common Problems You May Encounter
  • 20. Production Checklist
  • 21. Final Architecture
  • Conclusion

What We Are Building#

By the end of this guide, the deployment will look roughly like this:

text
                         Internet
                            │
                            ▼
                    ┌───────────────┐
                    │    Domain     │
                    │ example.com   │
                    └───────┬───────┘
                            │
                            ▼
                    ┌───────────────┐
                    │     Nginx     │
                    │ Reverse Proxy │
                    └───────┬───────┘
                            │
                            ▼
                    ┌───────────────┐
                    │     PM2       │
                    │ Process Mgmt. │
                    └───────┬───────┘
                            │
                            ▼
                    ┌───────────────┐
                    │    Next.js    │
                    │   Node.js     │
                    └───────────────┘

The basic deployment flow is:

text
Git repository
      ↓
Clone project
      ↓
Install Node.js
      ↓
Install dependencies
      ↓
Configure environment variables
      ↓
Build Next.js application
      ↓
Configure PM2
      ↓
Configure Nginx
      ↓
Point domain to VPS
      ↓
Configure SSL
      ↓
Application is live

Prerequisites#

Before starting, you should have:

  • A VPS running Ubuntu or another Linux distribution
  • SSH access to the server
  • A Git repository containing your Next.js application
  • A domain name
  • DNS access for the domain
  • Node.js-compatible Next.js application
  • Root or sudo access on the VPS

This guide assumes an Ubuntu-based VPS and a standard Next.js application using the production Node.js server.

Note: The exact commands may need to be adjusted depending on your VPS provider, Ubuntu version, Node.js version, and Next.js version.


1. Connect to Your VPS#

Connect to your server using SSH:

text
ssh user@your-server-ip

For example:

text
ssh root@203.0.113.10

Once connected, verify the operating system:

text
cat /etc/os-release

It's also useful to check the server's current resources:

text
free -h
df -h

A Next.js application doesn't necessarily require a powerful server, but you should make sure the VPS has enough RAM and disk space for your application and its build process.


2. Update the Server#

Before installing the application stack, update the system packages:

text
sudo apt update
sudo apt upgrade -y

If you're logged in as root, you can omit sudo.

I also recommend installing a few basic utilities:

text
sudo apt install -y git curl build-essential

build-essential can be useful when npm packages contain native dependencies that need to be compiled.


3. Install Node.js#

Next.js requires Node.js, so the first major application dependency is the Node.js runtime.

There are several ways to install Node.js on a VPS. I generally prefer using NVM (Node Version Manager) because it makes switching Node.js versions much easier.

Install NVM:

text
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash

Reload your shell:

text
source ~/.bashrc

Verify that NVM is available:

text
nvm --version

Then install the Node.js version required by your application:

text
nvm install --lts

Set it as the default:

text
nvm alias default node

Verify the installation:

text
node -v
npm -v

Important: Use the Node.js version supported by your particular Next.js version. Don't blindly install the newest Node.js release if your application's dependencies require another version.


4. Clone the Next.js Application#

Choose a directory where you want to keep the application.

For example:

text
cd /var/www

Clone your repository:

text
git clone https://github.com/USERNAME/REPOSITORY.git my-next-app

Then enter the project:

text
cd my-next-app

Check that the project was cloned correctly:

text
ls

You should see files such as:

text
package.json
next.config.js
app/
public/

Your exact structure will depend on how your Next.js project is organized.


5. Install Dependencies#

Install the project's dependencies:

text
npm install

If the project uses a lockfile and you want reproducible production installs, use the package-manager command appropriate for your project.

For npm:

text
npm ci

The important distinction is that npm install may update the lockfile, while npm ci installs exactly what is specified in the lockfile.

For a production deployment, I generally prefer:

text
npm ci

when a valid package-lock.json is committed to the repository.


6. Configure Environment Variables#

Next.js applications commonly require environment variables for:

  • Database connections
  • API URLs
  • Authentication
  • Third-party services
  • Application secrets
  • Public configuration

Create your production environment file:

text
nano .env.production

For example:

text
NEXT_PUBLIC_API_URL=https://api.example.com
DATABASE_URL=your-database-connection
NEXTAUTH_URL=https://example.com

Use the variables required by your own application.

Don't commit production secrets#

Your .env.production file should generally not be committed to Git if it contains secrets.

Make sure it is covered by .gitignore:

text
.env
.env.local
.env.production

The exact environment-file strategy depends on your deployment architecture, but production secrets should never be pushed into a public Git repository.


7. Build the Next.js Application#

Once dependencies and environment variables are configured, create a production build:

text
npm run build

A successful build should generate the .next directory.

You can then test the production application directly:

text
npm run start

Depending on your application, Next.js will normally listen on port 3000.

You can verify that the process is listening:

text
ss -lntp | grep 3000

Or test locally from the VPS:

text
curl http://127.0.0.1:3000

If you receive the application's HTML response, the Next.js server is running.

Stop the temporary process with:

text
Ctrl + C

We don't want to keep it running manually.

That's where PM2 comes in.


8. Install PM2#

PM2 is a process manager for Node.js applications.

Instead of manually running:

text
npm run start

you can let PM2 manage the application.

Install it globally:

text
npm install -g pm2

Verify:

text
pm2 -v

PM2 can:

  • Keep the application running
  • Restart it after crashes
  • Start it automatically after server reboots
  • Manage application logs
  • Run multiple processes when appropriate

9. Create a PM2 Ecosystem File#

Rather than putting all your PM2 configuration into a command, create an ecosystem file.

For example:

text
nano ecosystem.config.js

A basic configuration could look like:

text
module.exports = {
  apps: [
    {
      name: "my-next-app",
      script: "npm",
      args: "start",
      cwd: "/var/www/my-next-app",
      instances: 1,
      autorestart: true,
      watch: false,
      max_memory_restart: "500M",
      env: {
        NODE_ENV: "production",
        PORT: 3000
      }
    }
  ]
};

The important options are:

name#

The name PM2 will use for the application.

text
name: "my-next-app"

script#

The command that PM2 executes:

text
script: "npm"

args#

Arguments passed to npm:

text
args: "start"

This effectively runs:

text
npm start

cwd#

The application directory:

text
cwd: "/var/www/my-next-app"

env#

Environment variables passed to the process:

text
env: {
  NODE_ENV: "production",
  PORT: 3000
}

Adjust this configuration to match your own project.


10. Start Next.js with PM2#

Start the application using the ecosystem file:

text
pm2 start ecosystem.config.js

Check the running processes:

text
pm2 list

You should see your Next.js application in the list.

Check its logs:

text
pm2 logs my-next-app

You can also inspect the process:

text
pm2 show my-next-app

Now test the application again:

text
curl http://127.0.0.1:3000

If everything works, your Next.js server is now being managed by PM2.


11. Make PM2 Start After Reboots#

A VPS can reboot because of:

  • Operating system updates
  • Provider maintenance
  • Hardware issues
  • Manual reboots
  • Unexpected crashes

You don't want to manually start your application every time.

Generate the startup configuration:

text
pm2 startup

PM2 will print a command that you need to execute.

Run that command exactly as PM2 provides it.

Then save the currently running processes:

text
pm2 save

Now PM2 can restore your application after a server restart.

You can test this with:

text
sudo reboot

After reconnecting:

text
pm2 list

Your application should be running again.


12. Configure Nginx as a Reverse Proxy#

At this point the application is running on:

text
http://127.0.0.1:3000

But users shouldn't need to visit:

text
example.com:3000

Instead, we'll put Nginx in front of Next.js.

The architecture becomes:

text
Browser
   │
   ▼
example.com:443
   │
   ▼
Nginx
   │
   ▼
127.0.0.1:3000
   │
   ▼
Next.js

Install Nginx if it isn't already installed:

text
sudo apt install nginx -y

Check its status:

text
sudo systemctl status nginx

13. Create the Nginx Virtual Host#

Create a configuration file for your domain:

text
sudo nano /etc/nginx/sites-available/example.com

A basic configuration:

text
server {
    listen 80;
    listen [::]:80;

    server_name example.com www.example.com;

    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;
    }
}

The server_name should contain your actual domain.

The important part is:

text
proxy_pass http://127.0.0.1:3000;

This tells Nginx to forward incoming requests to the Next.js server.

Optional but important#

Handling next static files#

Next.js builds hashed, immutable static assets into .next/static/. These are requested directly by the browser under the /_next/static/ path, and there's no reason to route them through the Node.js process at all, Nginx can serve them straight from disk, which is faster and takes load off PM2.

Add this location block above the general location / block in the same server block:

text
location /_next/static/ {
    alias /var/www/my-next-app/.next/static/;
    expires 365d;
    access_log off;
}

A few things to note:

  • alias (not root) is important here — it maps /_next/static/ directly onto the .next/static/ folder, stripping the prefix.
  • The path must match wherever you cloned the app in Section 4 — adjust /var/www/my-next-app to your actual cwd.
  • expires 365d; is safe because Next.js fingerprints these filenames on every build, so a changed file gets a new URL rather than overwriting a cached one.
  • access_log off; just cuts noise from your Nginx logs — static asset hits aren't usually worth logging.

Your full server block now looks like:

text
server {
    listen 80;
    listen [::]:80;

    server_name example.com www.example.com;

    location /_next/static/ {
        alias /var/www/my-next-app/.next/static/;
        expires 365d;
        access_log off;
    }

    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;
    }
}

Note: Order matters in Nginx location blocks — a static prefix match like /_next/static/ should sit before the catch-all location / so it gets evaluated first.


14. Enable the Virtual Host#

Create a symbolic link:

text
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/example.com

Test the Nginx configuration:

text
sudo nginx -t

Do not reload Nginx until this succeeds.

You should see something similar to:

text
syntax is ok
test is successful

Then reload:

text
sudo systemctl reload nginx

15. Configure DNS#

Your domain needs to point to the VPS.

Create an A record:

text
Type: A
Name: @
Value: YOUR_SERVER_IP

For www, you can use either another A record or a CNAME depending on your DNS setup:

text
Type: CNAME
Name: www
Value: example.com

DNS propagation can take some time depending on the provider and TTL.

You can verify DNS resolution from your local machine:

text
dig example.com

Or:

text
nslookup example.com

Once the domain resolves to your VPS, visiting:

text
http://example.com

should reach Nginx and then your Next.js application.


16. Add HTTPS / SSL#

Never leave a production application running only over plain HTTP.

For a typical Nginx setup, Let's Encrypt can provide a free TLS certificate.

Install Certbot:

text
sudo apt install certbot python3-certbot-nginx -y

Then request a certificate:

text
sudo certbot --nginx -d example.com -d www.example.com

Certbot can configure the Nginx HTTPS settings for you.

Afterward, your request flow becomes:

text
HTTPS
  ↓
Nginx
  ↓
HTTP localhost:3000
  ↓
Next.js

Test your application:

text
https://example.com

Also verify certificate renewal:

text
sudo certbot renew --dry-run

17. Updating the Application#

One of the biggest advantages of using Git is that updating the production application becomes straightforward.

After pushing changes to your repository:

text
cd /var/www/my-next-app

Pull the latest code:

text
git pull

Install any dependency changes:

text
npm ci

Create a new production build:

text
npm run build

Then restart the PM2 process:

text
pm2 restart my-next-app

The basic deployment cycle becomes:

text
Developer
   ↓
git push
   ↓
Production VPS
   ↓
git pull
   ↓
npm ci
   ↓
npm run build
   ↓
pm2 restart

For a small project, this manual workflow can be perfectly reasonable.

As the project grows, you can automate it using GitHub Actions or another CI/CD system.


18. A Better PM2 Deployment Workflow#

You can also define deployment commands directly inside the PM2 ecosystem configuration.

For example:

text
module.exports = {
  apps: [
    {
      name: "my-next-app",
      script: "npm",
      args: "start",
      cwd: "/var/www/my-next-app",
      env: {
        NODE_ENV: "production",
        PORT: 3000
      }
    }
  ],

  deploy: {
    production: {
      user: "deploy",
      host: "YOUR_SERVER_IP",
      ref: "origin/main",
      repo: "git@github.com:USERNAME/REPOSITORY.git",
      path: "/var/www/my-next-app",
      "post-deploy":
        "npm ci && npm run build && pm2 reload ecosystem.config.js --env production"
    }
  }
};

Whether you should use this approach depends on your deployment architecture.

For many small applications, GitHub Actions + SSH + PM2 can provide a cleaner CI/CD workflow.


19. Common Problems You May Encounter#

A Next.js VPS deployment can fail in several different places.

Understanding where to look makes troubleshooting much easier.

Application isn't running#

Check PM2:

text
pm2 list

Then:

text
pm2 logs my-next-app

Also test Next.js directly:

text
curl http://127.0.0.1:3000

If this fails, the problem is probably with Next.js or Node.js rather than Nginx.


Nginx returns 502 Bad Gateway#

A 502 usually means Nginx cannot reach the upstream application.

Check:

text
curl http://127.0.0.1:3000

If that fails, check PM2:

text
pm2 logs

If the Next.js process is running correctly, inspect your Nginx configuration:

text
sudo nginx -t

And check the Nginx error log:

text
sudo tail -f /var/log/nginx/error.log

Application works on port 3000 but not through the domain#

This usually means the application itself is fine and the problem is somewhere around:

text
DNS
 ↓
Nginx
 ↓
Reverse Proxy

Check DNS first:

text
dig example.com

Then verify the Nginx server_name and proxy_pass configuration.


Build fails on the VPS#

Run:

text
npm run build

directly rather than through PM2.

This makes build errors easier to read.

Also check:

text
node -v
npm -v

A different Node.js version between development and production can cause unexpected build failures.


Environment variables aren't available#

Remember that changing environment variables may require a new build depending on how the variable is used.

For example, variables prefixed with:

text
NEXT_PUBLIC_

are intended to be exposed to client-side code and can be embedded during the build.

Never put private secrets in NEXT_PUBLIC_* variables.


20. Production Checklist#

Before considering the deployment complete, verify:

  • VPS is updated
  • Node.js version matches application requirements
  • Git repository cloned
  • Dependencies installed
  • Production environment variables configured
  • npm run build succeeds
  • Next.js runs correctly on localhost
  • PM2 manages the application
  • PM2 startup configured
  • PM2 processes saved
  • Nginx installed
  • Virtual host configured
  • Nginx configuration tested
  • Domain points to VPS
  • HTTPS configured
  • SSL renewal tested
  • Application works through the domain
  • PM2 logs checked
  • Nginx error logs checked
  • Server firewall configured appropriately
  • Production secrets are not committed to Git

21. Final Architecture#

After everything is configured, your production environment should look approximately like this:

text
                         ┌──────────────────┐
                         │      GitHub      │
                         └────────┬─────────┘
                                  │
                              git pull
                                  │
                                  ▼
┌───────────────────────────────────────────────────────────┐
│                         VPS                               │
│                                                           │
│  ┌─────────────┐      ┌─────────────┐                     │
│  │    Nginx    │────▶│     PM2     │                     │
│  │             │      │             │                     │
│  │ :80 / :443  │      │ Next.js     │                     │
│  └─────────────┘      │ :3000       │                     │
│                       └──────┬──────┘                     │
│                              │                            │
│                              ▼                            │
│                       Next.js Application                 │
│                                                           │
└───────────────────────────────────────────────────────────┘
                                  ▲
                                  │
                              HTTPS
                                  │
                         ┌────────┴────────┐
                         │     Browser     │
                         └─────────────────┘

The important separation is:

Nginx handles incoming HTTP/HTTPS traffic.

PM2 keeps the Node.js process alive.

Next.js serves the application.

Git provides the deployment source.

Each component has a specific responsibility, which makes the system much easier to reason about and troubleshoot.


Conclusion#

Deploying Next.js to a VPS involves more manual configuration than platforms such as Vercel, but you gain considerably more control over the environment and infrastructure.

The core workflow is straightforward once you understand how the pieces fit together:

text
Git
 ↓
Node.js
 ↓
Next.js build
 ↓
PM2
 ↓
Nginx
 ↓
Domain
 ↓
HTTPS

Deployment is only half the story, once your app is live, performance and SEO determine whether it actually gets found and ranks. I cover that in detail in Next.js Performance & SEO Optimization: 2026 Best Practices.

Once this setup is working, the next step is usually automating the deployment process so that a push to your main branch can build and deploy the application without manually SSHing into the server.

That is where a CI/CD pipeline using GitHub Actions can take this workflow to the next level.

Enjoyed the article?

I write about web development, Laravel, React, Next.js, and lessons learned from building real-world applications.

Explore more articles
DopeScripts

Building things. Writing about the journey.

HomeProjectsBlogsContactBook a Call
© 2026 DopeScripts. All rights reserved.