Loading...
Complete guide to deploying Next.js applications on a VPS — Node.js setup, PM2 process management, Nginx reverse proxy, SSL certificates, and CI/CD automation.
| Feature | Vercel | VPS |
|---|---|---|
| Pricing | Per-function/bandwidth | Fixed monthly |
| Custom server | ❌ (limited) | ✅ Full control |
| WebSocket | ❌ (workarounds) | ✅ Native |
| File storage | ❌ External needed | ✅ Local disk |
| Database | External only | Same server or separate |
| Cron jobs | Vercel Cron (limited) | Full crontab |
| SSH access | ❌ | ✅ |
| Cost at scale | Expensive | Predictable |
# Ubuntu 22.04 LTS on your VPS
# Update system
sudo apt update && sudo apt upgrade -y
# Install PostgreSQL 16
sudo apt install -y postgresql postgresql-contrib
# Create database and user
sudo -u postgres psql
CREATE USER portfolio_user WITH PASSWORD 'secure_password';
CREATE DATABASE portfolio_db OWNER portfolio_user;
GRANT ALL PRIVILEGES ON DATABASE portfolio_db TO portfolio_user;
\q
# Test connection
psql -U portfolio_user -d portfolio_db -h localhost
# Clone repository
git clone https://github.com/your-repo/portfolio.git /home/deploy/portfolio
cd /home/deploy/portfolio
# Install dependencies
npm ci --production=false # Include dev deps for build
# Create .env file
cat > .env << EOF
DATABASE_URL="postgresql://portfolio_user:secure_password@localhost:5432/portfolio_db"
JWT_SECRET="your-64-char-hex-secret"
CRON_SECRET="your-cron-secret"
NODE_ENV="production"
EOF
# Run Prisma migrations
npx prisma migrate deploy
# Seed initial data
npm run seed
# Build Next.js
npm run build
// ecosystem.config.js
module.exports = {
apps: [
{
name: "portfolio",
script: "node_modules/.bin/next",
args: "start",
cwd: "/home/deploy/portfolio",
env: {
NODE_ENV: "production",
PORT: 3000,
},
instances: "max", // Use all CPU cores
exec_mode: "cluster", // Cluster mode for multi-core
max_memory_restart: "500M",
log_date_format: "YYYY-MM-DD HH:mm:ss",
error_file: "/home/deploy/logs/portfolio-error.log",
out_file: "/home/deploy/logs/portfolio-out.log",
},
{
name: "websocket",
script: "dist/server.js",
cwd: "/home/deploy/portfolio/hk-websocket-server",
env: {
NODE_ENV: "production",
PORT: 3001,
},
instances: 1, // Single instance for WebSocket
max_memory_restart: "200M",
},
],
};
# Start applications
pm2 start ecosystem.config.js
# Save PM2 process list (survives reboot)
pm2 save
# Generate startup script
pm2 startup systemd
# Run the generated command
# Monitor
pm2 monit
pm2 logs
# /etc/nginx/sites-available/portfolio
server {
listen 80;
server_name hardikkanajariya.in www.hardikkanajariya.in;
# Redirect to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name hardikkanajariya.in www.hardikkanajariya.in;
# SSL certificates (Certbot)
ssl_certificate /etc/letsencrypt/live/hardikkanajariya.in/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/hardikkanajariya.in/privkey.pem;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Strict-Transport-Security "max-age=31536000" always;
# Gzip compression
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml;
# Static files (serve directly)
location /_next/static {
proxy_pass http://localhost:3000;
add_header Cache-Control "public, max-age=31536000, immutable";
}
location /uploads {
alias /home/deploy/portfolio/public/uploads;
add_header Cache-Control "public, max-age=86400";
}
# WebSocket server
location /socket.io {
proxy_pass http://localhost:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
# Next.js application
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
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;
}
}
# Enable site and test
sudo ln -s /etc/nginx/sites-available/portfolio /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
# Install Certbot
sudo apt install -y certbot python3-certbot-nginx
# Generate SSL certificate
sudo certbot --nginx -d hardikkanajariya.in -d www.hardikkanajariya.in
# Auto-renewal (certbot installs a cron automatically)
sudo certbot renew --dry-run
#!/bin/bash
# deploy.sh
set -e
cd /home/deploy/portfolio
echo "Pulling latest code..."
git pull origin main
echo "Installing dependencies..."
npm ci
echo "Running migrations..."
npx prisma migrate deploy
echo "Building..."
npm run build
echo "Restarting PM2..."
pm2 reload portfolio --update-env
echo "Deployment complete!"
Before going live:
□ .env configured with production values
□ Database migrated and seeded
□ SSL certificate installed
□ Nginx configured and tested
□ PM2 startup script generated
□ Firewall configured (80, 443 only)
□ Backups configured (daily)
□ Monitoring set up (uptime checks)
□ Domain DNS pointing to VPS
□ Test all critical flows
Our Developer Portfolio Platform includes all deployment configurations out of the box — PM2 ecosystem file, Nginx templates, and deployment scripts.
Related reads:
Follow on LinkedIn for DevOps insights.
Production-ready out of the box. Our Developer Portfolio Platform includes PM2, Nginx, and deployment scripts — $299.
Get the latest articles, tutorials, and updates delivered straight to your inbox. No spam, unsubscribe at any time.