# 02 — Inventário dos containers do `cms` (Concordia CMS)

Base: `cms/docker-compose.yml`, `cms/containers/**`, `cms/Makefile`, `cms/infra-audit.sh`,
`cms/.env.docker.example`. Nada foi alterado.

## 1. Topologia

`cms/docker-compose.yml` (433 linhas) — núcleo sempre ativo; o resto por profile.

| Serviço | Imagem / build | Publicação | Profile | Healthcheck |
|---|---|---|---|---|
| `app` | `maxima/cms:${CMS_TARGET:-dev}`, `target: ${CMS_TARGET:-dev}` (`:38-50`) | — | núcleo | herdado da imagem |
| `balancer` | `maxima/balancer:${INFRA_TARGET:-dev}` (`:80-89`) | `17010:8080` tcp+udp (`:91-92`) | núcleo | herdado da imagem |
| `database` | `maxima/database` (`:112-121`) | — | núcleo | `pg_isready` (`:134-139`) |
| `cache` | `maxima/cache` (`:142-150`) | — | núcleo | `valkey-cli ping` sem senha (`:158-162`) |
| `storage` | `maxima/storage` (MinIO AIStor) (`:169-178`) | `17011:9000`, `17012:9001` (`:179-181`) | `storage` | `curl /minio/health/live` (`:193-197`) |
| `storage-init` | mesma imagem, `user: 0:0` (`:199-217`) | — | `storage` | `service_healthy` |
| `telemetry` | `maxima/telemetry` (Vector) (`:226-234`) | `17017:8686` (`:248`) | `telemetry` | probe `/dev/tcp` (`:253-257`) |
| `loki` | `maxima/loki` (`:265-273`) | — | `telemetry` | — |
| `tempo` | `maxima/tempo` (`:284-292`) | `17020:4317` (`:296`) | `telemetry` | — |
| `observability` | `maxima/observability` (Grafana) (`:305-313`) | `17015:3000` (`:317`) | `telemetry` | — |
| `mailer` | `maxima/mailer` (Mailpit) (`:336-344`) | `17013:8025`, `17014:1025` (`:348-349`) | `debug` | — |
| `dozzle` | `${DOZZLE_IMAGE:-amir20/dozzle:latest}` (`:354-362`) | `17016:8080` | `debug` | — |
| `sonarqube` | `sonarqube:26.5.0.122743-community` (`:368-373`) | `17018:9000` | `sonar` | — |
| `sonar-db` | `postgres:17-alpine` (`:394-396`) | — | `sonar` | `pg_isready` (`:408-413`) |

- `name: ${COMPOSE_PROJECT_NAME:-concordia}` (`:6`).
- Âncoras: `x-rootless` (UID/GID, `:18-19`) e `x-logging` (`json-file` 10m×3, `:21-25`).
  `x-rootless` **não é aplicado ao `app`** (`:36-76`).
- Redes: `concordia-net` (bridge) + `telemetry-net` (`internal: true`) (`:415-419`).
- Sem `deploy`, `mem_limit`, `cpus`, `pids_limit`, `security_opt`, `cap_drop`, `read_only`,
  `sysctls`, `secrets` (grep = 0).
- `container_name` fixo em quase todos (`:49`, `:88`, …) e `pull_policy: missing` (`:48`).
- `app` monta `/var/run/docker.sock` (`:66`) e recebe `GITLAB_TOKEN`/`SONAR_TOKEN` no ambiente
  (`:60-61`).
- Portas publicadas em faixa reservada `17010-17020`, definidas em `.env.docker.example:34-42`.

## 2. Imagem `cms` (`containers/cms/Dockerfile`, 219 linhas)

Cinco estágios: `base` → `assets-builder` → `dev` / `stage` / `prod`.

| Estágio | Conteúdo | Linha |
|---|---|---|
| `base` | `php:8.4.21-fpm-trixie@sha256:8d76b552…` | 10 |
| `base` | apt: `bash tzdata locales passwd libfcgi-bin b3sum qpdf zstd libmagic1 libvips-tools ghostscript icc-profiles-free poppler-utils` | 28-30 |
| `base` | `groupmod/usermod` para UID/GID do host + `chown -R www-data /var/www` | 35-38 |
| `base` | `ADD .../install-php-extensions/releases/latest/download/...` (**sem versão/digest**) | 49 |
| `base` | extensões via `install-php-extensions` (inclui `opentelemetry`, `sodium`, `sockets`) | 53-68 |
| `base` | BLAKE3: `git clone` **sem tag/commit** | 76 |
| `base` | YARA `v4.5.0` compilado (tag pinada) | 93-110 |
| `base` | Python + `pip install --no-cache-dir docling` (**sem versão**) | 113-127 |
| `base` | `STOPSIGNAL SIGQUIT`; `HEALTHCHECK` via `cgi-fcgi` `/ping` | 133-137 |
| `assets-builder` | `npm run build` offline, copiando o contexto nomeado `app` | 142-152 |
| `dev` | Xdebug lazy, Composer, zsh, `sudo NOPASSWD`, agentes (Gemini/Copilot/Claude) | 157-186 |
| `stage` | código + assets no build, `USER www-data`, **usa `fpm.conf` de dev** | 191-201 |
| `prod` | código + assets, `fpm_prod.conf`, `USER www-data` | 206-219 |

