Inkress ADRsarchitecture decisions
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 on pserve. 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, not main. Shipping means landing a change on that branch; main is 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 push to 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 main directly: 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.
← 0014 Background jobs and outbound merchant webhooks run on Oban, migrating off Verk 0016 Schema migrations run automatically at deploy, so every migration must be expand/contract →