Skip to content

Latest commit

 

History

History
331 lines (253 loc) · 10.4 KB

File metadata and controls

331 lines (253 loc) · 10.4 KB

SETUP.md — VPS deployment with PM2

End-to-end recipe to run DBSMO on a Linux VPS using npm + pm2. Assumes Ubuntu/Debian and SSH access.

1. One-time host setup

Install Node 20+, PostgreSQL, and PM2.

# Node 20 (NodeSource)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs

# PostgreSQL
sudo apt-get install -y postgresql postgresql-contrib

# Sandboxed Asymptote diagram rendering
sudo apt-get install -y asymptote bubblewrap imagemagick ghostscript texlive-latex-base

# PM2 (global, runs as your deploy user)
sudo npm install -g pm2

Create a Postgres database and a role:

sudo -u postgres psql <<SQL
CREATE USER dbsmo WITH PASSWORD 'change-me';
CREATE DATABASE dbsmo OWNER dbsmo;
SQL

2. Clone and configure

git clone https://github.com/CosmicCrusader23/dbsmo.git
cd dbsmo
cp .env.example .env

Edit .env:

DATABASE_URL=postgresql://dbsmo:change-me@localhost:5432/dbsmo
NEXTAUTH_URL=https://your.domain.example
NEXTAUTH_SECRET=<run: openssl rand -base64 32>
GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...
SCHOOL_EMAIL_DOMAINS=g.dbs.edu.hk,dbs.edu.hk
AUTH_DEV_BYPASS=false
STORAGE_DRIVER=local
LOCAL_STORAGE_ROOT=./storage
MAX_JSON_UPLOAD_MB=5
MAX_ZIP_UPLOAD_MB=50
ASYMPTOTE_ENABLED=true
ASYMPTOTE_RENDER_TIMEOUT_MS=10000

SCHOOL_EMAIL_DOMAINS is an exact, comma-separated allowlist. Keep AUTH_DEV_BYPASS=false in production; the credentials bypass is available only when it is explicitly set to true outside production.

Local storage is appropriate on a VPS when LOCAL_STORAGE_ROOT is persistent. For an S3-compatible backend, set STORAGE_DRIVER=s3 and also configure S3_ENDPOINT, S3_BUCKET, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, and optionally S3_REGION.

MAX_JSON_UPLOAD_MB and MAX_ZIP_UPLOAD_MB may be lowered for a smaller deployment; the application hard-caps them at 5 MB and 50 MB respectively.

