⚓  TWO MODES — advise (detect + alert only) or auto (detect + repair in seconds)
v1.0 · 63/63 tests pass · MIT · 5s reconcile loop

Kubernetes-grade reconciliation
for the compose stacks everyone runs.

Moor diffs the real Docker Engine API against your docker-compose.yml, classifies every divergence as drift or intended change, alerts in Slack-webhook format, and in auto mode repairs the environment in seconds — every action written to an immutable audit trail. No mock code. No simulations.

63/63
tests passing
5s
reconcile loop
~3s
drift → repaired
0
mock code
moor :8080 moor-db alert-sink :9099 drift-injector web · cache · db

Watch it repair

One take, no cuts, no synthetic events. Every click of the dashboard's ⚡ Inject drift button performs one real Docker mutation.

moor dashboard — localhost:8080
Moor demo: the Inject drift button triggers real Docker mutations — a killed container, two rogue scale-ups, a mutated env var — and the live console window shows auto mode detecting and repairing each one

The button is clicked four times: a killed container, a rogue scale-up, a mutated env var, another rogue scale-up. The console window streams the real audit trail as auto mode detects and repairs each drift (one drift needs a retry after the engine's backoff — and still converges). Prefer the terminal? make chaos or moor chaos [kill|scale|env|image|random] is the console twin: same API endpoint, same real Docker mutations, same audit trail.

Architecture

One control loop, six containers, every arrow real. The same OBSERVE → DIFF → PLAN → ACT loop Kubernetes runs — pointed at Docker.

┌──────────────────────────────────────────────────────────────────────────────────┐
                                                                                  
   docker-compose.yml  (the declaration, mounted read-only)              
        image · replicas · env · command · ports  per managed service                
                                                                               
              desired state, every 5 seconds                                     
   moor engine   OBSERVE ─► DIFF ─► PLAN ─► ACT                                   
               actual state via the real Docker Engine API (mounted socket)          
              divergence? classify:                                              
                reality moved ──► DRIFT              declaration moved ──► INTENDED CHANGE   
                restore the declaration            converge forward (GitOps pull)          
                                                                               
              immutable event chain (audit trail, persisted in Postgres):          
   chaos.injected ─► drift.detected ─► alert.sent ─► action.ok ─► drift.resolved       
                                                                               
              Slack-format webhook                                               
   alert-sink :9099  (or your real Slack / Teams / PagerDuty webhook)          
                                                                                      
   moor dashboard :8080  ──►  live SSE timeline + audit backfill + ⚡ Inject drift button  
   moor CLI  ──►  moor status / plan / apply / watch / mode / chaos  (kubectl-style split)  
                                                                                  
└──────────────────────────────────────────────────────────────────────────────────┘

Seven containers, every one real

Three managed workloads plus a four-piece control plane. Nothing is faked: the reconciler talks to the Docker socket, the injector performs the same mutations a tired engineer would, the sink receives the same payloads Slack would.

MOR

moor

control plane · :8080

FastAPI + the reconciliation engine. Serves the dashboard, the REST API, the SSE event stream, and the kubectl-style CLI over the same API. Declared state is diffed against live Docker every MOOR_INTERVAL seconds.

Opts itself out: moor.manage: "false"
DB

moor-db

Postgres event store

The immutable audit trail and the last-seen state hashes. Survives restarts: crash the moor container mid-remediation and it comes back, re-reads the store, and finishes the job.

Persists: events + state hashes
SNK

alert-sink

webhook receiver · :9099

Receives the exact Slack incoming-webhook JSON Moor emits (attachments[] cards with color, title, fields) and renders it. Point MOOR_ALERT_WEBHOOK at real Slack and the payload is identical.

Real mode: Slack webhook · Demo: local cards
CHA

drift-injector

chaos tool · runs on demand

Performs real Docker Engine mutations: kill a container, spawn rogue replicas, mutate a declared env var, swap the image. Same API path as the dashboard button (POST /api/chaos); every injection audited as chaos.injected.

Run it: make chaos · moor chaos
WEB

web (managed)

nginx:1.27-alpine · :8081

A managed demo workload: published 8081:80, env MOOR_TIER=frontend. When the injector kills it, auto mode restores the container with the declared config — typically within one loop cycle.

Watch it: moor watch
CCH

cache (managed)

redis:7.2-alpine

A managed demo workload with REDIS_MODE=cache. The rogue scale-up chaos spawns name-mangled replicas; the planner removes them and the executor restores the declared replica count.

Diff kinds: replicas · env · image · command · ports
DAT

db (managed)

postgres:16-alpine

A managed demo workload (POSTGRES_PASSWORD=demo, masked in every report and event). Mutating its env demonstrates env drift detection and recreate-with-declared-env remediation.

Secrets: masked as ****

Advise mode vs. Auto mode

Moor runs in two modes. Advise is today's status quo: visibility without control. Auto is the control loop: detect, alert, repair.

Advise Detect + alert only

The injector kills web, rogue-scales cache 1→4, and mutates db's password. The dashboard turns red, the webhook card lands in the sink, moor plan prints the terraform-style diff — and nothing is repaired. Exactly what your monitoring does today: it watches the boat drift.

Arm it: make mode-advise or the dashboard mode switch.

Auto Detect + repair

Same injection, opposite outcome: rogue replicas removed, the drifted container recreated with its declared env, the killed container restored — every action idempotent, ordered (removes → starts → creates), and written to the audit trail before the webhook fires.

Arm it: make mode-auto, moor mode auto, POST /api/mode, or the dashboard confirm dialog.

Classify Drift vs. intended change

Moor persists the hash of the last-seen declaration and reality. Reality moved, declaration didn't → drift → restore. Declaration moved (git push) → intended change → converge forward. The same binary is a drift guard and a GitOps pull-deployer.

Crash-safe Restart mid-flight

Drift injected, then the moor container itself is killed mid-remediation. It restarts, re-reads the event store, still detects the drift and still repairs it. Act 4 of the demo proves it live; the audit trail shows no gaps.

Safety Rails, not hopes

  • Only touches containers carrying the project's compose labels
  • One label removes any service: moor.manage: "false"
  • Compose file mounted read-only · no code execution · no host writes
  • Non-converging services enter a 20s cooldown backoff — no restart storms, you get a drift.persistent alert instead
  • Auto mode requires an explicit switch (CLI, API, or dashboard confirm)

Audit Immutable, durable, masked

Every cycle appends typed events (chaos.injected, drift.detected, alert.sent, action.ok, drift.resolved) to the Postgres store, queryable via GET /api/events, streamed live over SSE, and replayed into the dashboard console on load. Secrets are masked as **** everywhere.

The tl;dr: the reconciler talks to the real Docker Engine through the mounted socket; the injector performs the same mutations a tired engineer would; the alert sink receives the same HTTP payloads Slack would. No mock code. No simulations. Runs anywhere Docker 24+ runs — including a GitHub Codespace with Docker-in-Docker.

63/63 tests pass

The offline suite runs in ~1.5s with no Docker daemon — it drives the engine through a test-double gateway that implements the exact DockerGateway interface (conformance-checked by its own test).

Test Description Status Duration
test_each_action_produces_its_drift[kill]chaos kill → replicas_missing (same for scale, env, image — 4 parametrized cases)✓ pass0.01 s
test_kill_is_a_real_dead_containerchaos kill leaves a genuinely dead container in the gateway state✓ pass<5 ms
test_chaos_event_lands_in_audit_trailevery injection is audited before detection fires✓ pass0.01 s
test_auto_mode_detects_and_repairs_chaosauto mode converges an injected drift back to the declaration✓ pass0.02 s
test_unmanaged_service_is_rejectedchaos refuses to touch services with moor.manage: "false"✓ pass<5 ms
test_advise_mode_detects_and_alerts_without_mutatingadvise mode never writes — visibility only✓ pass0.02 s
test_auto_mode_remediates_all_driftone cycle clears every drift kind at once✓ pass0.03 s
test_intended_change_classified_and_convergeddeclaration edits converge forward, not restore backward✓ pass0.02 s
test_persistent_drift_backs_offnon-converging services enter cooldown, no restart storms✓ pass0.02 s
test_no_duplicate_alerts_for_unchanged_driftalert dedupe — one card per drift, not per cycle✓ pass0.01 s
test_rogue_replicas_produce_removalsplanner removes rogue containers before restoring declared ones✓ pass<5 ms
test_env_drift_produces_recreateenv drift → recreate with declared environment✓ pass<5 ms
test_image_baked_env_is_not_driftPATH & friends baked into the image never count as drift✓ pass<5 ms
test_alert_payload_is_slack_formatwebhook payload is valid Slack attachments[] JSON✓ pass0.01 s
test_reconcile_endpoint_in_auto_mode_fixesPOST /api/reconcile triggers a real repair cycle✓ pass0.02 s
test_fake_gateway_implements_every_real_gateway_methodinterface conformance: the test double mirrors the real Docker gateway 1:1✓ pass<5 ms
63 / 63 passed · 0 failed · full suite in 1.4–1.6 s, offline · 9 actions · 9 api · 15 chaos · 5 compose · 13 diff · 11 engine · 1 interface
Durations are from the run that produced this badge; re-run make test (or pytest tests in control-plane/) to reproduce.

Two ways to run it

GitHub Codespace (Docker-in-Docker preconfigured) · or any machine with Docker 24+ and make.

CODESPACE docker-in-docker · ~3 min narrated demo
# open the repo in a Codespace (devcontainer includes DinD)
open https://codespaces.new/adventurewave-labs/moor

# build + pre-pull images (~1-2 min)
make setup

# full narrated 5-act demo (~3 min)
make demo

# while it runs, open in a browser tab:
http://localhost:8080   # moor dashboard + ⚡ Inject drift
http://localhost:9099   # alert sink (Slack-format cards)
http://localhost:8081   # the managed web workload

# or go hands-on:
make mode-auto       # arm auto mode
make chaos           # inject one random drift
ANY DOCKER HOST Linux · macOS · CI runner · Docker 24+
git clone https://github.com/adventurewave-labs/moor.git
cd moor

make setup
make demo

# everyday commands (run inside the moor container):
make status          # compliance snapshot
make plan            # terraform-style drift diff
make apply           # reconcile now
make watch           # live event stream
make test            # the offline suite (63 tests)
make reset           # down -v && up