DEV Community

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), 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.

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.