# ADR-055 — Isolamento do projeto Compose por instalação (nome único por checkout)

| Campo | Valor |
|---|---|
| **Status** | Aceito (2026-10-26) |
| **Data** | 2026-10-26 |
| **Plano de origem** | `wiki/implementation/07-runtime-docker.md` (runtime) · spec 17/18 |
| **Relacionadas** | ADR-001, ADR-005, ADR-006, ADR-007, ADR-017, ADR-031, ADR-044, ADR-045 |

## Contexto

O nome do projeto Compose era o **slug do `APP_NAME`** (`concordia`, `prefeitura-santa-barra`).
Em produção, o `APP_NAME` é o nome do **produto**, não do cliente: o `.env.example` nasce com
`APP_NAME="Concordia"` e dois clientes instalados no mesmo servidor WHM (dois checkouts em
`/home/<cliente>/...`) resolvem para o **mesmo** projeto `concordia`.

Com o mesmo nome de projeto, o Compose compartilha o namespace inteiro:

| Recurso | Prefixo | Efeito da colisão |
|---|---|---|
| containers | `<projeto>-<serviço>-1` | `up` recusa/recria containers da outra instância |
| redes | `<projeto>_edge`, `<projeto>_data` | os dois apps falam na mesma rede |
| volumes nomeados | `<projeto>_valkeydata`, `<projeto>_caddydata` | cache/sessão das duas instâncias misturados |
| porta publicada | `WEB_PORT` (default 50700) | a segunda instância não sobe (ou pior, o edge aponta para o app errado) |

O banco (`${DATA_PATH}/pgdata`) e o storage (RustFS) são bind mounts e **não** dependem do
nome — o risco maior é justamente dois containers `postgres` do mesmo checkout/nome no mesmo
PGDATA. Um deploy de um cliente podia derrubar ou recriar o stack de outro.

O ADR-044 havia rejeitado derivar o nome do caminho **por padrão** porque isso mudaria o nome
(e os volumes nomeados) de quem já tinha banco. Naquele momento a isolação era opt-in de dev
(`make env INSTANCE=1`). Em produção multi-cliente, porém, a colisão não é hipotética: é o
estado inicial de todo cliente novo (mesmo produto, mesmo `APP_NAME`).

## Decisão

1. **O nome derivado é `slug(APP_NAME)-<hash8 do caminho físico do checkout>`.**
   - `tools/project-name.sh` resolve: `$COMPOSE_PROJECT_NAME` → `.env`
     (`COMPOSE_PROJECT_NAME`) → `APP_NAME` + discriminador da instalação;
   - o discriminador é `sha256(realpath do checkout)[0..8]` (`pwd -P`: symlinks resolvidos);
     determinístico e estável enquanto o checkout não se move;
   - exemplos: `concordia-6d83ae07`, `prefeitura-santa-barra-1a2b3c4d`;
   - um `COMPOSE_PROJECT_NAME` **explícito** (ambiente ou `.env`) vence e **não** recebe
     sufixo — é a saída para pinar/migrar uma instalação (e o CI usa `publicv7_ci`).
2. **Nenhum ponto de entrada do Compose usa o nome sem o resolvedor.** `Makefile`,
   `scripts/update_prod.sh`, `tools/*.sh` e o CI passam `-p "$(tools/project-name.sh)"`; o `name:` do
   `docker-compose.yml` fica como fallback de inspeção manual (raw `docker compose`), nunca
   como caminho de deploy.
