# ADR-031 — Imagens-base pinadas (públicas + digests)

| Campo | Valor |
|---|---|
| **Status** | Aceito (2026-10-16) |
| **Data** | 2026-10-15 |
| **Plano de origem** | `wiki/plan/04-runtime-containers.md` |
| **Relacionadas** | ADR-002, ADR-005, ADR-006, ADR-007, ADR-024, ADR-026, ADR-029, ADR-030 |

## Contexto

O runtime (ADR-005) usa imagens de terceiros; o ADR-002 exige vendorização, licenças permissivas e
*reproducibilidade*; o ADR-001 exige builds determinísticos. Portanto **toda imagem é pinada por
`tag@sha256`** (nunca `latest`).

> **Nota de revisão:** as bases **DHI** (`dhi.io/*`) foram **abandonadas** porque exigem
> `docker login dhi.io`, o que cria atrito no build e no deploy. Todas as imagens passam a ser
> **públicas** e pinadas por digest. O *non-root* passa a ser garantido pelo Compose (`user:`),
> não pela imagem.

## Decisão

- **Imagens públicas pinadas por digest** (`imagem:tag@sha256:...`); `.env` guarda apenas
  portas/segredos.
- **Base do PHP:** imagem oficial `php:8.4.27-fpm-trixie` (Debian 13) — **um único estágio** que já
  traz `docker-php-ext-install`/`pecl`; instalamos **apenas** as extensões que a base não traz
  (`intl`, `pdo_pgsql`, `pgsql`, `zip`, `bcmath`, `exif`, `imagick`, `apcu`, `redis`) + **libvips**
  (`libvips-tools`, com os plugins AVIF do `libheif`) + o **PECL
  `blake3`** (ADR-029).

> **CVE-2026-109618/109619 (php2shell, 2026-10-21):** a base foi movida de 8.4.26 para **8.4.27**,
> a primeira release 8.4 com as correções. O `php.ini` desliga `session.upload_progress.enabled`
> e o pool o trava com `php_admin_flag`; o `entrypoint.sh` falha o boot se a diretiva voltar a `On`.
> O digest 8.4.26 é proibido pelo gate `tools/security-check.sh` — não reverter.
- **Composer** vem de um estágio próprio (`composer:2`) e o binário é **copiado** para a imagem
  PHP (permite `composer install` no deploy/`scripts/update_prod.sh`).
- **`queue` reusa a mesma imagem** do `php-fpm`.
- **Non-root:** o Compose define `user: "${UID}:${GID}"` (imagens oficiais rodam como root por
  padrão); `web` escuta em porta alta (`:8080`).
- **Profiles dev-only:** `debug` (mailpit) e `telemetry` (jaeger) — nunca em produção (ADR-024).
- **Config montada, nunca embutida** (ADR-005): `user_ini.filename=`, `PHPRC`, `PHP_INI_SCAN_DIR`,
  pool FPM.

## Matriz de imagens

| Serviço | Imagem pinada (`tag@sha256`) | Papel | Porta |
|---|---|---|---|
| `web` | `caddy:2.11.7@sha256:685ae3d7c34d9b5f8caeddc5d14051db7bbd88ff1f5a2fa0c16bd8ca328fb7af` | HTTP do app (dev+prod), non-root `:8080` | `127.0.0.1:${WEB_PORT}` |
| `php-fpm` / `queue` | `php:8.4.27-fpm-trixie@sha256:e2d8e787dc775bdbd9d7e0c5e8f2f9ddb41ce5977c8bbaf99a2ba2e7d90fd8ae` | PHP 8.4 FPM (ext + `blake3` + `libvips`/AVIF) | **nenhuma** |
| `postgres` | `postgres:18-trixie@sha256:74935e72241653ca55e0414067e6d8763aceb8a810eb51b452253ec3dcfc4336` | PostgreSQL 18 | **nenhuma** |
| `valkey` | `valkey/valkey:9-alpine@sha256:48332870af354a799964c0012ae1194a0bf2bf894eb508f945810596dc2d8d11` | cache/sessão/fila | **nenhuma** |
| `storage` | `rustfs/rustfs:1.0.2-glibc@sha256:1617cf459224b161bde874144f96969421ad07b4eb086a44c38d1fe31f36c595` | object storage S3 (ADR-030) | **nenhuma** |
| `mailpit` | `axllent/mailpit:v1.31.6@sha256:23fe39e81bde596a918000b3135c0f85dc8bb4c9ea6be274b051143f3e300387` | e-mail (perfil `debug`) | `127.0.0.1:8025` |
| `jaeger` | `jaegertracing/jaeger:2.11.0@sha256:b585df1b6299bbbd16bf7c679da30389349736e4b6bc8f4f500142a75bf26ca8` | traces + **receptor OTLP nativo** (ADR-024) | UI `127.0.0.1:16686` · OTLP `4318` |
| build de assets | `node:26-alpine@sha256:6f08a523443ddedf52941874c203475b0ad503e859ad70cb26ab8be9a517dbd5` | `npm ci && npm run build` (Vite 8) | — |
| build (Composer) | `composer:2@sha256:af98f42dfff7c68ba8d53c2164fd9fde1087b7d449514baa38c418b1f6bc4bac` | `composer install` (binário copiado) | — |

