How it works
Follow one request from “I edited a YAML file” to “the browser got bytes.”
The path
Section titled “The path”- You drop or edit
apps_dir/myapp/docker-compose.yml. The file declares one or more services andsimpledeploy.endpoints.0.*labels for any public domains. - The reconciler’s fsnotify watcher fires (or the next periodic tick runs). It scans
apps_dir/, parses everydocker-compose.yml, and computes a SHA-256 of each. It compares the hash against thecompose_hashrow in SQLite. - For new or changed apps, the reconciler calls the deployer. The deployer shells out:
docker compose -f <path> -p simpledeploy-myapp up -d --remove-orphans. Stdout/stderr are streamed line by line into a per-app deploy-log buffer that the UI subscribes to over WebSocket. - After every reconcile pass, the reconciler rebuilds the route table from all parsed apps, calls
caddy.Load()with the new JSON config. Caddy reloads in-place with no socket churn. - A request hits Caddy on port 443. Caddy matches by
Hostheader, runs the per-app handler chain (IP allowlist, rate limit, request-metrics, thenreverse_proxy), and forwards to the upstream container.
Sequence
Section titled “Sequence”sequenceDiagram participant Op as Operator participant FS as apps_dir participant R as Reconciler participant D as Deployer participant DC as docker compose participant Cy as Caddy participant App as Container
Op->>FS: write docker-compose.yml FS-->>R: fsnotify event (debounced 1s) R->>R: scanAppsDir + hash compare R->>D: Deploy(app, regAuths) D->>DC: docker compose up -d DC-->>D: stdout / stderr (streamed) D-->>R: DeployResult{Output, Err} R->>Cy: SetRoutes(routes) Cy->>Cy: caddy.Load(JSON) Note over Op,App: Later, end users: participant U as Browser U->>Cy: GET https://app.example.com/ Cy->>App: reverse proxy App-->>U: responseWhat you wrote vs what runs
Section titled “What you wrote vs what runs”The compose project name is always simpledeploy-<slug>. That label (com.docker.compose.project) is how the metrics collector and reconciler find the right containers later. The endpoint upstream is resolved per route: if the service publishes a host port, the route uses localhost:<host_port>; otherwise it uses the Docker network address <service>:<port> (Caddy is on the host network, but Docker DNS resolution still works for published-network targets when configured).
Where each thing lives
Section titled “Where each thing lives”- Compose parsing: /internal/compose/
- Reconciler loop: /internal/reconciler/reconciler.go
- Deployer (shells out to
docker compose): /internal/deployer/deployer.go - Caddy config builder + custom modules: /internal/proxy/
- Log fan-out to WebSocket: /internal/logbuf/logbuf.go