Chapter 06 โ Accounts, hosting, DNS, email: from zero to a domain
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)
- Domain โ buy first; everything else hangs off it.
- DNS host (Cloudflare, free tier) โ points the name at machines and routes mail.
- A VPS โ one modest Linux box runs the entire system.
- Email sending (SES SMTP) โ accounts need a verified identity and API keys.
- Object storage (S3-compatible) โ PDFs and feedback attachments.
- Payments (Stripe) โ for payment links and subscriptions.
- 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 record โ
foxyinvoice.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
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), sodocker 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.
Next: Chapter 07 โ CI/CD: push to main and it's
live โ the pipeline, its gates, and the three times it
fooled us.