End-to-end recipe to run DBSMO on a Linux VPS using npm + pm2. Assumes Ubuntu/Debian and SSH access.
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 pm2Create a Postgres database and a role:
sudo -u postgres psql <<SQL
CREATE USER dbsmo WITH PASSWORD 'change-me';
CREATE DATABASE dbsmo OWNER dbsmo;
SQLgit clone https://github.com/CosmicCrusader23/dbsmo.git
cd dbsmo
cp .env.example .envEdit .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 32npm ci
npx prisma generate
npx prisma db push # creates / updates the schema
npm run buildThis repo uses prisma db push (not prisma migrate). See §7 for why
and how to handle subsequent schema changes.
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 onceVerify:
pm2 status
pm2 logs dbsmo --lines 50
curl -I http://localhost:3000Minimal 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.examplecd ~/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 30When .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 generatemust run beforepm2 reloadwheneverschema.prismachanges. The pm2 process loads@prisma/clientonce at startup; if you skip generate, the client won't know about new models and any API touching them will return 500.
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.
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-envThe 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.
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"'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 dbsmoVerify 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.
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 dbsmodb 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.
pg_dump -U dbsmo -h localhost dbsmo | gzip > backups/dbsmo-$(date +%F).sql.gzCron 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 -deletepm2 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 dbsmothen re-startif the process gets stuck.
- 502 Bad Gateway —
pm2 statussayserrored/stopped. Checkpm2 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, runpsql "$DATABASE_URL" -c '\dt'to confirm credentials. - Google sign-in loops —
NEXTAUTH_URLmust be your public HTTPS URL exactly, and the OAuth redirect URI in Google Cloud must includehttps://your.domain.example/api/auth/callback/google. - Schema out of sync — re-run
npx prisma db pushthenpm2 reload dbsmo. Ifpm2 logs --errshows "Unknown field" or "Unknown model", the running pm2 process is loading a stale@prisma/client. Runnpx prisma generatethenpm2 reload dbsmo. - Asymptote returns 503 — confirm
ASYMPTOTE_ENABLED=trueis visible inpm2 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
-nosafeor bypass bubblewrap; the source is executable input.