ASYMPTOTE_ENABLED=true enables staff-only diagram compilation. Production requires Linux bubblewrap; there is intentionally no unsandboxed fallback. The renderer also uses /usr/bin/prlimit (provided by Ubuntu's util-linux) and defaults to /usr/bin/asy plus /usr/bin/bwrap. Override ASYMPTOTE_BIN, ASYMPTOTE_BWRAP_BIN, or ASYMPTOTE_PRLIMIT_BIN only when the packages are installed elsewhere. ASYMPTOTE_RENDER_TIMEOUT_MS accepts 1,000-30,000 milliseconds.

Generate the secret if you don't have one:

openssl rand -base64 32

3. Install + schema + build

npm ci
npx prisma generate
npx prisma db push          # creates / updates the schema
npm run build

This repo uses prisma db push (not prisma migrate). See §7 for why and how to handle subsequent schema changes.

4. Start with PM2

The repo doesn't ship a PM2 ecosystem file. Two options:

Option A — single command:

pm2 start "npm run start" --name dbsmo --time
pm2 save
pm2 startup        # follow the printed command (sets up systemd hook)

Option B — ecosystem file (recommended for redeploys):

Create ecosystem.config.cjs in the project root:

module.exports = {
  apps: [
    {
      name: "dbsmo",
      script: "node_modules/next/dist/bin/next",
      args: "start -p 3000",
      cwd: __dirname,
      instances: 1,
      exec_mode: "fork",
      max_memory_restart: "512M",
      env: { NODE_ENV: "production" },
      out_file: "logs/out.log",
      error_file: "logs/err.log",
      time: true,
    },
  ],
};

Then:

mkdir -p logs
pm2 start ecosystem.config.cjs
pm2 save
pm2 startup       # run the printed sudo command once

Verify:

pm2 status
pm2 logs dbsmo --lines 50
curl -I http://localhost:3000

5. Reverse proxy (nginx) + TLS

Minimal nginx site file at /etc/nginx/sites-available/dbsmo:

server {
  server_name your.domain.example;

  # App routes enforce their own lower, route-specific streamed body limits.
  # This ceiling accommodates the largest supported image/PDF authoring request.
  client_max_body_size 180m;

  location / {
    proxy_pass http://127.0.0.1:3000;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 60s;
  }

  listen 80;
}

The application rejects oversized JSON, multipart, PDF, image, ZIP, backup, and restore payloads while streaming them. Keep nginx's ceiling at 180m so supported admin authoring requests reach those route-specific checks; do not set it to unlimited.

Enable + TLS:

sudo ln -s /etc/nginx/sites-available/dbsmo /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d your.domain.example

6. Redeploy after a code change

cd ~/dbsmo
git pull origin main
npm ci
npx prisma generate         # regen client when schema.prisma changes
npx prisma db push          # apply schema (this repo uses db push, not migrate)
npm run build
pm2 reload dbsmo            # zero-downtime reload
pm2 logs dbsmo --lines 30

When .env changes, reload with pm2 reload dbsmo --update-env. Verify the running process received the renderer setting with pm2 env <id> | grep ASYMPTOTE.

If you only changed CSS or a static asset, pm2 restart dbsmo is fine.

Important: prisma generate must run before pm2 reload whenever schema.prisma changes. The pm2 process loads @prisma/client once at startup; if you skip generate, the client won't know about new models and any API touching them will return 500.

Deploying this pull (image-asset support)

This commit adds the ProblemSetAsset table and a few related fields. Run the standard redeploy block above — prisma db push will create the new table and indexes, prisma generate rebuilds the client, and the pm2 reload picks up the new code. Verify with:

psql -U dbsmo -h localhost dbsmo -c '\d "ProblemSetAsset"'

You should see columns id, problemSetId, key, fileId, createdAt.

Deploying this pull (Asymptote and multiple choice)

Install the renderer packages from section 1, add the ASYMPTOTE_* environment values, then run the standard redeploy block. prisma db push adds MULTIPLE_CHOICE to the AnswerType enum and the non-null Problem.options text array.

Verify the database and Linux sandbox:

psql "${DATABASE_URL%%\?*}" -c '\d "Problem"'
sudo -u judge_user bwrap --unshare-all --ro-bind /usr /usr /usr/bin/true
command -v asy bwrap prlimit gs
command -v magick || command -v convert
pm2 reload dbsmo --update-env

The Problem table should include options text[], and the bubblewrap command should exit successfully without output. If the PM2 process runs as another account, use that account instead of judge_user for the sandbox check. Do not enable an unsandboxed renderer to work around a failed check; fix user namespaces/package paths instead.

Deploying this pull (classes and assignments)

Standard redeploy. prisma db push will create three new tables: Class, ClassMember, Assignment. Verify with:

psql -U dbsmo -h localhost dbsmo -c '\d "Class"'
psql -U dbsmo -h localhost dbsmo -c '\d "ClassMember"'
psql -U dbsmo -h localhost dbsmo -c '\d "Assignment"'

Deploying this pull (account-scoped sidebar settings)

The settings sidebar now stores its normalized layout in the optional User.sidebarPreferences column. Run the standard redeploy block above so prisma generate updates the client and prisma db push adds the column:

npx prisma generate
npx prisma db push
pm2 reload dbsmo

Verify the additive schema change with:

psql -U dbsmo -h localhost dbsmo -c '\d "User"'

Existing users start with the default sidebar. The browser cache is scoped to the authenticated user ID; the database value remains authoritative across browsers and devices. Do not manually write sidebar URLs into the column.

7. Database schema changes

This repo uses prisma db push (not prisma migrate), so any schema.prisma change applies the same way every time:

git pull
npx prisma generate         # regen the client
npx prisma db push          # apply schema to the live DB
pm2 reload dbsmo

db push is idempotent — running it when nothing changed is a no-op. It does not drop columns unless you explicitly accept the prompt, so it's safe for additive changes (new tables, new optional columns, new indexes). Back up first (pg_dump, see §8) if a change is destructive or you're not sure.

If you ever need to switch this repo to migration files, run prisma migrate dev --name baseline on dev once to capture the current state, commit prisma/migrations/, and from then on use prisma migrate deploy on the VPS.

8. Backups

pg_dump -U dbsmo -h localhost dbsmo | gzip > backups/dbsmo-$(date +%F).sql.gz

Cron it:

0 3 * * * cd /home/<you>/dbsmo && pg_dump -U dbsmo -h localhost dbsmo | gzip > backups/dbsmo-$(date +\%F).sql.gz
0 4 * * 0 find /home/<you>/dbsmo/backups -name '*.sql.gz' -mtime +30 -delete

9. Monitoring + maintenance

  • pm2 monit — live CPU + memory, log tail.
  • pm2 logs dbsmo --err — error stream only.
  • pm2 reload dbsmo --update-env — pick up new env vars after editing .env.
  • pm2 delete dbsmo then re-start if the process gets stuck.

10. Troubleshooting

  • 502 Bad Gatewaypm2 status says errored/stopped. Check pm2 logs dbsmo --err.
  • Build OOM on VPS — small VPS? NODE_OPTIONS="--max-old-space-size=2048" npm run build.
  • Prisma can't connect — verify DATABASE_URL, run psql "$DATABASE_URL" -c '\dt' to confirm credentials.
  • Google sign-in loopsNEXTAUTH_URL must be your public HTTPS URL exactly, and the OAuth redirect URI in Google Cloud must include https://your.domain.example/api/auth/callback/google.
  • Schema out of sync — re-run npx prisma db push then pm2 reload dbsmo. If pm2 logs --err shows "Unknown field" or "Unknown model", the running pm2 process is loading a stale @prisma/client. Run npx prisma generate then pm2 reload dbsmo.
  • Asymptote returns 503 — confirm ASYMPTOTE_ENABLED=true is visible in pm2 env, all renderer commands exist, and PM2 was reloaded with --update-env.
  • Asymptote sandbox fails — run the bubblewrap check from section 6 as the PM2 account. Do not set -nosafe or bypass bubblewrap; the source is executable input.