# Plano 04 — Runtime em containers no layout padrão do WHM (web · FPM · Valkey · PG 18)

> **Milestone:** M1 · **Depende de:** 03 · **Habilita:** 05, 06, 15
> **Objetivo:** subir o Public V7 **estritamente em containers**, **mantendo o layout padrão**
> (`git clone --depth=1` em `/home/<tenant>/public_html`) e fazendo o **container ignorar** os
> arquivos que o WHM/cPanel cria no docroot. O **Apache 2.4 do WHM é um proxy burro**. Um comando
> (`up_prod.sh`) sobe/atualiza com todas as otimizações. Ver **ADR-005/006/007** e
> **spec 16** (`edge-apache-whm`) + **spec 17** (`up-prod`).

---

## 1. Topologia (ADR-005)

```
Produção (host WHM/cPanel — layout padrão)
[Internet] → Apache 2.4 (TLS/AutoSSL/domínios) ──ProxyPass──▶ web (Caddy) ──fastcgi──▶ php-fpm
                 │ (só encaminha)                                  ├─ postgres:18     (rede interna)
                 └── public_html/ (repo clonado)                    ├─ valkey:9        (rede interna)
                     .user.ini/php.ini/.htaccess/error_log          ├─ storage:rustfs  (S3 local)  ── ADR-030
                     = IGNORADOS por configuração                   └─ (profiles) mailpit/jaeger

Dev (máquina local)
[host] → web (Caddy, mesmo Caddyfile) ──fastcgi──▶ php-fpm → postgres/valkey/storage
```

