Current compose state
!test -f docker-compose.yml && grep -E "^\s{2}\S+:$" docker-compose.yml || echo "(no docker-compose.yml at project root — run project-bootstrap first)"
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.
How it's invoked
Three paths, all real: /docker-architect by hand, when the user wants a service added
or checked; chained by persistence-architect, messaging-architect, or
test-architect, mid-procedure, when their spec names a dependency
docker-compose.yml doesn't have yet; 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.
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.
The guard against firing on an unborn project isn't the frontmatter: it's the entry rule
above.
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.
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-persistencia.md's call, this skill reads it |
persistence-architect |
Engine choice, schema, datasource properties (20-persistencia.md) |
docker-compose.yml directly — invokes this skill instead |
messaging-architect |
Broker choice, topic, consumer group (25-mensageria.md) |
docker-compose.yml directly — invokes this skill 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 |
Procedure
Confirm the base pair exists. docker-compose.yml and Dockerfile at the
project root. Missing either → stop, name what's missing, point to
project-bootstrap.
Find out what's needed.
- If
$ARGUMENTS names a UC-NNN-<slug> folder: read 20-persistencia.md for the
engine and version, 25-mensageria.md for the broker and topic, and 40-testes.md
for whether testcontainers is active and which image tag test-architect already
pinned in TestcontainersConfiguration.java
(grep -rn "DockerImageName.parse" src/test).
- If called from
project-bootstrap at generation time: the feature already decided
it — persistence-jpa means Postgres (the engine application.yml.example's
datasource.url already assumes), observability means the OTLP collector. No
engine question to ask; go straight to step 3.
- If invoked manually with no folder:
AskUserQuestion — engine (Postgres, MySQL,
Kafka, other), version/tag, port, whether it needs an init script. Don't ask what a
given spec already answers.
Check what's already there. grep -A2 "^services:" docker-compose.yml and the
service names under it. A service already present gets left alone — this step never
duplicates or silently overwrites a hand-edited block.
Merge the service block. From the matching templates/<engine>-service.yml.example,
append under services: — indentation matched to the file's own, never reformatting
what's already there. Use the same image tag test-architect pinned, when one
exists, so the dev-time container and the integration-test container run identical
software. If none is pinned yet, use the tag the engine's template ships with and say
so in the report — test-architect's setup mode is what should pick it up from here,
not the other way around.
Wire the app service's environment, only the variables that change because of
step 4 (SPRING_DATASOURCE_URL/_USERNAME/_PASSWORD pointing at the new service's
hostname and port from compose's internal network; OTLP_ENDPOINT pointing at the
collector's, e.g. http://otel-collector:4318/v1/traces, for the OTLP service).
Don't invent datasource properties beyond connectivity — sizing and the rest are
@.claude/rules/persistence.md's and persistence-architect's call, not this
skill's.
Init script. Write it under docker/init/<service>/ and mount it read-only in
the service's volumes:, from the matching templates/<name>-config.<ext>.example
when one exists. For a database service this is conditional on step 2 finding one is
needed, and skipped entirely when the schema comes from a Flyway migration instead —
one source of schema truth, not two. For the OTel collector it isn't conditional:
the collector has no built-in default pipeline and refuses to start without
templates/otel-collector-config.yml.example mounted, so this step always runs for
that service.
Report and stop. Service added, image tag used (and whether it matches
test-architect's pin), files changed. Don't invoke anyone — a sibling skill that
chained this one resumes on its own thread.
Service catalog
| Engine |
Template |
When not |
| PostgreSQL |
templates/postgres-service.yml.example |
Engine chosen in 20-persistencia.md isn't Postgres — or, at bootstrap time, when persistence-jpa isn't active in the blueprint |
| MySQL |
templates/mysql-service.yml.example |
Engine chosen in 20-persistencia.md isn't MySQL |
| Kafka |
templates/kafka-service.yml.example |
Broker chosen in 25-mensageria.md isn't Kafka, or there is none |
| 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 to make here — one vendor-neutral collector, always the same shape, unlike Postgres/MySQL/Kafka which branch on a real decision |
| H2 |
— no service |
In-memory, runs inside the JVM; nothing to containerize |
Other engines and brokers (Oracle, RabbitMQ, SQS via LocalStack) follow the same shape as
the templates above: image, fixed dev port, named volume for data, healthcheck,
environment for user/password/database (or broker-equivalent). Write the block by hand
from that shape; don't wait for a template to exist before extending a project that needs
one today.
Contract
Reads docs/use-cases/UC-NNN-<slug>/20-persistencia.md, 25-mensageria.md, and
40-testes.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
only, never the base image or base stages project-bootstrap wrote), and docker/init/**
when an init script is needed. 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-persistencia.md) or the broker (messaging-architect's call via
25-mensageria.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/**.
1---2name: docker-architect3description: Extends a project's docker-compose.yml and Dockerfile after project-bootstrap's base generation — adds the database or messaging service a modeled use case needs, keeps the compose-side image tag consistent with the one test-architect pins in TestcontainersConfiguration.java, and syncs with 20-persistencia.md / 40-testes.md. Use when the request involves adding a service to docker-compose, containerizing a new dependency, configuring Testcontainers at the compose level, or "docker-compose is missing the database" — also fires when persistence-architect or test-architect detect a service their spec needs isn't in docker-compose.yml yet.4---56## Current compose state78!`test -f docker-compose.yml && grep -E "^\s{2}\S+:$" docker-compose.yml || echo "(no docker-compose.yml at project root — run project-bootstrap first)"`910## Target1112$ARGUMENTS1314---1516# Docker Architect1718Keeps `docker-compose.yml` and `Dockerfile` in sync with what the project actually19needs, **after** they're born. `project-bootstrap` writes the base pair — one `app`20service, nothing else — at project creation. Every service added afterward (Postgres,21MySQL, a broker) goes through this skill, so there's one owner instead of22`persistence-architect` and `test-architect` each editing the same YAML their own way.2324**Entry rule: without `docker-compose.yml` at the project root, there's nothing to25extend.** If it's missing, stop and say to run `project-bootstrap` — this skill never26generates the base pair, only grows it.2728## How it's invoked2930Three paths, all real: `/docker-architect` by hand, when the user wants a service added31or checked; chained by `persistence-architect`, `messaging-architect`, or32`test-architect`, mid-procedure, when their spec names a dependency33`docker-compose.yml` doesn't have yet; and chained by `project-bootstrap` itself,34right after it writes the base pair in its own step 4.10, once per blueprint feature35that's already active and needs a container (`persistence-jpa` → Postgres,36`observability` → the OTLP collector) — see `project-bootstrap/SKILL.md` step 4.10.37That's why it carries no `disable-model-invocation` — a skill the model can't see is a38skill a sibling skill can't call.3940The guard against firing on an unborn project isn't the frontmatter: it's the entry rule41above.4243## Why this is a skill and not a subagent4445Form 1, motivated by the "Ambos" trigger axis: chained by sibling skills mid-procedure,46and invocable by hand. The closest rejected form was a subagent — it fails the § 547counter-test in `claude-code-architect-designer`'s decision matrix on all three points:48the service choice is short, its shape fits in `templates/`, and the diff it produces is49a few YAML lines. No context to isolate, no tool to restrict, no model change justified.50Full record: `@.claude/decisions/0029-docker-architect-skill.md`.5152## Boundary with neighboring pieces5354| Piece | Owns | Doesn't touch |55|---|---|---|56| `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 |57| **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-persistencia.md`'s call, this skill reads it |58| `persistence-architect` | Engine choice, schema, datasource properties (`20-persistencia.md`) | `docker-compose.yml` directly — invokes this skill instead |59| `messaging-architect` | Broker choice, topic, consumer group (`25-mensageria.md`) | `docker-compose.yml` directly — invokes this skill instead |60| `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 |6162## Procedure63641. **Confirm the base pair exists.** `docker-compose.yml` and `Dockerfile` at the65 project root. Missing either → stop, name what's missing, point to66 `project-bootstrap`.67682. **Find out what's needed.**69 - If `$ARGUMENTS` names a `UC-NNN-<slug>` folder: read `20-persistencia.md` for the70 engine and version, `25-mensageria.md` for the broker and topic, and `40-testes.md`71 for whether `testcontainers` is active and which image tag `test-architect` already72 pinned in `TestcontainersConfiguration.java`73 (`grep -rn "DockerImageName.parse" src/test`).74 - If called from `project-bootstrap` at generation time: the feature already decided75 it — `persistence-jpa` means Postgres (the engine `application.yml.example`'s76 `datasource.url` already assumes), `observability` means the OTLP collector. No77 engine question to ask; go straight to step 3.78 - If invoked manually with no folder: `AskUserQuestion` — engine (Postgres, MySQL,79 Kafka, other), version/tag, port, whether it needs an init script. Don't ask what a80 given spec already answers.81823. **Check what's already there.** `grep -A2 "^services:" docker-compose.yml` and the83 service names under it. A service already present gets left alone — this step never84 duplicates or silently overwrites a hand-edited block.85864. **Merge the service block.** From the matching `templates/<engine>-service.yml.example`,87 append under `services:` — indentation matched to the file's own, never reformatting88 what's already there. Use the **same image tag** `test-architect` pinned, when one89 exists, so the dev-time container and the integration-test container run identical90 software. If none is pinned yet, use the tag the engine's template ships with and say91 so in the report — `test-architect`'s setup mode is what should pick it up from here,92 not the other way around.93945. **Wire the app service's environment**, only the variables that change because of95 step 4 (`SPRING_DATASOURCE_URL`/`_USERNAME`/`_PASSWORD` pointing at the new service's96 hostname and port from compose's internal network; `OTLP_ENDPOINT` pointing at the97 collector's, e.g. `http://otel-collector:4318/v1/traces`, for the OTLP service).98 Don't invent datasource properties beyond connectivity — sizing and the rest are99 `@.claude/rules/persistence.md`'s and `persistence-architect`'s call, not this100 skill's.1011026. **Init script.** Write it under `docker/init/<service>/` and mount it read-only in103 the service's `volumes:`, from the matching `templates/<name>-config.<ext>.example`104 when one exists. For a database service this is conditional on step 2 finding one is105 needed, and skipped entirely when the schema comes from a Flyway migration instead —106 one source of schema truth, not two. For the OTel collector it isn't conditional:107 the collector has no built-in default pipeline and refuses to start without108 `templates/otel-collector-config.yml.example` mounted, so this step always runs for109 that service.1101117. **Report and stop.** Service added, image tag used (and whether it matches112 `test-architect`'s pin), files changed. Don't invoke anyone — a sibling skill that113 chained this one resumes on its own thread.114115## Service catalog116117| Engine | Template | When **not** |118|---|---|---|119| PostgreSQL | `templates/postgres-service.yml.example` | Engine chosen in `20-persistencia.md` isn't Postgres — or, at bootstrap time, when `persistence-jpa` isn't active in the blueprint |120| MySQL | `templates/mysql-service.yml.example` | Engine chosen in `20-persistencia.md` isn't MySQL |121| Kafka | `templates/kafka-service.yml.example` | Broker chosen in `25-mensageria.md` isn't Kafka, or there is none |122| 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 to make here — one vendor-neutral collector, always the same shape, unlike Postgres/MySQL/Kafka which branch on a real decision |123| H2 | — no service | In-memory, runs inside the JVM; nothing to containerize |124125Other engines and brokers (Oracle, RabbitMQ, SQS via LocalStack) follow the same shape as126the templates above: image, fixed dev port, named volume for data, healthcheck,127environment for user/password/database (or broker-equivalent). Write the block by hand128from that shape; don't wait for a template to exist before extending a project that needs129one today.130131## Contract132133**Reads** `docs/use-cases/UC-NNN-<slug>/20-persistencia.md`, `25-mensageria.md`, and134`40-testes.md` when a folder is given; the active blueprint's `features:` (`persistence-jpa`,135`observability`) when called from `project-bootstrap` at generation time; plus136`docker-compose.yml`, `Dockerfile`, `TestcontainersConfiguration.java` (image tag only),137`@.claude/rules/persistence.md`, and `@.claude/rules/messaging.md`.138139**Writes** `docker-compose.yml` (every service block, including the ones added at140generation time for an already-active feature), `Dockerfile` (build-stage additions141only, never the base image or base stages `project-bootstrap` wrote), and `docker/init/**`142when an init script is needed. No other skill writes a service block into143`docker-compose.yml` — not even `project-bootstrap`, which only decides *when* to call144this skill — this is the single owner.145146**Does not** choose the database engine (`persistence-architect`'s call via147`20-persistencia.md`) or the broker (`messaging-architect`'s call via148`25-mensageria.md`), pin the Testcontainers image tag inside Java (`test-architect`'s one149line in `TestcontainersConfiguration.java`), or write the base150`docker-compose.yml`/`Dockerfile` (`project-bootstrap`, once, at generation). Doesn't151write business code, doesn't touch `.claude/rules/**`.