Deploying a Production-Ready Next.js Full-Stack Application on Linux VPS

Setting up a modern Next.js app with server-side rendering, authentication, and database integration is not easy. It takes a lot of work to get all the parts working together. This guide shows you how to set up a Linux VPS to run a production-ready Next.js 16 full-stack application. It covers setting up PostgreSQL, NextAuth authentication, Prisma ORM, PM2 process management, and Nginx reverse proxy.

At the end of this tutorial, you’ll have a web app that works and can be accessed through HTTP and HTTPS. It will have automatic restarts, proper database migrations, and a modern, responsive dashboard interface.

System Requirements and Prerequisites

Before starting, ensure you have:

  • A Linux VPS (Ubuntu 24.04 LTS or RHEL 9/AlmaLinux 9)
  • Root or sudo access
  • At least 2GB RAM and 20GB storage
  • SSH access configured
  • A domain name or public IP address

Installing Node.js 20 LTS

Next.js 16 requires Node.js 20 or higher. Install using the NodeSource repository:

Ubuntu/Debian:

curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs

RHEL/CentOS/AlmaLinux:

curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash -
sudo dnf install -y nodejs

Verify the installation:

node --version
npm --version

Expected output:

v20.11.1
10.2.4

Installing and Configuring PostgreSQL

Install PostgreSQL 14 or higher for your distribution:

Ubuntu/Debian:

sudo apt update
sudo apt install -y postgresql postgresql-contrib
sudo systemctl start postgresql
sudo systemctl enable postgresql

RHEL/CentOS/AlmaLinux:

sudo dnf install -y postgresql-server postgresql-contrib
sudo postgresql-setup --initdb
sudo systemctl start postgresql
sudo systemctl enable postgresql

Create the application database and user:

sudo -u postgres psql

Inside the PostgreSQL prompt:

CREATE DATABASE testapp_production;
CREATE USER appuser WITH PASSWORD 'AppPassword2024';
GRANT ALL PRIVILEGES ON DATABASE testapp_production TO appuser;
\q

Configure PostgreSQL to accept password authentication. Edit /var/lib/pgsql/data/pg_hba.conf (RHEL) or /etc/postgresql/14/main/pg_hba.conf (Ubuntu):

# Change peer to md5 for local connections
local   all             all                                     md5
host    all             all             127.0.0.1/32            md5

Restart PostgreSQL:

sudo systemctl restart postgresql

Test the connection:

PGPASSWORD=AppPassword2024 psql -U appuser -h localhost -d testapp_production -c "SELECT version();"

Expected output:

                                                version
--------------------------------------------------------------------------------------------------------
 PostgreSQL 14.10 (Ubuntu 14.10-0ubuntu0.22.04.1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0, 64-bit
(1 row)

Initializing the Next.js Project

Create a new Next.js project with TypeScript and Tailwind CSS:

npx create-next-app@latest nextjs-app --typescript --tailwind --app --no-src-dir
cd nextjs-app

Install core dependencies:

npm install @prisma/client@^5.22.0 next-auth@^4.24.13 bcrypt@^6.0.0 zod@^4.2.1 react-hook-form@^7.69.0
npm install -D prisma@^5.22.0 @types/bcrypt@^6.0.0

The package.json should include these versions:

{
  "dependencies": {
    "@prisma/client": "^5.22.0",
    "next-auth": "^4.24.13",
    "bcrypt": "^6.0.0",
    "next": "^16.1.1",
    "react": "^19.2.3",
    "tailwindcss": "^3.4.0"
  }
}

Setting Up Prisma ORM

Initialize Prisma with PostgreSQL:

npx prisma init

This creates prisma/schema.prisma. Configure the database schema:

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id            String    @id @default(cuid())
  email         String    @unique
  name          String?
  password      String
  createdAt     DateTime  @default(now())
  updatedAt     DateTime  @updatedAt
  posts         Post[]
}

model Post {
  id        String   @id @default(cuid())
  title     String
  content   String   @db.Text
  published Boolean  @default(false)
  authorId  String
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  author    User     @relation(fields: [authorId], references: [id], onDelete: Cascade)

  @@index([authorId])
}

