RUL-004 — Incus, not Docker, for containers

Ruling

The user runs containers on Incus (LXD), which he considers more stable and powerful than Docker. docker-compose.yml is the service-topology REFERENCE only — actual deployment and test-isolation should target Incus.

Evidence

Enforcement

Verification

scripts/check_incus_not_docker.py is an introduction ratchet with four rules: (A) shell/systemd invocation of docker, (B) AST-position Python — subprocess argv[0] docker, import docker, or a Docker detection predicate such as Path("Dockerfile").exists(), (C) a Docker endpoint in config/env (DOCKER_HOST, a docker distribution in pyproject.toml), (D) introduction of a docker-compose.yml/Dockerfile outside the allowlisted docker/ and infra/docker/ reference trees. Invoked from scripts/health_check.py as "Incus not Docker (RUL-004)". docker is also a .claude/banned-libraries.txt entry, so the Docker SDK is blocked by the no-banned-deps.sh PreToolUse guard and code-review.py's BANNED-DEP gate as well.

Measured on the live tree: 2789 in-scope files scanned, 19 carrying a docker token graded, 0 violations — the gate lands green with no baseline and no suppressions. The AST/grammar rules are load-bearing, not decorative: a naive raw-text docker regex over the same scope yields 3 hits, all 3 false positives (a prose string literal in scripts/gen_level0.py, a comment and a docstring in tests/agents/test_runtime_main.py); the designed rules yield 0. Those sites are regression-locked so a future "simplify it to grep" fails loudly.

False-positive hardening, 2026-08-05 — two classes found by adversarial review, both fixed, neither losing recall:

Recall verified against a 13-violation battery that still fires in full: docker compose up, sudo docker run, incus exec … -- docker compose up, bash -c "docker run", sudo sh -lc 'docker compose up', subprocess.run(["docker", …]), shell=True, ["bash","-lc",…], os.system, create_subprocess_shell, systemd ExecStart, DOCKER_HOST, and a deploy/docker-compose.yml.

The allowlisted docker/ tree keeps docker-compose.yml legal as topology reference; what the gate blocks is a caller — an identical docker compose ... up -d line stays silent inside docker/up.sh and fires from scripts/boot.sh. Two frozen handover artifacts (rtm/scripts/setup_test_infrastructure.sh, rtm/scripts/check_test_infrastructure.sh) run docker run -d / docker exec ... cypher-shell and are reported on every run as a non-blocking QUARANTINE with a delete recommendation; the live equivalent is scripts/start_test_services.py against the Incus test fleet. Known bounds, stated rather than hidden: a Docker path added entirely inside an allowlisted tree with no in-scope caller is undetected; dot-directories are pruned from the walk; src/orchestrata_tools/ is exempt from the detection-predicate clause only (it analyses foreign projects for a living) and stays covered for subprocess-argv and import docker.