3 min read
One Server Is Easy to Reason About
A single VPS, Caddy and Docker Compose
Part nine of the boring stack. The app is a single Go binary with its data in one SQLite file. Where should that run?
The fashionable answers are a managed platform, serverless functions or a Kubernetes cluster. I’ve run Kubernetes on bare metal, and I’ve written about it on this site. For my own apps, I rent one virtual server.
The setup
- One VPS at Hetzner, with more CPU and memory than my apps use.
- Caddy in front. Caddy is a web server that gets and renews TLS certificates on its own. It reverse-proxies each domain to the right app. Static websites are folders that Caddy serves directly.
- Docker Compose for the apps. Each app is a container with two folders mounted from the host: one for the SQLite database, one for uploaded and generated files. All state lives in those two folders.
- Cloudflare in front of everything, for DNS and caching.
How a deploy works
A push to master triggers the pipeline on a self-hosted GitHub runner. It lints the code, builds a Docker image and tags it with the commit hash. Then it copies a compose file with exactly that tag to the server and runs docker compose pull && docker compose up -d.
The image tag is written into the file as plain text, not as a variable. A later restart on the server always uses exactly the image that was deployed, not whatever latest happens to point to.
Static sites skip all of that. They’re built and the changed files are rsynced to the server.
Why one server
- I can understand all of it. One machine, one web server config, one compose file per app. When something is wrong, there are few places to look.
- It’s cheap. One fixed monthly price for everything, with no bill that grows with traffic or requests.
- Backups are simple. Everything that matters is in a few folders. Copying them somewhere else is the backup.
- It’s fast. No cold starts, no network hop between app and database.
The setting that bites
One lesson, learned more than once: with Cloudflare in front of Caddy, every new domain needs its SSL/TLS mode set to Full (strict). The default for a new zone is “Flexible”, where Cloudflare talks to the server over plain HTTP. Caddy answers HTTP with a redirect to HTTPS, Cloudflare passes it to the browser, and the browser asks again. The page never loads. The symptom is an HTTP 308 that points at the very URL you requested.
It’s now a line in my project template, so the next site starts with the right setting.
What it costs
- One server is one point of failure. If it goes down, everything on it goes down. For my apps, a short outage is acceptable. For a hospital system, it isn’t.
- I’m the operations team. Updates, firewall, disk space and monitoring are on me. Keeping the setup small keeps that work small.
- Scaling is vertical. When an app needs more, I rent a bigger server. That goes surprisingly far.
When I’d choose differently
When downtime costs real money or puts people at risk, I’d want redundancy, and with it a more complex platform. At work, that’s often the right call. For the apps I build myself, one server I fully understand beats a platform I only partly do.
Next, and last: the websites.