Toda rota passa por um before_request que redireciona para /login quem não tem sessão — inclusive os POSTs, então /exportar e /salvar-sd sem cookie não chegam ao handler. Ficam de fora só a própria entrada e /static (a tela de login precisa da folha de estilo). Credencial única em constante (admin@iasis.com.br / admin123), comparada com compare_digest nos dois campos e com a mesma mensagem para usuário errado e senha errada — dizer qual dos dois falhou entrega metade da credencial. Sem banco, sem cadastro e sem recuperação de senha: a app é ferramenta interna de uso individual, e isto existe para o upload e o writeback não ficarem abertos a quem alcançar a porta quando ela roda hospedada. SECRET_KEY não tem default assado na imagem — chave fixa numa imagem vale para toda cópia dela. Sem a variável, o servidor sorteia uma por processo e o login cai a cada restart, que é a mesma vida útil que a sessão da SD já tem; é seguro porque o gunicorn roda com -w 1. APP_USUARIO/APP_SENHA seguem a convenção do Dockerfile: default lá, compose só espelha. O Sair encerra a sessão do navegador sem descartar a SD em memória — é a mesma máquina, o mesmo processo. E o teste_e2e faz login uma vez no test client, senão todo request dele voltaria 302. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
111 lines
5.7 KiB
Docker
111 lines
5.7 KiB
Docker
# Imagem da aplicação de Exportação da SD — deploy em VPS via painel Coolify.
|
|
#
|
|
# O que NÃO entra na imagem, e por que: clientes/. O app lê
|
|
# clientes/ses-mg/contrato/itens.yaml em tempo de execução (regras_sd.carregar_canonico),
|
|
# consulta contrato/linhas-os.yaml se existir (fallback da linha de OS; ausente,
|
|
# `linha_os` é exigido no SD.md) e grava SD.md de volta em
|
|
# clientes/ses-mg/projetos/ (escrita_sd.gravar). Esses são dados do cliente,
|
|
# versionados fora do git — entram por VOLUME montado em /app/clientes. Sem esse
|
|
# volume, a imagem sobe e a tela inicial responde, mas o primeiro upload falha
|
|
# com "Arquivo canônico ausente". E sem linhas-os.yaml no volume, o VPS se
|
|
# comporta diferente do local: o fallback desliga e `linha_os` vira pendência.
|
|
#
|
|
# DOIS MODOS de deploy no Coolify — a imagem é a mesma, muda onde se configura:
|
|
#
|
|
# 1. Application (Dockerfile) — o que está em produção (2026-09-04). O Coolify
|
|
# IGNORA o docker-compose.yml. Volume: Persistent storage → Add mount →
|
|
# "Directory mount", Source /data/coolify/applications/<uuid>/clientes
|
|
# (o Coolify sugere esse caminho; ele sobrevive a redeploy) → Destination
|
|
# /app/clientes. Variáveis: aba Environment Variables. Réplicas: 1.
|
|
# 2. Docker Compose — usa o docker-compose.yml; volume e variáveis vêm de lá.
|
|
#
|
|
# Por isso TODO default de variável mora AQUI (ENV + ${VAR:-default} no CMD),
|
|
# e o compose só espelha: variável que existe só no compose não existe no VPS.
|
|
#
|
|
# Build local: docker build -t exporta-sd .
|
|
# Run local: docker run --rm -p 5000:5000 -v "$PWD/clientes:/app/clientes" exporta-sd
|
|
|
|
FROM python:3.13-slim
|
|
|
|
# PYTHONUNBUFFERED: log do gunicorn sai na hora no painel do Coolify.
|
|
# LANG/LC_ALL em UTF-8: identificadores de SD trazem "·" (P2·SD8) e o
|
|
# escrita_sd monta nome de arquivo a partir deles.
|
|
# TZ: sessao.marcar_exportada carimba datetime.now() na tela ("exportada em");
|
|
# sem isto o container está em UTC e o horário sai 3h adiantado.
|
|
#
|
|
# Variáveis que o operador pode sobrescrever (Coolify → Environment Variables,
|
|
# ou .env com o compose — ver .env.example). Os defaults valem nos dois modos:
|
|
# PORT porta do gunicorn dentro do container
|
|
# TZ fuso do carimbo de exportação
|
|
# GUNICORN_TIMEOUT segundos por request (upload + análise da SD)
|
|
# GUNICORN_LOG_LEVEL debug | info | warning | error
|
|
# APP_USUARIO e-mail da credencial única da tela de login
|
|
# APP_SENHA senha dessa credencial
|
|
# SECRET_KEY assina o cookie de sessão — SEM default aqui de
|
|
# propósito: chave fixa assada na imagem é pior que
|
|
# nenhuma (vale para toda cópia dela). Sem a variável, o
|
|
# servidor.py sorteia uma por processo e o login cai a
|
|
# cada restart; cadastre no Coolify para o login durar.
|
|
ENV PYTHONUNBUFFERED=1 \
|
|
PYTHONDONTWRITEBYTECODE=1 \
|
|
LANG=C.UTF-8 \
|
|
LC_ALL=C.UTF-8 \
|
|
PORT=5000 \
|
|
TZ=America/Sao_Paulo \
|
|
GUNICORN_TIMEOUT=120 \
|
|
GUNICORN_LOG_LEVEL=info \
|
|
APP_USUARIO=admin@iasis.com.br \
|
|
APP_SENHA=admin123
|
|
|
|
WORKDIR /app
|
|
|
|
# gosu: o entrypoint precisa iniciar como root (só root faz chown no volume
|
|
# recém-montado) e depois derrubar privilégio para `sd` antes do gunicorn.
|
|
# tzdata: zoneinfo para o TZ acima ter efeito (a slim não garante que venha).
|
|
RUN apt-get update \
|
|
&& apt-get install -y --no-install-recommends gosu tzdata \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# Dependências em camada própria: mudar código não reinstala pacote.
|
|
# requirements.txt é o mesmo do venv Windows; o gunicorn fica separado porque
|
|
# é só do deploy (no Windows ele não instala).
|
|
COPY requirements.txt requirements-deploy.txt ./
|
|
RUN pip install --no-cache-dir -r requirements.txt -r requirements-deploy.txt
|
|
|
|
# Só o código. O resto (clientes/, backlog, .md da raiz) está no .dockerignore.
|
|
COPY scripts/ ./scripts/
|
|
COPY app/ ./app/
|
|
|
|
# Usuário sem privilégio — quem roda o gunicorn, via entrypoint.sh.
|
|
RUN mkdir -p /app/clientes /app/_derivados \
|
|
&& useradd --create-home --uid 10001 sd \
|
|
&& chown -R sd:sd /app
|
|
|
|
# chmod aqui, e não só no arquivo do host: git em Windows não carrega bit de
|
|
# execução, então sem isso o entrypoint chega ao Coolify sem permissão de
|
|
# executar.
|
|
COPY entrypoint.sh /entrypoint.sh
|
|
RUN chmod +x /entrypoint.sh
|
|
|
|
# Container começa como root DE PROPÓSITO — é o entrypoint.sh que faz o
|
|
# chown do volume recém-montado e derruba para `sd` antes do gunicorn. Ver
|
|
# entrypoint.sh.
|
|
ENTRYPOINT ["/entrypoint.sh"]
|
|
|
|
EXPOSE 5000
|
|
|
|
# GET / responde 200 sem sessão (renderiza a tela de upload).
|
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
|
CMD python -c "import urllib.request,os,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:'+os.environ.get('PORT','5000')+'/',timeout=4).status==200 else 1)"
|
|
|
|
# -w 1 NÃO é ajuste de desempenho, é requisito de correção: o estado da sessão
|
|
# é a global ATUAL no processo do servidor.py (uma SD por vez, em memória).
|
|
# Com dois workers, dois requests do MESMO usuário caem em processos
|
|
# diferentes e a sessão desaparece de forma aleatória. Pelo mesmo motivo, o
|
|
# número de réplicas no Coolify tem de ficar em 1 — e é por isso que o número
|
|
# de workers NÃO é variável de ambiente: não existe valor certo além de 1.
|
|
#
|
|
# --chdir app + servidor:app: o servidor.py insere ../scripts no sys.path a
|
|
# partir de __file__, então funciona igual sob gunicorn e sob `python app/servidor.py`.
|
|
CMD ["sh", "-c", "exec gunicorn --chdir app servidor:app -w 1 -b 0.0.0.0:${PORT:-5000} --timeout ${GUNICORN_TIMEOUT:-120} --log-level ${GUNICORN_LOG_LEVEL:-info} --access-logfile - --error-logfile -"]
|