Overview / Infrastructure
ADR 0015
Commerce deploys on self-hosted Coolify, auto-deployed from the version/4.1-beta release branch
AcceptedInfrastructure
Context
The commerce platform is several long-running services (api, worker, web, adapters, storefront) plus their datastores. We host on our own Hetzner servers via Coolify (a self-hosted PaaS) rather than a managed cloud platform — for cost control and to keep the fleet on infrastructure we operate. That choice raises three follow-on questions: which server the production commerce apps run on, what git ref production tracks, and how a deploy is triggered.
Decision
Deploy the production commerce fleet to a dedicated Coolify server (pserve), track a long-lived release branch (version/4.1-beta) rather than main, and trigger deploys by push to that branch via the Coolify GitHub App.
- One prod server for commerce. The
prod-commerce-*apps (api, worker, adapters, web, storefront) and their Postgres/Redis run together onpserve. Other platforms live on other Coolify servers; the Coolify control-plane server (cserve) is never a deploy target — building there starves the control plane (it has taken the whole fleet down), so app deploys go to app servers only. - A release branch is the deploy ref. Production tracks HEAD of
version/4.1-beta, notmain. Shipping means landing a change on that branch;mainis decoupled from what is live, so the release branch can stabilise independently of ongoing merges. - GitHub-App auto-deploy, not deploy keys. Repositories are connected to Coolify through the installed GitHub App (per org), which delivers push webhooks and triggers a build on push to the deploy branch. Per-repo deploy keys and hand-wired webhooks are not used. Because auto-deploy is wired, a change is shipped by landing on the branch — firing a manual deploy as well only causes a duplicate build.
- Build-and-swap image model. A deploy rebuilds the container image (install + compile + asset digest + migrations, see ADR 0016) and swaps the container; there is no long-lived mutable host to patch, so schema and code arrive together per release.
Consequences
- Positive: deploys are
git pushto the release branch; no bespoke CI/CD to maintain, and the same model applies across the fleet. - Positive: the release branch keeps "what's on main" separate from "what's in prod," so main can move without risking production.
- Neutral / to revisit: self-hosting means we own capacity and disk. Build peaks can pressure a server's disk and briefly drop SSH; pruning images/build cache after deploys is part of the routine, and the control-plane server must be kept out of the deploy path.
- Neutral / to revisit: the deploy target, source-connection and branch are operational configuration (in Coolify), not in the repos — this ADR records the model; the exact server ids, hosts and tokens live in ops notes/secrets, deliberately not here.
Alternatives considered
- A managed cloud PaaS / Kubernetes: rejected — higher cost and operational surface than the team needs; self-hosted Coolify on Hetzner gives push-to-deploy on infrastructure we already run.
- Deploy from
maindirectly: rejected — it couples every merge to production; a dedicated release branch lets prod stabilise independently and makes "what's live" explicit. - Per-repo deploy keys + hand-wired webhooks: rejected — the org-level GitHub App already delivers push events and manages access centrally; deploy keys were an earlier habit that added per-repo secrets for no benefit.