- **Layout padrão preservado:** repo em `/home/<tenant>/public_html`; bind mount `.:/var/www/html:Z`.
- **Conexão mínima com o WHM:** certificados/**AutoSSL**, **domínios**, **Apache 2.4** (proxy).
- **`web` (Caddy 2)** é o único servidor HTTP do app (dev e prod), non-root; dono de
  headers/CSP/compressão/ETag (ADR-020/ADR-022).
- **`postgres`/`valkey`** só na rede interna, **sem porta publicada**.
- **`storage` (RustFS)** é o **object storage S3-like local** (mídia/anexos), interno e **sem porta
  publicada**; troca pelo **AWS S3** do cliente só mudando `.env` (ADR-030).
- **`queue`** (worker) usa a **mesma imagem** do `php-fpm`, rodando `php artisan queue:work`; sem
  Supervisor/Horizon (Docker já reinicia; ADR-001/ADR-002).
- **Profiles dev-only:** `debug` (mailer) e `telemetry` (OTel) — nunca em produção (ADR-024).
- **Um comando de produção:** `./up_prod.sh` (seção 7 / spec 17).

## 2. Arquivos do cPanel: ignorados por configuração

O container **não apaga** nada (o painel recria); ele **ignora**:

| Arquivo do cPanel | Como é ignorado | Onde |
|---|---|---|
| `.user.ini` | `user_ini.filename=` (vazio) + `php_admin_value[user_ini.filename]=` | `zz-custom.conf` (FPM) |
| `php.ini` (docroot) | `PHPRC=/usr/local/etc/php` + `PHP_INI_SCAN_DIR=/usr/local/etc/php/conf.d` | `Dockerfile`/`php.ini` |
| `.htaccess` | Caddy **nunca** lê `.htaccess`; Apache não mapeia o app (`ProxyPass`) + `AllowOverride None` | `Caddyfile` + `app.conf` |
| `error_log` | `php_admin_value[error_log]=/proc/self/fd/2`; Caddy **nega** `error_log`/`*.ini`/dotfiles | `zz-custom.conf` + `Caddyfile` |

```ini
; docker/php-fpm/zz-custom.conf
[www]
clear_env = yes
user_ini.filename =
php_admin_value[user_ini.filename] =
php_admin_value[open_basedir]      = /var/www/html:/tmp
php_admin_value[doc_root]          = /var/www/html/public
php_admin_value[session.save_path] = /var/www/html/storage/framework/sessions
php_admin_value[upload_tmp_dir]    = /tmp
php_admin_value[error_log]         = /proc/self/fd/2
```

```caddyfile
# docker/web/Caddyfile — dono do HTTP do app; ignora dotfiles/*.ini/error_log
{
    auto_https off              # TLS/AutoSSL ficam no Apache do WHM
    admin off                   # sem Admin API (:2019) em produção
    servers {
        trusted_proxies static private_ranges   # IP real vindo do Apache
        timeouts {
            read_body   10s
            read_header 5s
            write       30s
            idle        5m
        }
    }
}
:8080 {                         # porta alta → roda non-root
    root * /var/www/html/public
    encode zstd gzip
    @forbidden path *.ini *.log error_log .*
    respond @forbidden 404
    php_fastcgi php-fpm:9000    # já inclui try_files + file_server
    file_server {
        hide .user.ini .htaccess error_log
        precompressed br zstd gzip   # serve .br/.zst/.gz do Vite (ADR-008)
    }
}
```

## 3. Componentes e imagens

| Serviço | Imagem | Porta publicada | Observação |
|---|---|---|---|
| `web` | `caddy:2.11.7@sha256:685ae3d7…` (non-root, escuta `:8080`; Debian, sem alpine) | `127.0.0.1:${WEB_PORT}` | serve `public/` + `php_fastcgi`; dev e prod; TLS/AutoSSL no Apache |
| `php-fpm` | `php:8.4.27-fpm-trixie@sha256:e2d8e787…` (oficial; extensões + `blake3`) | **nenhuma** | non-root via Compose; Dockerfile fino |
| `queue` | **mesma imagem** do `php-fpm` | **nenhuma** | worker `queue:work` (Valkey) — sem Horizon/Supervisor |
| `postgres` | `postgres:18-trixie@sha256:74935e72…` | **nenhuma** | tuning em `docker/postgres/postgresql.conf` |
| `valkey` | `valkey/valkey:9-alpine@sha256:48332870…` | **nenhuma** | AOF + `volatile-lru` + `requirepass` (renderizado no conf de runtime pelo entrypoint — a senha **não** fica no argv) |
| `storage` | `rustfs/rustfs:1.0.1-glibc@sha256:87763ed4…` | **nenhuma** | S3 local (ADR-030) |
| `mailpit` | `axllent/mailpit:v1.31.6@sha256:23fe39e8…` | `127.0.0.1:8025` | perfil **`debug`** (dev-only) |
| `jaeger` | `jaegertracing/jaeger:2.11.0@sha256:b585df1b…` (perfil `telemetry`) | `127.0.0.1:16686` (UI) / `4318` (OTLP) | Jaeger v2 = receptor **OTLP nativo**; dispensa `otel-collector` |
| build de assets | `node:26-alpine@sha256:6f08a523…` (dev: `node:26-alpine@sha256:6f08a523…`) | — | `npm ci && npm run build` (Vite 8, nunca em runtime) |

- A matriz **canônica** de imagens/digests e os **pontos em aberto** estão no **ADR-031**.
- `php-fpm`/`queue` = **imagem oficial de PHP** (`php:8.4-fpm-trixie`); o
  Dockerfile é **fino** (só extensões/PECL + Composer); config é montada. Matriz/digests: ADR-031.
- Extensões (estágio `-dev`, copiadas ao runtime): `intl`, `pdo_pgsql`, `pgsql`, `zip`, `imagick`,
  `apcu`, **`redis`** (phpredis, driver do Valkey — evita o pacote Predis), **`pcntl`**+`posix`
  (worker com sinais/`queue:restart`), **`blake3`** (integridade/auditoria — extensão nativa, pinada — ADR-029),
  `sodium`, `bcmath`, `exif`, `opcache`; `opentelemetry` só no perfil `telemetry`.
- **Descartados por simplicidade** (justificar no ADR se reconsiderar): **Octane/FrankenPHP**
  (Caddy+FPM+OPcache+`optimize` já cobre; Octane exigiria `scoped`/cuidado com memória) e
  **Horizon/Pulse/Telescope** (dashboard/BD extras; observabilidade é JSON+OTel, ADR-024).

## 4. Storage (local + object storage S3)

- **Laravel `storage/` (framework):** padrão `public_html/storage` → `/var/www/html/storage`
  (zero atrito); `STORAGE_PATH=/srv/<tenant>/storage` (disco maior, **fora da cota**) é bind-montado
  por cima. Guarda logs, sessões, cache, framework e tmp de upload.
- **Mídia/anexos (domínio):** object storage **S3** pelo disco **`s3`** do Laravel — backend local
  **RustFS** (container `storage`, ADR-030), dados em `${DATA_PATH:-./s3-local}/rustfs/data`.
  Troca pelo **AWS S3 do cliente** só mudando `.env`.
- **Dados do Postgres:** `${DATA_PATH:-./s3-local}/pgdata` (fora da cota; evita estourar a
  conta e o backup do painel).

## 5. Estrutura de infra

```
docker/
├── web/
│   ├── Caddyfile               # root public/, php_fastcgi, headers, encode, hide cPanel files
│   └── app.conf                # include do Apache do WHM (spec 16)
├── php-fpm/
│   ├── Dockerfile              # oficial php:8.4-fpm-trixie (non-root via Compose)
│   ├── php.ini                 # hardening + OPcache (ADR-008/ADR-020) + PHPRC/scan dir
│   ├── zz-custom.conf          # user_ini.filename=, php_admin_value, slowlog
│   └── entrypoint.sh           # UID/GID, permissões, avisa se .user.ini não está ignorado
├── postgres/
│   ├── postgresql.conf         # tuning PG 18 (io_method=worker, shared_buffers…)
│   └── init/                   # 00-extensions.sql, 01-test-db.sql
├── valkey/
│   ├── valkey.conf             # AOF, volatile-lru, logfmt, PII mascarada
│   └── entrypoint.sh           # renderiza o requirepass no conf de runtime (fora do argv)
├── telemetry/
│   └── jaeger.yaml             # Jaeger v2 (receptor OTLP nativo; dev-only)
├── docker-compose.yml          # name: ${COMPOSE_PROJECT_NAME}; rede interna; healthchecks
└── docker-compose.debug.yml    # overlays opcionais dos profiles
up_prod.sh                      # sobe/atualiza produção com otimizações (raiz)
Makefile                        # interface de dev
```

## 6. Compose (recorte)

```yaml
name: ${COMPOSE_PROJECT_NAME:-publicv7}
services:
  web:
    image: caddy:2.11.7@sha256:685ae3d7c34d9b5f8caeddc5d14051db7bbd88ff1f5a2fa0c16bd8ca328fb7af   # oficial Debian
    restart: unless-stopped
    user: "1000:1000"                     # non-root (porta 8080, sem bind privilegiado)
    environment:
      XDG_DATA_HOME: /data
      XDG_CONFIG_HOME: /config
    volumes:
      - .:/var/www/html:Z
      - ./docker/web/Caddyfile:/etc/caddy/Caddyfile:ro
      - caddydata:/data
      - caddyconfig:/config
    ports: [ "127.0.0.1:${WEB_PORT:-8080}:8080" ]
    depends_on: [ php-fpm ]
    networks: [ internal ]

  php-fpm:
    build: { context: ., dockerfile: docker/php-fpm/Dockerfile, target: runtime }
    restart: unless-stopped
    working_dir: /var/www/html
    user: "${UID:-1001}:${GID:-1001}"
    volumes:
      - .:/var/www/html:Z
      - ${STORAGE_PATH:-./storage}:/var/www/html/storage:Z
      - ./docker/php-fpm/php.ini:/usr/local/etc/php/conf.d/zz-app.ini:ro
      - ./docker/php-fpm/zz-custom.conf:/usr/local/etc/php-fpm.d/zz-custom.conf:ro
    environment:
      APP_ENV: ${APP_ENV:-local}
      DB_HOST: postgres
      REDIS_HOST: valkey
      MAIL_HOST: mailpit
      OTEL_EXPORTER_OTLP_ENDPOINT: http://jaeger:4318
      AWS_ENDPOINT: http://storage:9000
      AWS_BUCKET: ${AWS_BUCKET:-public-v7}
      AWS_USE_PATH_STYLE_ENDPOINT: "true"
      AWS_ACCESS_KEY_ID: ${S3_ACCESS_KEY:?defina S3_ACCESS_KEY}
      AWS_SECRET_ACCESS_KEY: ${S3_SECRET_KEY:?defina S3_SECRET_KEY}
      AWS_DEFAULT_REGION: ${AWS_DEFAULT_REGION:-us-east-1}
    depends_on:
      postgres: { condition: service_healthy }
      valkey:   { condition: service_healthy }
      storage:  { condition: service_healthy }
    networks: [ internal ]

  queue:                            # worker nativo (mesma imagem do php-fpm)
    build: { context: ., dockerfile: docker/php-fpm/Dockerfile, target: runtime }
    restart: unless-stopped
    working_dir: /var/www/html
    user: "${UID:-1001}:${GID:-1001}"
    command: [ "php", "artisan", "queue:work", "--sleep=1", "--tries=3", "--max-time=3600", "--max-jobs=1000" ]
    volumes:
      - .:/var/www/html:Z
      - ${STORAGE_PATH:-./storage}:/var/www/html/storage:Z
      - ./docker/php-fpm/php.ini:/usr/local/etc/php/conf.d/zz-app.ini:ro
      - ./docker/php-fpm/zz-custom.conf:/usr/local/etc/php-fpm.d/zz-custom.conf:ro
    environment:
      APP_ENV: ${APP_ENV:-local}
      DB_HOST: postgres
      REDIS_HOST: valkey
      AWS_ENDPOINT: http://storage:9000
      AWS_BUCKET: ${AWS_BUCKET:-public-v7}
      AWS_USE_PATH_STYLE_ENDPOINT: "true"
      AWS_ACCESS_KEY_ID: ${S3_ACCESS_KEY:?defina S3_ACCESS_KEY}
      AWS_SECRET_ACCESS_KEY: ${S3_SECRET_KEY:?defina S3_SECRET_KEY}
    depends_on:
      postgres: { condition: service_healthy }
      valkey:   { condition: service_healthy }
      storage:  { condition: service_healthy }
    networks: [ internal ]

  storage:                          # object storage S3-like local (ADR-030)
    image: rustfs/rustfs:1.0.1-glibc@sha256:87763ed4dcb2bed3fc52cbf6cb7d865411e085b0b952dc78e50dffa9ece0ed9e
    restart: unless-stopped
    command: [ "/data" ]
    environment:
      RUSTFS_VOLUMES: "/data"
      RUSTFS_ADDRESS: ":9000"
      RUSTFS_CONSOLE_ENABLE: "false"   # console só no perfil debug
      RUSTFS_REGION: ${AWS_DEFAULT_REGION:-us-east-1}
      RUSTFS_ACCESS_KEY: ${S3_ACCESS_KEY:?defina S3_ACCESS_KEY}
      RUSTFS_SECRET_KEY: ${S3_SECRET_KEY:?defina S3_SECRET_KEY}
      RUSTFS_HEALTH_ENDPOINT_ENABLE: "true"
      RUSTFS_OBS_LOGGER_LEVEL: "error"
      RUSTFS_CHECK_UPDATE: "false"
    volumes:
      - ${DATA_PATH:-./s3-local}/rustfs/data:/data
    healthcheck:
      test: [ "CMD", "curl", "-fsS", "http://127.0.0.1:9000/health" ]
      interval: 10s
      timeout: 3s
      retries: 10
    # sem `ports:` (rede internal)
    networks: [ internal ]

  postgres:
    image: postgres:18-trixie@sha256:74935e72241653ca55e0414067e6d8763aceb8a810eb51b452253ec3dcfc4336
    restart: unless-stopped
    environment: { POSTGRES_DB: ${DB_DATABASE}, POSTGRES_USER: ${DB_USERNAME}, POSTGRES_PASSWORD: ${DB_PASSWORD} }
    command: [ "postgres", "-c", "config_file=/etc/postgresql/postgresql.conf" ]
    volumes:
      - ${DATA_PATH:-./s3-local}/pgdata:/var/lib/postgresql/data:Z
      - ./docker/postgres/postgresql.conf:/etc/postgresql/postgresql.conf:ro
      - ./docker/postgres/init:/docker-entrypoint-initdb.d:ro
    healthcheck: { test: [ "CMD-SHELL", "pg_isready -U $$POSTGRES_USER" ], interval: 10s, retries: 5 }
    networks: [ internal ]          # ⚠ sem `ports:`

  valkey:
    image: valkey/valkey:9-alpine@sha256:48332870af354a799964c0012ae1194a0bf2bf894eb508f945810596dc2d8d11
    restart: unless-stopped
    command: [ "valkey-server", "/etc/valkey/valkey.conf" ]
    volumes:
      - valkeydata:/data
      - ./docker/valkey/valkey.conf:/etc/valkey/valkey.conf:ro
    healthcheck: { test: [ "CMD", "valkey-cli", "ping" ], interval: 10s, retries: 5 }
    networks: [ internal ]          # ⚠ sem `ports:`

  mailpit: { image: "axllent/mailpit:v1.31.6@sha256:23fe39e81bde596a918000b3135c0f85dc8bb4c9ea6be274b051143f3e300387", profiles: [ "debug" ], ports: [ "127.0.0.1:8025:8025" ], networks: [ internal ] }
  # jaeger: profiles [ "telemetry" ] (ADR-024) — v2 já recebe OTLP nativamente

networks: { internal: {} }
volumes:  { valkeydata: {}, caddydata: {}, caddyconfig: {} }
```

## 7. `up_prod.sh` — um comando para produção

Script idempotente na **raiz** do repo; detalhes e contrato completo em `wiki/specs/17-up-prod.md`.

```bash
./up_prod.sh [--dry-run] [--no-pull] [--skip-migrate]
```
Sequência (com `set -Eeuo pipefail` + `flock` para evitar execuções concorrentes + rollback):
1. valida pré-requisitos (`docker`, `.env`, permissões) e trava o deploy;
2. registra o commit atual (rollback) e faz `git pull --ff-only` (ou `fetch --depth=1` + reset);
3. `docker compose pull/build` e `up -d --remove-orphans` (overlay de produção);
4. espera `postgres`/`valkey` **healthy**;
5. `composer install --no-dev --optimize-autoloader --classmap-authoritative`;
6. `php artisan migrate --force --isolated`;
7. `php artisan optimize` (config/route/view/event) + `storage:link` + permissões;
8. recarrega o **php-fpm** (limpa OPcache/JIT) + `php artisan queue:restart` (recicla workers)
   e faz **healthcheck** (`/up` com listener `DiagnosingHealth` que checa PG e Valkey);
9. em falha: volta ao commit anterior e re-aplica caches; loga em `storage/logs/deploy-*.log`.

**Otimizações ativadas** (framework + PHP + edge): `artisan optimize`, autoload
`classmap-authoritative`, OPcache `validate_timestamps=0`, `jit=tracing`, `opcache.preload`
opcional, `realpath_cache`; Caddy `encode zstd gzip` + `precompressed`; **HTTP/2/3 no Apache do WHM**
(edge TLS) — o Caddy só fala h1 com o Apache e `php_fastcgi` com o FPM (H2 só em dev); assets Vite
imutáveis (ADR-022); PostgreSQL 18 tunado (ADR-007).

## 8. Makefile (dev)

```
make up / down / destroy     make logs / reload / ps
make shell / exec cmd='...'  make optimize / reset
make check / check E2E=1     make create:module / create:service
make deploy                  make help
docker compose exec postgres psql   # console do banco (sem alvo make)
```

## 9. Atividades

- [x] `docker/php-fpm/Dockerfile` **fino**: `FROM php:8.4.27-fpm-trixie@sha256:e2d8e787…` (build)
      → `php:8.4.27-fpm-trixie@sha256:e2d8e787…` (oficial; instala só extensões faltantes)
      + **extensão nativa `blake3`** (ExinOne, Apache-2.0, commit pinado); config montada.
- [x] `php.ini`/pool com **`user_ini.filename=`**, `PHPRC`, `PHP_INI_SCAN_DIR`, `php_admin_value[…]`,
      `error_log` em stderr (ignora `.user.ini`/`php.ini` do docroot).
- [x] `docker/web/Caddyfile`: bloco global (`auto_https off`, `admin off`, `servers{trusted_proxies,timeouts}`),
      `:8080`, `root public/`, `php_fastcgi`, `encode zstd gzip` + `file_server{precompressed}`, headers do
      ADR-020 (um só dono) e **hide/deny** de `.user.ini`/`.htaccess`/`error_log`/`*.ini`. Validar com
      `caddy validate`/`caddy fmt` (skill `caddy`) antes do `up`.
- [x] `docker/web/app.conf` = include do Apache do WHM (std+ssl): `/.well-known !`,
      `ProxyPreserveHost`, timeouts, `AllowOverride None` (spec 16).
- [x] `entrypoint.sh`: sanidade (avisa se `.user.ini` **não** está sendo ignorado); **sem chown**
      (imagem non-root — UID/GID vêm do Compose).
- [x] OPcache (ADR-008): `validate_timestamps=0` (prod), `max_accelerated_files=20000`
      (JIT `tracing` só depois de medir — `jit=off`).
      alto, preload opcional.
- [x] `docker/postgres/postgresql.conf` (PG 18: `io_method=worker`, `shared_buffers`,
      `effective_cache_size`, `work_mem`, `random_page_cost=1.1`, `jit`, `pg_stat_statements`).
- [x] `docker/valkey/valkey.conf` (AOF, `volatile-lru`, `maxmemory`, logfmt) + `docker/valkey/entrypoint.sh` (o `requirepass` sai do argv). **Sem porta.**
- [x] **`storage` (RustFS, ADR-030):** `rustfs/rustfs` (Apache-2.0, pin por digest), `/data`,
      `RUSTFS_CONSOLE_ENABLE=false`, `RUSTFS_CHECK_UPDATE=false`, healthcheck `/health` (curl confirmado);
      **sem porta** (rede `internal`).
- [x] `config/filesystems.php`: disco **`s3`** lendo `AWS_*` (`AWS_USE_PATH_STYLE_ENDPOINT=true`
      para RustFS, `throw=true`); bucket garantido de forma **idempotente** via
      `php artisan storage:ensure-bucket` (chamado pelo `up_prod.sh`).
- [x] Storage configurável (`STORAGE_PATH`) + `DATA_PATH` (fora da cota) para `storage`/`postgres`.
- [x] **`up_prod.sh`** implementado, idempotente, com lock, rollback e healthcheck (spec 17).
      Validado: `/up` 200 · 2ª execução 0 (idempotente) · `flock` bloqueia concorrente ·
      rollback dispara em falha (sem reset em `--allow-dirty`) · config/route/view/event cacheados.
- [ ] Serviço **`queue`** com a mesma imagem do `php-fpm` (`queue:work --max-time`), reciclado por
      `queue:restart` no deploy; jobs `ShouldQueue` idempotentes, com `tries`/`timeout`/`backoff`.
- [x] `/up` com listener `DiagnosingHealth` (PG + Valkey) e **não cacheável** (ADR-022).
- [x] `TrustProxies`/`TrustHosts` no `bootstrap/app.php` + Caddy `trusted_proxies` (spec 16 §proxy).
- [x] `docker-compose.yml` com `name:`, rede `internal`, healthchecks, logging JSON com rotação.
- [x] Documentar gotchas: reload de OPcache no deploy (USR2); aceitar o proxy do
      WHM; `env-sync` no dev.

## 10. Testes e evidências

- [x] `make up` idempotente (2×); `./up_prod.sh --dry-run` mostra o plano sem alterar nada.
- [x] **Ignorar cPanel:** com `public/.user.ini` (`display_errors=On`) o `display_errors` permaneceu
      **Off**; `php -i | grep user_ini` → `no value`.
- [x] `.htaccess` do cPanel **não** afeta a app (Caddy/ProxyPass).
- [ ] `curl -I https://<domain>/error_log` → **404**; `/.user.ini`/`php.ini` não são servidos.
- [x] **Portas:** só `web` publica `127.0.0.1:${WEB_PORT}`; `postgres`/`valkey`/`storage`/`php-fpm`/
      `queue` apenas `EXPOSE` (sem binding no host).
- [x] **Storage:** `curl -fsS http://storage:9000/health` **healthy** (`{"status":"ok"}`);
      `storage:ensure-bucket` criou `public-v7` (2ª execução: "já existe"); `Storage::disk('s3')->put/get/exists`
      e `allFiles()` funcionam contra o RustFS; trocar `AWS_ENDPOINT`/`AWS_USE_PATH_STYLE_ENDPOINT`
      no `.env` muda o backend **sem** alteração de código (ADR-030).
- [x] `php-fpm` roda **non-root** via Compose (`uid=1000`) com digest pinado; RustFS idem
      (`user:` + `/logs` em tmpfs).
- [x] `SELECT version();` = **PostgreSQL 18.6**; `SELECT uuidv7();` funciona; AOF no Valkey.
- [ ] **AutoSSL:** `/.well-known/acme-challenge` **não** é proxiado e o certificado renova.
- [ ] `curl -kI` (prod) com headers do ADR-020 e CSP sem `unsafe-inline`.
- [x] `./up_prod.sh` termina com `php artisan about` mostrando config/route/view/event cacheados e
      OPcache recarregado (`opcache.validate_timestamps=Off`).
- [ ] Perfil `telemetry`: trace no Jaeger; **sem** o perfil, app normal. Perfil `debug`: e-mail no
      Mailpit; **sem** o perfil, `MAIL_MAILER=log`.
- [ ] **Fila:** job `ShouldQueue` de teste é processado pelo `queue`; `queue:restart` recicla os
      workers sem perder jobs; nenhuma porta do `queue` publicada.
- [ ] Duas cópias do repo no mesmo host sobem sem colisão (projeto/portas internos isolados).

## 11. Definition of Done

- [x] `make up` (dev) e `./up_prod.sh` (prod) entregam ambiente funcional, sem passos manuais.
- [x] Layout padrão do WHM **intacto**; arquivos do cPanel **ignorados por configuração**.
- [x] Único acoplamento com o WHM: o `app.conf` no Apache 2.4.
- [x] Nenhuma porta de banco/cache exposta; `web`/`php-fpm` non-root.
- [x] Mídia/anexos em **object storage S3** (RustFS local), trocável por **AWS S3** só com `.env`.
- [x] Todas as otimizações aplicadas no `up_prod.sh`; rollback funciona.
- [x] Imagens idênticas em dev e produção; profiles são dev-only.

## 12. Riscos

| Risco | Mitigação |
|---|---|
| `.user.ini` do cPanel vazar para o FPM | `user_ini.filename=` + teste de comportamento (seção 10) |
| `.htaccess` do cPanel interferir | Caddy ignora; Apache não mapeia (`ProxyPass`) + `AllowOverride None` |
| AutoSSL quebrar por proxy do `/.well-known` | `ProxyPass /.well-known !` **antes** do `/`; testar renovação |
| Storage local indisponível | healthcheck `/health` + `depends_on: service_healthy`; bucket idempotente; troca por AWS S3 via `.env` |
| `ProxyPass` sumir após update do painel | Include em `userdata` (persiste `rebuildhttpdconf`) — spec 16 |
| Cota do cPanel estourar | `STORAGE_PATH`/`DATA_PATH` fora da cota; repo pequeno |
| Fonte do PHP/base diferente do previsto | Confirmar no dia 1; matriz de imagens pinadas no ADR-031 |
| PG 18 recém-lançado | Pin de patch (`18.6-trixie`) + backup do volume antes de updates |
| Deploy parcial (migração/otimização) | `up_prod.sh` com lock, rollback e healthcheck (spec 17) |
| Jobs perdidos/duplicados no deploy | `queue:restart` + jobs idempotentes (`ShouldBeUnique`/`WithoutOverlapping`) |
| IP/HTTPS errados atrás do proxy | `TrustProxies` (faixas privadas) + `TrustHosts` + Caddy `trusted_proxies` (spec 16) |
