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
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:
- Set
DB_PROVIDER=mongodb(Atlas free tier is fine to start) orDB_PROVIDER=supabase(runproviders/db/supabase/schema.sqlonce in their SQL editor). - Add every variable from
.env.examplein the Vercel dashboard — never commit secrets. - Set
NEXTAUTH_URLto your production domain. - 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.