DEV Community

Cover image for The $5 VPS SaaS Playbook: A Practical Next.js 15 Setup (No Vercel, No Supabase, No Clerk)
NorthernDev
NorthernDev

Posted on

The $5 VPS SaaS Playbook: A Practical Next.js 15 Setup (No Vercel, No Supabase, No Clerk)

A practical one-server playbook for running a production-shaped SaaS on a cheap VPS: auth, Stripe subscriptions, webhooks, backups, and a boring deploy workflow.

A few days ago I wrote about the cloud tax we’ve normalized — paying $50–$100/month before you validate an idea or get your first customer.

This is the follow-up people asked for: the practical setup.

No philosophy. No “just use Kubernetes”. Just a boring, production-shaped way to run a SaaS on one box.

If you want to argue with the premise, great, do it in the comments.

But at least now we’re arguing with something concrete.


The goal

A production baseline on a single VPS:

  • users can log in (magic link + Google OAuth)
  • users can pay (Stripe subscriptions)
  • your app survives mistakes (automated backups + simple restore)
  • deploys are boring (Docker Compose)
  • costs stay around $5–$10/month until you have a real reason to scale

What this guide covers

  1. VPS setup (minimal)
  2. A simple Docker Compose layout (app + proxy + backups)
  3. SQLite in production (WAL + safe backups)
  4. Stripe subscriptions + webhooks (what actually matters)
  5. A short “production checklist” before you ship

Shortcut (if you want to skip wiring): I packaged this exact baseline (auth + Stripe + Docker + backups) into a starter kit:

👉 Get the starter kit

If you prefer DIY, keep reading, the steps below are the blueprint.

1) The VPS: keep it boring

Any provider works. A $5–$10 box is enough to start.

Minimum I like:

  • 1 vCPU, 2GB RAM
  • SSD/NVMe storage
  • Ubuntu LTS

On the server:

  • create a non-root user
  • enable a firewall (allow 22, 80, 443)
  • install Docker + Docker Compose
  • point your domain (A record) to the VPS

That’s it. You don’t need a service mesh. You need a box that runs.


2) The architecture: one server, three responsibilities

I keep it to three moving parts:

A) App container

Your Next.js app + API routes for auth + Stripe webhooks.

B) Reverse proxy

Nginx (or Caddy) terminates TLS and forwards traffic to the app.

C) Backups

A tiny scheduled job that snapshots your SQLite DB and keeps retention.

This is the part people underestimate: production is mostly about recovery.


3) SQLite in production: WAL + backups that actually restore

SQLite gets dismissed because people imagine “toy database”.

In reality, for many early-stage SaaS workloads, it’s a cheat code:

  • zero external dependency
  • one file to move/backup
  • low latency (same machine)

WAL mode

WAL improves concurrency characteristics and reduces write contention.

Backups: don’t be clever

My baseline approach is intentionally boring:

  • use SQLite’s built-in .backup command (safe snapshots)
  • compress backups
  • keep 30 days of history
  • have restore steps that take 2 minutes

Conceptually:

  • snapshot: sqlite3 db.sqlite ".backup 'backup_YYYYMMDD_HHMMSS.db'"
  • gzip it
  • delete old backups

Important: test a restore once. Most “backup strategies” fail at the restore step, not the backup step.


4) Stripe subscriptions: the part that bites you later

If you’ve integrated Stripe before, you already know the pain points:

  • webhooks get retried
  • events can arrive out of order
  • “it worked in test mode” means nothing without idempotency

Here’s what I treat as non-negotiable in a production baseline:

A) Verify webhook signatures

If you accept unsigned webhook requests, you don’t have webhooks — you have a public admin endpoint.

B) Make webhooks idempotent

Stripe retries. Networks fail. You will see duplicates.

Your handlers must safely handle:

  • “same event again”
  • “same subscription update again”

The easiest pattern:

  • store Stripe IDs (customer/subscription)
  • upsert subscription state
  • don’t assume checkout events only fire once

C) Use metadata to map users

Don’t rely on email matching as your primary join key.

Attach your internal userId as metadata when creating the checkout session.

5) The production checklist (before you ship)

This is the stuff that prevents the 2am disaster:

  • [ ] HTTPS enabled + auto-renewing (Let’s Encrypt)
  • [ ] secrets are environment variables (not committed)
  • [ ] webhook signature verification is on
  • [ ] webhook handlers are idempotent (safe replays)
  • [ ] backups are running + retention set
  • [ ] you tested a restore once
  • [ ] logs are accessible (docker compose logs -f)
  • [ ] containers run as non-root (where possible)

Why this matters (and where it breaks)

I’m not claiming one server is the endgame.

I’m claiming it’s the fastest way to:

  • validate the product
  • collect payments
  • keep runway
  • avoid early lock-in

When does this stop being enough?

  • heavy write contention
  • multi-region requirements
  • strict HA requirements
  • enterprise compliance constraints

That’s the point: upgrade when reality forces it, not because a tutorial told you to.


If you want the exact setup (without rebuilding it)

I bundled this “one-server SaaS baseline” as a starter kit:

  • Next.js 15 app structure
  • magic link auth + Google OAuth
  • Stripe subscriptions + billing page + customer portal + webhooks
  • SQLite + migrations
  • Docker deployment + automated daily backups + restore steps

👉 Get the kit here


Discussion (I genuinely want opinions)

1) What’s your line for when SQLite stops being acceptable?

2) If you self-host, what tends to go wrong first: SSL, deploys, backups, or webhooks?

3) Would you rather pay the managed-service tax early, or keep infra minimal until revenue?

If you disagree with anything above, I’m happy to debate it — with numbers or real failure modes.

Top comments (0)