# 01 — Inventário do runtime Docker do `public-v7`

Base: `docker-compose.yml`, `docker/`, `up_prod.sh`, `.gitlab-ci.yml`, `config/logging.php`,
`config/environments.php`, `config/observability.php`, `Makefile` na raiz do `public-v7`.
Nenhuma linha foi alterada.

## 1. Topologia do Compose

Arquivo: `docker-compose.yml` (183 linhas, 8 serviços, 2 sob profile).

| Serviço | Imagem | Porta no host | Usuário | Healthcheck | Depende de |
|---|---|---|---|---|---|
| `web` | `caddy:2.11.7@sha256:685ae3d7…` (`:15`) | `127.0.0.1:8080` (`:28`) | `${UID}:${GID}` (`:17`) | **nenhum** | `php-fpm` healthy (`:30-31`) |
| `php-fpm` | build local, `target: runtime` (`:35-39`) | — | `${UID}:${GID}` (`:43`) | TCP `fsockopen` 9000 (`:56-62`) | PG, Valkey, storage healthy (`:49-55`) |
| `queue` | **mesmo** build (`:64-68`) | — | `${UID}:${GID}` (`:72`) | **nenhum** | PG, Valkey, storage healthy (`:78-84`) |
| `postgres` | `postgres:18-trixie@sha256:74935e72…` (`:88`) | — | root → `postgres` (sem `user:`) | `pg_isready` (`:101-107`) | — |
| `valkey` | `valkey/valkey:9-alpine@sha256:48332870…` (`:109`) | — | padrão da imagem | `valkey-cli -a … ping` (`:117-122`) | — |
| `storage` | `rustfs/rustfs:1.0.1-glibc@sha256:87763ed4…` (`:126`) | — | `${UID}:${GID}` (`:130`) | `curl /health` (`:148-153`) | — |
| `mailpit` | `axllent/mailpit:v1.31.6@sha256:23fe39e8…` (`:156`) | `127.0.0.1:8025` (`:160`) | padrão | **nenhum** | — |
| `jaeger` | `jaegertracing/jaeger:2.11.0@sha256:b585df1b…` (`:165`) | `16686`, `4318` (`:170-171`) | padrão | **nenhum** | — |

- `name: ${COMPOSE_PROJECT_NAME:-concordia}` (`:6`).
- Logging comum: `json-file`, `max-size: 10m`, `max-file: 3` (`:9-13`).
- Rede única `internal` tipo bridge (`:176-177`); nenhum serviço em rede `internal: true`.
- Volumes nomeados: `caddydata`, `caddyconfig`, `valkeydata` (`:180-183`).
- Dados persistentes em bind mount: `${DATA_PATH}/pgdata` (`:99`) e `${DATA_PATH}/rustfs/data`
  (`:141`); storage da app em `${STORAGE_PATH}` (`:45`, `:74`).
- `mailpit` e `jaeger` só sob `profiles: ["debug"]` / `["telemetry"]` (`:155`, `:164`).

### Observações factuais

1. `php-fpm` e `queue` **declaram o mesmo build sem `image:`** (`:35-42`, `:64-71`). Cada serviço
   recebe uma tag própria; `up_prod.sh:336` e `.gitlab-ci.yml:32` constroem os dois.
   A decisão registrada no knudge (`decision_00a1jcss`) diz que o `queue` **reusa a imagem** do
   `php-fpm`; o Compose atual não expressa isso.
2. O fallback do nome do projeto é `concordia` (`:6`) — o mesmo fallback do `cms`
   (`cms/docker-compose.yml:6`). Sem `COMPOSE_PROJECT_NAME` no ambiente, os dois repositórios
   resolvem para o mesmo projeto Compose; `down --remove-orphans` de um remove containers do outro.
3. O PostgreSQL roda como root no container e o entrypoint oficial faz o drop para `postgres`;
   é o único serviço de dados sem `user:`.

## 2. Imagem PHP (`docker/php-fpm/Dockerfile`, 58 linhas)

