Building FoxyInvoice - Chapter 6: Accounts, hosting, DNS, email - from zero to a domain
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)
- 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 below covers 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>. - MX records - inbound mail routing (below).
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.
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:
postgreshealthy →apihealthy →workerrunning - [ ] 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.
Comments
No comments yet. Start the discussion.