Skip to content

State

Holden has no database. It persists your app registrations and encryption keys in /data, but derives all deployment state from two sources:

  • Desired state — Your git repos (holden.yml files)
  • Current state — Docker containers (and their labels)

Reconciliation compares these and makes them match. If Holden restarts, it reads both sources again and picks up where it left off. Nothing is lost because nothing was stored.

Traditional orchestrators store state in a database: what should be running, what is running, deployment history. That database becomes critical infrastructure—if it’s corrupted or lost, you’re restoring from backups and hoping.

Holden inverts this. Your git repo is the source of truth for what should exist. Docker is the source of truth for what does exist. Holden just compares them and reconciles the difference.

Holden uses a consistent naming scheme for containers:

ContainerName
App service{app-id}-{service-name}
App service (during update){app-id}-{service-name}-next
Needs container{app-id}-needs-{need} (e.g., myapp-needs-postgres)
Holden orchestratorholden
Holden (during update)holden-next
Overseerholden-overseer

The -next suffix appears during zero-downtime deployments and Overseer self-updates while the replacement container is being health-checked.

When Holden creates a container, it attaches labels with metadata. When Holden needs to know what’s running, it reads those labels back from Docker. The labels aren’t a separate state store—they’re part of the containers themselves.

Every Holden-managed container has:

LabelExamplePurpose
holden.managedtrueIdentifies Holden containers
holden.app-idmyappWhich app owns this container
holden.service-namewebService name within the app
holden.needspostgresType of needs container (only on needs containers)
holden.domainsapp.example.com,www.example.comDomains for DDNS (set on services with domain:)
holden.cf.proxytruePer-container override of Cloudflare proxy mode
holden.sidecarddnsIdentifies sidecar containers (e.g., DDNS)

Holden also stores configuration values as labels for change detection. When any of these differ between the desired state and the running container, Holden triggers an update:

LabelExampleTracks
holden.env-hasha1b2c3d4e5f67890Hash of resolved env vars
holden.command["bun","worker.ts"]Command override
holden.memory512mMemory limit
holden.cpu0.5CPU limit

Networks created by Holden have:

LabelValue
holden.managedtrue

The Holden container itself has:

LabelValuePurpose
holden.orchestratortrueIdentifies the orchestrator (required in docker-compose)
holden.created-at2024-01-15T03:00:00ZWhen Overseer created this container
holden.domainsholden.example.comPublic domain, set when HOLDEN_PUBLIC_DOMAIN is configured

The Overseer uses holden.orchestrator to find and replace Holden during updates.

Use Docker’s label filters to inspect Holden’s state:

Terminal window
# All Holden containers
docker ps --filter label=holden.managed=true
# Containers for one app
docker ps --filter label=holden.app-id=myapp
# Show labels on a container
docker inspect <container> --format '{{json .Config.Labels}}' | jq

Restart anytime. Holden has no state to lose. Kill it, restart it, it figures out what’s running.

Debug with Docker. The labels are the state. You can inspect them directly with Docker commands.

Git is the config backup. Your app repos contain the holden.yml files that define your deployments. Re-register your apps and you’re back. (Your app data still needs backups.)