DEV Community

Lith SEO
Lith SEO

Posted on Originally published at foxyinvoice.com

Building FoxyInvoice — Chapter 6: Accounts, hosting, DNS, email — from zero to a domain

This series is written in the open, from a real production system. This chapter is the step-by-step shopping list for turning code on a laptop into a URL someone else can type. [All chapters and diagrams live in the public repo.]

This is the chapter people skip and then suffer: the accounts, the
names, the records that turn code on your laptop into a URL someone
else can type. It's step-by-step because order matters — several steps
depend on earlier ones existing. Budget an afternoon and about the cost
of two coffees a month.

The shopping list (in dependency order)

  1. Domain — buy first; everything else hangs off it.
  2. DNS host (Cloudflare, free tier) — points the name at machines and routes mail.
  3. A VPS — one modest Linux box runs the entire system.
  4. Email sending (SES SMTP) — accounts need a verified identity and API keys.
  5. Object storage (S3-compatible) — PDFs and feedback attachments.
  6. Payments (Stripe) — for payment links and subscriptions.
  7. GitHub — code + CI/CD (you have this if you're reading on it).

Each subsection: what to click, what it costs, and the trap we hit.

Domain + Cloudflare

Buy the domain anywhere reputable; point its nameservers at
Cloudflare. Two record concepts do all the work in this project:

  • A recordfoxyinvoice.com → <VPS IP>. The trap: for Caddy's automatic TLS (next section) to prove domain ownership, the certificate authority must reach your server directly — so records stay grey-cloud (DNS-only), not proxied. One toggle; TLS silently fails without it.
  • MX records — inbound mail routing (below).

Cloudflare also gives you Email Routing free: rules like
support@yourdomain → your-real-inbox. The war story from this repo:
rules existed, but the destination address had an unverified state —
and every feedback notification bounced as a MAILER-DAEMON storm for
a day before anyone noticed the pattern. Lesson: email infrastructure
needs a delivery test, not just a config save.
We now send probe
mails on any routing change.

The VPS and the compose file

One Linux box (any provider; ours is a small OVH instance) runs
everything as Docker Compose services:

postgres (per product) · api (per product) · worker (per product) · caddy
Enter fullscreen mode Exit fullscreen mode

Two details that earned their place in the repo docs:

  • Compose projects are namespaces. The freemium stack runs with -p fox --env-file .env.freemium; forget the env-file once and compose interpolates the other stack's database password into the container, which crash-loops on auth failure. Environment files are credentials wearing a hat.
  • Data lives in volumes (pgdata), so docker compose down, rebuild, up — the data never moves.

Caddy: TLS you never think about

Caddy sits in front, terminates TLS, proxies /api/* to the api
container by name, and serves the built SPA from disk. Its
superpower is automatic certificates — Let's Encrypt/ZeroSSL issue
and renew with zero cron jobs. The trap that cost us an evening: the
Caddyfile is bind-mounted read-only as a single file into the
container; editing the host file with any inode-replacing tool
(sed -i) is invisible to the running container, and reloads happily
keep the old config. Fix: edit in place + docker restart caddy.
Config-drift bugs look like caching bugs and aren't.

Email: SES and the sandbox saga

Transaction email needs a real SMTP relay (your VPS's port-25 mail will
land in spam purgatory). We use AWS SES via SMTP credentials — with the
honest story that our production-grade SES lives in an older AWS
account while the newer one sits in sandbox (can only email
verified addresses). The pragmatic architecture that fell out: the
mailer takes host/credentials from env vars — swap relays without code
changes. Also: feedback emails set Reply-To to the reporter, so
staff replies reach users instead of a dead inbox.

Object storage + payments

S3-compatible storage holds rendered invoice PDFs and feedback
attachments (screenshots and screen recordings — ≤ 15 MB, with a
server-side size guard so a giant recording can't choke memory).
Stripe needs only a developer account to start: payment links are
created per invoice server-side; the webhook is the source of truth for
"paid." Subscriptions (Pro/Business) run through Stripe Checkout so,
again, we never see a card number.

Secrets: generated, never committed

A generate-secrets.sh produces random DB passwords, JWT signing keys,
and admin passwords at first setup; they live in .env files on the
host, referenced by compose. The repo carries a secret-scanning gate
(gitleaks) in CI because everyone eventually pastes a key somewhere —
the gate turns a bad Tuesday into a red check.

The launch checklist

  • [ ] Domain bought, nameservers at Cloudflare
  • [ ] A record → VPS IP, grey-cloud
  • [ ] Compose up: postgres healthy → api healthy → worker running
  • [ ] Caddy obtained certificates (site loads with the padlock)
  • [ ] SES identity verified; probe email sent and received
  • [ ] Email routing rules + destination verified (probe again)
  • [ ] Backups cron'd, size-checked, landing off-box
  • [ ] Secrets generated on-host; repo scan green
  • [ ] Stripe webhook URL configured and tested

Recap. Domain → DNS (grey-cloud!) → one VPS running compose →
Caddy's auto-TLS → SES with probes → storage → Stripe → secrets by
script. The traps are all configuration drift that looks like caching
— until you write probes for each hop.


Reading this and want a domain of your own to point at? Create a free workspace at
foxyinvoice.com, then redeem founding code
U8B4Z8S87X on the Upgrade page — 6 months of Pro, free, no card. If anything
breaks, there's a feedback button in the app. I read every one.


Next: Chapter 7 — CI/CD: push to main and it's live.

Top comments (0)