One server for every backend: how we run GHZ Creative’s products
Until September 2026, the backends behind our products were spread over several VPS at different hosts, each set up its own way. One of them simply vanished from the internet in August, when its network prefix was withdrawn. Another had its IP changed under us, which made dictation ten times slower.
So we moved everything to one server, with one way to deploy, one way in and one backup system. The GHZ Creative API moved first, on 27 September. GHZ Composite, getbrief and CrossDesk followed within a week.
- 1
- server for every backend
- 0
- public SSH or web ports
- 15 min
- between backup collections
One way in: Cloudflare Tunnel
The server has no public web port. A cloudflared container opens an outbound tunnel to Cloudflare, and every request arrives through it. Behind the tunnel, Traefik routes each hostname to the right container, with one small route file per project.
Each product keeps its own domain (api.crossdesk.xyz, getbrief.xyz, api.ghzcreative.ai). On the Cloudflare side, every domain gets the same baseline: HTTPS forced, TLS 1.2 minimum, a firewall rule for common attack signatures and a per-IP rate limit.
SSH is closed to the internet too. It only answers on our private Tailscale network, with keys, never passwords.
One way to deploy, with a rollback
After the deploy mistakes of June, we stopped deploying by hand. A single script does it for every project:
ghz-deploy ghz # validate: which services would be rebuilt
ghz-deploy ghz --apply # build, start, check health, roll back on failure- It builds from a copy of the source that excludes
.env, dependencies and local data, so a deploy can never touch secrets. - It builds a new, uniquely tagged image before stopping anything. A failed build changes nothing in production.
- It refuses to continue if the compose file or the running images changed during the build, which matters when several agents work on the same machine.
- It waits for the health check. If the new version is not healthy, it restarts the previous image and says so.
- It never runs migrations and never deletes a volume. Database changes stay a separate, deliberate step.
One backup system
Every hour the server exports each project: a Postgres dump, Redis, and the files needed to rebuild it. Every 15 minutes a separate Linux machine pulls those exports into an encrypted Restic repository, with snapshots and retention per project. The pull goes one way: the backup machine has read-only access to the export, and the copies live off the server.
A watcher on that machine checks the public sites, the age of the last copy and the integrity of the repository, so a silent failure shows up as an alert instead of a surprise on the day we need a restore.