Ziwen

← Back to blog

Life After Squeezing a Dozen Servers into One Compose File

Over the years I have accumulated six VPSs across three cloud providers, running a dozen self-hosted services: DNS, certificate issuance, file sharing, monitoring probes, and so on. Every service was deployed differently — some via a docker run with a parameter list too long to look at, some via a shell script written at 3 a.m., and one I could only describe as "something that seems to be running on that box." Last month I finally pulled everything into docker compose files. This is how it went.

The Hidden Cost of Ad-Hoc Scripts

The problem with ad-hoc scripts is not that they work — it is that they only capture a snapshot of one particular evening. Six months later, changing one environment variable turns into archaeology: is this script still the source of truth, or did I hand-edit the container afterwards? I once rebooted a machine and three services never came back, because they relied on a script that had long since fallen out ofcrontab. That incident taught me the distance between "it runs" and "it is under control" is exactly one declarative config.

The first step was almost embarrassingly simple: one directory per machine, and inside each directory exactly two things — a compose.yaml and a .env. One rule governs everything: any change to any service is made in the file first, then applied with a command. Never the other way around.

A Typical Compose Snippet

The resulting files look boring, which is precisely the point. Take the monitoring probe:

services:
  probe:
    image: probehub/agent:latest
    restart: unless-stopped
    env_file: .env
    networks:
      - probe-net
    healthcheck:
      test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:9100/health"]
      interval: 30s
      retries: 3

networks:
  probe-net:
    name: probe-net

A few deliberate choices: restart: unless-stopped so services survive reboots; a healthcheck on every service so docker compose ps tells the whole story at a glance; secrets live only in .env, so the compose file itself can go straight into git. No magic anywhere — that is the feature.

What Declarative Config Actually Buys You

The payoff turned out to be more concrete than I expected:

  • Migration got cheap. Last month I moved a service from Tokyo to Frankfurt in forty minutes: copy the directory, compose up -d, flip DNS. Most of that was waiting for TTL.
  • Upgrades gained an undo button. Change one image tag, compose up -d, and if something breaks, git revert and run it again. The pre-upgrade incense-burning ritual is retired.
  • Auditing became a diff. "What changed on this box in three months?" is now a git log question, not a shell-history excavation.

Some things are deliberately unsolved: cross-machine orchestration is still manual, each machine's compose file is independent, and I have no plans for Swarm or Kubernetes — six machines do not justify that complexity. The boundary of "declarative" is drawn exactly at the edge of what I can keep in my head.

Backups: Declarative Is Not a Backup Strategy

A necessary cold shower: a compose file describes how things run, not where the data lives. My backup strategy has two layers. The config files are a git repository, pushed daily from every machine to a private remote. Data volumes are tiered by service: databases get nightly dumps with the app's own tooling, synced to a VPS at a different provider and cold-stored again; pure cache volumes are not backed up at all — losing them means rebuilding.

I set the acceptance bar deliberately harsh: once a year, pick one service and do a real restore drill — bring it up on a clean machine from nothing but the git repo and the backups, with a timer running. This year's drill took forty minutes to full service, twice as fast as last year, because the runbook this time was the compose file itself.

"Boring" Is the Highest Compliment

My strongest feeling after the cleanup is that the system has become boring. No service has a personality I need to remember, no machine is "special," and rebooting, upgrading, and migrating all follow the same path. I used to think the fun of infrastructure was the tinkering; now I think the fun is precisely not having to tinker — sleeping through the night, not watching dashboards by day, and locating any problem in five minutes.

If your servers still host a few scripts that "worked once and nobody dared touch since," spend a weekend writing them into compose files. The process contains zero surprises — and zero surprises is the entire point.