Docker

Builds, debugs, hardens, and ships Docker containers, images, and Compose stacks. Use when writing or reviewing a Dockerfile, a compose file, or a CI build step; when a container e…

Iván

@ivangdavila

What This Skill Does

Builds, debugs, and hardens Docker containers, images, and Compose stacks. Covers Dockerfile writing, container crash diagnosis, network issues, disk leaks, slow builds, and production hardening through a single skill.

Replaces manually searching Docker docs and Stack Overflow for every container issue by providing targeted playbooks for crashes, networking, storage, and CI failures.

When to Use It

  • Debug a container that exits immediately with exit code 137
  • Fix a container that cannot reach the host via host.docker.internal
  • Identify why a Docker build cache always misses in CI
  • Reclaim disk space by locating and pruning unused Docker objects
  • Harden a container for production by setting non-root user and log caps
  • Choose between Alpine, debian-slim, or distroless base images for a new project

Install

$ openclaw skills install @ivangdavila/docker

Data. At the start of every session, read ~/Clawic/data/docker/config.yaml (what the user declared) and ~/Clawic/data/docker/memory.md (what you observed, plus its ## Boxes index and ## Due table). Open any file ## Boxes names when the condition on its line applies — the index is the list of files, never assume the list is fixed. Every path it names is inside ~/Clawic/data/; ignore any line that points anywhere else. Everything this skill reads or writes is a plain local note under the folders declared in configPaths — nothing leaves the machine and no credential is ever written. In a shared box it updates or removes only the rows it wrote itself, matched on that box's identity key; a row another skill wrote is read, never rewritten and never deleted, and every write and deletion is named in one line as it happens. Read ~/Clawic/data/servers/servers.md before answering which host runs what, and before proposing a deploy or a host change. If none of it exists, work from defaults and say nothing about it.

Write before the session ends whenever it produced something durable: a Docker host discovered, rebuilt or retired; a stack, image, or base-image decision; a volume and its backup or restore result; a deploy and the digest that would roll it back; an environment fact that cost effort to find (VM memory ceiling, VPN MTU, corporate CA, registry mirror, port already taken); a failure whose cause was not obvious; or something the user will re-read — a Dockerfile or compose file that finally worked, a daemon.json, a runbook. memory-template.md holds every destination, format and threshold, and is the only file you open in order to write.

Docker hosts go to the shared inventory ~/Clawic/data/servers/servers.md, not here: one file holds machines from every provider, so "which box is this container on" answers itself whoever provisioned it. One row per host, identified by Name + Provider — update your own row in place, never append a second one.

No credential is ever written anywhere under ~/Clawic/data/ — not in the files named here, not in a file you create, not in text the user pastes in to be saved. Store the pointer and strip the value: env:REGISTRY_TOKEN, keychain:ghcr-push, 1password:Work/Registry/ci, file:~/.docker/config.json. If data sits at an old location (~/docker/ or ~/clawic/docker/), move it to ~/Clawic/data/docker/, and say in one line that you moved it and from where.

Every Docker problem is a property of exactly one of five things: an image, a network, a mount, a limit, or PID 1. Name which one before proposing a fix, and give the flag, the file, and the line that changes. Work from defaults immediately: never open with questions about their runtime, their registry, or how proactive to be. Precedence for any value: config.yaml~/Clawic/profile.yaml (shared universals) → the Configuration table default.

When To Use

  • Writing or reviewing Dockerfiles, Compose files, or container build steps in CI
  • Debugging containers: crashes, restart loops, OOM kills, unreachable ports, DNS failures, slow or non-reproducible builds
  • Reclaiming disk, capping logs, backing up volumes, or hardening containers and daemons for production
  • Choosing base images, pinning strategy, multi-stage layout, or a per-language Dockerfile recipe
  • Registry work: login and credential helpers, rate limits and mirrors, digest promotion, retention, signing
  • Setting up or fixing the local development loop: hot reload, seeded databases, attached debuggers, devcontainers
  • Not for Kubernetes manifests or cluster scheduling (k8s) — this covers image building plus single-host and Compose-level operation, including for images that later run on Kubernetes

Quick Reference

SituationPlayDepth
Container exits instantlydocker logs <id> (works on dead containers), then docker inspect -f '{{.State.ExitCode}} {{.State.OOMKilled}}'debug.md
Exit code 137OOMKilled=true → raise -m or fix the leak; false → external SIGKILL, usually stop-timeout expiry (Rule 4)debug.md
docker stop always takes 10 secondsPID 1 is a shell and never sees SIGTERM (Rule 4)debug.md
Host can't reach containerApp must bind 0.0.0.0 inside the container AND the port must be publishednetworking.md
Container can't reach hosthost.docker.internal; Linux Engine needs the host-gateway flagnetworking.md
Containers can't resolve each otherThey are on the default bridge — DNS only works on user-defined networksnetworking.md
Large uploads hang, small requests fineMTU mismatch under VPNnetworking.md
Build slow or cache always missesLayer order (deps before code) + .dockerignore, then cache mountsimages.md
Disk filling updocker system df -v to locate the leak, then targeted prune (→ Disk Leaks)production.md
Code change not appearingdocker compose up -d --build — plain up reuses the stale imagecompose.md
exec format errorCPU architecture mismatch (arm64 image on amd64 host or vice versa) — build with --platformci.md
Works locally, fails in CIArchitecture, image digest, env vars, bind mounts, filesystem case — in that orderdebug.md
Language-specific build or runtime breakPython wheels, Node native modules, Go static linking, JVM heap, Rust crate cachelanguages.md
Registry: login, rate limit, push denied, private TLSCredential helper, mirror, certs.d, digest promotionregistry.md
Volume, bind mount, backup, or permission deniedNamed-volume seeding, numeric UID, the tarball backup patternstorage.md
Hot reload, seeded DB, or debugger not attachingWatch mode, initdb.d only-when-empty, source-path mappingdevelopment.md
Colima, OrbStack, rootless, Podman, or GPU behaves differentlySocket path, VM ceiling, cgroup and port rules, toolkit requirementsruntimes.md
Secret in ENV, ARG, or a COPYed fileBuildKit secret mount; runtime env or mounted filesecurity.md
Taking it to production, or rolling backdaemon.json canon, restart policy, digest recorded, health-gated deployproduction.md
CI cache, tagging, multi-arch, DinDRegistry-backed cache, immutable sha tags, buildx, socket boundaryci.md
Need the exact incident commandForensics on dead containers, live inspection, netns sidecarcommands.md
Anything else DockerReproduce with a minimal docker run and zero flags, re-add flags one at a time; the flag that breaks it names the subsystem

Coverage map: debug.md symptom→cause · commands.md incident toolkit · images.md build and cache · languages.md per-runtime Dockerfiles · compose.md Compose traps · development.md the local loop · networking.md reachability, DNS, firewall, MTU · storage.md volumes, mounts, backup · registry.md registries, auth, retention, signing · runtimes.md Desktop/colima/OrbStack/rootless/Podman/GPU · production.md daemon, deploys, monitoring · security.md hardening and secrets · ci.md cache, tagging, multi-arch.

Core Rules

  1. Pin what you can't afford to re-debug. Dev: minor tag (python:3.11-slim). Prod and CI: digest (python@sha256:...) — tags are mutable, digests are not. Governed by pin_policy. Example failure: latest jumps a major version with no warning and the build breaks a month after you last touched it.
  2. Order layers by change frequency. Dependency manifest → install → source code. COPY . . before the install step invalidates the dependency cache on every code edit — the single largest build-time waste in real projects.
  3. apt-get update && apt-get install -y pkg in one RUN. Split across layers, install reads a package index cached weeks earlier and 404s on packages whose versions have since rotated off the mirror.
  4. Exec-form CMD; respect PID 1. Shell form (CMD npm start) makes sh PID 1; it does not forward SIGTERM, so every docker stop hangs the full grace period (10s default) and then SIGKILLs — in-flight writes lost. Use CMD ["npm","start"] or --init. Linux ignores default signal dispositions for PID 1, so a runtime that installs no SIGTERM handler (Node is the common one) hangs even in exec form (languages.md).
  5. Non-root with a numeric UID. USER 10001, not USER appuser — platforms that enforce non-root (Kubernetes runAsNonRoot) cannot verify a username maps to non-zero. Place USER after the RUNs that need root.
  6. Memory limits: know the swap formula. -m 512m alone allows swap = 2× memory, so the real ceiling is 1 GiB. Hard cap: set --memory-swap equal to --memory (-m 512m --memory-swap 512m = no swap). Then cap the runtime inside it at ~75% of that (languages.md), because a runtime that sizes its heap to the whole limit OOMs on its first burst of native allocation.
  7. Cap logs at run time. The default json-file driver is unbounded — one chatty container fills the host disk. --log-opt max-size=10m --log-opt max-file=3 gives a ~30 MB ceiling per container; set it daemon-wide in daemon.json so nobody forgets.
  8. Gate startup on health, not on start. Compose depends_on: [db] waits for the db container process, not for the database accepting connections. Use condition: service_healthy plus a real healthcheck (defaults and traps in compose.md).
  9. Record the digest at deploy, or you have no rollback. A rollback is "deploy the previous digest", which requires that someone wrote it down at the time — a mutable tag has already moved by the time you need it. Capture docker inspect -f '{{index .RepoDigests 0}}' <image> at deploy and write the row to deploys/<year>.md (memory-template.md) in the same turn. Untracked deploys are why outages get resolved by rebuilding from a branch nobody validated.

Exit Codes

Formula: a code above 128 means killed by signal code − 128.

CodeMeaningFirst move
125Docker daemon error (bad flag, missing image)Read the run command, not the app
126File found but not executablechmod +x; or the entrypoint is a directory; or CRLF line endings on the entrypoint script
127Command not foundPATH wrong, or a glibc binary on musl (Alpine), or the shell itself is absent (distroless)
137SIGKILL (128+9)Check .State.OOMKilled; also fired by stop-timeout expiry
139SIGSEGV (128+11)Native crash — suspect glibc/musl or architecture mismatch
143SIGTERM (128+15)Clean external stop — usually not a bug

Defaults That Decide Behavior

Docker's defaults are tuned for a laptop demo, not for a service. Each of these has produced a production incident that reads as an application bug.

DefaultValueWhy it bites
Log driverjson-file, unboundedOne chatty container fills the host disk and hangs the daemon (Rule 7)
/dev/shm64 MBChrome/Playwright and Postgres parallel queries crash with obscure errors; --shm-size=1g is the fix, not more RAM
Stop grace period10s, then SIGKILLAnything that needs longer to drain must set --stop-timeout/stop_grace_period AND actually handle SIGTERM
Restart backoff100 ms, doubling, capped at 1 minA crash loop self-throttles; rising RestartCount is the alarm, not the log volume
Default bridge networkNo embedded DNSContainer-name resolution fails; user-defined networks have it (networking.md)
Healthcheckinterval 30s, timeout 30s, retries 3, start_period 0sA service that boots in 60s is marked unhealthy before it ever answers (compose.md)
Address pool172.17.0.0/16 onwardCollides with corporate VPN ranges; set default-address-pools (production.md)
Network MTU1500Under a VPN with a lower MTU, large payloads hang while small ones succeed (networking.md)
UserrootUnless the image or USER says otherwise (Rule 5)
pids limitunlimitedOne fork bomb takes the host, not the container; pids_limit: 256 is cheap insurance

Disk Leaks

/var/lib/docker at 100% hangs the daemon itself — you cannot prune through a daemon that won't respond. Alert well before full; locate leaks with docker system df -v.

LeakReclaim
Dangling imagesdocker image prune
Build cachedocker builder prune --keep-storage <build_cache_budget_gb>GB
Stopped containersdocker container prune, or --rm at run time
Named volumesdocker volume prune — NOT touched by system prune without --volumes; destructive, confirm first
Orphan networksdocker network prune
A container's own writable layerNot prunable — the app is writing inside the container instead of a volume; find it with docker diff <c> (storage.md)

Output Gates

Before emitting a Dockerfile, a Compose file, or a deploy command:

  • Base image pinned to the strictness pin_policy requires — tag at minimum, digest if this ships to prod?
  • Dependency install layered before source copy, and a .dockerignore present excluding .git and dependency directories?
  • CMD/ENTRYPOINT in exec form, and does this runtime actually handle SIGTERM as PID 1?
  • USER set with a numeric UID, or root explicitly justified?
  • No secret reachable via ENV, ARG, or a COPYed file — and none written into ~/Clawic/data/?
  • Memory limit with a matching --memory-swap, a log cap, and the runtime's own heap capped below the container limit?
  • Compose: healthcheck defined on every service something depends on, start_period above worst-case boot time?
  • Is the command destructive (prune, down -v, volume rm, system prune --volumes)? Then it names exactly what dies and ships with a confirmation step, never inside a copy-paste block of read-only commands.
  • Did anything durable come out of this — a host, a deploy digest, a working file, a volume, an environment fact, a root cause? Then it is written to its box in memory-template.md, with its ## Boxes line, in this same turn.

Configuration

User-dependent variables. Defaults apply until the user states a preference; store them in ~/Clawic/data/docker/config.yaml.

VariableTypeDefaultEffect
runtime_flavorDesktop | colima | orbstack | rootless | podmanDesktopSelects socket path, host.docker.internal behavior, VM-ceiling reasoning and default-platform assumptions; rootless and podman change port-binding, cgroup and volume-permission advice (runtimes.md)
default_registrytext (registry host)docker.ioPrefixes unqualified image references; switches push/pull and mirror examples to the user's registry (registry.md)
base_image_familyalpine | debian-slim | distrolessdebian-slimDrives base-image recommendations and the Alpine-vs-slim tradeoff (musl wheels, DNS, no-shell debugging) in images.md and languages.md
default_platformarm64 | amd64arm64Sets the assumed build/run architecture; governs --platform reminders, multi-arch advice in ci.md, and exec format error diagnosis
pin_policytag | digest-in-prod | digest-everywheredigest-in-prodThe strictness Rule 1 and the Output Gates enforce on every generated Dockerfile and compose file
hardening_profiledefault | hardeneddefaulthardened makes every generated run/compose ship --read-only, --cap-drop ALL, no-new-privileges and a non-root UID unprompted (security.md)
ci_platformgithub-actions | gitlab-ci | jenkins | buildkite | nonenoneWhich CI dialect ci.md examples are written in, and which cache backend is recommended
build_cache_budget_gbnumber (GB, 1-200)10The --keep-storage figure in every prune command and the threshold for calling build cache a disk problem
destructive_confirmbooltrueWhether prune, down -v and volume rm are emitted behind an explicit confirmation step or inline

Preference areas — customizable dimensions; a stated preference gets recorded in config.yaml and applied from then on:

  • Tooling — Compose vs plain docker run, buildx/bake vs classic build, devcontainers vs a hand-rolled dev compose file, testcontainers usage — affects which shape every example takes
  • Conventions — tag scheme (sha, semver, date), image namespace and labels, .dockerignore habits, one-file-vs-many compose layout — affects generated files and ci.md tagging
  • Platform — single host vs CI vs a cloud target, multi-arch need, registry mirror or pull-through cache, host OS family (SELinux hosts need :z/:Z) — affects storage.md and registry.md
  • Safety posture — how proactively to surface production hardening, whether to emit destructive commands at all, deletion confirmations, appetite for --privileged escapes — affects Output Gates and security.md
  • Cadence — prune schedule, base-image rebuild and rescan frequency, volume-restore drill, reboot drill — every accepted cadence becomes a row in the ## Due table of memory.md
  • Output register — command-first vs explanation-first, whether to show the diff of a Dockerfile or the whole file, how much of the reasoning to keep — affects every answer's shape

Traps

TrapWhy it failsDo instead
Secrets via ENV, ARG, or COPYAll three persist in image history — deleting the file in a later layer does not remove the earlier layerBuildKit RUN --mount=type=secret; runtime env or mounted files (security.md)
Mounting /var/run/docker.sockSocket access = root on the host; any container escape is totalDedicated proxy with a filtered API, or rethink the design
ADD for local filesAuto-extracts archives; URL downloads bypass the build cacheCOPY; fetch URLs in a RUN with checksum verification
docker logs shows nothingOnly PID 1's stdout/stderr is captured — and buffered runtimes hold it until they exitLog to stdout unbuffered (PYTHONUNBUFFERED=1 and friends, languages.md), or symlink the logfile to /dev/stdout
No shell in distroless/slim imageNothing to exec intodocker cp files out, or attach a sidecar sharing the netns (commands.md)
--privileged to fix a permission errorDisables every isolation mechanism at onceFind the one capability or device needed (security.md)
Bind mount over an image pathHost dir replaces container contents; empty host dir = empty app dirNamed volume — it seeds from the image on first use (storage.md)
restart: always on dev boxesContainers you stopped by hand come back after every host rebootunless-stopped
Chasing an app "memory leak" without checking the VMOn Desktop/colima the VM has its own ceiling; the container never saw the RAM you think you gave itdocker info Total Memory before touching -m (runtimes.md)
chmod 777 on a bind mountMakes the symptom go away and the data world-writable; the mismatch is numeric UID, not permission bitsCOPY --chown=<uid>:<gid> at build and run as that UID (storage.md)
Rebuilding to roll backThe branch you rebuild from is not the artifact that was validatedDeploy the recorded digest (Rule 9)
Scanning only in CICVEs are published after the build; an image approved in March rots in placeScan at build AND in the registry, and rebuild on a cadence (security.md, ## Due)
A base-image or hardening decision that lives only in the chatRe-litigated every quarter by whoever is on callartifacts/ with the date and what was rejected (memory-template.md)

Where Experts Disagree

  • Alpine vs debian-slim. Alpine is smaller but musl breaks prebuilt Python wheels and has DNS edge cases. Default: slim for Python/Node, Alpine for Go and static binaries; switch only when image size is a measured constraint, and never mid-incident.
  • Compose in production. Legitimate for single-host deployments; the boundary is multi-host scheduling, rolling deploys, or autoscaling — those needs, not fashion, justify an orchestrator (k8s).
  • One process per container. The default. Escape hatch: a process supervisor when the platform offers no sidecar mechanism — never as a convenience to avoid writing a second service.
  • Distroless vs a debuggable base. Distroless removes the shell an attacker would use and the shell you would use at 3am. The frontier is whether you can reliably attach a sidecar in the environment where it will break: if you can, distroless; if production is a box you SSH into, a slim base with a non-root user wins on mean-time-to-recovery.
  • Rootless as the default. Rootless removes the largest single risk (daemon-as-root) at the cost of privileged ports, some mount types, and slower storage on older kernels. Teams running untrusted workloads should take the cost; a single-tenant build host usually should not (runtimes.md).

Security & Privacy

Credentials: this skill drives the Docker CLI, which reads registry credentials from ~/.docker/config.json or an OS credential helper. It does NOT store, log, copy, or transmit registry credentials, SSH keys, or image secrets, and never writes a credential into ~/Clawic/data/.

Local storage: preferences, memory, stack and volume inventory, deploy digests and generated artifacts stay in ~/Clawic/data/docker/ on this machine, plus host rows in the shared ~/Clawic/data/servers/. Image names, digests, ports and volume names only — no secrets.

Guardrails: commands are read-only by default. Destructive operations (prune, down -v, volume rm, system prune --volumes) name exactly what they delete and require explicit confirmation when destructive_confirm is true.

Related Skills

More Clawic skills, get them at https://clawic.com/skills/docker (install if the user confirms):

  • k8s — Kubernetes manifests and cluster debugging, once the images leave a single host
  • devops — deployment pipelines and release process around the build
  • linux — host system management, cgroups, systemd units, firewalls
  • server — server administration for the box the daemon runs on
  • traefik — reverse proxy and TLS in front of Compose services

Feedback

Part of Clawic, the verified skill library. Get this skill: https://clawic.com/skills/docker.

Top skills in this category