| Item | Valor | Linha |
|---|---|---|
| Base | `php:8.4.27-fpm-trixie@sha256:e2d8e787…` | 12 |
| Composer | `composer:2@sha256:af98f42d…`, copiado só o binário | 11, 27, 50 |
| APT | `curl ca-certificates libicu-dev libpq-dev libzip-dev libonig-dev libmagickwand-dev libvips-tools libheif-plugin-aomenc libheif-plugin-aomdec qpdf` | 22-24 |
| Extensões compiladas | `intl pdo_pgsql pgsql zip bcmath exif` | 25 |
| PECL | `apcu redis imagick pcov` — **sem versão** | 28 |
| BLAKE3 | tarball do GitHub com commit pinado (`BLAKE3_REF`), **sem checksum** | 12, 38-44 |
| Toolchain | instala `$PHPIZE_DEPS` e **não purga** | 37-47 |
| Usuário | **sem `USER`**; non-root só via Compose `user:` | — |
| Sinais | `ENTRYPOINT` + `CMD ["php-fpm","--nodaemonize"]`; `STOPSIGNAL` herdado da base (verificar) | 59-60 |
| Config | `PHPRC` e `PHP_INI_SCAN_DIR` fixos em `/usr/local/etc/php*` | 51-52 |
| Cópia no build | apenas `docker/php-fpm/entrypoint.sh` | 50 |

- Contexto de build: `context: .` (`docker-compose.yml:36`), ou seja, **a raiz do repositório**.
  O `.dockerignore` exclui VCS, `.env`, `node_modules`, `vendor`, `public/build`, `wiki`, docs,
  `s3-local`, `build`, `.phpunit.cache` — mas **não exclui** `app/`, `src/`, `config/`, `database/`,
  `resources/`, `routes/`, `tests/`, `public/`, `tools/`, `stubs/`. Como o Dockerfile só copia
  `entrypoint.sh`, todo o resto é contexto enviado ao builder sem uso.
- Memória do projeto (`no fact_0106hlw9`) afirma contexto mínimo com “só docker/php-fpm”; o
  arquivo atual não corresponde.
- Nenhum `--mount=type=cache`, nenhum `SOURCE_DATE_EPOCH`, nenhuma label OCI.

## 3. Configuração PHP/FPM

### `docker/php-fpm/php.ini` (montado como `zz-app.ini`)

- `expose_php=Off`, `display_errors=Off`, `log_errors=On`, `error_log=/proc/self/fd/2` (`:4-8`).
- `user_ini.filename=` vazio (`:11-12`) — ADR-005.
- OPcache: `enable=1`, `enable_cli=0`, `memory_consumption=192`, `interned_strings_buffer=16`,
  `max_accelerated_files=20000`, `save_comments=1` (`:15-22`).
- `validate_timestamps=0` por padrão (`:25`); o entrypoint sobrescreve por `APP_ENV`.
- JIT **off** com medição registrada: `off 5,8 ms · function 7,5 ms · tracing 13,7 ms` (`:27-31`).
- `realpath_cache_size=4096K`, `ttl=600` (`:35-36`).
- `memory_limit=256M`, `max_execution_time=30`, `max_input_time=30`,
  `upload_max_filesize=110M`, `post_max_size=114M`, `max_file_uploads=20` (`:39-44`).
- Sessão: `use_strict_mode=1`, `cookie_httponly=1`, `cookie_secure=1`, `samesite=Lax` (`:47-50`).
- `pcov.enabled=0`; PCOV só no alvo de cobertura (`:63-65`).

### `docker/php-fpm/zz-custom.conf` (pool `www`)

- `pm = dynamic`, `max_children=10`, `start_servers=3`, `min_spare=2`, `max_spare=5`,
  `max_requests=500` (`:12-17`).
- Logs em stderr (`catch_workers_output`, `access.log`, `slowlog`) (`:20-27`).
- `request_slowlog_timeout=5s` (`:26`), **sem `request_terminate_timeout`**.
- **Sem** `ping.path`, `pm.status_path`, `listen.backlog`, `emergency_restart_*`,
  `process_control_timeout`, `security.limit_extensions`, `rlimit_*`.
- `php_admin_value[upload_tmp_dir]=/tmp`, `sys_temp_dir=/tmp` (`:36-37`).

### `docker/php-fpm/entrypoint.sh` (95 linhas)

- `set -eu` (`:10`), sem `pipefail` (POSIX sh não tem).
- Deriva o perfil de `APP_ENV` (ou do `.env`) e gera
  `${DOCROOT}/storage/framework/php-env/zzz-env.ini` com OPcache/assertions/display/cookies
  (`:15-81`); anexa o diretório ao `PHP_INI_SCAN_DIR` (`:77`).
- Avisa (não falha) se `user_ini.filename` não estiver vazio e se o docroot tiver
  `.user.ini`/`php.ini`/`.htaccess`/`error_log` (`:83-96`).
- `exec "$@"` (`:98`).

## 4. Edge (`docker/web/Caddyfile`, 83 linhas)

- `auto_https off` e `admin off` (`:5-6`); `trusted_proxies static private_ranges` +
  `trusted_proxies_strict` (`:8-9`).