3. **Adoção de stack antiga é explícita e automática no primeiro deploy.**
   - `scripts/update_prod.sh` e `tools/up-dev.sh` procuram containers do **próprio checkout**
     (`com.docker.compose.project.working_dir = realpath`), cujo projeto tenha nome anterior;
   - a stack antiga é **iniciada** com `compose start` (nunca `up -d`: o compose novo trocaria
     o mount antes da adoção) e o PGDATA é classificado por `tools/pgdata-adopt.sh`:
     bind ativo → nada a fazer; cluster num volume anônimo → cópia de arquivos para
     `${DATA_PATH}/pgdata` com o postgres parado; bind do host já ocupado ou cluster
     inacessível → **aborta** (o `pg_dump` da fase 0.5 é a saída);
   - só depois disso o stack antigo é derrubado (`compose -p <antigo> down --remove-orphans`);
     dois `postgres` no mesmo PGDATA corrompem o banco;
   - o `postgres` monta o bind em `/var/lib/postgresql` (o caminho do VOLUME da imagem) e usa
     `PGDATA=/var/lib/postgresql/pgdata`: o bind antigo em `/var/lib/postgresql/data` era
     **mascarado** pelo VOLUME da imagem no Docker (medido), deixando o banco num volume
     anônimo, fora do `DATA_PATH` (ADR-006/007). O `archive_command` deriva de `$PGDATA`;
   - volumes nomeados antigos (`valkeydata`, `caddydata`) não são herdados (cache/sessão são
     voláteis por definição); o deploy recria com o prefixo novo;
   - `scripts/update_prod.sh` ainda **falha antes de qualquer efeito** se o nome resolvido pertencer a
     **outro** checkout (`working_dir` diferente) — o caso de `.env` copiado entre instâncias.
4. **A porta publicada é validada, não inferida.** O nome do projeto não isola `WEB_PORT`:
   - `scripts/update_prod.sh` (Docker) aborta se `WEB_PORT` já for publicado por uma stack de **outra**
     instalação, com a instrução de definir `WEB_PORT` próprio e atualizar o `ProxyPass`;
   - o stack do próprio checkout (o que está sendo adotado) é permitido;
   - a porta da instância continua sendo dado do `.env`/default `50700`; o deploy nunca muda a
     porta de uma instalação existente (isso quebraria o Apache do WHM sem aviso).
5. **A stack parada deixou de ser confundida com “primeiro deploy”.** Se a instalação já tem
   containers (mesmo parados), o deploy sobe a stack antiga antes do WAL/backup e faz o
   `pg_dump`; sem containers, é primeiro deploy de fato (backup pulado).
6. **A imagem do php-fpm também é por projeto.** O `docker-compose.yml` aceita
   `PHP_IMAGE`/`PHP_TAG`; o `scripts/init_prod.sh` grava
   `PHP_IMAGE=<projeto>-php-fpm` no `.env`. A tag default `concordia/php-fpm:local` fica
   apenas como fallback de inspeção manual (`docker compose` cru): um build de um cliente
   não pode trocar a imagem que o outro usaria no próximo recreate. O `queue` reusa a mesma
   imagem do `php-fpm` (ADR-031).

### Ciclo de vida operacional (`scripts/`)

`scripts/init_prod.sh` e `scripts/update_prod.sh` são o caminho de produção **sem `make`**
(o runtime é resolvido como Docker; podman continua opt-in de dev, ADR-045). Ambos:

- são **dry-run por padrão** (`--execute` aplica); cada fase é um arquivo em
  `scripts/init/` ou `scripts/update/`, orquestrado pelo entrypoint;
- escrevem um log datado em `storage/logs/{init,update}-{dry-run,execute}-<UTC>.log`
  (comandos, saída, fase que falhou e resumo), sem segredos;
- criam/validam a identidade da instalação (nome, `PHP_IMAGE`, marcador de posse do
  `DATA_PATH`) e recusam um checkout que ainda esteja sob o projeto Compose anterior
  (a adoção fica no `scripts/update_prod.sh`, uma vez, para não iniciar um banco vazio);
- no update, o **backup do banco + storage é a primeira ação** (depois de garantir a stack
  viva): `pg_dump -Fc` validado por `pg_restore --list`, `media:backup` quando há offsite,
  snapshot do RustFS com o serviço parado e do `storage/` Laravel; sem backup provado, não
  há pull/build/down;
- o **build (imagem + assets) acontece antes do `down`**; o `down` não usa `-v`, e o
  `composer install` roda com o banco parado preservando o `vendor` anterior (restaurado se
  a instalação falhar); a imagem anterior fica em `:previous`;
- migração que falha **não** restaura o banco automaticamente: o app fica no ar, o dump é
  registrado e a intervenção é consciente (nunca um reset de git silencioso).

## Justificativa (alternativas descartadas)

- **Nome do projeto = domínio (`APP_URL`)**: muda quando o cliente troca de domínio/entra em
  homologação e órfã os volumes; além disso o primeiro deploy nasce com `example.gov.br`.
