Deploy: variáveis de ambiente configuráveis + docs dos dois modos do Coolify
TZ (com tzdata), GUNICORN_TIMEOUT e GUNICORN_LOG_LEVEL agora têm default no Dockerfile e podem ser sobrescritos via Coolify → Environment Variables ou .env (modelo em .env.example). TZ corrige o carimbo "exportada em" (sessao.marcar_exportada), que sem isso saía em UTC. Workers continua literal em -w 1, de propósito — não é variável. Cabeçalhos do Dockerfile e docker-compose.yml reescritos para descrever os dois modos de deploy no Coolify (Application/Dockerfile, em produção desde 04/09, e Docker Compose) e o caminho de volume que de fato funciona: Directory mount em /data/coolify/applications/<uuid>/clientes, dados enviados por scp da pasta ses-mg/. .gitattributes fixa LF também em Dockerfile, docker-compose.yml e .env.example — o autocrlf do Windows já tinha reescrito o Dockerfile para CRLF uma vez nesta máquina. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,3 +17,6 @@ scripts/backlog
|
|||||||
Dockerfile
|
Dockerfile
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
.dockerignore
|
.dockerignore
|
||||||
|
.env
|
||||||
|
.env.example
|
||||||
|
.gitattributes
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Variáveis da Exportação da SD. Copie para .env (gitignored) para rodar com o
|
||||||
|
# compose; no Coolify em modo Application, cadastre em Environment Variables.
|
||||||
|
# Tudo tem default no Dockerfile — só declare o que quer mudar.
|
||||||
|
|
||||||
|
# ── Lidas DENTRO do container (valem nos dois modos do Coolify) ──────────────
|
||||||
|
|
||||||
|
# Porta do gunicorn dentro do container. No Coolify, a porta exposta da app
|
||||||
|
# tem de ser a mesma.
|
||||||
|
PORT=5000
|
||||||
|
|
||||||
|
# Fuso do carimbo "exportada em" (sessao.marcar_exportada usa datetime.now()).
|
||||||
|
TZ=America/Sao_Paulo
|
||||||
|
|
||||||
|
# Segundos por request. Upload + análise de uma SD leva bem menos que isso;
|
||||||
|
# subir só se o painel mostrar "WORKER TIMEOUT".
|
||||||
|
GUNICORN_TIMEOUT=120
|
||||||
|
|
||||||
|
# debug | info | warning | error
|
||||||
|
GUNICORN_LOG_LEVEL=info
|
||||||
|
|
||||||
|
# ── Lidas pelo COMPOSE ao montar volumes (só no Modo 2 / local) ─────────────
|
||||||
|
# No Modo 1 (Application) não têm efeito: o volume é o Directory mount da UI.
|
||||||
|
|
||||||
|
# Pasta do host com os dados do cliente (tem de conter ses-mg/contrato/itens.yaml).
|
||||||
|
# Fora da pasta que o Coolify clona — ela é recriada a cada deploy.
|
||||||
|
#CLIENTES_DIR=/dados/exporta-sd/clientes
|
||||||
|
|
||||||
|
# Saída da linha de comando (exporta_sd.py --dir). Opcional.
|
||||||
|
#DERIVADOS_DIR=/dados/exporta-sd/_derivados
|
||||||
@@ -3,3 +3,8 @@
|
|||||||
# autocrlf=true do Git no Windows reescreve o arquivo na próxima operação que
|
# autocrlf=true do Git no Windows reescreve o arquivo na próxima operação que
|
||||||
# o toque e o build local passa a falhar mesmo com a imagem correta.
|
# o toque e o build local passa a falhar mesmo com a imagem correta.
|
||||||
*.sh text eol=lf
|
*.sh text eol=lf
|
||||||
|
# Mesma razão para os arquivos de deploy: são lidos por ferramentas Linux e o
|
||||||
|
# autocrlf já reescreveu o Dockerfile para CRLF uma vez nesta máquina.
|
||||||
|
Dockerfile text eol=lf
|
||||||
|
docker-compose.yml text eol=lf
|
||||||
|
.env.example text eol=lf
|
||||||
|
|||||||
@@ -18,3 +18,6 @@ clientes/
|
|||||||
|
|
||||||
# Lock de sessão local do Claude Code — efêmero (PID, sessionId), não é config do projeto.
|
# Lock de sessão local do Claude Code — efêmero (PID, sessionId), não é config do projeto.
|
||||||
.claude/scheduled_tasks.lock
|
.claude/scheduled_tasks.lock
|
||||||
|
|
||||||
|
# Variáveis locais do compose — .env.example é o modelo versionado.
|
||||||
|
.env
|
||||||
|
|||||||
+35
-8
@@ -4,10 +4,23 @@
|
|||||||
# clientes/ses-mg/contrato/itens.yaml em tempo de execução (regras_sd.carregar_canonico),
|
# 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,
|
# 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
|
# `linha_os` é exigido no SD.md) e grava SD.md de volta em
|
||||||
# clientes/ses-mg/projetos/ (escrita_sd.gravar). Esses
|
# clientes/ses-mg/projetos/ (escrita_sd.gravar). Esses são dados do cliente,
|
||||||
# são dados do cliente, versionados fora do git — entram por VOLUME montado em
|
# versionados fora do git — entram por VOLUME montado em /app/clientes. Sem esse
|
||||||
# /app/clientes. Sem esse volume, a imagem sobe e a tela inicial responde, mas o
|
# volume, a imagem sobe e a tela inicial responde, mas o primeiro upload falha
|
||||||
# primeiro upload falha com "Arquivo canônico ausente".
|
# 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 .
|
# Build local: docker build -t exporta-sd .
|
||||||
# Run local: docker run --rm -p 5000:5000 -v "$PWD/clientes:/app/clientes" exporta-sd
|
# Run local: docker run --rm -p 5000:5000 -v "$PWD/clientes:/app/clientes" exporta-sd
|
||||||
@@ -17,18 +30,31 @@ FROM python:3.13-slim
|
|||||||
# PYTHONUNBUFFERED: log do gunicorn sai na hora no painel do Coolify.
|
# 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
|
# LANG/LC_ALL em UTF-8: identificadores de SD trazem "·" (P2·SD8) e o
|
||||||
# escrita_sd monta nome de arquivo a partir deles.
|
# 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
|
||||||
ENV PYTHONUNBUFFERED=1 \
|
ENV PYTHONUNBUFFERED=1 \
|
||||||
PYTHONDONTWRITEBYTECODE=1 \
|
PYTHONDONTWRITEBYTECODE=1 \
|
||||||
LANG=C.UTF-8 \
|
LANG=C.UTF-8 \
|
||||||
LC_ALL=C.UTF-8 \
|
LC_ALL=C.UTF-8 \
|
||||||
PORT=5000
|
PORT=5000 \
|
||||||
|
TZ=America/Sao_Paulo \
|
||||||
|
GUNICORN_TIMEOUT=120 \
|
||||||
|
GUNICORN_LOG_LEVEL=info
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# gosu: o entrypoint precisa iniciar como root (só root faz chown no volume
|
# 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.
|
# 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 \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends gosu \
|
&& apt-get install -y --no-install-recommends gosu tzdata \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Dependências em camada própria: mudar código não reinstala pacote.
|
# Dependências em camada própria: mudar código não reinstala pacote.
|
||||||
@@ -67,8 +93,9 @@ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
|||||||
# é a global ATUAL no processo do servidor.py (uma SD por vez, em memória).
|
# é 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
|
# 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
|
# 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.
|
# 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
|
# --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`.
|
# 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 120 --access-logfile - --error-logfile -"]
|
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 -"]
|
||||||
|
|||||||
+45
-21
@@ -1,47 +1,71 @@
|
|||||||
# Deploy em VPS pelo painel Coolify (recurso do tipo "Docker Compose").
|
# Deploy da Exportação da SD — a imagem é a do Dockerfile; este arquivo é UM dos
|
||||||
|
# dois jeitos de subi-la no Coolify, e serve também para rodar local.
|
||||||
#
|
#
|
||||||
# ANTES do primeiro deploy, ponha os dados do cliente em um caminho FIXO do
|
# ── MODO 1 · Application (Dockerfile) — o que está em produção (2026-09-04) ──
|
||||||
# host, FORA da pasta que o Coolify clona — ele re-clona a cada deploy, e
|
|
||||||
# clientes/ está no .gitignore, então o clone nunca traz esses dados:
|
|
||||||
#
|
#
|
||||||
# scp -r clientes/ usuario@vps:/dados/exporta-sd/clientes
|
# Neste modo o Coolify IGNORA este arquivo por completo: builda o Dockerfile e
|
||||||
|
# configura tudo pela interface. O que o volume e as variáveis daqui dizem tem
|
||||||
|
# de ser reproduzido lá:
|
||||||
#
|
#
|
||||||
# Não precisa de chown manual: o entrypoint.sh da imagem roda como root no
|
# Persistent storage → Add mount → "Directory mount"
|
||||||
# início do container e ajusta o dono do volume para o usuário `sd` antes de
|
# Source Path: /data/coolify/applications/<uuid-da-app>/clientes
|
||||||
# derrubar privilégio e subir o gunicorn — ver entrypoint.sh e o ENTRYPOINT
|
# (o próprio Coolify sugere esse caminho; ele sobrevive
|
||||||
# no Dockerfile. (Uma reconsequência: cada `scp` novo devolve os arquivos ao
|
# a redeploy — é a pasta de storage da app, não o clone)
|
||||||
# usuário do ssh no host; some sozinho no próximo restart do container.)
|
# Destination Path: /app/clientes
|
||||||
# Depois, no Coolify → Environment Variables, aponte o volume para lá:
|
# Mesmo esquema para _derivados, só se for usar a linha de comando.
|
||||||
#
|
#
|
||||||
# CLIENTES_DIR=/dados/exporta-sd/clientes
|
# Dados: a pasta do host nasce VAZIA. Copie o conteúdo para dentro dela —
|
||||||
# DERIVADOS_DIR=/dados/exporta-sd/_derivados # só se usar a linha de comando
|
# aponte para "ses-mg", não para "clientes", ou o nível duplica
|
||||||
|
# (.../clientes/clientes/ses-mg):
|
||||||
#
|
#
|
||||||
# Sem essas variáveis, o padrão ./clientes cai dentro da pasta do clone, o
|
# scp -r "D:/Exporta-SD/clientes/ses-mg" usuario@vps:/data/coolify/applications/<uuid>/clientes/
|
||||||
# compose cria o diretório VAZIO sem avisar e o primeiro upload morre com
|
|
||||||
# "Arquivo canônico ausente".
|
|
||||||
#
|
#
|
||||||
# Resto do painel: porta exposta 5000, réplicas 1 (ver o comentário de -w 1 no
|
# e confira no host: ls /data/coolify/applications/<uuid>/clientes/ses-mg/contrato/
|
||||||
# Dockerfile), e o domínio o Coolify publica via Traefik.
|
# Tem de listar itens.yaml E linhas-os.yaml — sem o segundo, o fallback da
|
||||||
|
# linha de OS desliga e `linha_os` vira pendência só no VPS.
|
||||||
|
#
|
||||||
|
# Variáveis: aba Environment Variables (nomes e defaults em .env.example).
|
||||||
|
# Réplicas: 1 (ver o comentário de -w 1 no Dockerfile).
|
||||||
|
#
|
||||||
|
# ── MODO 2 · Docker Compose (ou local) ──────────────────────────────────────
|
||||||
|
#
|
||||||
|
# O Coolify lê este arquivo. Ponha os dados num caminho fixo do host, fora da
|
||||||
|
# pasta que ele clona, e aponte CLIENTES_DIR para lá (Environment Variables no
|
||||||
|
# Coolify, ou um .env ao lado deste arquivo — copie .env.example). Sem a
|
||||||
|
# variável, o padrão ./clientes cai dentro do clone, o compose cria o
|
||||||
|
# diretório VAZIO sem avisar e o primeiro upload morre com "Arquivo canônico
|
||||||
|
# ausente".
|
||||||
|
#
|
||||||
|
# Nos dois modos não há chown manual: o entrypoint.sh roda como root ao subir
|
||||||
|
# o container, ajusta o dono do volume para o usuário `sd` e só então derruba
|
||||||
|
# privilégio e inicia o gunicorn. Um scp novo devolve os arquivos ao usuário do
|
||||||
|
# ssh; resolve sozinho no próximo restart.
|
||||||
|
|
||||||
services:
|
services:
|
||||||
exporta-sd:
|
exporta-sd:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
image: exporta-sd:local
|
||||||
# Sem `ports:` de propósito: no Coolify o Traefik alcança o container pela
|
# Sem `ports:` de propósito: no Coolify o Traefik alcança o container pela
|
||||||
# rede interna. `expose` publica a porta só para dentro dessa rede.
|
# rede interna. `expose` publica a porta só para dentro dessa rede.
|
||||||
# Para rodar fora do Coolify, troque por: ports: ["5000:5000"].
|
# Para rodar fora do Coolify, troque por: ports: ["5000:5000"].
|
||||||
expose:
|
expose:
|
||||||
- "5000"
|
- "5000"
|
||||||
|
# Só ESPELHA os defaults do Dockerfile — o valor de verdade mora lá (ENV e
|
||||||
|
# ${VAR:-default} no CMD), porque no Modo 1 este bloco nem é lido. Aqui a
|
||||||
|
# única função é deixar o operador trocar pelo .env sem editar YAML.
|
||||||
environment:
|
environment:
|
||||||
PORT: "5000"
|
PORT: ${PORT:-5000}
|
||||||
|
TZ: ${TZ:-America/Sao_Paulo}
|
||||||
|
GUNICORN_TIMEOUT: ${GUNICORN_TIMEOUT:-120}
|
||||||
|
GUNICORN_LOG_LEVEL: ${GUNICORN_LOG_LEVEL:-info}
|
||||||
volumes:
|
volumes:
|
||||||
# OBRIGATÓRIO. O app lê clientes/ses-mg/contrato/itens.yaml a cada
|
# OBRIGATÓRIO. O app lê clientes/ses-mg/contrato/itens.yaml a cada
|
||||||
# análise (e contrato/linhas-os.yaml, se existir, como fallback da linha
|
# análise (e contrato/linhas-os.yaml, se existir, como fallback da linha
|
||||||
# de OS) e grava SD.md em clientes/ses-mg/projetos/. O caminho no
|
# de OS) e grava SD.md em clientes/ses-mg/projetos/. O caminho no
|
||||||
# container é fixo: caminhos.py resolve clientes/ a partir da raiz do
|
# container é fixo: caminhos.py resolve clientes/ a partir da raiz do
|
||||||
# código, que é o WORKDIR /app. Sem esta montagem, a tela inicial abre
|
# código, que é o WORKDIR /app.
|
||||||
# e o primeiro upload falha com "Arquivo canônico ausente".
|
|
||||||
- ${CLIENTES_DIR:-./clientes}:/app/clientes
|
- ${CLIENTES_DIR:-./clientes}:/app/clientes
|
||||||
# Saída da linha de comando (exporta_sd.py --dir _derivados/export).
|
# Saída da linha de comando (exporta_sd.py --dir _derivados/export).
|
||||||
# A aplicação web entrega o JSON pelo download, não por aqui.
|
# A aplicação web entrega o JSON pelo download, não por aqui.
|
||||||
|
|||||||
Reference in New Issue
Block a user