operations/deploy-vps.md

VPS Deployment Guide

Prerequisites

  • Ubuntu 22.04+ / Debian 12+
  • Node.js 24 (use nvm)
  • PostgreSQL 16+
  • Redis 7+
  • MinIO (optional, for file uploads)
  • Nginx (reverse proxy)
  • PM2 (process manager)

1. Server Setup

Install Node.js 24

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
source ~/.bashrc
nvm install 24
nvm use 24
npm install -g yarn pm2

Install PostgreSQL

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

# Create user + database
sudo -u postgres psql <<SQL
CREATE USER booking WITH PASSWORD 'YOUR_DB_PASSWORD';
CREATE DATABASE booking_prod OWNER booking;
SQL

Install Redis

sudo apt install -y redis-server
sudo systemctl enable redis-server

Install MinIO (optional)

wget https://dl.min.io/server/minio/release/linux-amd64/minio
chmod +x minio
sudo mv minio /usr/local/bin/

# Create data directory
sudo mkdir -p /data/minio
sudo chown $USER:$USER /data/minio

# Create systemd service
sudo tee /etc/systemd/system/minio.service > /dev/null <<EOF
[Unit]
Description=MinIO
After=network-online.target

[Service]
User=$USER
Environment="MINIO_ROOT_USER=booking_minio"
Environment="MINIO_ROOT_PASSWORD=YOUR_MINIO_PASSWORD"
ExecStart=/usr/local/bin/minio server /data/minio --console-address ":9001"
Restart=always

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl enable minio
sudo systemctl start minio

2. Clone & Install

mkdir -p /home/projects/booking
cd /home/projects/booking

git clone git@github.com:novagoo/booking-api.git
git clone git@github.com:novagoo/booking-web.git

# API
cd booking-api
nvm use
yarn install --frozen-lockfile

# Web
cd ../booking-web
nvm use
yarn install --frozen-lockfile

3. Environment Variables

booking-api/.env

PORT=3010
NODE_ENV=production

# Database (local PostgreSQL, port 5432)
DATABASE_URL=postgresql://booking:YOUR_DB_PASSWORD@localhost:5432/booking_prod?schema=public

# Redis (local, default port)
REDIS_HOST=localhost
REDIS_PORT=6379

# MinIO
MINIO_ENDPOINT=localhost
MINIO_PORT=9000
MINIO_ACCESS_KEY=booking_minio
MINIO_SECRET_KEY=YOUR_MINIO_PASSWORD

# Auth
JWT_SECRET=GENERATE_A_STRONG_SECRET_HERE
JWT_EXPIRES_IN=30m
JWT_REFRESH_EXPIRES_IN=30d

# Bull Board (admin)
BULL_BOARD_USER=admin
BULL_BOARD_PASS=YOUR_BULL_PASSWORD

# Email (Resend)
RESEND_API_KEY=re_xxxxxxxxx
EMAIL_FROM=BookingSystem <noreply@yourdomain.com>

# Frontend URL (for CORS + email links)
FRONTEND_URL=https://yourdomain.com
CORS_ORIGIN=https://yourdomain.com

booking-web/.env

NEXT_PUBLIC_API_URL=http://localhost:3010/api
NEXT_PUBLIC_APP_NAME=BookingSystem
NEXT_PUBLIC_APP_SHORT_NAME=BS

Note: NEXT_PUBLIC_API_URL points to localhost because Nginx proxies external requests, and Next.js server-side rewrites connect directly to the API.

4. Build & Migrate

# API: generate Prisma client + run migrations + seed
cd /home/projects/booking/booking-api
yarn prisma generate
yarn prisma migrate deploy
yarn seed    # only for first deploy, creates demo data
yarn build

# Web: build
cd /home/projects/booking/booking-web
yarn build

5. PM2 Process Manager

Create ecosystem config

cat > /home/projects/booking/ecosystem.config.cjs << 'EOF'
module.exports = {
  apps: [
    {
      name: 'booking-api',
      cwd: '/home/projects/booking/booking-api',
      script: 'dist/src/main.js',
      instances: 1,
      env: {
        NODE_ENV: 'production',
      },
    },
    {
      name: 'booking-web',
      cwd: '/home/projects/booking/booking-web',
      script: 'node_modules/.bin/next',
      args: 'start -p 3020',
      instances: 1,
      env: {
        NODE_ENV: 'production',
      },
    },
  ],
};
EOF

Start services

cd /home/projects/booking
pm2 start ecosystem.config.cjs
pm2 save
pm2 startup    # follow instructions to enable auto-start on boot

Useful PM2 commands

pm2 status              # check status
pm2 logs                # view logs
pm2 logs booking-api    # API logs only
pm2 restart all         # restart both
pm2 reload all          # zero-downtime reload

6. Nginx Reverse Proxy

sudo apt install -y nginx

Create config

sudo tee /etc/nginx/sites-available/booking > /dev/null << 'EOF'
server {
    listen 80;
    server_name yourdomain.com;

    # API
    location /api/ {
        proxy_pass http://localhost:3010/api/;
        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;
        client_max_body_size 10M;
    }

    # Web (Next.js)
    location / {
        proxy_pass http://localhost:3020;
        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;
    }
}
EOF

sudo ln -s /etc/nginx/sites-available/booking /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com

7. Deploy Updates

#!/bin/bash
# deploy.sh — run from /home/projects/booking

# API
cd booking-api
git pull
yarn install --frozen-lockfile
yarn prisma generate
yarn prisma migrate deploy
yarn build
pm2 restart booking-api

# Web
cd ../booking-web
git pull
yarn install --frozen-lockfile
yarn build
pm2 restart booking-web

echo "Deploy complete!"

Make it executable: chmod +x deploy.sh

8. Monitoring

# PM2 monitoring
pm2 monit

# Check logs
pm2 logs --lines 100

# Check disk/memory
df -h
free -h

# PostgreSQL connections
sudo -u postgres psql -c "SELECT count(*) FROM pg_stat_activity WHERE datname = 'booking_prod';"

Ports Summary

Service Port Access
API (NestJS) 3010 Internal only
Web (Next.js) 3020 Internal only
PostgreSQL 5432 Internal only
Redis 6379 Internal only
MinIO API 9000 Internal only
MinIO Console 9001 Internal only
Nginx HTTP 80 Public
Nginx HTTPS 443 Public

All services are accessed internally. Only Nginx port 80/443 is exposed to the internet.