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.ymlfiles) - 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.
Why Stateless
Section titled “Why Stateless”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.
Container Names
Section titled “Container Names”Holden uses a consistent naming scheme for containers:
| Container | Name |
|---|---|
| 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 orchestrator | holden |
| Holden (during update) | holden-next |
| Overseer | holden-overseer |
The -next suffix appears during zero-downtime deployments and Overseer self-updates while the replacement container is being health-checked.
Docker Labels
Section titled “Docker Labels”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.
Container Labels
Section titled “Container Labels”Every Holden-managed container has:
| Label | Example | Purpose |
|---|---|---|
holden.managed | true | Identifies Holden containers |
holden.app-id | myapp | Which app owns this container |
holden.service-name | web | Service name within the app |
holden.needs | postgres | Type of needs container (only on needs containers) |
holden.domains | app.example.com,www.example.com | Domains for DDNS (set on services with domain:) |
holden.cf.proxy | true | Per-container override of Cloudflare proxy mode |
holden.sidecar | ddns | Identifies 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:
| Label | Example | Tracks |
|---|---|---|
holden.env-hash | a1b2c3d4e5f67890 | Hash of resolved env vars |
holden.command | ["bun","worker.ts"] | Command override |
holden.memory | 512m | Memory limit |
holden.cpu | 0.5 | CPU limit |
Network Labels
Section titled “Network Labels”Networks created by Holden have:
| Label | Value |
|---|---|
holden.managed | true |
Orchestrator Labels
Section titled “Orchestrator Labels”The Holden container itself has:
| Label | Value | Purpose |
|---|---|---|
holden.orchestrator | true | Identifies the orchestrator (required in docker-compose) |
holden.created-at | 2024-01-15T03:00:00Z | When Overseer created this container |
holden.domains | holden.example.com | Public domain, set when HOLDEN_PUBLIC_DOMAIN is configured |
The Overseer uses holden.orchestrator to find and replace Holden during updates.
Inspecting State
Section titled “Inspecting State”Use Docker’s label filters to inspect Holden’s state:
# All Holden containersdocker ps --filter label=holden.managed=true
# Containers for one appdocker ps --filter label=holden.app-id=myapp
# Show labels on a containerdocker inspect <container> --format '{{json .Config.Labels}}' | jqWhat This Means
Section titled “What This Means”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.)