---
name: "docker_for_agents"
description: "Write Dockerfiles, build/run containers, and set up multi-service docker compose stacks. Triggers when containerizing an app, debugging container failures, or orchestrating local services."
---

# Docker for Agents

## Purpose
Go from an un-containerized app to a working `Dockerfile` and `docker run` command, or a multi-service `compose.yaml`, without guessing at syntax. Covers the instructions and flags that actually matter, layer-cache pitfalls, and the failure modes agents hit most.

## Workflow

**1. Check the daemon.** `docker info` should return server details. If it fails with a connection error, the Docker daemon isn't running (or isn't installed) — report that and stop; nothing else below will work.

**2. Write the Dockerfile.** Start minimal; add only what's needed:

```dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["python", "app.py"]
```

- `FROM` — pinned base image (`:3.12-slim`, not `:latest`, so rebuilds don't drift).
- `WORKDIR /app` — sets the working directory for everything after it.
- `COPY <host> <image>` — copy files in. **Copy dependency manifests first, then install, then copy code** so a code edit doesn't bust the dependency layer cache.
- `RUN` — shell commands at build time (installs, compiles).
- `CMD ["a", "b"]` — the default process. Exec form (JSON array, no shell) is the default; `CMD python app.py` (shell form) runs via `/bin/sh -c`, which can break signal handling.
- `.dockerignore` (next to the Dockerfile) keeps build context small: `venv/`, `.git/`, `__pycache__/`, `node_modules/`, `*.pyc`.

**3. Build and run.**

```bash
docker build -t myapp:0.1.0 .
docker run --rm -p 8000:8000 --name myapp myapp:0.1.0
```

Flags that matter:
- `--rm` — delete the container when it exits (default to this for one-off runs so dead containers don't pile up).
- `-d` — detached (background), for long-running services. `-it` — interactive TTY, for shells/debugging (`docker run --rm -it myapp:0.1.0 bash`). Pick one; they're for different jobs.
- `-p 8000:8000` — publish `host:container` port. Without it, the container's port is unreachable from the host.
- `-v ./data:/app/data` — bind-mount a host path for dev or persistence; `-v appdata:/app/data` — named volume, data survives container removal.
- `--env-file .env` — load `KEY=VALUE` lines (comments with `#`); `-e KEY=VAL` for one-offs.
- `--name` — stable name so you can `docker logs <name>` / `docker exec -it <name> bash`.

**4. Shrink with multi-stage builds** when the final image only needs the built artifact:

```dockerfile
FROM golang:1.23-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o server ./cmd/server

FROM alpine:3.20
WORKDIR /app
COPY --from=build /src/server .
CMD ["./server"]
```

Toolchain stays in the `build` stage; the runtime image only carries the binary.

**5. Multi-service: `docker compose`.** Compose v2 is a plugin — the command is `docker compose` (no hyphen); the legacy standalone `docker-compose` is end-of-life. Preferred filename is `compose.yaml` (`docker-compose.yml` still works). No `version:` field — it's deprecated.

```yaml
services:
  api:
    build: .
    ports:
      - "8000:8000"
    env_file: .env
    depends_on:
      - db
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: secret
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:
```

`docker compose up --build` builds and starts everything; `docker compose up -d` runs detached; `docker compose down` stops and removes containers/network. Services reach each other by service name (`db` above → `api` connects to host `db`).

## Output Contract
When asked to containerize something, deliver: a `Dockerfile`, a `.dockerignore`, and the exact `docker build` / `docker run` commands (or a `compose.yaml` plus the `up`/`down` commands). State the host:container port mapping and where data persists, if anywhere.

## Operating Rules
1. **Layer caching:** copy manifests before code. `COPY . .` before `RUN pip install` means every edit reinstalls dependencies — slow and wasteful.
2. **Port already in use:** the daemon refuses the publish (`bind: address already in use`). Pick a free host port (`-p 8001:8000`) or stop the conflicting container.
3. **Container exits immediately:** the main process died or the CMD was wrong. Run `docker logs <name>` first; check the command actually exists in the image and, for servers, that it binds `0.0.0.0` (a server bound to `127.0.0.1` inside the container is unreachable).
4. **Never `--env-file` a secrets file into an image build context carelessly:** env at `docker run` stays out of the image; a secret copied in at build time is baked into the image layers forever.
5. **One process per container** as the default pattern; reach for compose when you need more than one service, not `CMD a && b`.
6. Commands above are verified from docs.docker.com (2026-09-17); Docker is not installed on this machine, so nothing here was executed locally — confirm against the target host's daemon before asserting behavior.
