fivenines
34/40

Guided Problem

MiniDock Build 29: Converge a Declarative ParcelFlow Application

Time
25m
Level
intermediate
Artifacts
not specified
Progress0%
Lesson 34 · Orchestration

Compose: Declarative Applications

Design claim: A multi-container application should be expressed as desired state and converged, not replayed as an imperative command history.

Starting model

  • You can connect private container addresses across hosts without misdelivery from stale location state.
  • The useful vocabulary at this point is deliberately small: declaration, declared service, desired state, actual state.

Describe the destination, not the trip

ParcelFlow needs repeatable services, networks, volumes, and configuration across creation, update, and repeated operator commands. Orchestration exists because actual state keeps changing after an initial deployment succeeds. Desired state must remain the durable reference point.

The tempting shortcut is straightforward: encode the deployment as an ordered shell script of run commands. The shortcut records actions, not the condition the system is supposed to maintain. Scripts describe actions rather than the intended end state, so retries duplicate work and drift is difficult to detect or repair. Retries, readiness races, or host loss then produce duplicate work or unowned deficits.

Normalize, observe, diff, act

An imperative deploy script encodes steps; when reality drifts (one container crashed, one image is stale), the script is wrong and humans improvise. A compose file encodes desired state: "these three services, on these networks, with these volumes." A service is a template for containers — image, config, mounts, networks — not a container itself; that indirection is what later lets one service mean five replicas. From one file, the engine derives the whole resource graph and creates a default network so every service can dial every other by service name.

A Compose model declares resources and relationships, and a convergence engine compares normalized desired state with observed engine state. The mechanism represents intent explicitly, derives work from the current difference, and limits authority to the component able to commit that intent.

Read the control path from durable intent to observed tasks, distinguishing authority from execution.

Architecture — one file, expanded into resources
  flowchart TB
    F["compose file (declared intent)
services: web, api, db
volumes: dbdata
networks: default"] F --> W["service web
image web:2 · port 443:8443"] F --> A["service api
image api:5 · env …"] F --> D["service db
image pg:16 · mount dbdata"] F --> NET["network: app_default
(auto-created, embedded DNS)"] F --> VOL[("volume: dbdata")] W --- NET A --- NET D --- NET D --- VOL

It maps a Compose declaration to services, networks, volumes, and observed engine objects. Its central claim is that reapplying the same declaration to matching state produces no unintended change; the labels therefore describe authority rather than decorative grouping.

Convergence makes repetition safe

up means converge: compare declared state with what exists, then create, recreate, or leave alone per resource. Running up twice in a row does nothing the second time — the fixed point is the definition of correctness. Change one service's image and only that service is recreated; the volume and network survive untouched:

The engine parses and validates the model, derives a dependency graph, computes create-update-remove differences, and applies only required actions. Each action should reduce a measured difference and remain safe when observation causes the loop to run again.

The second figure tests the same model in motion: it traces parsing, diffing, action, observation, and a no-op second pass.

Sequence — up, after editing only api's image tag
  sequenceDiagram
    participant U as User
    participant C as Compose engine
    participant E as Engine API
    U->>C: up
    C->>E: inspect existing: network, volume, 3 containers
    E-->>C: all present, but api runs image api:4 while file says api:5
    C->>C: diff → only api differs
    C->>E: recreate api container (new image, same network + names)
    C->>E: web, db, network, volume: untouched
    C-->>U: converged — 1 changed, 4 unchanged
      

It traces parsing, diffing, action, observation, and a no-op second pass. The ordering is valid only when it continues to preserve the stated invariant under retries and interruption.

A second apply is a test of the model

Compose is invoked twice after the first run completed, so the second pass should observe convergence rather than create duplicate resources. Failure changes observed state, not the desired result; recovery should therefore repair only the new difference.

Reapplying the same declaration to matching state produces no unintended change. The rule combines explicit conditions, quorum authority, and repeatable reconciliation without making workers owners of truth.

Put application intent above MiniDock

MiniDock gains an application-level desired-state client over the same daemon API used by individual commands. MiniDock can compose its existing APIs into continuous control while the design still requires a placement choice.

What carries forward

  • Reapplying the same declaration to matching state produces no unintended change.
  • Create and update a multi-container application from one durable desired-state model.
  • The rejected shortcut remains a diagnostic: if the design starts depending on it again, the original constraint has probably been lost.
Next step

See what actually stuck.

Take the practice scenarios now.