- **Nome do projeto = usuário do host (`$USER`)**: colide entre cópias no mesmo usuário e
  não é derivável dentro do container; o caminho já carrega o usuário no layout WHM.
- **Hash do remote git**: identifica o **produto**, não a instalação — todos os clientes usam
  o mesmo repositório.
- **Pinar `COMPOSE_PROJECT_NAME` em todo `.env.example`**: o dono decidiu que o `.env` é
  identidade + segredos e o default funcional mora no Compose (`decision_01wk4gsy`); um valor
  fixo no exemplo voltaria a colidir entre clientes.
- **Sufixar sempre (inclusive sobre `COMPOSE_PROJECT_NAME` explícito)**: impediria o operador
  de migrar/pinar uma instalação e quebraria o CI (`publicv7_ci`).

## Consequências

- Containers, redes e volumes ficam **garantidamente exclusivos** por checkout, mesmo com o
  mesmo `APP_NAME` no mesmo host.
- Mover o checkout **muda** o nome derivado. A adoção automática derruba a stack antiga do
  próprio checkout, mas o deploy precisa ser rodado **no novo caminho** (o antigo stack é
  identificado pelo `working_dir`). Para mover sem trocar de nome, defina
  `COMPOSE_PROJECT_NAME` no `.env`.
- `docker compose down/ps/logs` manuais sem `-p`/`.env` continuam um caminho de inspeção: sem
  o resolvedor, o `name:` do arquivo cai no fallback (`concordia`). O deploy e o `make` nunca
  usam esse caminho.
- O primeiro deploy após esta ADR faz uma adoção: o stack antigo é iniciado, o PGDATA é
  classificado (bind/volume/ambíguo/inacessível), o cluster é copiado quando necessário e só
  então o novo stack sobe no mesmo `DATA_PATH`; sessões/cache do Valkey são recriados (uma vez).
- Mover/substituir o `DATA_PATH` com a stack rodando deixa o cluster preso ao namespace do
  container: a classificação devolve **12** e o deploy **aborta** (o `pg_dump` da fase 0.5 é
  a saída). Nunca há `up` silencioso sobre um banco vazio.
- O bind do `postgres` mudou de `/var/lib/postgresql/data` para `/var/lib/postgresql`; o
  layout no host continua `<DATA_PATH>/pgdata/pgdata`, e o `archive_command` acompanha o
  `$PGDATA` (sem caminho fixo).
- O CI mantém o nome explícito (`COMPOSE_PROJECT_NAME=publicv7_ci`), sem sufixo — o
  isolamento é por instalação, não por job.

## Exemplo mínimo comparativo

Antes (dois clientes, mesmo servidor):

```
/home/cliente-a/public_html  → projeto concordia  → concordia-web-1, concordia_postgres-data
/home/cliente-b/public_html  → projeto concordia  → mesmo namespace (colisão)
```

Depois:

```
/home/cliente-a/public_html  → concordia-091fb33f → concordia-091fb33f-web-1 …
/home/cliente-b/public_html  → concordia-7f2c1a90 → concordia-7f2c1a90-web-1 …
```

## Verificação

- `tools/project-name.sh`: dois checkouts com o mesmo `APP_NAME` → nomes diferentes;
  mesmo checkout duas vezes → mesmo nome; `COMPOSE_PROJECT_NAME` explícito → sem sufixo
  (`ProjectIsolationTest`, no `make check`).
- `docker compose ls`: mostra o nome qualificado (ex.: `concordia-091fb33f running(6)`).
- `/up` responde 200 na porta do `.env`; `docker ps` mostra os containers com o prefixo novo.
- `tools/pgdata-adopt.sh --check`: 0 (bind ativo/layout novo), 10 (migrar), 11 (bind do host
  já ocupado) ou 12 (cluster inacessível); `--copy` copia do volume para `${DATA_PATH}/pgdata`
  com o postgres parado. O `DeployPipelineTest` cobra o mount no caminho do VOLUME da imagem
  e o `archive_command` derivado de `$PGDATA`.
- `scripts/update_prod.sh` falha com mensagem clara quando `WEB_PORT` pertence a outra stack, antes do
  backup/build/migrate.
