# ADR-044 — Podman rootless como runtime de desenvolvimento

| Campo | Valor |
|---|---|
| **Status** | Aceito (2026-10-22) — **emendas: ADR-045** (Docker é o padrão; podman é opt-in de dev e nunca em produção) e **ADR-055** (o nome do projeto é único por instalação; o caminho do checkout entra na derivação) |
| **Data** | 2026-10-22 |
| **Plano de origem** | `wiki/implementation/07-runtime-docker.md` (F0/T3) |
| **Relacionadas** | ADR-001, ADR-005, ADR-006, ADR-007, ADR-020, ADR-031, ADR-032, ADR-036 |

## Contexto

O runtime é declarado como `docker compose` (ADR-005/ADR-031) e validado por `make check`.
Desenvolvedores em Fedora/RHEL usam **podman rootless** como motor padrão; rodar o projeto
exige que os mesmos alvos (`make up`, `make check`, `make shell`, `tools/*.sh`) funcionem sem
um segundo arquivo Compose mantido à mão e sem `if podman` espalhado por 15 scripts.

Medições no host de referência (podman 5.8.9 rootless, SELinux **Enforcing**, Docker sem
SELinux) mostraram que "só trocar o binário" não basta:

| Sintoma medido | Causa |
|---|---|
| `touch` em bind mount do repo → `Permission denied` | rootless mapeia o uid 1000 do container para o **subuid** do host (ex.: 524288); o `user: "${UID}:${GID}"` do Compose deixa de casar com o dono dos arquivos |
| valkey não sobe (`exit 126`, `exec: ... Permission denied`) | bind mount do `entrypoint.sh` sem `:Z` e SELinux Enforcing no podman (o Docker do projeto roda sem SELinux) |
| `:Z` no `${DATA_PATH}` → `lsetxattr(...pgdata): operation not permitted` | o `pgdata` pertence ao uid do container; o relabel recursivo do diretório de dados falha |
| `podman compose` → `Cannot connect to the Docker daemon` | falta o `podman.socket` do usuário ativo |
| `podman compose` recusa | falta um provider (`docker-compose`/`podman-compose`) |

O que **não** mudou e não deve mudar: o artefato é um só (`APP_ENV`, ADR-032), o deploy é
Docker + Apache do WHM (ADR-005), o Postgres continua sem porta publicada e a rede de dados
continua `internal: true` (ADR-007/ADR-020).

## Decisão

1. **Runtime resolvido em um lugar** — `tools/runtime.sh`:
   - `CONTAINER_RUNTIME` (ambiente → `.env` → `docker`) escolhe `ENGINE=(docker|podman)`;
   - podman acrescenta `docker-compose.podman.yml` a `COMPOSE_FILES` e roda os containers
     avulsos com `RUN_ARGS=(--userns=keep-id --security-opt label=disable)`;
   - `MOUNT_LABEL` (`:Z` no Docker, **vazio** no podman) é exportado pelo helper — o
     `docker-compose.yml` interpola `${MOUNT_LABEL-:Z}` nos bind mounts do repo/storage.
     `${VAR-...}` (um hífen) é intencional: **vazio** é valor válido, e `:-` religaria o `:Z`.
   - Makefile e todos os `tools/*.sh` montam o Compose com `${ENGINE[@]} compose … "${COMPOSE_FILES[@]}"`;
     nenhum ponto de entrada chama `docker` literal.
2. **Override exclusivo do podman** — `docker-compose.podman.yml`:
   - `userns_mode: keep-id` em todo serviço com `user:` (web, php-fpm, queue, storage, valkey);
   - `security_opt: ["no-new-privileges:true", "label=disable"]` em todos os serviços;
   - **`postgres` sem `userns_mode`**: o entrypoint roda como root no namespace e faz `chown`
     do PGDATA (com keep-id o root do namespace não é dono do diretório montado);
   - o override **não** declara `ports:`/`networks:` (a postura de segurança não muda).
3. **`make podman-prepare`** — prepara o host de forma idempotente: podman ≥ 5, `podman.socket`
   do usuário (start/enable), provider de Compose, subuid/subgid (falha com o comando exato de
   `usermod`), aviso de SELinux, `.env` com `CONTAINER_RUNTIME=podman` (via `tools/dev-env.sh`),
   diretórios de dados graváveis e validação do Compose mesclado. `--check` só verifica.
   Não instala pacotes nem desliga o SELinux do host.