Create the .env file with your database connection:

DATABASE_URL="postgresql://appuser:AppPassword2024@localhost:5432/testapp_production"
NEXTAUTH_SECRET="your-secret-key-here"
NEXTAUTH_URL="http://85.29.10.87"
NODE_ENV="production"

Generate a secure NextAuth secret:

openssl rand -base64 32

Output example:

NGFR39BFGxtN6018TnjC4RoyYC+FUnyaBRD+Ae6P79o=

Run the initial migration:

npx prisma migrate dev --name init

Expected output:

Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "testapp_production", schema "public" at "localhost:5432"

Applying migration `20240307103000_init`

The following migration(s) have been created and applied from new schema changes:

migrations/
  └─ 20240307103000_init/
    └─ migration.sql

Your database is now in sync with your schema.

✔ Generated Prisma Client (v5.22.0) to ./node_modules/@prisma/client in 169ms

Verify tables were created:

PGPASSWORD=AppPassword2024 psql -U appuser -h localhost -d testapp_production -c '\dt'

Output:

                List of relations
 Schema |        Name        | Type  |  Owner
--------+--------------------+-------+---------
 public | Post               | table | appuser
 public | User               | table | appuser
 public | _prisma_migrations | table | appuser
(3 rows)

Building the Application

Update package.json scripts for production:

"scripts": {
  "dev": "next dev",
  "build": "prisma generate && prisma migrate deploy && next build",
  "start": "next start -p 3000",
  "migrate:deploy": "prisma migrate deploy"
}

Build the application:

npm run build

Expected output excerpt:

▲ Next.js 16.1.1 (Turbopack)
- Environments: .env

  Creating an optimized production build ...
✓ Compiled successfully in 6.1s
  Running TypeScript ...
  Collecting page data using 3 workers ...
  Generating static pages using 3 workers (0/9) ...
✓ Generating static pages using 3 workers (9/9) in 292.3ms
  Finalizing page optimization ...

Route (app)
┌ ○ /
├ ƒ /api/auth/[...nextauth]
├ ƒ /dashboard
├ ○ /login
└ ○ /signup

ƒ  (Dynamic)  server-rendered on demand
○  (Static)   prerendered as static content

Installing and Configuring PM2

PM2 is a production process manager for Node.js applications with automatic restarts and monitoring.

Install PM2 globally:

sudo npm install -g pm2

Ubuntu/Debian:

sudo apt install -y pm2

RHEL/CentOS:

sudo npm install -g pm2

Create ecosystem.config.js in your project root:

module.exports = {
  apps: [{
    name: 'nextjs-app',
    script: 'npm',
    args: 'start',
    instances: 1,
    exec_mode: 'fork',
    env: {
      NODE_ENV: 'production',
      PORT: 3000
    }
  }]
};

Start the application with PM2:

pm2 start ecosystem.config.js

Output:

[PM2] Starting npm in fork mode (1 instance)
[PM2] Done.
┌────┬───────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┐
│ id │ name          │ namespace   │ version │ mode    │ pid      │ uptime │ ↺    │ status    │ cpu      │ mem      │
├────┼───────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┤
│ 0  │ nextjs-app    │ default     │ N/A     │ fork    │ 25018    │ 0s     │ 0    │ online    │ 0%       │ 56.3mb   │
└────┴───────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┘

Save the PM2 process list and configure auto-start on system boot:

pm2 save
pm2 startup systemd -u yourusername --hp /home/yourusername

The output provides a command to run with sudo:

sudo env PATH=$PATH:/usr/bin pm2 startup systemd -u devopslinx --hp /home/devopslinx

Verify PM2 status:

pm2 list

View real-time logs:

pm2 logs nextjs-app --lines 50

Output example:

0|nextjs-a | ▲ Next.js 16.1.1
0|nextjs-a | - Local:         http://localhost:3000
0|nextjs-a | - Network:       http://85.29.10.87:3000
0|nextjs-a |
0|nextjs-a | ✓ Starting...
0|nextjs-a | ✓ Ready in 246ms