O `php.ini` de `base` (`configs/php.ini`) é **sobrescrito** por `99_<target>.ini` em todos os
estágios finais (`:166`, `:192`, `:207`).

Pontos positivos: base e Composer pinados por digest; `STOPSIGNAL`; healthcheck que fala FCGI
(não apenas TCP); `install-php-extensions` reduz o ruído de compilação; limpeza de APT.

Pontos negativos: downloads sem pin (`latest`, `git clone`, `pip install`), toolchain Python/
Docling/YARA no runtime (imagem grande), `sudo NOPASSWD` e agentes de IA na imagem, entrypoint
como root com `chown -R` no bind mount (`configs/entrypoint.sh:11-31`), token de Git gravado no
`.git/config` (`:72-86`).

## 3. PHP/FPM do CMS

`configs/php.ini`:

- `display_errors=On`, `html_errors=On`, `error_reporting=E_ALL` (`:37-40`) — postura de dev no
  `base`, corrigida só por `99_prod.ini`.
- `user_ini.filename=".user.ini"` e `ttl=300` (`:21-22`) — o oposto do ADR-005 do V7.
- `memory_limit=512M`, `max_execution_time=600`, `max_input_time=600`,
  `post_max_size=200M` (`:30-34`).
- `allow_url_fopen=On`, `allow_url_include=Off` (`:15-16`).
- OPcache desligado no `base` (`:51`); `99_dev.ini` liga revalidando; `99_prod.ini` liga com
  `validate_timestamps=0`, `memory_consumption=512`, `max_accelerated_files=32531`,
  `jit=1255`, `jit_buffer_size=128M`, `preload=/var/www/html/storage/framework/preload.php`
  (`99_prod.ini:2-16`).

`configs/fpm.conf` (dev/stage):

- `pm=ondemand`, `max_children=20`, `process_idle_timeout=60s`, `max_requests=500` (`:11-14`).
- `pm.status_path=/status`, `ping.path=/ping`, `ping.response=pong` (`:16-18`).
- `request_slowlog_timeout=5s`, `request_terminate_timeout=600s` (`:28-30`).
- `rlimit_files=4096`, `clear_env=no`, `chdir=/var/www/html` (`:33-42`).

## 4. Borda (`containers/balancer`)

Nginx nativo Debian Trixie, rootless (`USER www-data`), TLS 1.3 + HTTP/3/QUIC em `:8080`.

- `worker_processes auto`, `worker_rlimit_nofile 16384`, `worker_connections 8192` (`nginx.conf:13-21`).
- `server_tokens off` (`:37`); `ssl_protocols TLSv1.3`, 0-RTT ligado (`:44-51`), com anti-replay
  `return 425` para métodos não-GET/HEAD (`site.conf.template:36-47`).
- `real_ip` para faixas privadas (`:78-83`).
- Maps por subdomínio sensível (`cms|super|parceiros`, `api|webhook`): `X-Robots-Tag`,
  `Cache-Control: no-store`, `X-Frame-Options`, CSP (`:88-132`).
- Access log JSON com `request_id`, `request_time`, `upstream_response_time`,
  `upstream_connect_time`, `upstream_status` (`:151-169`).
- Compressão gzip + brotli (`:174-218`).
- **Sem `limit_req`, `limit_conn` ou `proxy_cache`** (grep = 0): rate limit, tarpit e cache de
  página são feitos em Lua contra o Valkey (DBs 2/3) (`site.conf.template:61-121`).
- `/health` responde 200; `/(status|ping)` só de faixas privadas (`:126-150`).
- Headers globais: `Alt-Svc`, HSTS, `Permissions-Policy`, `X-Request-ID` (`headers.conf:7-14`).
- Bloqueio de `composer.json/lock`, `package*.json`, `phpunit.xml`, `vite.config.js` e dotfiles
  (`site.conf.template:235-243`).
