# Permissões do armazenamento local em Docker Escopo: `docker-compose.yml` (desenvolvimento local). Homologação e produção usam o bucket privado do Supabase e não montam volume em `/app/.data` — `docker-compose.staging.yml` não foi alterado. ## Problema Cadastro de `modelo_imoveis_teste.xlsx` no compose local respondeu `Não foi possível continuar — Erro interno.` ``` uid=1001(adaptador) gid=1001(adaptador) drwxr-xr-x 2 root root /app/.data STORAGE_NOT_WRITABLE ``` Contornado à mão no volume local com `chown 1001:1001` e `chmod 750` em `/app/.data`, depois `STORAGE_WRITABLE`. Esta correção elimina o passo manual. ## Causa raiz 1. A imagem não continha `/app/.data`. `LocalPrivateStorage` grava em `/app/.data/storage` e só cria o diretório em runtime. 2. Quando um volume nomeado é montado num caminho que não existe na imagem, o Docker cria o ponto de montagem como `root:root 755`. O volume vazio não tem de onde herdar dono. 3. Web e worker rodam como `adaptador` (1001) sem capacidades. Não escrevem num diretório de root. 4. O health check do storage local só fazia `mkdir -p`, que passa quando o diretório já existe. O processo subia saudável e o erro só aparecia no primeiro upload, como 500 genérico. Reproduzido num volume descartável com a imagem anterior: `drwxr-xr-x 0 0 /app/.data` e `touch: Permission denied`. ## Correção Duas camadas, porque `RUN chown` no Dockerfile sozinho não alcança volume que já existe. | Camada | Arquivo | O que faz | Alcança | | --- | --- | --- | --- | | Imagem | `Dockerfile` | Cria `/app/.data/storage` com dono `adaptador` e modo `750`. Um volume nomeado **vazio** herda dono e modo na primeira montagem | Volumes novos | | Inicialização | `docker-compose.yml` (`storage-init`) + `docker/entrypoint.sh prepare-storage` | Serviço de uma execução, antes de web e worker, que ajusta **só** `/app/.data` e `/app/.data/storage` e prova a escrita como uid 1001 | Volumes existentes, inclusive os criados antes desta correção | | Falha segura | `docker/entrypoint.sh` (`web`, `worker`) | Antes de iniciar, grava e apaga um arquivo de prova em `/app/.data/storage`. Sem escrita, sai com código 1 e `STORAGE_NOT_WRITABLE` | Qualquer ambiente que use o storage local | Alternativa A (preparar dentro do entrypoint de web e worker) foi descartada: exigiria iniciar os processos como root e rebaixar o usuário depois. A alternativa B mantém web e worker exatamente como antes — `USER adaptador`, sem capacidades — e concentra o único passo privilegiado num contêiner que termina. ## Fluxo de inicialização ``` postgres (healthy) ─┐ redis (healthy) ────┤ storage-init ───────┼─ completed_successfully ─► web ─► worker (root, sem rede, │ termina) │ ``` `storage-init`: - `user: "0:0"`, `network_mode: none`, `cap_drop: [ALL]` e só `CHOWN`, `FOWNER`, `DAC_OVERRIDE`, `DAC_READ_SEARCH`, `SETUID`, `SETGID`; `no-new-privileges`. - Recusa link simbólico em `/app/.data` ou `/app/.data/storage`. - Muda dono/modo somente quando diferem de `1001:1001 750`, e registra cada mudança. - **Não é recursivo.** Itens com outro dono dentro do volume são contados e reportados, não alterados. - Prova a escrita com `setpriv --reuid 1001` e falha com `STORAGE_NOT_WRITABLE` se não conseguir. Falha do `storage-init` impede web e worker de subir. - UID/GID vêm de `id -u adaptador` na própria imagem, a mesma fonte do `USER` do Dockerfile. ## Permissões esperadas | Caminho | Dono | Modo | | --- | --- | --- | | `/app/.data` | `1001:1001` | `750` | | `/app/.data/storage` | `1001:1001` | `750` | | Subpastas e arquivos criados pela aplicação | `1001:1001` | padrão do processo (`755`/`644`), protegidos pelo `750` acima | | Processo | Usuário | Capacidades efetivas | | --- | --- | --- | | web | `adaptador` (1001) | nenhuma | | worker | `adaptador` (1001) | nenhuma | | storage-init | root, só durante a preparação | as seis listadas acima | Web e worker montam o mesmo volume `adaptador-planilhas-local-storage` em `/app/.data`. O volume não é servido por HTTP: o download continua passando pela rota autenticada. ## Volume novo × existente - **Novo:** herda `1001:1001 750` da imagem; `storage-init` só confirma e não altera nada. - **Existente com raiz de root:** `storage-init` ajusta os dois diretórios. Conteúdo, dono e modo dos arquivos existentes ficam como estavam. - **Existente já corrigido à mão:** nenhuma mudança de dono; o modo de `/app/.data/storage` passa de `755` para `750` na primeira execução. ## Validação ```bash docker compose --parallel 1 build web ADAPTER_IMAGE=adaptador-planilhas:local bash .github/scripts/storage-permissions.sh ``` O script usa apenas volumes e contêineres descartáveis com prefixo `adaptador-storage-test-` e remove só o que criou. Casos: | Caso | Verifica | | --- | --- | | A | Volume novo: `1001:1001 750` sem `storage-init`, escrita pelo uid 1001, `STORAGE_WRITABLE` | | B | Volume com raiz `root:root 755` e arquivos: diretórios corrigidos, conteúdo e dono dos arquivos intactos, item de root apenas reportado | | C | Duas execuções extras: snapshot de dono, modo, tamanho e SHA-256 idêntico; nenhuma mudança registrada | | C2 | `--fix-existing` ajusta o dono só quando pedido e preserva o conteúdo | | D | Um contêiner grava, outro lê e apaga; `web` real sobe e responde liveness; `worker` real passa da verificação | | E | web como 1001 com `CapEff` zero; imagem com `USER adaptador`; `prepare-storage` recusa rodar sem root | | F | Volume sem escrita: `web` e `worker` saem com código 1 e `STORAGE_NOT_WRITABLE`; com Supabase configurado a verificação não bloqueia | O job `image` do CI executa o mesmo script. ## Recuperação Se `storage-init` reportar `AVISO: N item(ns) ... com outro dono`, os arquivos continuam íntegros, mas a aplicação pode não conseguir sobrescrevê-los ou apagá-los. Ajuste controlado, limitado ao volume, sem seguir link simbólico e sem atravessar outro sistema de arquivos — **execute só com autorização**: ```bash docker compose run --rm storage-init prepare-storage --fix-existing ``` Se `storage-init` falhar com `STORAGE_NOT_WRITABLE`: confira `docker compose logs storage-init`. Causas prováveis são volume montado somente leitura ou driver de volume que não aceita `chown`. Não use `chmod 777` nem rode web/worker como root. ## Limitações - Bind mount (`./pasta:/app/.data`) não herda dono da imagem. No Docker Desktop/WSL2 sobre o disco do Windows o `chown` pode não ter efeito; a falha segura impede o processo de subir em silêncio. O compose usa volume nomeado. - `--fix-existing` é recursivo dentro do volume e por isso é manual. - O ajuste do volume local real `adaptador-planilhas-local-storage` acontece quando os contêineres forem recriados com o compose novo. Esta validação não recriou os contêineres em execução. - Não valida homologação: o storage de homologação é o bucket privado do Supabase (item D7 do checklist continua PENDENTE).