Declarative Production

ForkTex Engineering · September 5, 2026 · 3 min

Ask a repository how it runs in production and you usually get a scavenger hunt: a Dockerfile, a compose file that may or may not be current, a CI workflow with the real environment variables, a runbook in a wiki, and one engineer who knows the parts that are not written down anywhere.

Every consumer of that information re-derives it, and one of them is wrong.

So a ForkTex repository states how it runs in a single file a machine can read. One manifest describes the services, the infrastructure they need, and the deployment strategy, and the same file drives both the local stack and the production one.

What the manifest declares

{
  "apiVersion": "forktex.cloud/v1",
  "kind": "ProjectDeployment",
  "metadata": { "name": "example", "environment": "production" },
  "infrastructure": {
    "servers": [{
      "id": "primary",
      "primary": true,
      "provider": "hetzner",
      "flavour": "standard",
      "region": "eu-central",
      "image": "ubuntu-24.04",
      "gateway": {
        "domain": "example.forktex.com",
        "ssl": { "provider": "letsencrypt", "challenge": "dns-01" }
      }
    }]
  },
  "deployment": { "strategy": "blue-green", "gracefulStopSeconds": 30 },
  "services": [
    { "id": "api", "type": "compute", "port": 8080, "healthPath": "/api/health" },
    { "id": "db",  "type": "persistence", "image": "postgres:17-alpine" }
  ]
}

Services carry a type, and the type is what the platform reasons about. A compute service runs in two slots and gets cut over. A persistence service does not. It is a database, it runs once, and it gets volumes and health checks appropriate to its image. A daemon has no port because it consumes work rather than serving it.

That typing is what lets the platform generate the parts nobody should be hand-writing. The proxy layer is not user-defined infrastructure; it is derived from the service list. Nobody writes a routing config, which means nobody forgets to update one.

forktex.jsonservices · infra+ forktex.local.jsonmounted source · mail catcher+ forktex.production.jsondomains · TLS · observabilitydocker composelocalhost, hot reloadHetzner VPSblue / green behind HAProxyone descriptionoverlays are the exhaustive list of differences
One manifest, two overlays, two environments.

Environments are overlays, not copies

A base manifest plus a per-environment overlay, deep-merged. Objects merge recursively; services merge by id; scalars overwrite.

The local overlay mounts source directories and swaps the mail relay for a catcher. The production overlay sets real domains and turns observability on. Neither is a separate description of the system. Both are diffs against one.

This is the difference that matters. When local and production are two hand-maintained files, they drift, and the drift is invisible until a code path that only exists in production fails in production. When they are one file plus a small overlay, the overlay *is* the exhaustive list of ways the environments differ, and it is short enough to read.

Secrets are referenced, never embedded

Values that must not live in a repository are referenced:

"environment": { "DB_PASSWORD": "${vault:DB_PASSWORD}" }

Resolved at compile time from an encrypted store. The manifest stays committable, which is the whole point. The moment a manifest cannot be committed, it stops being the source of truth and a private copy becomes the real one.

Blue-green, and why the boring part matters

A deploy starts the new version alongside the old one. The new slot must pass health checks before it receives traffic. Then the proxy moves, the old slot drains for a grace period, and stops.

The interesting property is not zero downtime. It is that rollback is a proxy change. The previous version is still running, still healthy, still holding its connections. Reverting is moving traffic back, which takes about as long as moving it forward. There is no rebuild, no re-deploy, no waiting for an image to pull while a site is down.

That changes the risk calculus of shipping. When rollback is a rebuild, a bad deploy is an incident; teams respond by deploying less often, in bigger batches, which makes each deploy riskier. When rollback is a route change, a bad deploy is an inconvenience, and deploys get smaller and more frequent. Which is what actually makes them safe.

The grace period is worth a sentence too. Cutting traffic instantly severs in-flight requests. Draining lets them finish. It is unglamorous and it is the difference between a clean cutover and a scattering of 502s in the logs that someone will investigate next week.

Sovereign by default

The compute underneath is European. Not as a compliance checkbox, though it helps with clients for whom data residency is a real constraint, but because the platform's value depends on not being locked to one provider's control plane.

The manifest describes a system in terms of servers, containers, routes and certificates. Those are portable concepts. A manifest that instead described managed queues and proprietary function runtimes would be a description of a system that can only exist in one vendor's account.

The tradeoff is honest: we carry operational work that a managed platform would absorb. Backups, patching and provisioning are ours. What we get is that every project ships on the same inspectable stack, and no client's system is one pricing change away from a migration.

The failure it prevents

Not an outage. Something slower.

Without a manifest, production knowledge diffuses. It ends up in a CI file, a wiki, and someone's memory, and those three drift apart at different rates. Nothing fails. Until a server is rebuilt, or the person who knew the undocumented part is on leave, and the system turns out to depend on a fact nobody wrote down.

One file a machine can read does not eliminate that risk. It does make the gap visible: if the manifest does not describe it, it is not deployed.