- Healthcheck por `wget --no-check-certificate https://localhost:8080/health` (`Dockerfile:110-111`).

## 5. Dados

### `containers/database/configs/postgresql.conf`

`max_connections=100`, `tcp_keepalives_idle=300`, `work_mem=4MB`, `maintenance_work_mem=64MB`,
`dynamic_shared_memory_type=posix` (`:10-18`); `log_destination='stderr'`,
`logging_collector=off`, `log_statement='ddl'`, `log_checkpoints`, `log_connections`,
`log_disconnections`, `log_lock_waits`, `log_temp_files=0`, `log_autovacuum_min_duration=0`
(`:23-32`); `password_encryption=scram-sha-256` (`:51`); locale `pt_BR` (`:42-46`); `include
'env.conf'` (`:56`). **Sem `shared_buffers`/`effective_cache_size`/`io_method`** (fica no default).

### `containers/cache/configs/valkey.conf`

`log-format logfmt`, `hide-user-data-from-log yes` (`:15-20`), `io-threads 2`,
`activedefrag yes`, `disable-thp yes` (`:25-30`), `rename-command FLUSHALL/FLUSHDB/SHUTDOWN ""`
e `CONFIG` renomeado (`:36-39`), `lazyfree-*` (`:43-47`), `maxmemory 512mb` +
`volatile-lru` (`:52-53`), `commandlog-*` e `latency-monitor-threshold 100` (`:57-63`),
`oom-score-adj` (`:67-68`).
**Sem `requirepass` em nenhum arquivo** e o healthcheck do Compose não autentica
(`docker-compose.yml:158-162`); com senha configurada, o probe falharia.

## 6. Telemetria e observabilidade

- `telemetry/configs/vector.yaml`: fontes `opentelemetry` (gRPC/HTTP) e `docker_logs`; injeta
  `tenant_id`/`environment`; `mask_pii` mascara IP (v4/v6), `cpf`, `email`, `password`, `token`
  em raiz, `context` e `attributes` (`:31-77`).
- **Sinks apenas `console`** (`:80-96`): não há sink para Loki/Tempo nem buffer em disco.
- `loki.yaml`: `auth_enabled: false`, filesystem, `limits_config` sem `retention_period`/
  compactor (`:1-39`).
- `tempo.yaml`: storage local, sem política de retenção (`:16-22`).
- Grafana: datasources provisionados **somente no estágio `dev`** (`observability/Dockerfile:78-84`, `COPY` em `:82`);
  `GF_SECURITY_ADMIN_PASSWORD` default `admin` (`docker-compose.yml:320`).
- `infra-audit.sh`: audita HTTPS/HTTP3/HSTS do balancer, conectividade do Valkey, OPcache/sodium,
  timezone do PG e API do Vector. É a inspiração do `make doctor` (U-03).

## 7. O que aproveitar (com adaptação)

| Prática do CMS | Evidência | Item no V7 |
|---|---|---|
| Healthcheck FCGI `/ping` + `ping.path`/`status_path` | `Dockerfile:136-137`, `fpm.conf:16-18` | P-02, S-05 |
| `request_terminate_timeout` e limites de recurso do pool | `fpm.conf:28-34` | P-02 |
| Access log JSON de borda com `request_id` e tempos de upstream | `nginx.conf:151-169` | O-01, S-10 |
| GUCs de log do Postgres | `postgresql.conf:23-32` | P-05 |
| Tuning e privacidade do Valkey | `valkey.conf:15-63` | P-06 |
| Máscara de PII no pipeline de telemetria | `vector.yaml:31-77` | O-02 |
| Profiles Compose + faixa de portas documentada | `docker-compose.yml`, `.env.docker.example:30-42` | U-02 |
| Auditoria de infraestrutura automatizada | `infra-audit.sh` | U-03 |
| `STOPSIGNAL SIGQUIT` e labels de imagem | `Dockerfile:12-13,133` | S-07 |
| Bloqueio de arquivos sensíveis no edge | `site.conf.template:235-243` | S-06 |
| Rede de telemetria `internal: true` | `docker-compose.yml:415-419` | S-03 |

## 8. O que não se copia

Três alvos de build e config congelada por ambiente; `php.ini` de dev no `base`; `.user.ini`
habilitado; entrypoint root com `chown -R` no bind mount; `sudo NOPASSWD`; token de Git no
`.git/config`; `docker.sock` na aplicação; `latest`/downloads sem pin; JIT `1255`; HSTS no proxy
interno; `container_name`; senha padrão do Grafana; sinks de console no Vector; `dozzle:latest`.
Motivos e evidências em `05-nao-adotar.md`.
