Complete guide for deploying a Next.js frontend and Node.js API backend to a VPS with WHM+cPanel.
# SSH as root
ssh root@your-server.com
# Install Node.js 20 (if not installed)
curl -fsSL https://rpm.nodesource.com/setup_20.x | bash -
yum install -y nodejs
# Verify installation
node --version # Should be v20.x.x
npm --version
# Install PM2 globally
npm install -g pm2
# Verify PM2
pm2 --version
# Install PostgreSQL
yum install -y postgresql-server postgresql-contrib
# Initialize database
postgresql-setup --initdb
# Start PostgreSQL
systemctl start postgresql
systemctl enable postgresql
# Verify PostgreSQL is running
systemctl status postgresql
# Switch to postgres user
su - postgres
# Access PostgreSQL
psql
# Create database
CREATE DATABASE your_db_name;
# Create user with password
CREATE USER your_db_user WITH PASSWORD 'your_secure_password';
# Grant privileges
GRANT ALL PRIVILEGES ON DATABASE your_db_name TO your_db_user;
ALTER DATABASE your_db_name OWNER TO your_db_user;
# Exit psql
\q
# Exit postgres user
exit
# Find PostgreSQL data directory
sudo -u postgres psql -c "SHOW data_directory;"
# Edit pg_hba.conf (path from above command)
nano /var/lib/pgsql/data/pg_hba.conf
Add these lines before the generic ident rules:
# Your application
host your_db_name your_db_user 127.0.0.1/32 md5
host your_db_name your_db_user ::1/128 md5
Restart PostgreSQL:
systemctl restart postgresql
# Test connection (will prompt for password)
psql -U your_db_user -d your_db_name -h localhost -W
# As root, switch to cPanel user
su - cpanel_username
# Navigate to home directory
cd ~
# Upload your code (via git, scp, or FTP)
# Example with git:
git clone https://github.com/yourusername/your-repo.git
cd your-repo
# Create .env file
nano .env
Add your production environment variables:
# Database
DATABASE_URL="postgresql://your_db_user:URL_ENCODED_PASSWORD@localhost:5432/your_db_name"
# Application URLs
APP_URL=https://app.yourdomain.com
API_URL=https://api.yourdomain.com
NEXT_PUBLIC_APP_URL=https://app.yourdomain.com
# NextAuth
NEXTAUTH_URL=https://app.yourdomain.com
NEXTAUTH_SECRET=your_nextauth_secret
# Other secrets
JWT_SECRET=your_jwt_secret
SESSION_SECRET=your_session_secret
# Add all other required environment variables
@ → %40& → %26# → %23# Install npm dependencies
npm install
# Generate Prisma client
npx prisma generate --schema=./db/schema.prisma
# Run migrations
npx prisma migrate deploy --schema=./db/schema.prisma
# Verify migrations
npx prisma migrate status --schema=./db/schema.prisma
cd apps/web
npm run build
cd ../..
cd apps/api
npm run build
cd ../..
Verify builds exist:
ls -la apps/web/.next/
ls -la apps/api/dist/
# Create ecosystem.config.js in project root
nano ecosystem.config.js
Add PM2 configuration:
module.exports = {
apps: [
{
name: 'your-api',
script: './apps/api/dist/index.js',
cwd: '/home/cpanel_username/your-project',
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: '1G',
env: {
NODE_ENV: 'production',
PORT: 4000
}
},
{
name: 'your-web',
script: 'node_modules/.bin/next',
args: 'start -p 3000',
cwd: '/home/cpanel_username/your-project/apps/web',
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: '1G',
env: {
NODE_ENV: 'production',
PORT: 3000
}
}
]
};
# Start applications
pm2 start ecosystem.config.js
# Save PM2 process list
pm2 save
# Setup PM2 to start on system boot
pm2 startup
# Copy and run the command PM2 outputs
# Example: sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u cpanel_username --hp /home/cpanel_username
# Verify apps are running
pm2 status
# Check logs
pm2 logs
# Test if apps respond locally
curl -I http://localhost:3000
curl -I http://localhost:4000
app → Full domain: app.yourdomain.comapi → Full domain: api.yourdomain.com/home/cpanel_username/public_html/app.yourdomain.com)app.yourdomain.comapi.yourdomain.com# As root
mkdir -p /etc/apache2/conf.d/userdata/std/2_4/cpanel_username/app.yourdomain.com
mkdir -p /etc/apache2/conf.d/userdata/std/2_4/cpanel_username/api.yourdomain.com
mkdir -p /etc/apache2/conf.d/userdata/ssl/2_4/cpanel_username/app.yourdomain.com
mkdir -p /etc/apache2/conf.d/userdata/ssl/2_4/cpanel_username/api.yourdomain.com
cat > /etc/apache2/conf.d/userdata/std/2_4/cpanel_username/app.yourdomain.com/proxy.conf << 'EOF'
ProxyPreserveHost On
ProxyPass / http://localhost:3000/
ProxyPassReverse / http://localhost:3000/
RewriteEngine On
RewriteCond %{HTTP:Upgrade} =websocket [NC]
RewriteRule /(.*) ws://localhost:3000/$1 [P,L]
EOF
cat > /etc/apache2/conf.d/userdata/ssl/2_4/cpanel_username/app.yourdomain.com/proxy.conf << 'EOF'
ProxyPreserveHost On
ProxyPass / http://localhost:3000/
ProxyPassReverse / http://localhost:3000/
RewriteEngine On
RewriteCond %{HTTP:Upgrade} =websocket [NC]
RewriteRule /(.*) ws://localhost:3000/$1 [P,L]
EOF
cat > /etc/apache2/conf.d/userdata/std/2_4/cpanel_username/api.yourdomain.com/proxy.conf << 'EOF'
ProxyPreserveHost On
ProxyPass / http://localhost:4000/
ProxyPassReverse / http://localhost:4000/
EOF
cat > /etc/apache2/conf.d/userdata/ssl/2_4/cpanel_username/api.yourdomain.com/proxy.conf << 'EOF'
ProxyPreserveHost On
ProxyPass / http://localhost:4000/
ProxyPassReverse / http://localhost:4000/
EOF
# Rebuild Apache config to include new files
/usr/local/cpanel/scripts/rebuildhttpdconf
# Restart Apache
systemctl restart httpd
# Verify Apache is running
systemctl status httpd
Visit these URLs in your browser:
https://app.yourdomain.comhttps://api.yourdomain.com# As cPanel user
su - cpanel_username
# Check PM2 status
pm2 status
# View logs
pm2 logs --lines 50
# Test API health
curl https://api.yourdomain.com/health
# Test frontend
curl -I https://app.yourdomain.com
yourdomain.comhttps://app.yourdomain.comRoot Cause: PM2 not configured as system service, causing daemon restarts
Permanent Fix:
# As cPanel user, run PM2 startup
cd ~/your-project
pm2 startup
# Copy the command PM2 outputs and run it as ROOT
# Example: sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u cpanel_username --hp /home/cpanel_username
# After running startup command, save PM2 process list
pm2 save
# Verify apps are running
pm2 status
Quick Recovery Command (when apps crash):
cd ~/your-project && pm2 restart all
Check Why Apps Crashed:
# View error logs
pm2 logs --lines 100 --err
# Check memory usage
pm2 monit
Cause: PM2 apps not running or Apache can't connect
Solution:
# Check PM2 status
pm2 status
# Restart apps if stopped
cd ~/your-project
pm2 start ecosystem.config.js
# Test local connectivity
curl -I http://localhost:3000
curl -I http://localhost:4000
Cause: Apache proxy not configured correctly
Solution:
# Verify proxy config files exist
ls -la /etc/apache2/conf.d/userdata/ssl/2_4/cpanel_username/app.yourdomain.com/
ls -la /etc/apache2/conf.d/userdata/ssl/2_4/cpanel_username/api.yourdomain.com/
# Rebuild and restart Apache
/usr/local/cpanel/scripts/rebuildhttpdconf
systemctl restart httpd
Cause: PostgreSQL authentication or wrong credentials
Solution:
# Check pg_hba.conf has md5 auth for your user
cat /var/lib/pgsql/data/pg_hba.conf | grep -v "^#" | grep -v "^$"
# Test database connection
psql -U your_db_user -d your_db_name -h localhost -W
# Verify DATABASE_URL in .env has URL-encoded password
grep DATABASE_URL ~/your-project/.env
Solution:
# Re-run AutoSSL in cPanel
# Or manually via command line:
/usr/local/cpanel/bin/autossl_check --all --verbose
# View status
pm2 status
# View logs (all apps)
pm2 logs
# View logs (specific app)
pm2 logs your-web
pm2 logs your-api
# Restart apps
pm2 restart all
pm2 restart your-web
pm2 restart your-api
# Stop apps
pm2 stop all
# Delete apps from PM2
pm2 delete all
# Restart Apache
systemctl restart httpd
# View Apache status
systemctl status httpd
# View Apache error logs
tail -f /etc/apache2/logs/error_log
# View domain-specific logs
tail -f /etc/apache2/logs/domlogs/app.yourdomain.com
# Access database
psql -U your_db_user -d your_db_name -h localhost
# Run new migrations
cd ~/your-project
npx prisma migrate deploy --schema=./db/schema.prisma
# Reset database (CAUTION: Deletes all data)
npx prisma migrate reset --schema=./db/schema.prisma
Use this checklist for future deployments:
npm audit fix)chmod 600 .env
chmod 755 apps/
For better performance, increase instances in ecosystem.config.js:
instances: 'max', // Use all available CPU cores
exec_mode: 'cluster'
# Check if mod_deflate is enabled
apachectl -M | grep deflate
Add to Apache proxy configs:
<FilesMatch "\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$">
ExpiresActive On
ExpiresDefault "access plus 1 year"
</FilesMatch>
# Create backup script
cat > ~/backup-db.sh << 'EOF'
#!/bin/bash
DATE=$(date +%Y%m%d_%H%M%S)
pg_dump -U your_db_user -h localhost your_db_name > ~/backups/db_backup_$DATE.sql
# Keep only last 7 days
find ~/backups/ -name "db_backup_*.sql" -mtime +7 -delete
EOF
chmod +x ~/backup-db.sh
# Setup cron job (daily at 2 AM)
crontab -e
# Add: 0 2 * * * /home/cpanel_username/backup-db.sh
Use git for version control and regular commits.