| name | docker-doctor |
| description | Maintainer-level Docker doctor for clade consumers. Use when creating or reviewing Dockerfiles/Compose files, debugging container build/runtime/deploy issues, or running fleet health checks across clade consumers. Use when user says 'docker-doctor', '檢查 Docker', 'Dockerfile 最佳實踐', 'docker compose 壞了', 'container build 失敗', 'healthcheck', '掃 clade consumers 的 docker', '定期 Docker 健康檢查', 'container 狀態', 'OOM', 'restart loop'. Starts read-only, diagnoses root cause, checks official Docker best practices, and only edits after an approved plan. |
| effort | medium |
| license | MIT |
| metadata | {"author":"clade","version":"1.0"} |
docker-doctor
Maintainer-level Docker doctor for clade consumers. Behaves like a doctor: start read-only, diagnose root cause before edits, separate best-practice violations from intentional local exceptions, prefer repeatable audit commands over one-off guessing, produce findings with severity and evidence.
Step 0: Safety and scope
- Start read-only. Do not edit any file until Step 4.
- Identify the repo and its Docker surface (Dockerfile, .dockerignore, docker-compose*.yml, compose*.yaml, deploy scripts referencing docker/buildx/compose).
- If no Docker surface is found, say so and stop — do not generate Docker files unless the user explicitly asked to add Docker (Mode C).
- Determine execution context:
- Local: Docker daemon is available on this machine. Static + live checks both possible.
- Remote: Target containers run on a Proxmox LXC (perno, yuntech, TDMS monitoring). Coordinate with the appropriate Proxmox skill (
bigbyte-proxmox / fongchen-proxmox / yudefine-proxmox) or ask user for shell access. See Step 5.
- NEVER run destructive Docker commands (
docker system prune, docker volume rm, docker rmi, docker compose down) without explicit per-invocation user confirmation.
- NEVER run
docker compose up on a production host without explicit user confirmation.
Step 1: Mode dispatch
| User input pattern | Mode |
|---|
| "新增 Docker" / "containerize" / "add Docker support" | C: New containerization |
| Build error / compose config failure / Dockerfile syntax issue | B1: Build/Config triage |
| Runtime log / unhealthy container / OOM / restart loop / port conflict / "container 壞了" | B2: Runtime diagnostics |
| "檢查" / "health check" / "audit" / "最佳實踐" / no specific error | A: Static audit (+ optional live extension) |
| "all consumers" / "fleet" / "掃 clade" / "全部掃一遍" | D: Fleet scan |
| "看一下 <consumer> production" / explicit remote host / "container 狀態" on remote | E: Remote host diagnosis |
When the mode is ambiguous, default to A (static audit). If static audit reveals runtime-relevant findings, suggest escalating to B2 or E.
Step 2: Static baseline
Run for every mode except C (new containerization skips to Step 4 directly).
-
Detect Docker surface:
Dockerfile*
*.dockerignore
docker-compose*.yml / compose*.yaml
- Deploy scripts:
grep -rl 'docker\|buildx\|compose' scripts/ 2>/dev/null
-
Classify repo:
- app-image: Has a Dockerfile that builds an application (Nuxt, Node, etc.)
- image-only-deploy: Compose references pre-built images (e.g., GHCR digest), no
build: directive
- local-dev-compose: Compose has
build: for local development
- infra-ops: Compose runs third-party infrastructure (monitoring, databases, queues)
- no-surface: No Docker files found
-
Read package manager and runtime from package.json / lockfile to inform Dockerfile review.
-
Run static validators when available:
docker compose -f <file> config --quiet — syntax validation (safe, no side effects)
docker build --check . — BuildKit lint checks (requires buildx, safe, no image built)
-
Dockerfile structure analysis (for app-image and local-dev-compose classifications):
- Parse
FROM directives: count stages (multi-stage detection), extract base image tags.
- Flag full/default base images: if
FROM tag has no -alpine / -slim / -distroless suffix and image is a well-known runtime (node, python, ruby, golang, etc.) → P1 dockerfile.bloated_base_image.
- Flag single-stage app builds: only one
FROM + repo has a build step (TypeScript compile, Nuxt build, framework build) → P1 dockerfile.no_multistage.
- Count consecutive unchained
RUN instructions: ≥ 3 RUN lines in sequence performing related work (package install, config, cleanup) without && chaining → P2 dockerfile.unchained_run_layers.
- Estimate final image size opportunity: if base image is full variant + single stage + no
.dockerignore, flag compound impact as P1 dockerfile.image_size_compound with estimated savings.
-
.dockerignore completeness analysis (for app-image and local-dev-compose classifications):
-
If no .dockerignore exists → P1 dockerignore.missing (already in § P1).
-
If .dockerignore exists, read it and check for these essential exclusion categories:
| Category | Expected patterns (any match counts) | If missing |
|---|
| Package manager cache | node_modules, .pnpm-store | P1 dockerignore.no_node_modules |
| Version control | .git | P1 dockerignore.no_vcs |
| Secrets / env files | .env* or .env + .env.* | P1 dockerignore.no_env_files |
| Build output | .nuxt, .output, dist (framework-dependent) | P2 dockerignore.no_build_output |
| Test / coverage | test/ or tests/, coverage/ | P2 dockerignore.no_test_dirs |
| Dev tooling | .claude/, .agents/, .codex/ | P2 dockerignore.no_dev_tooling |
| IDE / OS artifacts | .vscode/ or .idea/ or .DS_Store | P3 dockerignore.no_ide_artifacts |
| Docker self-reference | Dockerfile* or docker-compose* | P3 dockerignore.no_docker_self_ref |
| Documentation | docs/ or *.md | P3 dockerignore.no_docs |
| Logs | *.log | P3 dockerignore.no_logs |
-
Pattern matching is case-insensitive glob. Trailing / is optional (both test and test/ count).
-
Compound escalation: Dockerfile uses COPY . . AND .dockerignore has ≥ 2 gaps at P1 level → escalate to P0 dockerignore.copy_all_plus_gaps (build context leaks secrets and/or ships host node_modules into image).
-
Check known-accept registry — suppress expected contextual findings (see § Known-accept registry).
Step 2.5: Runtime baseline
Run only when Docker daemon is available on the target host (local or remote via Step 5). Skip entirely if Docker is not installed or not running.
-
Container state assessment:
docker compose ps — service state matrix (running / unhealthy / restarting / exited).
docker stats --no-stream — snapshot of CPU / MEM / NET / IO per container.
docker inspect <container> --format '{{json .State}}' — exact status, exit code, OOM killed flag, restart count.
docker inspect <container> --format '{{json .HostConfig.Memory}}' vs docker stats --no-stream --format '{{.MemUsage}}' — declared limit vs actual usage.
- Flag: mem usage > 80% of declared
mem_limit.
-
Log pattern scan:
docker compose logs --tail 200 <service> — tail recent logs.
- Grep for fatal patterns:
OOMKilled, SIGKILL, SIGSEGV, unhandledRejection, ECONNREFUSED, EADDRINUSE, Error: listen, exec format error, no such file or directory.
- For restart loops:
docker inspect --format '{{json .RestartCount}}' + compare with expected (0 for healthy).
-
Image layer audit:
docker image inspect <image> — total size, layer count, architecture.
docker history <image> --no-trunc — find abnormally large layers (> 500 MB single layer for a Nuxt app is suspicious).
- Check if secrets are visible in layer history (ARG values baked into metadata).
-
Network connectivity probe:
docker exec <container> wget -qO- http://127.0.0.1:<port>/api/_health — healthcheck from inside the container.
docker network inspect <network> — verify containers are on expected network.
- Port conflict:
docker port <container> vs ss -tlnp | grep <host-port>.
-
Volume and mount verification:
docker inspect <container> --format '{{json .Mounts}}' — verify bind mount targets exist and have correct permissions.
docker volume inspect <name> — check driver, mountpoint, labels.
Step 3: Findings
Report findings with the following structure:
- Severity: P0 / P1 / P2 / P3
- Location: file:line (static) or container/service (runtime)
- Evidence: the exact line, value, or command output that shows the issue
- Why it matters: one sentence explaining the risk
- Fix recommendation: concrete action to take
- Known-accept tag: if applicable (e.g.,
[TDMS-monitoring-tailscale])
P0: must block / fix before deploy
- Secret-looking values passed as build
ARG or ENV that persist in image layer history (docker history readable). Includes: SECRET, TOKEN, KEY, PASSWORD, SUPABASE_KEY, SENTRY_AUTH_TOKEN, PRIVATE, CREDENTIAL.
- Production compose exposes app on
0.0.0.0 when architecture is origin-behind-Caddy/cloudflared/Cloudflare.
- Production deploy compose has
build: when design is image-only digest deploy.
- Dockerfile or compose references missing required files — build/deploy cannot work.
- Healthcheck points to a non-existent endpoint.
COPY . . in Dockerfile AND .dockerignore missing or has ≥ 2 P1-level gaps (dockerignore.copy_all_plus_gaps). Build context leaks secrets (node_modules platform binaries, .env*, .git history) directly into image layers.
P1: high risk
- Production image uses unpinned mutable tags where digest/pinned tag is expected.
- No
.dockerignore for app image builds (dockerignore.missing).
.dockerignore exists but missing critical category: node_modules / .git / .env* (dockerignore.no_node_modules / dockerignore.no_vcs / dockerignore.no_env_files). Host node_modules in build context can shadow the container's installed deps and leak platform-specific binaries; .git adds 10–100 MB+ of history; .env* leaks secrets into image layers.
- Build context copies entire repo (
COPY . .) before dependency install — cache invalidation on any file change.
- No healthcheck for long-running service.
- No restart policy for self-hosted service.
- No log rotation for self-hosted long-running container.
- No memory/CPU limits on shared LXC/VM hosts.
- Container memory usage > 80% of declared
mem_limit (runtime).
RestartCount > 0 or OOMKilled: true (runtime).
- Dockerfile
FROM uses full/default base image (no -alpine / -slim / -distroless variant) for app-image builds. Full images carry build tools, system docs, and package managers that inflate image 3–10× vs Alpine and expand attack surface.
- Dockerfile has a single
FROM stage for app-image builds — no multi-stage build. Build dependencies (compilers, dev packages, full node_modules with devDependencies) ship into the production image.
P2: important / contextual
.dockerignore exists but missing recommended category: build output (.nuxt/.output/dist), test dirs (test//coverage/), or dev tooling (.claude//.agents//.codex/). Not a security risk but inflates build context and image size (dockerignore.no_build_output / dockerignore.no_test_dirs / dockerignore.no_dev_tooling).
- Container runs as root. Evaluate LXC/AppArmor constraints before forcing non-root.
network_mode: host. May be intentional for monitoring/exporters.
:latest tags in infra compose. Risk depends on context.
- Compose project/network names are implicit — collision risk on shared hosts.
- Build cache is suboptimal but correct.
- Dev/prod compose concerns mixed but not currently broken.
- No
mem_limit/cpus on dedicated-purpose host (low contention).
- Multiple consecutive
RUN instructions (≥ 3) that perform related work (package install + cleanup, config file setup) without && chaining. Each RUN creates a separate cached layer; unchained sequences inflate final image and reduce build cache efficiency.
P3: polish / maintainability
.dockerignore missing polish-level patterns: IDE artifacts (.vscode//.idea//.DS_Store), Docker self-reference (Dockerfile*/docker-compose*), documentation (docs//*.md), logs (*.log) (dockerignore.no_ide_artifacts / dockerignore.no_docker_self_ref / dockerignore.no_docs / dockerignore.no_logs).
- Missing comments around intentional exceptions.
- Legacy compose file naming or inconsistent env file naming.
- Duplicate environment values that can be centralized.
GF_SERVER_ROOT_URL or similar config hardcodes hostname instead of env injection.
- Final image > 200 MB for a Node/Nuxt app with no evidence of size optimization. Recommend
wagoodman/dive for interactive layer inspection and slimtoolkit/slim for automated minification. For maximum reduction, consider distroless base images (no shell, no package manager).
Step 4: Edit plan
- For static audit / runtime diagnostics (A/B1/B2): report findings. Do not edit unless user requests fixes.
- For incident triage with clear root cause: present root cause + proposed fix, wait for user confirmation unless typo-level.
- For new containerization (C): apply minimal scoped changes following § New containerization rules below.
- After edits: verify with
docker compose config --quiet, docker build --check, or actual build/run as appropriate.
New containerization rules (Mode C)
Only when user explicitly asks to add Docker support.
- First decide whether Docker is appropriate. Cloudflare Workers/Pages projects (nuxt-edge-agentic-rag, rental-scout, co-purchase, yudefine-blog) do not need Docker.
- For Nuxt/Nitro self-hosted app, start from clade's proven shape (perno as reference):
- Base image:
node:24-alpine (or match repo engine).
# syntax=docker/dockerfile:1.7 header.
- Corepack + pinned pnpm version from
package.json.
- Copy dependency manifests (
package.json, pnpm-lock.yaml, pnpm-workspace.yaml, .npmrc) before source.
pnpm install --frozen-lockfile --ignore-scripts.
pnpm exec nuxt prepare when Nuxt needs generated types.
- Selective source copy (each directory explicitly, not
COPY . .).
- Build selected workspace/client, copy
.output into runner stage.
- Runner:
NODE_ENV=production, HOST=0.0.0.0, PORT=3000.
EXPOSE 3000, CMD ["node", ".output/server/index.mjs"].
- Healthcheck targeting
/api/_health or equivalent real endpoint.
- Compose must match deployment model:
- Local build compose may include
build:.
- Image-only production deploy compose must NOT include
build:.
- Host port: bind
127.0.0.1 for origin-behind-proxy.
- Add:
restart: unless-stopped, mem_limit, cpus, logging rotation, healthcheck, named network.
- Create
.dockerignore excluding: node_modules, .nuxt, .output, .git, .github, .claude, .agents, .codex, test/, coverage/, docs/, screenshots/, *.log, .env*.
Step 5: Remote diagnosis (Mode E)
Use when the user asks to check a running Docker deployment on a remote host.
-
Identify target host. Known hosts:
- perno staging:
bigbyte-perno-app-staging LXC (BigByte Proxmox)
- perno production:
bigbyte-perno-app-production LXC (BigByte Proxmox)
- yuntech: deployment host TBD — ask user
- TDMS monitoring: FongChen Proxmox LXC (
fc-monitoring.manx-alkaid.ts.net on Tailscale)
-
Coordinate with Proxmox skill: use bigbyte-proxmox / fongchen-proxmox / yudefine-proxmox to SSH into the target LXC and run commands.
-
Run Step 2.5 runtime checks inside the remote context.
-
If Proxmox skill unavailable: output the exact commands for user to run manually and offer to interpret the pasted output.
-
Constraints:
- NEVER run destructive commands on remote production hosts without per-invocation confirmation.
- Do not store SSH credentials. Rely on existing Tailscale/Proxmox connectivity.
- Report results with
host: <hostname> field in the finding.
Local clade rules
perno
- Compose is image-only deploy.
IMAGE_DIGEST env (stamped by deploy script) is the single image source of truth. NEVER add build: to docker-compose.yml.
127.0.0.1:3010:3000 port bind is intentional (origin-behind-Caddy/cloudflared).
apparmor:unconfined is intentional for unprivileged LXC reality.
- Named network
perno-net prevents collision with other compose projects on shared hosts.
- Dockerfile is reference-grade — new consumer Dockerfiles should use this as the template.
yuntech-usr-sroi
- Has confirmed P0 issues. Dockerfile uses plain
ARG for secrets (SUPABASE_KEY, SENTRY_AUTH_TOKEN) that get baked into image layer history.
- Uses
COPY . . instead of selective copy — cache-busting on any file change.
- Compose uses mutable
image: yuntech-usr-sroi:latest.
Fix Recipe
NUXT_PUBLIC_* vars → runtime env (Nitro reads at startup); SUPABASE_KEY / SENTRY_AUTH_TOKEN → BuildKit secret mount or CI env; COPY . . → selective copy (perno pattern).
TDMS
infra/monitoring/docker-compose.yml has 5 services (prometheus, grafana, alertmanager, alertmanager-discord, blackbox-exporter) on FongChen Proxmox LXC Tailscale segment.
- See § Known-accept registry for suppressed findings.
- Supabase / n8n / mongodb-bridge compose overrides are upstream-managed. Default: skip (report
surface: infra-override, skipped: upstream-managed). Only audit if user explicitly requests.
Cloudflare/void/wrangler repos
nuxt-edge-agentic-rag, rental-scout, co-purchase, yudefine-blog: no Docker generation unless explicitly requested. Report surface: [], status: pass.
Known-accept registry
Findings tagged with a known-accept key are listed in the report but do not count toward fail status.
| Finding | Severity | Key | Reason |
|---|
:latest tag on TDMS monitoring images (5 services) | P2 | TDMS-monitoring-tailscale | Ops monitoring stack, manually maintained |
network_mode: host on prometheus, alertmanager, blackbox-exporter | P2 | TDMS-monitoring-tailscale | Prometheus scrapes host exporters; standard monitoring sidecar pattern |
Grafana port 3000:3000 not bound 127.0.0.1 | P2 | TDMS-monitoring-tailscale | Only reachable on Tailscale network |
GF_SERVER_ROOT_URL hardcodes Tailscale hostname | P3 | TDMS-monitoring-tailscale | Single monitoring host, parameterization not needed yet |
No mem_limit/cpus on TDMS monitoring services | P2 | TDMS-monitoring-tailscale | Dedicated-purpose LXC, low contention |
Report format
Human format
Docker Doctor: <repo>
Host: local | <remote-hostname>
Surface: <classification>
Status: pass | warn | fail (P0=N P1=N P2=N P3=N)
P0
- <file>:<line> <description>
Why: <risk explanation>
Fix: <concrete action>
P1
- ...
Known-accepts (suppressed):
- <description> [<known-accept-key>]
Runtime (if available):
- <container>: <status>, mem <usage>/<limit> (<pct>%), restarts: <N>
Commands run:
- <command> — <status>
Skipped:
- <check> — <reason>
JSON format
{
"schema": "docker-doctor/v1",
"repo": "/path/to/repo",
"host": "local",
"surface": ["dockerfile", "compose"],
"status": "fail",
"counts": { "P0": 1, "P1": 2, "P2": 1, "P3": 0 },
"findings": [
{
"severity": "P0",
"code": "dockerfile.secret_in_arg",
"file": "Dockerfile",
"line": 14,
"message": "SUPABASE_KEY passed as plain ARG, baked into image layer history.",
"why": "docker history exposes the value. Anyone with image access can read it.",
"fix": "Move to runtime env_file or use BuildKit secret mount."
}
],
"known_accepts": [
{
"code": "compose.latest_tag",
"file": "docker-compose.yml",
"reason_key": "TDMS-monitoring-tailscale",
"note": "Ops monitoring stack on isolated Tailscale segment"
}
],
"runtime": {
"available": true,
"containers": [
{
"name": "perno-app",
"status": "running",
"health": "healthy",
"mem_usage": "312MiB / 2GiB",
"mem_pct": 15.2,
"restart_count": 0,
"oom_killed": false
}
]
},
"commands": [
{ "cmd": "docker compose config --quiet", "status": "pass" }
],
"skipped": [
{ "check": "docker build --check", "reason": "no Docker daemon available" }
]
}
Non-goals
- Do not become a Kubernetes skill.
- Do not manage cloud-specific ECS/GKE/Cloud Run deployments beyond general image hygiene.
- Do not replace repo-specific deploy scripts (
scripts/deploy/*-deploy.sh).
- Do not run destructive cleanup (
docker system prune, volume deletion, image deletion) unless explicitly requested and separately confirmed per invocation.
- Do not add Docker to repos whose runtime is clearly Cloudflare/void/wrangler unless the user explicitly asks for containerization.
Official Docker references