docker-architect
Dono do docker-compose.yml e do Dockerfile: adiciona os serviços de que um projeto ou um caso de uso precisa.
docker-architect, seção a seção
As seções do próprio SKILL.md da skill, em trechos. Passe o mouse ou clique numa delas para ver quais regras do projeto, templates e arquivos de references/ ela cita. As seções também são chips: clique num para ir até ela.
## Current compose state
!R="${CLAUDE_PROJECT_DIR:-.}"; F="$R/docker-compose.yml"; if test ! -d "$R"; then echo "(could not look: project root '$R' is not a readable directory — this is not an answer about docker-compose.yml)"; elif test -f "$F"; then S=$(awk '/^services:[[:space:]]*$/{s=1;next} /^[^[:space:]#]/{s=0} s&&/^ [A-Za-z0-9_.-]+:[[:space:]]*(#.*)?$/{sub(/[[:space:]]*#.*$/,"");print}' "$F"); if test -n "$S"; then echo "$S"; else echo "(docker-compose.yml exists, and the services: block declares nothing)"; fi; else echo "(no docker-compose.yml at project root — run project-bootstrap first)"; fi
## Target
$ARGUMENTS
# Docker Architect
Keeps docker-compose.yml and Dockerfile in sync with what the project actually needs, after they're born. project-bootstrap writes the base pair — one app service, nothing else — at project creation. Every service added afterward (Postgres, MySQL, a broker) goes through this skill, so there's one owner instead of persistence-architect and test-architect each editing the same YAML their own way.
Entry rule: without docker-compose.yml at the project root, there's nothing to extend. If it's missing, stop and say to run project-bootstrap — this skill never generates the base pair, only grows it.
The ## Current compose state block above answers one of three things, and only the second one arms that rule:
| Output | Meaning | What to do |
|---|---|---|
| A list of service names | The file exists | Proceed |
(no docker-compose.yml at project root …) | The project root was readable and the file is not there | Entry rule applies: stop |
(could not look: project root … is not a readable directory …) | The root itself couldn't be read — this is not an answer about docker-compose.yml | Don't apply the entry rule. Read the file at the path the user gave, or ask for the project root; run /arch-doctor, which reports CLAUDE_PROJECT_DIR NOT set |
(docker-compose.yml exists but declares no service …) | The file is there and malformed or empty | Entry rule doesn't apply — the base pair exists. Read the file before editing |
## How it's invoked
Two paths, both real: /docker-architect by hand — when the user wants a service added or checked, and that is how a pending service recorded by a use case spec gets materialized; and chained by project-bootstrap itself, right after it writes the base pair in its own step 4.10, once per blueprint feature that's already active and needs a container (persistence-jpa → Postgres, observability → the OTLP collector) — see project-bootstrap/SKILL.md step 4.10. A third path: chained by sonarqube-setup for a local sonarqube service when the project has no server of its own; a fourth, by transport-security-setup for the local edge of topology A or the Dockerfile port of topology B. A fifth, by compose-service-installer inside /new-feature-implement, with the use case folder, for every materialize row of the spec's ## Infrastructure — the answer the design run recorded through references/compose-service-decision.md, which this skill owns (@.claude/decisions/0137-pending-compose-service-decided-at-design-materialized-at-implement.md). That's why it carries no disable-model-invocation — a skill the model can't see is a skill a sibling skill can't call.
… cortado aqui, o resto está no arquivo
## Why this is a skill and not a subagent
Form 1, motivated by the "Ambos" trigger axis: chained by sibling skills mid-procedure, and invocable by hand. The closest rejected form was a subagent — it fails the § 5 counter-test in claude-code-architect-designer's decision matrix on all three points: the service choice is short, its shape fits in templates/, and the diff it produces is a few YAML lines. No context to isolate, no tool to restrict, no model change justified. Full record: @.claude/decisions/0029-docker-architect-skill.md.
Pinned to opus, effort inherited: the one procedural skill with an observed design failure (a broker published to the host and advertised only on the compose network). The pin lasts for the rest of the turn it fires in (@.claude/decisions/0080-procedural-skills-on-sonnet.md).
## Boundary with neighboring pieces
| Piece | Owns | Doesn't touch |
|---|---|---|
project-bootstrap | Base Dockerfile + docker-compose.yml (app service), once, at generation. Also *decides when* to call this skill for a feature already active in the blueprint (persistence-jpa, observability) — never writes the service block itself | Any service a use case adds later; the shape of any service block, ever |
| this skill | Every service block in docker-compose.yml/Dockerfile — the ones project-bootstrap calls it for at generation, and the ones added later by hand or chained from a UC-driven skill | The engine choice itself — that's 20-persistence.md's call, this skill reads it |
persistence-architect | Engine choice, schema, datasource properties (20-persistence.md) | docker-compose.yml, and this skill — records the decision in ## Compose services instead |
messaging-architect | Broker choice, topic, consumer group (25-messaging.md) | docker-compose.yml, and this skill — records the decision in ## Compose services instead |
test-architect | The pinned image tag inside TestcontainersConfiguration.java (one line, Java side) | The compose-side service definition — invokes this skill instead, and both should agree on the same tag |
The OTLP signal contract has two owners, one per half, and that is what made lessons-learned-008 possible. project-bootstrap's templates/features/observability/application-observability.yml.example decides which signals the application exports (management.otlp.*); this skill's templates/otel-collector-config.yml.example decides which signals the collector accepts (service.pipelines). The OTLP receiver registers no route for a signal that has no pipeline, so a mismatch is a 404 on every publish cycle, not a startup error. Adding or removing a signal on either side is a change to both files, in the same commit. No rule states this — rules/ is a leaf and cannot name either skill (invariant 1), so this table is where it lives.
## Procedure
Project directory. When the caller's one-line context names project: <absolute path>, that directory is the project root, not the session's: read and write every path of this procedure under it, and run every shell command as (cd "<path>" && …). The ## Current compose state injected above reads the session's root, so it does not describe that project: read <path>/docker-compose.yml instead.
- Confirm the base pair exists.
docker-compose.ymlandDockerfileat the
… cortado aqui, o resto está no arquivo
## Service catalog
| Engine | Template | When not |
|---|---|---|
| PostgreSQL | templates/postgres-service.yml.example | Engine chosen in 20-persistence.md isn't Postgres — or, at bootstrap time, when persistence-jpa isn't active in the blueprint |
| PostgreSQL with pgvector | templates/postgres-service.yml.example, its VARIANT — a vector store | 29-ai.md declares no vector store. Same postgres service, variables and volume; only the image is pgvector/pgvector. On a project whose postgres already exists, the change is that block's image: line — never a second database service. The extension itself is created by the project's migration, not here |
| MySQL | templates/mysql-service.yml.example | Engine chosen in 20-persistence.md isn't MySQL |
| MongoDB | templates/mongo-service.yml.example | No aggregate of the document family in 20-persistence.md. Always a single-node replica set with a keyfile — never a standalone server, which refuses the transactions the outbox and idempotency need; the healthcheck initiates the set once. Needs MONGO_PASSWORD in .env (step 4.5). An external MongoDB must be a replica set too |
| Elasticsearch | templates/elasticsearch-service.yml.example | 23-search.md names no Elasticsearch projection. Single node, authentication on, HTTP TLS off locally; the tag's major matches the client's. Needs SEARCH_PASSWORD in .env |
| OpenSearch | templates/opensearch-service.yml.example | 23-search.md names no OpenSearch projection. Single node, security plugin on, HTTP TLS off locally; the admin password must be strong or the container exits. Needs SEARCH_PASSWORD in .env |
| Kafka | templates/kafka-service.yml.example | Broker chosen in 25-messaging.md isn't Kafka, or there is none |
| Redis (cache) | templates/redis-service.yml.example | 22-cache.md § 2 chose Caffeine, or the cache is external. No volume and no persistence on purpose — a cache is recomputed from its source — and allkeys-lru under a memory limit, so a full cache evicts instead of refusing writes. Needs REDIS_PASSWORD in .env (step 4.5) |
| Schema Registry (Confluent) | templates/schema-registry-service.yml.example | 25-messaging.md kept the JSON default — which is the rule's default, so this is the common case. Only when the partial recorded the Avro/Protobuf upgrade and its trigger. Needs kafka in the file, has no volume on purpose (state lives in the _schemas topic), and its tag has to match the kafka-avro-serializer the POM resolves — verified in the registry, per step 4 |
| OpenTelemetry Collector | templates/otel-collector-service.yml.example + init script templates/otel-collector-config.yml.example | observability isn't active in the blueprint. No engine choice for the collector itself — one vendor-neutral ingest point, always the same shape. Where it *exports* to is a real choice, and it's the next two rows |
| Jaeger | templates/jaeger-service.yml.example | No otel-collector in the file yet, a backend is already there, or the project wants metrics too — Jaeger stores traces only |
| Grafana + Tempo + Prometheus | templates/grafana-stack-service.yml.example + three init scripts (tempo-config, prometheus-config, grafana-datasources) | No otel-collector yet, a backend is already there, or three containers is too much for what the project needs — Jaeger is the one-container answer |
| SonarQube (community) | templates/sonarqube-service.yml.example | The project already has an external SonarQube server or SonarCloud — sonarqube-setup asked, and only its *None* answer chains here. Embedded H2, no database service: local analysis, not a shared server |
| Caddy (local edge) | templates/caddy-edge-service.yml.example + templates/Caddyfile.example at docker/caddy/Caddyfile | The project's transport is not topology A, or the developer declined a local edge — transport-security-setup asked. Terminates TLS with its own local CA and forwards over h2c; never writes Strict-Transport-Security, which the application owns (@.claude/rules/transport-security.md § HSTS) |
| H2 | — no service | In-memory, runs inside the JVM; nothing to containerize |
The collector's default exporter is debug, which writes to its own stdout and is not a dashboard. That default is deliberate — this skill doesn't pick an observability vendor on its own — but it is a starting point, not the end of the road: step 2.5 exists so the gap gets named out loud instead of waiting for someone to notice that "the traces work" and "I can see the traces" are different sentences.
… cortado aqui, o resto está no arquivo
O que lê, escreve e recusa
O contrato da própria skill, do SKILL.md, no texto original.
Class
build — the territory is skill_classes.build's override for this skill in @.claude/schemas/extensions.json: the compose file, the docker/ tree, the Dockerfile, and what a database service needs next to them (step 4.5) — .env, .env.example, and the .env line of .gitignore and .dockerignore. Nothing else. ArchHook.java guard enforces it, and it also makes this skill unreachable from inside a design run — /new-feature and the layer skills record the missing service in the spec, and the user invokes /docker-architect from a prompt of its own afterwards.
Unfiltered Bash
verification drives docker compose (up, ps, logs), docker manifest, curl against health endpoints and java … compose — the subcommands depend on the services being added, so a fixed list trails the skill. Writes stay under guard/guard bash, and a force push is blocked by guard bash (guard.force_push).
Reads
docs/use-cases/UC-NNN-<slug>/20-persistence.md, 25-messaging.md, and 40-tests.md when a folder is given; the active blueprint's features: (persistence-jpa, observability) when called from project-bootstrap at generation time; plus docker-compose.yml, Dockerfile, TestcontainersConfiguration.java (image tag only), @.claude/rules/persistence.md, and @.claude/rules/messaging.md.
Writes
docker-compose.yml (every service block, including the ones added at generation time for an already-active feature), Dockerfile (build-stage additions and the EXPOSE line, never the base image or base stages project-bootstrap wrote), docker/init/** when an init script is needed, and docker/caddy/Caddyfile for the local edge. No other skill writes a service block into docker-compose.yml — not even project-bootstrap, which only decides *when* to call this skill — this is the single owner.
Does not
choose the database engine (persistence-architect's call via 20-persistence.md) or the broker (messaging-architect's call via 25-messaging.md), pin the Testcontainers image tag inside Java (test-architect's one line in TestcontainersConfiguration.java), or write the base docker-compose.yml/Dockerfile (project-bootstrap, once, at generation). Doesn't write business code, doesn't touch .claude/rules/**.
Fonte: SKILL.md § Contract, no nerviz e4cae27.
O que o guard faz com ela
build é a classe da skill, e cada skill dela tem território próprio. Quem chama decide qual território vale: sob o project-initializer, o do agent; da thread principal, nenhuma chamada passa. O texto à direita é o do próprio hook.
write_allow da classe docker-architect
- ✓
docker-compose*.yml - ✓
docker/** - ✓
Dockerfile - ✓
.env - ✓
.env.example - ✓
.gitignore - ✓
.dockerignore qualquer caminho fora desta lista, quando a própria skill abre a faseuma chamada da thread principal com o /init-project aberto
A recusa sai com código 2 e a mensagem volta para o modelo.
Encadeada pelo project-initializer, as escritas dela levam o agent_type do agent e são julgadas por agent_classes.driver, **. Esta lista vale quando a skill roda num prompt próprio.
PreToolUse Skill(docker-architect) # agent_type: project-initializer (class driver) guard call exit 0 # a classed agent's call leaves the phase as it was PreToolUse Write(../bank-app/docker-compose.yml) guard write exit 0 # judged by agent_classes.driver, write_allow: **
# phase open: init-project (class orchestrator) PreToolUse Skill(docker-architect) guard call exit 2 ❌ `docker-architect` is class `build` and `init-project` (class `orchestrator`) is open — a design run does not materialize files. Record what is missing in the partial and in the consolidated spec, finish the run, then invoke `/docker-architect` from a prompt of its own.
# in the generated project, on a prompt of its own > /docker-architect … UserPromptSubmit audit prompt exit 0 guard prompt exit 0 # phase opened: docker-architect (class build) PreToolUse Write(pom.xml) guard write exit 2 ❌ `docker-architect` is class `build` — pom.xml is outside its territory. write_allow: docker-compose*.yml, docker/**, Dockerfile, .env, .env.example, .gitignore, .dockerignore Put the content in the spec as a code block, or finish the run and invoke the skill that owns this path. If the path is legitimately this skill's, widen skill_classes.build in .claude/schemas/extensions.json — retrying will not help.
Onde ela fica no trilho
Cada card é um passo do /init-project. A entrevista roda na sessão principal; os passos com fundo colorido rodam dentro do project-initializer; o git-publish volta para a sessão principal, depois do audit genesis. Passos tracejados só rodam quando se aplicam.