- Timeouts anti-slowloris: `read_body 10s`, `read_header 5s`, `write 30s`, `idle 5m` (`:10-16`).
- `encode zstd gzip` (`:23`); `request_body max_size 110MiB` (`:26-28`).
- `@forbidden path *.ini *.log error_log .*` → `404` (`:32-33`).
- `php_fastcgi php-fpm:9000` (`:35`); `file_server` com `hide` e `precompressed br gzip`
  (`:37-40`).
- Headers: `-Server`, `-X-Powered-By`, `nosniff`, `Referrer-Policy`, `Permissions-Policy`,
  `X-Frame-Options SAMEORIGIN`, `X-XSS-Protection 0`, `COOP same-origin` (`:42-52`).
- CSP por área (`@cms`, `@portal`, `@embed`, `@api`) (`:59-77`); `CORP` same-origin no app e
  cross-origin na API (`:66-72`).
- Assets Vite imutáveis (`:81-82`).
- **Não há bloco `log`** → o Caddy não emite access log (padrão do servidor é desligado).
- **Não há healthcheck** no serviço `web` nem rota `/health` no Caddyfile.

## 5. Dados

### `docker/postgres/postgresql.conf`

`listen_addresses='*'`, `io_method=worker`, `shared_buffers=256MB`, `effective_cache_size=768MB`,
`work_mem=8MB`, `maintenance_work_mem=64MB`, `random_page_cost=1.1`,
`effective_io_concurrency=200`, `jit=on`, `track_io_timing=on`,
`shared_preload_libraries='pg_stat_statements'`, `pg_stat_statements.max=10000`,
`log_min_duration_statement=500ms`, `log_line_prefix='%m [%p] %q%u@%d '` (`:4-29`).
PITR: `wal_level=replica`, `archive_mode=on`, `archive_timeout=300`,
`archive_command='mkdir -p …/wal_archive && test ! -f … && cp %p …'` (`:37-40`).
**Não há `archive_cleanup_command`, retenção do WAL nem envio para S3** — o diretório
`PGDATA/wal_archive` cresce indefinidamente. Não há `shm_size` no Compose (default 64 MB).

### `docker/valkey/valkey.conf`

`bind 0.0.0.0`, `protected-mode yes`, `appendonly yes`, `appendfsync everysec`,
`save 900 1`, `save 300 10`, `maxmemory 256mb`, `maxmemory-samples 5`,
`maxmemory-policy volatile-lru` (`:4-21`). Senha por `--requirepass` no `command:` do Compose
(`docker-compose.yml:111`), visível em `docker inspect`/`ps`.
Sem `io-threads`, `lazyfree-*`, `latency-monitor-threshold`, `hide-user-data-from-log`,
`log-format`.

### `storage` (RustFS)

- Imagem `rustfs/rustfs:1.0.1-glibc@sha256:8776…` (`docker-compose.yml:126`), **single-node,
  single-drive** (`/data`, `:133,146`) → `EC:0` implícito e sem redundância.
- `RUSTFS_CONSOLE_ENABLE=false`, sem porta publicada; S3 path-style usado pelo Laravel
  (`config/filesystems.php:52-64`).
- Healthcheck em `/health` (`:148-153`) — é **liveness**, não readiness; o app usa
  `condition: service_healthy` (`:49-55,78-84`) e pode subir antes de o S3 aceitar escrita.
- Logs: `RUSTFS_OBS_LOGGER_LEVEL=error`, `/logs` em `tmpfs` (`:131-132,143`); sem stdout nem
  OTLP. Na prática, o log do storage não é retido.
- Credenciais root = `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY` do app (`:140-141`); sem
  usuário de workload. O bucket é criado por `storage:ensure-bucket`, que **só cria**
  (`EnsureStorageBucketCommand.php:20-54`): sem versioning, lifecycle, PAB ou limite de versões.
- Sem `mem_limit`, `pids_limit`, `security_opt`, `cap_drop` ou `stop_grace_period`
  (`docker-compose.yml:125-153`).
- `up_prod.sh` faz backup **só do banco** (`:42-47,260-297`); não há cópia dos objetos de mídia.
  O disco `s3` é o próprio RustFS, portanto `BACKUP_DISK=s3` não protege contra perda do host
  (`up_prod.sh:173-180`).

## 6. Logs e telemetria

- Docker: `json-file` 10 MB × 3 por serviço (`docker-compose.yml:9-13`).
- Laravel: `LOG_STACK` default `json,daily` (`config/logging.php:63`); `json` em
  `php://stderr` (`:68-76`); `daily` com `days=730` e `permission=0640` (`:86-96`).
