6.9 KiB
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
- A imagem não continha
/app/.data.LocalPrivateStoragegrava em/app/.data/storagee só cria o diretório em runtime. - 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. - Web e worker rodam como
adaptador(1001) sem capacidades. Não escrevem num diretório de root. - 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/.dataou/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 1001e falha comSTORAGE_NOT_WRITABLEse não conseguir. Falha dostorage-initimpede web e worker de subir. - UID/GID vêm de
id -u adaptadorna própria imagem, a mesma fonte doUSERdo 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 750da imagem;storage-initsó confirma e não altera nada. - Existente com raiz de root:
storage-initajusta 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/storagepassa de755para750na primeira execução.
Validação
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:
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 ochownpode 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-storageacontece 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).