**`otel-collector` não é necessário:** o Jaeger v2 recebe OTLP diretamente
(`OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318`).

Nota: todas as imagens **públicas** acima foram verificadas por `docker buildx imagetools inspect`
(digest confere, multi-arch `index`).

## Dockerfile de referência (php-fpm)

```dockerfile
ARG PHP=php:8.4.27-fpm-trixie@sha256:e2d8e787dc775bdbd9d7e0c5e8f2f9ddb41ce5977c8bbaf99a2ba2e7d90fd8ae
ARG COMPOSER=composer:2@sha256:af98f42dfff7c68ba8d53c2164fd9fde1087b7d449514baa38c418b1f6bc4bac
ARG BLAKE3_REF=3d86cdaf9bcc52ebfd4c52120e987fc6a7fb2984
# SHA-256 do tarball do BLAKE3: o commit pinado não garante o byte a byte.
ARG BLAKE3_SHA256=6e8e428399c1fc7dc9a1cb4cb0f63d5a2ca1054bdce795ab62c0997f43e3c84a
# PECL pinado: sem versão, o build pode mudar o artefato de runtime sem ninguém pedir.
ARG PECL_APCU=5.1.28
ARG PECL_REDIS=6.3.0
ARG PECL_IMAGICK=3.8.1
ARG PECL_PCOV=1.0.12
# SHA-256 de cada tarball PECL (o `pecl install` sozinho não garante o byte a byte).
ARG PECL_SHA_APCU=ca9c1820810a168786f8048a4c3f8c9e3fd941407ad1553259fb2e30b5f057bf
ARG PECL_SHA_REDIS=0d5141f634bd1db6c1ddcda053d25ecf2c4fc1c395430d534fd3f8d51dd7f0b5
ARG PECL_SHA_IMAGICK=3a3587c0a524c17d0dad9673a160b90cd776e836838474e173b549ed864352ee
ARG PECL_SHA_PCOV=23255c8c9335a9636ccb743f5302436a97a582a0bbde9869485be911bbc15da8

FROM ${COMPOSER} AS composer

FROM ${PHP} AS runtime
ARG BLAKE3_REF
ARG BLAKE3_SHA256
ARG PECL_APCU
ARG PECL_REDIS
ARG PECL_IMAGICK
ARG PECL_PCOV
ARG PECL_SHA_APCU
ARG PECL_SHA_REDIS
ARG PECL_SHA_IMAGICK
ARG PECL_SHA_PCOV
RUN set -eux; \
    apt-get update; \
    apt-get install -y --no-install-recommends \
        curl ca-certificates libicu-dev libpq-dev libzip-dev libonig-dev libmagickwand-dev \
        libvips-tools libheif-plugin-aomenc libheif-plugin-aomdec qpdf; \
    docker-php-ext-install -j"$(nproc)" intl pdo_pgsql pgsql zip bcmath exif; \
    # PECL: versão E checksum fixos, uma extensão por vez, download resiliente
    # (o CDN do pecl.php.net devolveu 504 em 2026-10-21 inclusive numa versão que existe).
    for spec in "apcu ${PECL_APCU} ${PECL_SHA_APCU}" "redis ${PECL_REDIS} ${PECL_SHA_REDIS}" "imagick ${PECL_IMAGICK} ${PECL_SHA_IMAGICK}" "pcov ${PECL_PCOV} ${PECL_SHA_PCOV}"; do \
        set -- $spec; name="$1"; version="$2"; sha="$3"; \
        ok=0; \
        for attempt in 1 2 3 4; do \
            for url in "https://pecl.php.net/get/${name}-${version}.tgz" "https://pecl.php.net/get/${name}-${version}"; do \
                if curl -fsSL --max-time 120 "$url" -o /tmp/pecl-pkg.tgz; then ok=1; break; fi; \
            done; \
            [ "$ok" = "1" ] && break; \
            sleep 5; \
        done; \
        [ "$ok" = "1" ] || { echo "download de ${name}-${version} falhou" >&2; exit 1; }; \
        echo "${sha}  /tmp/pecl-pkg.tgz" | sha256sum -c -; \
        pecl install /tmp/pecl-pkg.tgz; \
        rm -f /tmp/pecl-pkg.tgz; \
    done; \
    docker-php-ext-enable apcu redis imagick pcov; \
    apt-get install -y --no-install-recommends $PHPIZE_DEPS; \
    curl -fsSL "https://github.com/ExinOne/php-blake3-ext/archive/${BLAKE3_REF}.tar.gz" -o /tmp/blake3.tgz; \
    echo "${BLAKE3_SHA256}  /tmp/blake3.tgz" | sha256sum -c -; \
    mkdir -p /tmp/blake3 && tar -xzf /tmp/blake3.tgz -C /tmp/blake3 --strip-components=1; \
    ( cd /tmp/blake3 && phpize && ./configure --enable-blake3 && make -j"$(nproc)" && make -j"$(nproc)" install ); \
    docker-php-ext-enable blake3; rm -rf /tmp/blake3 /tmp/blake3.tgz; \
    # Imagem menor: a limpeza fica no **mesmo RUN** (em Docker, apagar em layer posterior
    # não reduz a imagem — os bytes ficam na layer anterior). **Sem `--auto-remove`**: as
    # libs de runtime vieram como dependencia dos `-dev` e um autoremove as levaria junto.
    docker-php-source delete; rm -rf /tmp/pear; \
    apt-get purge -y $PHPIZE_DEPS libicu-dev libpq-dev libzip-dev libonig-dev libmagickwand-dev; \
    rm -rf /var/lib/apt/lists/*; \
    php -r 'foreach (["apcu","redis","imagick","pcov","blake3"] as $e) { if (! extension_loaded($e)) { exit(1); } } if (! function_exists("blake3_hash")) { exit(1); } $i = new Imagick();'
COPY --from=composer /usr/bin/composer /usr/local/bin/composer
```

