Files
adaptador-planilhas/docs/spreadsheet-adapter/23-storage-permissions.md
T
Ferraz Fagundes 2be97ea969
ci / lint, typecheck, testes e build (push) Has been cancelled
ci / Postgres e Redis reais (push) Has been cancelled
ci / imagem Docker Linux (push) Has been cancelled
fix(docker): inicializar permissoes do storage com seguranca
2026-10-16 23:47:38 -03:00

6.9 KiB
Raw Blame History

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

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 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).