Configuring Nginx Reverse Proxy

Install Nginx:

Ubuntu/Debian:

sudo apt install -y nginx

RHEL/CentOS:

sudo dnf install -y nginx
sudo systemctl start nginx
sudo systemctl enable nginx

Create Nginx configuration at /etc/nginx/sites-available/nextjs-app:

server {
    listen 80;
    listen [::]:80;
    server_name 85.29.10.87;

    location / {
        proxy_pass http://localhost: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;
    }
}

Enable the site (Ubuntu/Debian):

sudo ln -s /etc/nginx/sites-available/nextjs-app /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default

For RHEL/CentOS, place the config in /etc/nginx/conf.d/nextjs-app.conf.

Test Nginx configuration:

sudo nginx -t

Output:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Reload Nginx:

sudo systemctl reload nginx

Configuring the Firewall

Allow HTTP traffic through the firewall:

Ubuntu (UFW):

sudo ufw allow 80/tcp comment 'HTTP'
sudo ufw allow 443/tcp comment 'HTTPS'
sudo ufw status

Output:

Status: active

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW       Anywhere                   # SSH
80/tcp                     ALLOW       Anywhere                   # HTTP
443/tcp                    ALLOW       Anywhere                   # HTTPS

RHEL/CentOS (firewalld):

sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
sudo firewall-cmd --list-all

Test the application:

curl -I http://85.29.10.87

Expected response:

HTTP/1.1 200 OK
Server: nginx/1.24.0 (Ubuntu)
Content-Type: text/html; charset=utf-8
X-Powered-By: Next.js

Performance Monitoring

Monitor PM2 processes in real-time:

pm2 monit

This displays:

  • CPU usage per process
  • Memory consumption
  • Process uptime
  • Real-time logs

Check application metrics:

pm2 show nextjs-app

Output includes:

Describing process with id 0 - name nextjs-app
┌───────────────────┬─────────────────────────────────────────────────┐
│ status            │ online                                          │
│ name              │ nextjs-app                                      │
│ namespace         │ default                                         │
│ version           │ N/A                                             │
│ restarts          │ 4                                               │
│ uptime            │ 8h                                              │
│ entire log path   │ /home/devopslinx/nextjs-app/logs/combined-0.log │
│ script path       │ /usr/bin/npm                                    │
│ script args       │ start                                           │
│ error log path    │ /home/devopslinx/nextjs-app/logs/err-0.log      │
│ out log path      │ /home/devopslinx/nextjs-app/logs/out-0.log      │
│ pid path          │ /home/devopslinx/.pm2/pids/nextjs-app-0.pid     │
│ interpreter       │ /usr/bin/node                                   │
│ interpreter args  │ N/A                                             │
│ script id         │ 0                                               │
│ exec cwd          │ /home/devopslinx/nextjs-app                     │
│ exec mode         │ fork_mode                                       │
│ node.js version   │ 20.19.6                                         │
│ node env          │ production                                      │
│ watch & reload    │ ✘                                               │
│ unstable restarts │ 0                                               │
└───────────────────┴─────────────────────────────────────────────────┘

Conclusion

You now have a production-ready Next.js full-stack app running on a Linux VPS with a PostgreSQL database, NextAuth authentication, PM2 process management, and an Nginx reverse proxy. The app restarts on crashes, survives server reboots, and serves traffic on HTTP port 80.

Major achievements are correct database migrations, secure authentication flow, process monitoring with PM2, and professional reverse proxy setup. For production, add SSL/TLS certificates with Let’s Encrypt, database backups, and monitoring with Prometheus and Grafana.

Avatar photo

Asif Khan

Responsible and proactive professional with more than 13 years of experience in IT systems, open source software applications, DevOps, Linux systems, and cloud operations. My main goals are to automate things, keep them safe, and make sure they are strong. I am very good at planning and building the infrastructure for services that people really want. I was drawn to the fast-paced world of cloud computing because it has resources that can be scaled up or down as needed. One of my best skills is being able to use a lot of different DevOps tools to set up, release management, and microservices ecosystems, as well as for provisioning, orchestration, and configuration management.