4. **Portas independentes por instância** — `tools/dev-env.sh`:
   - sem `WEB_PORT`, deriva `507xx` de `cksum(nome do projeto)`: a mesma cópia sobe
     sempre na mesma porta, sem "porta livre no momento" (ADR-001). Faixa fixa do projeto:
     **50700–50789** para o app (determinística) e **50790–50799** para os profiles dev-only
     (`MAILPIT_PORT=50790`, `JAEGER_UI_PORT=50791`, `JAEGER_OTLP_PORT=50792`); nenhuma outra
     porta é publicada no host (portas internas do Compose continuam privadas);
   - `APP_URL` acompanha a porta publicada quando o host é local (`localhost`/`127.0.0.1`);
   - `make env INSTANCE=1` isola a stack (`COMPOSE_PROJECT_NAME=<slug>-<hash8 do caminho>`):
     containers, redes e **volumes** próprios — dois clones/branches não compartilham banco;
   - `WEB_PORT=… make env` fixa a porta explicitamente (nunca é sobrescrita).
5. **Gate estático** — `tools/podman-check.sh` roda no `make check` (não exige podman): todo
   serviço com `user:` tem `userns_mode: keep-id`, todo serviço do override tem `label=disable`,
   `postgres` não tem `userns_mode`, o override não publica portas/redes e os mounts usam
   `${MOUNT_LABEL-:Z}`. Com `CONTAINER_RUNTIME=podman` valida também socket ativo e config.

## Justificativa (alternativas descartadas)

- **Shim `docker` → `podman`**: esconderia a decisão no PATH e quebraria `docker compose config`
  em máquinas com Docker real; a resolução explícita em um helper é auditável.
- **Usar o CLI `docker` contra o socket do podman**: funciona (medido), mas obrigaria o CLI do
  Docker em hosts podman-only e manteria dois clientes. O podman nativo é a dependência mínima.
- **`:Z` em tudo + keep-id, sem `label=disable`**: falha no `pgdata` (medido) e relabela o repo
  inteiro a cada `up`.
- **Projeto/porta sempre derivados do caminho**: isola por padrão, mas mudaria silenciosamente
  o nome do projeto (e os volumes) de quem já tem banco. A isolação é **opt-in** (`INSTANCE=1`).

## Consequências

- `make up`, `make check`, `make shell/exec`, backup/restore/PITR, e2e e sniffCSS passam a ser
  agnósticos ao motor; o `docker-compose.podman.yml` é o único artefato específico de podman.
- O Docker continua sendo o caminho de produção (WHM). `scripts/update_prod.sh` honra o runtime do `.env`,
  mas o perfil esperado é Docker.
- `podman compose` delega a um provider externo: o build pode criar um container auxiliar
  `buildx_buildkit_default` (efeito do provider `docker-compose`; inofensivo, o build cai na
  imagem local do podman).
- Sem `loginctl enable-linger`, o socket e os containers rootless morrem no logout —
  `podman-prepare` imprime o comando.
- O `PGDATA` sob podman fica com dono no intervalo de subuid; remover os arquivos exige
  `podman unshare rm -rf` (o `make destroy` renomeia para `.pi/trash` e não apaga).
- `security-check` passa a aceitar `${VAR-...}` como default funcional — necessário para o
  `MOUNT_LABEL` (vazio é um valor legítimo); `${VAR}` sem default continua reprovado.

## Verificação

```bash
# host (uma vez)
make podman-prepare          # podman >= 5 · socket · provider · subuid · .env
make ports                   # runtime/projeto/porta/APP_URL/diretórios

# ciclo normal — agora atrás do podman
make up
make check                   # inclui o gate `podman-config`
make check E2E=1             # e2e + axe + perf no mesmo runtime

# volta ao Docker
make runtime RUNTIME=docker
```

Provado no host de referência: `docker-compose.yml` + override resolvem `userns_mode: keep-id`
e `label=disable`; `postgres`, `valkey`, `storage` e `web` sobem **healthy** em podman rootless
com SELinux Enforcing; `podman inspect -f '{{.State.StartedAt}}'` e `compose exec` respondem
como no Docker; `tools/podman-check.sh` e `tools/network-check.sh --static-only` passam.