- Caddy: **sem access log**; nenhum registro de status/latência/`upstream_response_time`.
- `TELEMETRY_ENABLED` é `true` no perfil de produção (`config/environments.php:38`) e o endpoint
  default é `http://jaeger:4318` (`config/observability.php:34`). O `jaeger` só existe sob
  `profiles: ["telemetry"]` (`docker-compose.yml:164`) e o `up_prod.sh` **não ativa esse profile**
  → em produção o exportador falha e o circuit breaker (`OTEL_CIRCUIT_BREAKER_SECONDS=30`) descarta
  os spans.
- Não há coletor de logs (`Vector`/`Promtail`/`Fluent Bit`) nem retenção centralizada.

## 7. Deploy e CI

- `up_prod.sh`: `set -Eeuo pipefail`, `umask 077`, `.env` 600, `flock` (`:185-186`), rollback por
  trap, `pg_dump` antes de migrar, `wait_healthy` para PG/Valkey/storage (`:352-354`), build
  `php-fpm queue` (`:336`), `up -d --remove-orphans` (`:346`), `composer install` dentro do
  container (`:364`), `artisan optimize` (`:392-393`), reload FPM por `USR2` e restart do `web`
  (`:397-410`), health `GET /up` (`:157`).
- `up_prod.sh:151` adiciona `docker-compose.prod.yml` se existir; o arquivo não existe no repo.
- CI: `.gitlab-ci.yml:32` sobe `--build php-fpm queue postgres valkey storage web`; `:33` roda
  `composer install`. Não há buildx cache, scan de imagem, SBOM ou validação do Compose.
- `Makefile`: `COMPOSE` com `-p $(tools/project-name.sh)` (`:8`); `build` (`:45-46`); `logs S=`
  (`:42-43`); `shell`/`exec` (`:48-52`); `reload` por `USR2` (`:56-57`).

## 8. Lacunas priorizadas (rastreiam o catálogo)

| Lacuna | Evidência | Item |
|---|---|---|
| Sem access log no edge | `docker/web/Caddyfile` (sem `log`) | O-01 |
| Spans descartados em produção | `config/observability.php:34` + `docker-compose.yml:164` | O-03 |
| Sem `cap_drop`/`no-new-privileges` | `docker-compose.yml` (grep = 0) | S-01 |
| Sem rootfs read-only | idem | S-02 |
| Rede única, sem `internal: true` nos dados | `docker-compose.yml:176-177` | S-03 |
| FPM saudável por TCP, não por resposta | `docker-compose.yml:56-62` | S-05 |
| Healthcheck ausente em `web`/`queue`/`mailpit`/`jaeger` | `docker-compose.yml` | R-03 |
| Sem limites de memória/CPU/PIDs | `docker-compose.yml` | P-08 |
| Build duplicado de `php-fpm`/`queue` | `docker-compose.yml:35-42,64-71` | P-01 |
| Contexto de build com o repo inteiro | `docker-compose.yml:36` + `.dockerignore` | P-03 |
| PECL sem versão; BLAKE3 sem checksum | `docker/php-fpm/Dockerfile:26,38-46` | S-09 |
| WAL arquivado sem retenção | `docker/postgres/postgresql.conf:37-40` | R-05 |
| `.env.example` incompleto | `.env.example` (42 linhas) | U-01 |
| Retenção local de 730 dias sem teto | `config/logging.php:91` | O-04 |
| Sem `init`/`stop_grace_period` | `docker-compose.yml` | R-01/R-02 |
| RustFS: healthcheck de liveness, não readiness | `docker-compose.yml:148-153` | RS-02 |
| RustFS: logs `error` + `/logs` tmpfs, sem stdout | `docker-compose.yml:131-132,143` | RS-03 |
| RustFS 1.0.1 (skill 1.0.2) | `docker-compose.yml:126` | RS-01 |
| RustFS: sem backup dos objetos de mídia | `up_prod.sh:260-297` | RS-06 |
| RustFS: root compartilhado com o app | `docker-compose.yml:140-141` | RS-07 |
| RustFS: bucket sem versioning/lifecycle/PAB | `EnsureStorageBucketCommand.php:20-54` | RS-08 |
| Sem SBOM/scan de imagem | `.gitlab-ci.yml` | S-08 |
| Portas de profile fixas (8025/16686/4318) | `docker-compose.yml:160,170-171` | U-02 |
| `docker-compose.prod.yml` referenciado e inexistente | `up_prod.sh:151` | U-06 |
| Fallback de projeto igual ao do CMS (`concordia`) | `docker-compose.yml:6` | U-07 |
