↑ ↓ to move · Enter to open · Esc to closeTap a result

Architecture
skill · class build · opus

docker-architect

Owns docker-compose.yml and the Dockerfile: adds the services a project or a use case needs.

skill · buildopus7 allowed pathstravels to the generated project
§ the skill's definition

docker-architect, section by section

The sections of the skill's own SKILL.md, excerpted. Hover or click one to see which project rules, templates and references/ files it cites. The sections are also chips: click one to jump to it.

nerviz/.claude/skills/docker-architect/SKILL.md

## 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:

OutputMeaningWhat to do
A list of service namesThe file existsProceed
(no docker-compose.yml at project root …)The project root was readable and the file is not thereEntry 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.ymlDon'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 emptyEntry 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.

… cut here, the rest is in the file

## 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

PieceOwnsDoesn't touch
project-bootstrapBase 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 itselfAny service a use case adds later; the shape of any service block, ever
this skillEvery 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 skillThe engine choice itself — that's 20-persistence.md's call, this skill reads it
persistence-architectEngine choice, schema, datasource properties (20-persistence.md)docker-compose.yml, and this skill — records the decision in ## Compose services instead
messaging-architectBroker choice, topic, consumer group (25-messaging.md)docker-compose.yml, and this skill — records the decision in ## Compose services instead
test-architectThe 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.

  1. Confirm the base pair exists. docker-compose.yml and Dockerfile at the

… cut here, the rest is in the file

## Service catalog

EngineTemplateWhen not
PostgreSQLtemplates/postgres-service.yml.exampleEngine chosen in 20-persistence.md isn't Postgres — or, at bootstrap time, when persistence-jpa isn't active in the blueprint
PostgreSQL with pgvectortemplates/postgres-service.yml.example, its VARIANT — a vector store29-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
MySQLtemplates/mysql-service.yml.exampleEngine chosen in 20-persistence.md isn't MySQL
MongoDBtemplates/mongo-service.yml.exampleNo 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
Elasticsearchtemplates/elasticsearch-service.yml.example23-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
OpenSearchtemplates/opensearch-service.yml.example23-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
Kafkatemplates/kafka-service.yml.exampleBroker chosen in 25-messaging.md isn't Kafka, or there is none
Redis (cache)templates/redis-service.yml.example22-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.example25-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 Collectortemplates/otel-collector-service.yml.example + init script templates/otel-collector-config.yml.exampleobservability 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
Jaegertemplates/jaeger-service.yml.exampleNo otel-collector in the file yet, a backend is already there, or the project wants metrics too — Jaeger stores traces only
Grafana + Tempo + Prometheustemplates/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.exampleThe 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/CaddyfileThe 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 serviceIn-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.

… cut here, the rest is in the file

Open the full file on GitHub →
§ behavior

What it reads, writes and refuses

The skill's own contract, from SKILL.md, in the original wording.

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/**.

Source: SKILL.md § Contract, at nerviz e4cae27.

§ guard and hook

What the guard does with it

build is the skill's class, and each skill of it carries its own territory. Who calls it decides which territory applies: under project-initializer the agent's, from the main thread no call at all. The text on the right is the hook's own.

write_allow of class docker-architect

  • ✓docker-compose*.yml
  • ✓docker/**
  • ✓Dockerfile
  • ✓.env
  • ✓.env.example
  • ✓.gitignore
  • ✓.dockerignore
  • any path outside this list, when the skill opens the phase itself
  • a call from the main thread while /init-project is open

A refusal exits with code 2 and the message goes back to the model.

Chained by project-initializer, its writes carry the agent's agent_type and are judged by agent_classes.driver, **. This list applies when the skill runs on a prompt of its own.

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: **
§ orchestration

Where it sits on the rail

Each card is a step of /init-project. The interview runs in the main session; the tinted steps run inside project-initializer; git-publish comes back to the main session, after audit genesis. Dashed steps run only when they apply.

← back to the chains