deploy · tutorial

Deploying Boring: VPS with SQLite, or Vercel with MongoDB

Two supported paths, step by step. Pick the one that matches your wallet.

Boring Team

Boring Team

September 17, 2026 · 3 min read

There are exactly two ways to put Boring on the internet, and the choice comes down to one question: where should the database live?

  • SQLite lives in a file. Files need a disk that survives reboots. That's a VPS.
  • MongoDB and Supabase live on someone else's servers. That works anywhere — including Vercel.

Pick wrong and you'll learn about ephemeral filesystems at 2am. Pick right and deployment is a checklist, not an adventure.

Path A: VPS + SQLite ($5, full ownership)

This is the default personality of the template. One box, one file, no database bill.

1. Get a box. Any Ubuntu VPS with 1GB RAM. Point your domain's A record at its IP.

2. Copy the code over.

# on your laptop
scp -r . boring:/srv/boring
ssh boring
cd /srv/boring

3. Fill in .env.prod. Copy the example and set the real values. For SQLite you need exactly three lines to be right:

DB_PROVIDER=sqlite
SQLITE_PATH=/db/app.db
NEXTAUTH_URL=https://yourdomain.com

Generate the secret properly: openssl rand -base64 32.

4. Run it with Docker. The repo ships a Dockerfile tuned for SQLite:

docker build -t boring .
docker run -d --restart unless-stopped \
  -p 127.0.0.1:3000:3000 \
  -v boring-data:/db \
  --env-file .env.prod \
  --name boring boring

That -v flag is the whole ballgame. It mounts a persistent volume exactly where the database lives. Your essays live in data/blog/ inside the image — the volume only covers /db, so content and database never fight.

No Docker? Fine: npm ci --omit=dev && npm run build:prod && npm run start:prod. Just make sure SQLITE_PATH points somewhere that survives deploys.

5. Put nginx in front. Copy nginx/boring.conf to your server, replace __DOMAIN__, get a certificate, reload:

sudo cp nginx/boring.conf /etc/nginx/sites-enabled/boring
sudo cp nginx/proxy_params_boring /etc/nginx/
# replace __DOMAIN__ with your domain, then:
sudo certbot --nginx -d yourdomain.com
sudo nginx -t && sudo systemctl reload nginx

The config already handles the boring-but-critical parts: HTTP→HTTPS, security headers, rate limits on login, breathable limits on webhooks (providers retry — never 429 them into oblivion), and hard denies for *.db*, .env*, and .git paths.

6. Tell your providers where you live. In each payment dashboard, set the webhook URL to https://yourdomain.com/api/webhook/<stripe|lemonsqueezy|dodo> and paste the signing secret into .env.prod. Restart the container.

7. Back up the file. SQLite backups are glorified file copies:

npm run backup:db   # timestamped copy into ./db/backups, keeps 24

Cron it hourly. If the box ever dies, you restore one file and you're back. Try that with a managed cluster at 2am.

Path B: Vercel + MongoDB (or Supabase)

Vercel's filesystem is ephemeral — anything written to disk vanishes between requests. SQLite cannot live there. This is not a limitation of the template; it's physics. So on Vercel:

  1. Set DB_PROVIDER=mongodb (Atlas free tier is fine to start) or DB_PROVIDER=supabase (run providers/db/supabase/schema.sql once in their SQL editor).
  2. Add every variable from .env.example in the Vercel dashboard — never commit secrets.
  3. Set NEXTAUTH_URL to your production domain.
  4. Deploy. Webhooks work the same; only the database address changed.

Everything else — auth, billing rails, boards, market — behaves identically, because app code never touches a driver directly. That's the entire point of the provider folders.

The one-paragraph version

Solo on a budget: VPS + SQLite + Docker + nginx, back up the file. Team or scale: Vercel + MongoDB or Supabase. Never SQLite on serverless, never secrets in git, always HTTPS before webhooks.

Now go ship. The checklist is short on purpose.

deploytutorial