> `user_ini.filename=`, `PHPRC`, `PHP_INI_SCAN_DIR` e o pool FPM são **MONTADOS** (não embutidos).
> O `docker-compose.yml` dá **uma** imagem (`concordia/php-fpm`) ao `php-fpm` **e** ao `queue`:
> o `queue` não tem `build:` próprio (ADR-031: um runtime só).

## Consequências

**Positivas**
- Builds **sem credencial** e reprodutíveis (digest fixo); imagens oficiais são multi-arch.
- Um só runtime PHP (fpm+queue) e nenhuma porta de infraestrutura exposta.

**Custos / negativas**
- Perde-se o endurecimento extra das DHI; o *non-root* passa a ser responsabilidade do Compose.
- Revisar digest a cada bump (feito por `dependabot`/renovate futuramente).

## Verificação

- `docker buildx imagetools inspect <imagem>@<digest>` confere.
- `docker compose config` não contém `latest` nem imagem sem `sha256`.
- **Validado em build real (2026-10-21):** `php -m` lista `intl pdo_pgsql pgsql zip bcmath exif imagick apcu redis sodium opcache blake3`;
  `vips --version` responde (8.16.1) e `vips thumbnail entrada.jpg "saida.avif[Q=65,effort=4]" 360` gera AVIF **e**
  WebP — a digestão de mídia (ADR-026 §6: libvips como padrão, Imagick como alternativa) roda na imagem base;
  `blake3_hash("abc")` = `6437b3ac38465133ffb63b75273a8db548c558465d79db03fd359c6cd5bd9d85` (vetor oficial).
- **Extensões PECL pinadas** (versão + SHA-256 do tarball): `apcu 5.1.28`, `redis 6.3.0`,
  `imagick 3.8.1`, `pcov 1.0.12` — todas carregam e funcionam na imagem construída.
- **Uma imagem só:** `docker compose config --images` lista `concordia/php-fpm:local` para o
  `php-fpm` **e** para o `queue` (o `queue` não tem `build:` próprio).
- **Tamanho:** 812 MB (era 878 MB em **duas** imagens de 878 MB). A limpeza do toolchain só
  encolhe a imagem se estiver **no mesmo RUN** da instalação — apagar em layer posterior não
  reduz nada (os bytes ficam na layer anterior).
- `user_ini.filename` vazio com `php.ini` montado (ADR-005) e a imagem `rustfs/rustfs:1.0.2-glibc`
  traz `curl` (healthcheck `/health` ok).
- Nenhuma porta de `postgres`/`valkey`/`storage`/`php-fpm`/`queue` publicada.

## Referências

- ADR-002 · ADR-005 · ADR-006 · ADR-007 · ADR-024 · ADR-026 · ADR-029 · ADR-030
- `wiki/plan/04-runtime-containers.md` · `docker/php-fpm/` · `docker-compose.yml`
