--- name: docker-deploy description: | Use this skill when writing or reviewing a Dockerfile, docker-compose setup, or container image build for a project, or when debugging a container that won't build, start, or stay healthy. Triggers: user says "docker", "Dockerfile", "container", "image", "docker compose", "containerize", "multi-stage", "distroless", "registry", "healthcheck", "image size". version: 2.0.0 required-capabilities: [filesystem.read, filesystem.write, execution.shell] required-tools: [] --- # Docker Deploy ## Overview A good image is small, reproducible, runs as a non-root user, and rebuilds quickly because its layers are ordered by how often they change. Start from the project as it is: the language and package manager (lockfile), the build command, the start command, the port, and what the process needs at runtime (environment, volumes, writable paths). ## Rules 1. Multi-stage builds: build with the toolchain and dev dependencies, ship a runtime stage with only production artifacts. 2. Pin the base image to a specific version tag (optionally a digest); never `latest`. 3. Order layers for caching: copy dependency manifests and the lockfile, install, then copy the source. 4. Install from the lockfile (`npm ci`, `pnpm install --frozen-lockfile`, `pip install -r` with hashes, `go mod download`). 5. Run as a non-root user, and make only the paths the app writes to writable. 6. No secrets in the image — no `ARG`/`ENV` for credentials, no copied `.env`. Pass them at runtime, or use BuildKit secret mounts for build-time access. 7. Use the exec form for `ENTRYPOINT`/`CMD` so signals reach the process, and add an init (`--init`, or `tini`) when the app spawns children. 8. Keep a `.dockerignore` that excludes `.git`, dependency folders, build output, local env files, and tests. 9. Add a `HEALTHCHECK` for long-running services, pointing at a cheap readiness endpoint; one-shot jobs and CLIs don't need one. ## Patterns Node.js service with pnpm: ```dockerfile # syntax=docker/dockerfile:1 FROM node:22-bookworm-slim AS build WORKDIR /app RUN corepack enable COPY package.json pnpm-lock.yaml ./ RUN pnpm install --frozen-lockfile COPY . . RUN pnpm build && pnpm prune --prod FROM node:22-bookworm-slim AS runtime ENV NODE_ENV=production WORKDIR /app COPY --from=build --chown=node:node /app/node_modules ./node_modules COPY --from=build --chown=node:node /app/dist ./dist COPY --from=build --chown=node:node /app/package.json ./ USER node EXPOSE 3000 HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ CMD node -e "fetch('http://127.0.0.1:3000/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" CMD ["node", "dist/server.js"] ``` Build-time secret without baking it into a layer: ```dockerfile RUN --mount=type=secret,id=npmrc,target=/root/.npmrc pnpm install --frozen-lockfile ``` ```bash docker build --secret id=npmrc,src=$HOME/.npmrc -t app:$(git rev-parse --short HEAD) . ``` Compose for local development (the top-level `version:` key is obsolete): ```yaml services: app: build: . ports: ["3000:3000"] env_file: .env depends_on: db: condition: service_healthy db: image: postgres:17 environment: POSTGRES_PASSWORD: dev-only volumes: [db-data:/var/lib/postgresql/data] healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s retries: 10 volumes: db-data: ``` ## Debugging a container | Symptom | Check | |---|---| | Build cache never hits | Source copied before the dependency install; missing `.dockerignore` | | Exits immediately | `docker logs `; the `CMD` path relative to `WORKDIR`; missing build output in the runtime stage | | Works locally, fails in the image | Missing runtime env var, native module built for a different libc (alpine vs glibc), dev dependency needed at runtime | | Permission denied | Writing to a path owned by root while running as non-root | | Ctrl+C or stop takes 10 s | Shell-form `CMD` swallowing signals; no init process | | Unhealthy | Run the healthcheck command yourself inside the container | ## Anti-patterns - **`COPY . .` before installing dependencies** — every source change reinstalls everything. - **Secrets in `ARG`/`ENV`** — visible in image history to anyone who pulls it. - **Running as root** "because it works". - **`latest` tags** in anything deployed. - **One giant stage** that ships compilers and dev dependencies to production. ## Before returning - [ ] Multi-stage; runtime stage holds only production artifacts - [ ] Base image pinned; dependencies installed from the lockfile - [ ] Layers ordered so source changes don't invalidate the install - [ ] Non-root user; no secrets in layers; `.dockerignore` present - [ ] Exec-form start command; healthcheck for services - [ ] Image built and the container started successfully, if the environment allows ## Skills in scope - `security-scanner` — for image and configuration exposure review - `observability` — for logs and health endpoints in containers - `tech-stack` — for choosing and pinning base image versions