[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M71MX5PVA9748BDCHWEEHMC2":3,"skill-same:01M71MX5PVA9748BDCHWEEHMC2":83},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":15,"sourceHtmlUrl":16,"categories":17,"keywords":23,"publishedAt":44,"linkOnly":45,"markdownContent":46,"markdownBody":47,"frontMatter":48,"refName":55,"commitSha":56,"skillDirectory":57,"forksCount":58,"repositoryHtmlUrl":59,"repositoryHomepageUrl":60,"ownerHtmlUrl":61,"ownerBlogUrl":62,"ownerDescription":6,"updatedAt":63,"install":64,"resources":77},"01M71MX5PVA9748BDCHWEEHMC2","docker-patterns",null,"Use when writing or reviewing Dockerfiles and Docker Compose stacks: local dev services, volumes, networking, container security and installer test harnesses.","monoes\u002Fmonomind","monoes","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F276744696?v=4","01M71KWF37AJ2NJBP3KWBVSDGT","01M71KWF4CSF8ADH0M6HA09VD2",23,"MIT","https:\u002F\u002Fgithub.com\u002Fmonoes\u002Fmonomind\u002Fblob\u002Fmain\u002Fpackages\u002F@monomind\u002Fcli\u002Forg-skills\u002Fdocker-patterns\u002FLICENSE.txt","https:\u002F\u002Fgithub.com\u002Fmonoes\u002Fmonomind\u002Fblob\u002Fmain\u002Fpackages\u002F@monomind\u002Fcli\u002Forg-skills\u002Fdocker-patterns\u002FSKILL.md",[18],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000011","运维与部署","SCENARIO","使用场景",[24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43],"patterns","docker","docker patterns","stacks","services","test","local dev","use","dev","container","installer","compose","networking","security","container security","when","writing","local","docker compose","harnesses","2026-11-10 10:56:23",false,"---\nname: docker-patterns\ndescription: \"Use when writing or reviewing Dockerfiles and Docker Compose stacks: local dev services, volumes, networking, container security and installer test harnesses.\"\ntags: [\"devops\",\"docker\",\"security\"]\ntools: []\nlicense: MIT\nsource: https:\u002F\u002Fgithub.com\u002Faffaan-m\u002FECC\nsource_path: \"skills\u002Fdocker-patterns\"\nsource_commit: bf70150eb2df8070024e5bdf08e4aa08959e2735\n---\n# Docker Patterns\n\nDocker and Docker Compose best practices for containerized development.\n\n## Docker Compose for Local Development\n\n### Standard Web App Stack\n\n```yaml\n# docker-compose.yml\nservices:\n  app:\n    build:\n      context: .\n      target: dev                     # Use dev stage of multi-stage Dockerfile\n    ports:\n      - \"3000:3000\"\n    volumes:\n      - .:\u002Fapp                        # Bind mount for hot reload\n      - \u002Fapp\u002Fnode_modules             # Anonymous volume -- preserves container deps\n    environment:\n      - DATABASE_URL=postgres:\u002F\u002Fpostgres:postgres@db:5432\u002Fapp_dev\n      - REDIS_URL=redis:\u002F\u002Fredis:6379\u002F0\n      - NODE_ENV=development\n    depends_on:\n      db:\n        condition: service_healthy\n      redis:\n        condition: service_started\n    command: npm run dev\n\n  db:\n    image: postgres:16-alpine\n    ports:\n      - \"5432:5432\"\n    environment:\n      POSTGRES_USER: postgres\n      POSTGRES_PASSWORD: postgres\n      POSTGRES_DB: app_dev\n    volumes:\n      - pgdata:\u002Fvar\u002Flib\u002Fpostgresql\u002Fdata\n      - .\u002Fscripts\u002Finit-db.sql:\u002Fdocker-entrypoint-initdb.d\u002Finit.sql\n    healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready -U postgres\"]\n      interval: 5s\n      timeout: 3s\n      retries: 5\n\n  redis:\n    image: redis:7-alpine\n    ports:\n      - \"6379:6379\"\n    volumes:\n      - redisdata:\u002Fdata\n\n  mailpit:                            # Local email testing\n    image: axllent\u002Fmailpit\n    ports:\n      - \"8025:8025\"                   # Web UI\n      - \"1025:1025\"                   # SMTP\n\nvolumes:\n  pgdata:\n  redisdata:\n```\n\n### Development vs Production Dockerfile\n\n```dockerfile\n# Stage: dependencies\nFROM node:22-alpine AS deps\nWORKDIR \u002Fapp\nCOPY package.json package-lock.json .\u002F\nRUN npm ci\n\n# Stage: dev (hot reload, debug tools)\nFROM node:22-alpine AS dev\nWORKDIR \u002Fapp\nCOPY --from=deps \u002Fapp\u002Fnode_modules .\u002Fnode_modules\nCOPY . .\nEXPOSE 3000\nCMD [\"npm\", \"run\", \"dev\"]\n\n# Stage: build\nFROM node:22-alpine AS build\nWORKDIR \u002Fapp\nCOPY --from=deps \u002Fapp\u002Fnode_modules .\u002Fnode_modules\nCOPY . .\nRUN npm run build && npm prune --production\n\n# Stage: production (minimal image)\nFROM node:22-alpine AS production\nWORKDIR \u002Fapp\nRUN addgroup -g 1001 -S appgroup && adduser -S appuser -u 1001\nUSER appuser\nCOPY --from=build --chown=appuser:appgroup \u002Fapp\u002Fdist .\u002Fdist\nCOPY --from=build --chown=appuser:appgroup \u002Fapp\u002Fnode_modules .\u002Fnode_modules\nCOPY --from=build --chown=appuser:appgroup \u002Fapp\u002Fpackage.json .\u002F\nENV NODE_ENV=production\nEXPOSE 3000\nHEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http:\u002F\u002Flocalhost:3000\u002Fhealth || exit 1\nCMD [\"node\", \"dist\u002Fserver.js\"]\n```\n\n### Override Files\n\n```yaml\n# docker-compose.override.yml (auto-loaded, dev-only settings)\nservices:\n  app:\n    environment:\n      - DEBUG=app:*\n      - LOG_LEVEL=debug\n    ports:\n      - \"9229:9229\"                   # Node.js debugger\n\n# docker-compose.prod.yml (explicit for production)\nservices:\n  app:\n    build:\n      target: production\n    restart: always\n    deploy:\n      resources:\n        limits:\n          cpus: \"1.0\"\n          memory: 512M\n```\n\n```bash\n# Development (auto-loads override)\ndocker compose up\n\n# Production\ndocker compose -f docker-compose.yml -f docker-compose.prod.yml up -d\n```\n\n## Networking\n\n### Service Discovery\n\nServices in the same Compose network resolve by service name:\n```\n# From \"app\" container:\npostgres:\u002F\u002Fpostgres:postgres@db:5432\u002Fapp_dev    # \"db\" resolves to the db container\nredis:\u002F\u002Fredis:6379\u002F0                             # \"redis\" resolves to the redis container\n```\n\n### Custom Networks\n\n```yaml\nservices:\n  frontend:\n    networks:\n      - frontend-net\n\n  api:\n    networks:\n      - frontend-net\n      - backend-net\n\n  db:\n    networks:\n      - backend-net              # Only reachable from api, not frontend\n\nnetworks:\n  frontend-net:\n  backend-net:\n```\n\n### Exposing Only What's Needed\n\n```yaml\nservices:\n  db:\n    ports:\n      - \"127.0.0.1:5432:5432\"   # Only accessible from host, not network\n    # Omit ports entirely in production -- accessible only within Docker network\n```\n\n## Volume Strategies\n\n```yaml\nvolumes:\n  # Named volume: persists across container restarts, managed by Docker\n  pgdata:\n\n  # Bind mount: maps host directory into container (for development)\n  # - .\u002Fsrc:\u002Fapp\u002Fsrc\n\n  # Anonymous volume: preserves container-generated content from bind mount override\n  # - \u002Fapp\u002Fnode_modules\n```\n\n### Common Patterns\n\n```yaml\nservices:\n  app:\n    volumes:\n      - .:\u002Fapp                   # Source code (bind mount for hot reload)\n      - \u002Fapp\u002Fnode_modules        # Protect container's node_modules from host\n      - \u002Fapp\u002F.next               # Protect build cache\n\n  db:\n    volumes:\n      - pgdata:\u002Fvar\u002Flib\u002Fpostgresql\u002Fdata          # Persistent data\n      - .\u002Fscripts\u002Finit.sql:\u002Fdocker-entrypoint-initdb.d\u002Finit.sql  # Init scripts\n```\n\n## Container Security\n\n### Dockerfile Hardening\n\n```dockerfile\n# 1. Use specific tags (never :latest)\nFROM node:22.12-alpine3.20\n\n# 2. Run as non-root\nRUN addgroup -g 1001 -S app && adduser -S app -u 1001\nUSER app\n\n# 3. Drop capabilities (in compose)\n# 4. Read-only root filesystem where possible\n# 5. No secrets in image layers\n```\n\n### Compose Security\n\n```yaml\nservices:\n  app:\n    security_opt:\n      - no-new-privileges:true\n    read_only: true\n    tmpfs:\n      - \u002Ftmp\n      - \u002Fapp\u002F.cache\n    cap_drop:\n      - ALL\n    cap_add:\n      - NET_BIND_SERVICE          # Only if binding to ports \u003C 1024\n```\n\n### Secret Management\n\n```yaml\n# GOOD: Use environment variables (injected at runtime)\nservices:\n  app:\n    env_file:\n      - .env                     # Never commit .env to git\n    environment:\n      - API_KEY                  # Inherits from host environment\n\n# GOOD: Docker secrets (Swarm mode)\nsecrets:\n  db_password:\n    file: .\u002Fsecrets\u002Fdb_password.txt\n\nservices:\n  db:\n    secrets:\n      - db_password\n\n# BAD: Hardcoded in image\n# ENV API_KEY=sk-proj-xxxxx      # NEVER DO THIS\n```\n\n## Hardened CLI Installer Harnesses\n\nUse containers to test installer behavior against disposable project copies without allowing the test to mutate the source checkout.\n\n### Respect the Platform Boundary\n\n- Run real containers for Linux distributions such as Debian and Ubuntu.\n- macOS cannot run as a Docker container because Docker shares a Linux kernel. Run the same shell-free test entry point natively on macOS.\n- Windows containers require a Windows Docker engine. Run platform-independent logic on a native Windows CI runner and reserve Windows containers for a Windows host.\n- Keep a native Ubuntu\u002FmacOS\u002FWindows CI matrix for host-specific paths, command shims, quoting, and filesystem behavior.\n\nDo not claim that a Linux container validates macOS or Windows behavior.\n\n### Enforce the Isolation Contract\n\n- Pin base images by immutable digest and pin installed CLI versions.\n- Run as a non-root numeric UID\u002FGID when distro account names differ.\n- Mount the repository and source project read-only.\n- Copy the source project into a writable `tmpfs` workspace before any mutation.\n- Mount `\u002Fworkspace` with `noexec`, UID\u002FGID 1000, and `mode=0700` so only the\n  container user can inspect project data.\n- Keep npm and npx's executable cache at `NPM_CONFIG_CACHE=\u002Ftmp\u002Fnpm-cache` on\n  the executable `\u002Ftmp` mount. Its default size is 2 GiB and can be adjusted\n  with `ECC_TMPFS_SIZE`; `ECC_WORKSPACE_SIZE` separately controls the private\n  workspace mount.\n- Set `read_only: true`, `no-new-privileges:true`, `cap_drop: [ALL]`, and a finite `pids_limit`.\n- Keep the default real-CLI services on `network_mode: none`. Add network access\n  only through a visibly named opt-in service for an authenticated provider\n  session; never make it an accidental environment-driven default.\n- Create only the writable temporary paths the tool needs.\n- Do not pass host credentials into the container by default.\n- Default to a dry run and whitelist only the explicit `dry-run`, `install`,\n  `plugin`, and `shell` modes.\n- Use argument arrays or `spawnSync(..., { shell: false })` for cross-platform runners. Never interpolate project paths into a shell command.\n\n### Exercise the ECC Plugin Setup Harness\n\nUse `docker\u002Fplugin-setup\u002Fcompose.yaml` as the reference implementation. It provides:\n\n- `fixture-tests` for the focused install manifest, target, and executor suite.\n- `real-cli` for the pinned Debian-based generic Linux image.\n- `real-cli-ubuntu` for the pinned Ubuntu image.\n\nValidate the Compose model before building:\n\n```bash\ndocker compose -f docker\u002Fplugin-setup\u002Fcompose.yaml config --quiet\n```\n\nBuild both real Linux images:\n\n```bash\ndocker compose -f docker\u002Fplugin-setup\u002Fcompose.yaml \\\n  build real-cli real-cli-ubuntu\n```\n\nRun the safe default flow in each image:\n\n```bash\ndocker compose -p ecc-plugin-debian-test \\\n  -f docker\u002Fplugin-setup\u002Fcompose.yaml \\\n  run --rm -T real-cli dry-run\n\ndocker compose -p ecc-plugin-ubuntu-test \\\n  -f docker\u002Fplugin-setup\u002Fcompose.yaml \\\n  run --rm -T real-cli-ubuntu dry-run\n```\n\nThe dry run executes the current public command contract:\n\n```bash\necc install --profile core --target claude-project --dry-run --json\n```\n\nBefore that command runs, the container creates a locally packed npm artifact\nfrom the read-only checkout with `npm pack --ignore-scripts`. It extracts the\nself-created tarball under `\u002Ftmp`, validates the `ecc-universal` package name,\nrequired install manifests, and the confined `package.json` `bin.ecc` mapping,\nthen invokes the extracted `ecc` executable. The runtime stays on\n`network_mode: none`, does not execute package lifecycle scripts, and does not\nrely on host `node_modules`; its exact pinned production dependencies are\nalready present in the image.\n\nThe harness rejects an empty plan, a non-`claude-project` target, any operation\noutside `\u002Fworkspace\u002Fproject\u002F.claude`, or any dry run that creates the target\ndirectory. `install` performs the isolated apply twice, checks its managed\ninstall state, lists the installed target, and runs `doctor`.\n\n### Start, Open, Reconnect, and Clean Up a Named Session\n\nStart a detached container without `--rm` so leaving a terminal does not remove\nthe session:\n\n```bash\ndocker compose -p ecc-plugin-session \\\n  -f docker\u002Fplugin-setup\u002Fcompose.yaml \\\n  run --detach --name ecc-plugin-shell real-cli shell\n```\n\nThe container copies the read-only fixture to the stable private directory\n`\u002Fworkspace\u002Fproject`. Confirm it is running, then emit the Docker side of the\nterminal-opener v1 data contract:\n\n```bash\ndocker inspect --format '{{.State.Running}}' ecc-plugin-shell\nnode docker\u002Fplugin-setup\u002Finteractive-plan.js \\\n  --container ecc-plugin-shell \\\n  --workdir \u002Fworkspace\u002Fproject \\\n  --json \\\n  -- bash\n```\n\nThe JSON result has exactly an `executable` and `argv` boundary (plus\n`contractVersion: 1`): the executable is `docker`, and argv begins with\n`exec`, `-it`, and `-w`. Pass that data to the separate terminal-opener skill\nwhen it is installed. This Docker harness deliberately does not import a\nterminal adapter, interpolate a shell command, or manage a host GUI process.\nUntil then, open the same PTY in the current host terminal directly:\n\n```bash\ndocker exec -it -w \u002Fworkspace\u002Fproject ecc-plugin-shell bash\n```\n\nExit the shell without stopping the detached container. Reconnect with the\nsame `docker exec -it` command. When finished, remove the exact named container\nand its Compose project resources:\n\n```bash\ndocker rm --force ecc-plugin-shell\ndocker compose -p ecc-plugin-session \\\n  -f docker\u002Fplugin-setup\u002Fcompose.yaml \\\n  down --remove-orphans\n```\n\nHost credentials are absent by default and credential directories are never\nmounted. The default service also has no network access. When an authenticated\nprovider session genuinely needs a network, build `real-cli` first and then opt\nin visibly with `docker compose --profile networked run real-cli-networked\nshell`. Prefer authenticating inside that disposable session. If a CI run must\ninherit a host environment credential, make that opt-in at invocation with an\nexplicit Compose `--env NAME` flag, understand that the value is inspectable\nand can be exfiltrated for the container lifetime, and remove the exact named\ncontainer immediately after.\n\nRun the same focused suite natively on the host:\n\n```bash\nnpm run test:plugin-setup-platform\n```\n\nInspect the produced identity and environment before trusting the image:\n\n```bash\ndocker image inspect ecc-plugin-setup:debian ecc-plugin-setup:ubuntu\n```\n\nClean each named test project without deleting unrelated volumes or images:\n\n```bash\ndocker compose -p ecc-plugin-debian-test \\\n  -f docker\u002Fplugin-setup\u002Fcompose.yaml down --remove-orphans\ndocker compose -p ecc-plugin-ubuntu-test \\\n  -f docker\u002Fplugin-setup\u002Fcompose.yaml down --remove-orphans\n```\n\n## .dockerignore\n\n```\nnode_modules\n.git\n.env\n.env.*\ndist\ncoverage\n*.log\n.next\n.cache\ndocker-compose*.yml\nDockerfile*\nREADME.md\ntests\u002F\n```\n\n## Debugging\n\n### Common Commands\n\n```bash\n# View logs\ndocker compose logs -f app           # Follow app logs\ndocker compose logs --tail=50 db     # Last 50 lines from db\n\n# Execute commands in running container\ndocker compose exec app sh           # Shell into app\ndocker compose exec db psql -U postgres  # Connect to postgres\n\n# Inspect\ndocker compose ps                     # Running services\ndocker compose top                    # Processes in each container\ndocker stats                          # Resource usage\n\n# Rebuild\ndocker compose up --build             # Rebuild images\ndocker compose build --no-cache app   # Force full rebuild\n\n# Clean up\ndocker compose down                   # Stop and remove containers\ndocker compose down -v                # Also remove volumes (DESTRUCTIVE)\ndocker system prune                   # Remove unused images\u002Fcontainers\n```\n\n### Debugging Network Issues\n\n```bash\n# Check DNS resolution inside container\ndocker compose exec app nslookup db\n\n# Check connectivity\ndocker compose exec app wget -qO- http:\u002F\u002Fapi:3000\u002Fhealth\n\n# Inspect network\ndocker network ls\ndocker network inspect \u003Cproject>_default\n```\n\n## Anti-Patterns\n\n```\n# BAD: Using docker compose in production without orchestration\n# Use Kubernetes, ECS, or Docker Swarm for production multi-container workloads\n\n# BAD: Storing data in containers without volumes\n# Containers are ephemeral -- all data lost on restart without volumes\n\n# BAD: Running as root\n# Always create and use a non-root user\n\n# BAD: Using :latest tag\n# Pin to specific versions for reproducible builds\n\n# BAD: One giant container with all services\n# Separate concerns: one process per container\n\n# BAD: Putting secrets in docker-compose.yml\n# Use .env files (gitignored) or Docker secrets\n```\n","# Docker Patterns\n\nDocker and Docker Compose best practices for containerized development.\n\n## Docker Compose for Local Development\n\n### Standard Web App Stack\n\n```yaml\n# docker-compose.yml\nservices:\n  app:\n    build:\n      context: .\n      target: dev                     # Use dev stage of multi-stage Dockerfile\n    ports:\n      - \"3000:3000\"\n    volumes:\n      - .:\u002Fapp                        # Bind mount for hot reload\n      - \u002Fapp\u002Fnode_modules             # Anonymous volume -- preserves container deps\n    environment:\n      - DATABASE_URL=postgres:\u002F\u002Fpostgres:postgres@db:5432\u002Fapp_dev\n      - REDIS_URL=redis:\u002F\u002Fredis:6379\u002F0\n      - NODE_ENV=development\n    depends_on:\n      db:\n        condition: service_healthy\n      redis:\n        condition: service_started\n    command: npm run dev\n\n  db:\n    image: postgres:16-alpine\n    ports:\n      - \"5432:5432\"\n    environment:\n      POSTGRES_USER: postgres\n      POSTGRES_PASSWORD: postgres\n      POSTGRES_DB: app_dev\n    volumes:\n      - pgdata:\u002Fvar\u002Flib\u002Fpostgresql\u002Fdata\n      - .\u002Fscripts\u002Finit-db.sql:\u002Fdocker-entrypoint-initdb.d\u002Finit.sql\n    healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready -U postgres\"]\n      interval: 5s\n      timeout: 3s\n      retries: 5\n\n  redis:\n    image: redis:7-alpine\n    ports:\n      - \"6379:6379\"\n    volumes:\n      - redisdata:\u002Fdata\n\n  mailpit:                            # Local email testing\n    image: axllent\u002Fmailpit\n    ports:\n      - \"8025:8025\"                   # Web UI\n      - \"1025:1025\"                   # SMTP\n\nvolumes:\n  pgdata:\n  redisdata:\n```\n\n### Development vs Production Dockerfile\n\n```dockerfile\n# Stage: dependencies\nFROM node:22-alpine AS deps\nWORKDIR \u002Fapp\nCOPY package.json package-lock.json .\u002F\nRUN npm ci\n\n# Stage: dev (hot reload, debug tools)\nFROM node:22-alpine AS dev\nWORKDIR \u002Fapp\nCOPY --from=deps \u002Fapp\u002Fnode_modules .\u002Fnode_modules\nCOPY . .\nEXPOSE 3000\nCMD [\"npm\", \"run\", \"dev\"]\n\n# Stage: build\nFROM node:22-alpine AS build\nWORKDIR \u002Fapp\nCOPY --from=deps \u002Fapp\u002Fnode_modules .\u002Fnode_modules\nCOPY . .\nRUN npm run build && npm prune --production\n\n# Stage: production (minimal image)\nFROM node:22-alpine AS production\nWORKDIR \u002Fapp\nRUN addgroup -g 1001 -S appgroup && adduser -S appuser -u 1001\nUSER appuser\nCOPY --from=build --chown=appuser:appgroup \u002Fapp\u002Fdist .\u002Fdist\nCOPY --from=build --chown=appuser:appgroup \u002Fapp\u002Fnode_modules .\u002Fnode_modules\nCOPY --from=build --chown=appuser:appgroup \u002Fapp\u002Fpackage.json .\u002F\nENV NODE_ENV=production\nEXPOSE 3000\nHEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http:\u002F\u002Flocalhost:3000\u002Fhealth || exit 1\nCMD [\"node\", \"dist\u002Fserver.js\"]\n```\n\n### Override Files\n\n```yaml\n# docker-compose.override.yml (auto-loaded, dev-only settings)\nservices:\n  app:\n    environment:\n      - DEBUG=app:*\n      - LOG_LEVEL=debug\n    ports:\n      - \"9229:9229\"                   # Node.js debugger\n\n# docker-compose.prod.yml (explicit for production)\nservices:\n  app:\n    build:\n      target: production\n    restart: always\n    deploy:\n      resources:\n        limits:\n          cpus: \"1.0\"\n          memory: 512M\n```\n\n```bash\n# Development (auto-loads override)\ndocker compose up\n\n# Production\ndocker compose -f docker-compose.yml -f docker-compose.prod.yml up -d\n```\n\n## Networking\n\n### Service Discovery\n\nServices in the same Compose network resolve by service name:\n```\n# From \"app\" container:\npostgres:\u002F\u002Fpostgres:postgres@db:5432\u002Fapp_dev    # \"db\" resolves to the db container\nredis:\u002F\u002Fredis:6379\u002F0                             # \"redis\" resolves to the redis container\n```\n\n### Custom Networks\n\n```yaml\nservices:\n  frontend:\n    networks:\n      - frontend-net\n\n  api:\n    networks:\n      - frontend-net\n      - backend-net\n\n  db:\n    networks:\n      - backend-net              # Only reachable from api, not frontend\n\nnetworks:\n  frontend-net:\n  backend-net:\n```\n\n### Exposing Only What's Needed\n\n```yaml\nservices:\n  db:\n    ports:\n      - \"127.0.0.1:5432:5432\"   # Only accessible from host, not network\n    # Omit ports entirely in production -- accessible only within Docker network\n```\n\n## Volume Strategies\n\n```yaml\nvolumes:\n  # Named volume: persists across container restarts, managed by Docker\n  pgdata:\n\n  # Bind mount: maps host directory into container (for development)\n  # - .\u002Fsrc:\u002Fapp\u002Fsrc\n\n  # Anonymous volume: preserves container-generated content from bind mount override\n  # - \u002Fapp\u002Fnode_modules\n```\n\n### Common Patterns\n\n```yaml\nservices:\n  app:\n    volumes:\n      - .:\u002Fapp                   # Source code (bind mount for hot reload)\n      - \u002Fapp\u002Fnode_modules        # Protect container's node_modules from host\n      - \u002Fapp\u002F.next               # Protect build cache\n\n  db:\n    volumes:\n      - pgdata:\u002Fvar\u002Flib\u002Fpostgresql\u002Fdata          # Persistent data\n      - .\u002Fscripts\u002Finit.sql:\u002Fdocker-entrypoint-initdb.d\u002Finit.sql  # Init scripts\n```\n\n## Container Security\n\n### Dockerfile Hardening\n\n```dockerfile\n# 1. Use specific tags (never :latest)\nFROM node:22.12-alpine3.20\n\n# 2. Run as non-root\nRUN addgroup -g 1001 -S app && adduser -S app -u 1001\nUSER app\n\n# 3. Drop capabilities (in compose)\n# 4. Read-only root filesystem where possible\n# 5. No secrets in image layers\n```\n\n### Compose Security\n\n```yaml\nservices:\n  app:\n    security_opt:\n      - no-new-privileges:true\n    read_only: true\n    tmpfs:\n      - \u002Ftmp\n      - \u002Fapp\u002F.cache\n    cap_drop:\n      - ALL\n    cap_add:\n      - NET_BIND_SERVICE          # Only if binding to ports \u003C 1024\n```\n\n### Secret Management\n\n```yaml\n# GOOD: Use environment variables (injected at runtime)\nservices:\n  app:\n    env_file:\n      - .env                     # Never commit .env to git\n    environment:\n      - API_KEY                  # Inherits from host environment\n\n# GOOD: Docker secrets (Swarm mode)\nsecrets:\n  db_password:\n    file: .\u002Fsecrets\u002Fdb_password.txt\n\nservices:\n  db:\n    secrets:\n      - db_password\n\n# BAD: Hardcoded in image\n# ENV API_KEY=sk-proj-xxxxx      # NEVER DO THIS\n```\n\n## Hardened CLI Installer Harnesses\n\nUse containers to test installer behavior against disposable project copies without allowing the test to mutate the source checkout.\n\n### Respect the Platform Boundary\n\n- Run real containers for Linux distributions such as Debian and Ubuntu.\n- macOS cannot run as a Docker container because Docker shares a Linux kernel. Run the same shell-free test entry point natively on macOS.\n- Windows containers require a Windows Docker engine. Run platform-independent logic on a native Windows CI runner and reserve Windows containers for a Windows host.\n- Keep a native Ubuntu\u002FmacOS\u002FWindows CI matrix for host-specific paths, command shims, quoting, and filesystem behavior.\n\nDo not claim that a Linux container validates macOS or Windows behavior.\n\n### Enforce the Isolation Contract\n\n- Pin base images by immutable digest and pin installed CLI versions.\n- Run as a non-root numeric UID\u002FGID when distro account names differ.\n- Mount the repository and source project read-only.\n- Copy the source project into a writable `tmpfs` workspace before any mutation.\n- Mount `\u002Fworkspace` with `noexec`, UID\u002FGID 1000, and `mode=0700` so only the\n  container user can inspect project data.\n- Keep npm and npx's executable cache at `NPM_CONFIG_CACHE=\u002Ftmp\u002Fnpm-cache` on\n  the executable `\u002Ftmp` mount. Its default size is 2 GiB and can be adjusted\n  with `ECC_TMPFS_SIZE`; `ECC_WORKSPACE_SIZE` separately controls the private\n  workspace mount.\n- Set `read_only: true`, `no-new-privileges:true`, `cap_drop: [ALL]`, and a finite `pids_limit`.\n- Keep the default real-CLI services on `network_mode: none`. Add network access\n  only through a visibly named opt-in service for an authenticated provider\n  session; never make it an accidental environment-driven default.\n- Create only the writable temporary paths the tool needs.\n- Do not pass host credentials into the container by default.\n- Default to a dry run and whitelist only the explicit `dry-run`, `install`,\n  `plugin`, and `shell` modes.\n- Use argument arrays or `spawnSync(..., { shell: false })` for cross-platform runners. Never interpolate project paths into a shell command.\n\n### Exercise the ECC Plugin Setup Harness\n\nUse `docker\u002Fplugin-setup\u002Fcompose.yaml` as the reference implementation. It provides:\n\n- `fixture-tests` for the focused install manifest, target, and executor suite.\n- `real-cli` for the pinned Debian-based generic Linux image.\n- `real-cli-ubuntu` for the pinned Ubuntu image.\n\nValidate the Compose model before building:\n\n```bash\ndocker compose -f docker\u002Fplugin-setup\u002Fcompose.yaml config --quiet\n```\n\nBuild both real Linux images:\n\n```bash\ndocker compose -f docker\u002Fplugin-setup\u002Fcompose.yaml \\\n  build real-cli real-cli-ubuntu\n```\n\nRun the safe default flow in each image:\n\n```bash\ndocker compose -p ecc-plugin-debian-test \\\n  -f docker\u002Fplugin-setup\u002Fcompose.yaml \\\n  run --rm -T real-cli dry-run\n\ndocker compose -p ecc-plugin-ubuntu-test \\\n  -f docker\u002Fplugin-setup\u002Fcompose.yaml \\\n  run --rm -T real-cli-ubuntu dry-run\n```\n\nThe dry run executes the current public command contract:\n\n```bash\necc install --profile core --target claude-project --dry-run --json\n```\n\nBefore that command runs, the container creates a locally packed npm artifact\nfrom the read-only checkout with `npm pack --ignore-scripts`. It extracts the\nself-created tarball under `\u002Ftmp`, validates the `ecc-universal` package name,\nrequired install manifests, and the confined `package.json` `bin.ecc` mapping,\nthen invokes the extracted `ecc` executable. The runtime stays on\n`network_mode: none`, does not execute package lifecycle scripts, and does not\nrely on host `node_modules`; its exact pinned production dependencies are\nalready present in the image.\n\nThe harness rejects an empty plan, a non-`claude-project` target, any operation\noutside `\u002Fworkspace\u002Fproject\u002F.claude`, or any dry run that creates the target\ndirectory. `install` performs the isolated apply twice, checks its managed\ninstall state, lists the installed target, and runs `doctor`.\n\n### Start, Open, Reconnect, and Clean Up a Named Session\n\nStart a detached container without `--rm` so leaving a terminal does not remove\nthe session:\n\n```bash\ndocker compose -p ecc-plugin-session \\\n  -f docker\u002Fplugin-setup\u002Fcompose.yaml \\\n  run --detach --name ecc-plugin-shell real-cli shell\n```\n\nThe container copies the read-only fixture to the stable private directory\n`\u002Fworkspace\u002Fproject`. Confirm it is running, then emit the Docker side of the\nterminal-opener v1 data contract:\n\n```bash\ndocker inspect --format '{{.State.Running}}' ecc-plugin-shell\nnode docker\u002Fplugin-setup\u002Finteractive-plan.js \\\n  --container ecc-plugin-shell \\\n  --workdir \u002Fworkspace\u002Fproject \\\n  --json \\\n  -- bash\n```\n\nThe JSON result has exactly an `executable` and `argv` boundary (plus\n`contractVersion: 1`): the executable is `docker`, and argv begins with\n`exec`, `-it`, and `-w`. Pass that data to the separate terminal-opener skill\nwhen it is installed. This Docker harness deliberately does not import a\nterminal adapter, interpolate a shell command, or manage a host GUI process.\nUntil then, open the same PTY in the current host terminal directly:\n\n```bash\ndocker exec -it -w \u002Fworkspace\u002Fproject ecc-plugin-shell bash\n```\n\nExit the shell without stopping the detached container. Reconnect with the\nsame `docker exec -it` command. When finished, remove the exact named container\nand its Compose project resources:\n\n```bash\ndocker rm --force ecc-plugin-shell\ndocker compose -p ecc-plugin-session \\\n  -f docker\u002Fplugin-setup\u002Fcompose.yaml \\\n  down --remove-orphans\n```\n\nHost credentials are absent by default and credential directories are never\nmounted. The default service also has no network access. When an authenticated\nprovider session genuinely needs a network, build `real-cli` first and then opt\nin visibly with `docker compose --profile networked run real-cli-networked\nshell`. Prefer authenticating inside that disposable session. If a CI run must\ninherit a host environment credential, make that opt-in at invocation with an\nexplicit Compose `--env NAME` flag, understand that the value is inspectable\nand can be exfiltrated for the container lifetime, and remove the exact named\ncontainer immediately after.\n\nRun the same focused suite natively on the host:\n\n```bash\nnpm run test:plugin-setup-platform\n```\n\nInspect the produced identity and environment before trusting the image:\n\n```bash\ndocker image inspect ecc-plugin-setup:debian ecc-plugin-setup:ubuntu\n```\n\nClean each named test project without deleting unrelated volumes or images:\n\n```bash\ndocker compose -p ecc-plugin-debian-test \\\n  -f docker\u002Fplugin-setup\u002Fcompose.yaml down --remove-orphans\ndocker compose -p ecc-plugin-ubuntu-test \\\n  -f docker\u002Fplugin-setup\u002Fcompose.yaml down --remove-orphans\n```\n\n## .dockerignore\n\n```\nnode_modules\n.git\n.env\n.env.*\ndist\ncoverage\n*.log\n.next\n.cache\ndocker-compose*.yml\nDockerfile*\nREADME.md\ntests\u002F\n```\n\n## Debugging\n\n### Common Commands\n\n```bash\n# View logs\ndocker compose logs -f app           # Follow app logs\ndocker compose logs --tail=50 db     # Last 50 lines from db\n\n# Execute commands in running container\ndocker compose exec app sh           # Shell into app\ndocker compose exec db psql -U postgres  # Connect to postgres\n\n# Inspect\ndocker compose ps                     # Running services\ndocker compose top                    # Processes in each container\ndocker stats                          # Resource usage\n\n# Rebuild\ndocker compose up --build             # Rebuild images\ndocker compose build --no-cache app   # Force full rebuild\n\n# Clean up\ndocker compose down                   # Stop and remove containers\ndocker compose down -v                # Also remove volumes (DESTRUCTIVE)\ndocker system prune                   # Remove unused images\u002Fcontainers\n```\n\n### Debugging Network Issues\n\n```bash\n# Check DNS resolution inside container\ndocker compose exec app nslookup db\n\n# Check connectivity\ndocker compose exec app wget -qO- http:\u002F\u002Fapi:3000\u002Fhealth\n\n# Inspect network\ndocker network ls\ndocker network inspect \u003Cproject>_default\n```\n\n## Anti-Patterns\n\n```\n# BAD: Using docker compose in production without orchestration\n# Use Kubernetes, ECS, or Docker Swarm for production multi-container workloads\n\n# BAD: Storing data in containers without volumes\n# Containers are ephemeral -- all data lost on restart without volumes\n\n# BAD: Running as root\n# Always create and use a non-root user\n\n# BAD: Using :latest tag\n# Pin to specific versions for reproducible builds\n\n# BAD: One giant container with all services\n# Separate concerns: one process per container\n\n# BAD: Putting secrets in docker-compose.yml\n# Use .env files (gitignored) or Docker secrets\n```\n",{"name":5,"tags":49,"tools":51,"source":52,"license":14,"description":7,"source_path":53,"source_commit":54},[50,25,37],"devops",[],"https:\u002F\u002Fgithub.com\u002Faffaan-m\u002FECC","skills\u002Fdocker-patterns","bf70150eb2df8070024e5bdf08e4aa08959e2735","main","f7cd4595d0f8959822163e14f9787117765c931a","packages\u002F@monomind\u002Fcli\u002Forg-skills\u002Fdocker-patterns",2,"https:\u002F\u002Fgithub.com\u002Fmonoes\u002Fmonomind","https:\u002F\u002Fmonoes.github.io\u002Fmonomind\u002F","https:\u002F\u002Fgithub.com\u002Fmonoes","https:\u002F\u002Fmonoes.me","2026-11-14 03:30:52",{"source":8,"skillName":5,"commands":65,"prompt":75,"zipAvailable":76},[66,69,72],{"tool":67,"command":68},"npx","npx skills add monoes\u002Fmonomind --skill docker-patterns",{"tool":70,"command":71},"bunx","bunx skills add monoes\u002Fmonomind --skill docker-patterns",{"tool":73,"command":74},"pnpm","pnpm dlx skills add monoes\u002Fmonomind --skill docker-patterns","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fmonoes\u002Fmonomind\u002Fblob\u002Fmain\u002Fpackages\u002F@monomind\u002Fcli\u002Forg-skills\u002Fdocker-patterns\u002FSKILL.md",true,[78],{"path":79,"resourceType":80,"resourceTypeDesc":81,"sizeBytes":82,"sourceHtmlUrl":15},"LICENSE.txt","LICENSE","许可证",1071,[84,112,144,170,195,219],{"skillNo":85,"name":86,"nameZh":6,"description":87,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":88,"licenseFileUrl":6,"sourceHtmlUrl":89,"categories":90,"keywords":91,"publishedAt":111},"01M71PB3SHWKMNJM6MD65VX8D6","zk-steward","Org role guidance for a ZK steward: engineer zero-knowledge proof systems with circuit design, SNARK vs STARK choice and security review. This is zero-knowledge cryptography, not Zettelkasten note-taking.","Apache-2.0","https:\u002F\u002Fgithub.com\u002Fmonoes\u002Fmonomind\u002Fblob\u002Fmain\u002Fpackages\u002F@monomind\u002Fcli\u002Forg-skills\u002Fzk-steward\u002FSKILL.md",[],[92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,37,109,110],"zk","zk steward","steward","zero-knowledge","engineer","choice","design","knowledge","impact","query","snark","review","monomind","security review","stark","zettelkasten","cryptography","org","blockchain","2026-11-10 13:02:02",{"skillNo":113,"name":114,"nameZh":6,"description":115,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":116,"sourceHtmlUrl":117,"categories":118,"keywords":127,"publishedAt":111},"01M71PB18SDG28WBKPKJNB814K","writing-plans","Use when you have a spec or requirements for a multi-step coding task and must write an implementation plan before touching code. Produces bite-sized TDD tasks with exact file paths, code and commands for an engineer with zero context.","https:\u002F\u002Fgithub.com\u002Fmonoes\u002Fmonomind\u002Fblob\u002Fmain\u002Fpackages\u002F@monomind\u002Fcli\u002Forg-skills\u002Fwriting-plans\u002FLICENSE.txt","https:\u002F\u002Fgithub.com\u002Fmonoes\u002Fmonomind\u002Fblob\u002Fmain\u002Fpackages\u002F@monomind\u002Fcli\u002Forg-skills\u002Fwriting-plans\u002FSKILL.md",[119,122],{"categoryNo":120,"categoryName":121,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000018","写作与文案",{"categoryNo":123,"categoryName":124,"taxonomyType":125,"taxonomyTypeDesc":126},"01JBSEEDCAT000000000000036","内容创作者","PROFESSION","适用职业",[40,128,129,96,130,131,39,132,101,133,134,135,136,137,138,139,140,141,142,143],"writing plans","plans","superpowers","implementation plan","you","code","tasks","plan","product","write","commands","paths","suggest","coding","context","ZeRO",{"skillNo":145,"name":146,"nameZh":6,"description":147,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":88,"licenseFileUrl":148,"sourceHtmlUrl":149,"categories":150,"keywords":154,"publishedAt":111},"01M71PAXFM488X9ARXK0T04TF5","write-spec","Write a feature spec or PRD from a problem statement or feature idea. Use when turning a vague idea or user request into a structured document, scoping a feature with goals and non-goals, defining success metrics and acceptance criteria, or breaking a big ask into a phased spec.","https:\u002F\u002Fgithub.com\u002Fmonoes\u002Fmonomind\u002Fblob\u002Fmain\u002Fpackages\u002F@monomind\u002Fcli\u002Forg-skills\u002Fwrite-spec\u002FLICENSE.txt","https:\u002F\u002Fgithub.com\u002Fmonoes\u002Fmonomind\u002Fblob\u002Fmain\u002Fpackages\u002F@monomind\u002Fcli\u002Forg-skills\u002Fwrite-spec\u002FSKILL.md",[151],{"categoryNo":152,"categoryName":153,"taxonomyType":125,"taxonomyTypeDesc":126},"01JBSEEDCAT000000000000031","产品经理",[137,155,156,157,158,99,159,39,160,161,162,136,163,164,165,166,167,168,31,169],"write spec","spec","user request","problem","acceptance","ask","vague","work","request","scoping","idea","metrics","feature","problem statement","document",{"skillNo":171,"name":172,"nameZh":6,"description":173,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":88,"licenseFileUrl":174,"sourceHtmlUrl":175,"categories":176,"keywords":180,"publishedAt":111},"01M71PASSZY800C0RPEARBS918","write-query","Write optimized SQL for your dialect with best practices. Use when translating a natural-language data need into SQL, building a multi-CTE query with joins and aggregations, optimizing a query against a large partitioned table, or getting dialect-specific syntax for Snowflake, BigQuery, Postgres, etc.","https:\u002F\u002Fgithub.com\u002Fmonoes\u002Fmonomind\u002Fblob\u002Fmain\u002Fpackages\u002F@monomind\u002Fcli\u002Forg-skills\u002Fwrite-query\u002FLICENSE.txt","https:\u002F\u002Fgithub.com\u002Fmonoes\u002Fmonomind\u002Fblob\u002Fmain\u002Fpackages\u002F@monomind\u002Fcli\u002Forg-skills\u002Fwrite-query\u002FSKILL.md",[177],{"categoryNo":178,"categoryName":179,"taxonomyType":125,"taxonomyTypeDesc":126},"01JBSEEDCAT000000000000033","数据分析师",[101,137,181,182,183,184,99,31,39,185,186,187,188,162,189,190,191,192,193,194],"write query","postgres","for","snowflake","best practices","natural-language","with","multi","github","sql","https","syntax","plugins","data",{"skillNo":196,"name":197,"nameZh":6,"description":198,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":88,"licenseFileUrl":6,"sourceHtmlUrl":199,"categories":200,"keywords":205,"publishedAt":111},"01M71PAPTX0T3PF6ETFVR96SJJ","workflow-automation","Org role guidance for workflow automation: build and maintain CI\u002FCD definitions in GitHub Actions or equivalent: triggers, jobs, matrices, caching. Covers path filters, dependency caching, parallel jobs, timeouts and fail-fast ordering.","https:\u002F\u002Fgithub.com\u002Fmonoes\u002Fmonomind\u002Fblob\u002Fmain\u002Fpackages\u002F@monomind\u002Fcli\u002Forg-skills\u002Fworkflow-automation\u002FSKILL.md",[201,202],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":203,"categoryName":204,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",[206,207,208,209,183,210,211,212,213,100,214,215,216,101,104,189,217,218,50,191],"Workflow Automation","automation","workflow","parallel","actions","ci","triggers","github actions","maintain","filters","caching","build","dependency",{"skillNo":220,"name":221,"nameZh":6,"description":222,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":88,"licenseFileUrl":6,"sourceHtmlUrl":223,"categories":224,"keywords":226,"publishedAt":111},"01M71PAM19Z1XTV9NPCFSE2MY8","workflow-architect","Org role guidance for a workflow architect: map full workflow trees for systems, user journeys and agents, covering every branch and failure. Covers discovering workflows from code, handoff contracts, timeouts, recovery paths and observable state.","https:\u002F\u002Fgithub.com\u002Fmonoes\u002Fmonomind\u002Fblob\u002Fmain\u002Fpackages\u002F@monomind\u002Fcli\u002Forg-skills\u002Fworkflow-architect\u002FSKILL.md",[225],{"categoryNo":203,"categoryName":204,"taxonomyType":21,"taxonomyTypeDesc":22},[208,227,228,229,230,231,232,183,233,234,133,235,236,237,104,189,238,239,191,240],"workflow architect","architect","trees","user journeys","branch","planning","map","workflows","architecture","state","leadership","recovery","operations","systems"]