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.