🛰️ Atlas PaaS — Arquitetura & Tutorial de Deploy

Como o paas_form (Odoo) + paas-devops (VM) provisionam e operam apps estáticos com Coolify, Gitea, gate de SSO e wildcard TLS.

repo: paas-devopsrepo: paas-form alvo: VM Azure (PaasMachine)validado em VM real

1. Visão geral — quem faz o quê

🧩 paas_form (addon Odoo)

Roda no apphub (Odoo). É a interface: o usuário preenche um formulário, faz upload de um .html/.zip e clica em Deploy. O addon então fala com a infra da VM por API.

  • Cria o repositório e commita os arquivos no Gitea
  • Cria a aplicação e dispara o deploy no Coolify
  • Expõe o endpoint do gate (SSO/OTP) que o Traefik consulta
  • Lê status, gerencia promoção staging→produção (PR)

🖥️ paas-devops (a VM)

Scripts bash idempotentes (steps/00…70) que provisionam tudo numa VM Ubuntu, do zero: Docker, Coolify, Gitea, acme-dns, gate e o wildcard TLS.

  • Todas as imagens vêm de um mirror ghcr privado (nunca docker.io)
  • Gitea interno (IP fixo), Coolify orquestra os builds/containers
  • Wildcard *.<suffix> via acme-dns esconde os nomes dos apps do Certificate Transparency
  • make bootstrap / make update / make teardown

2. 🗺️ Mapa da arquitetura

Os números n nas setas correspondem à legenda de ações abaixo do desenho.

CLIENTES ODOO (apphub) VM PaaS (Azure) — Docker EXTERNOS 👤 Usuário final navegador → app/gate 🛠️ Dev / Admin SSH · make bootstrap 📋 paas_form addon Odoo (apphub) • form + upload html • Gitea/Coolify client • /gate/verify (SSO/OTP) • status / promoção PR Settings → PaaS (URLs, tokens, uuids) 🚦 Traefik (coolify-proxy) :80/:443 + gate (forwardAuth) · TLS wildcard *.suffix dynamic: paas-gate.yaml · wildcard-tls.yaml ⚙️ Coolify :8000 orquestra build+run db · redis · realtime servidor localhost id 0 📦 Gitea IP fixo 10.0.x.250 repos dos apps interno (opt-in :3000) 🔐 acme-dns :53 DNS-01 do wildcard zona acme.<dominio> ⏱️ timers (systemd) gate-sync (forwardAuth) wildcard-sync (lego) 🌐 Apps deployados — web-<uuid> container nginx (base ghcr static-web) na rede coolify host: <slug>-staging.<suffix> / <slug>.<suffix> 🐙 ghcr mirror PRIVADO infra + static-web 🔏 Let's Encrypt HTTP-01 (coolify/gitea) DNS-01 (wildcard) 🌍 DNS GoDaddy / Cloudflare A * · acme NS/A _acme-challenge CNAME 🧱 Squid egress proxy (opt-in) 10.3.0.28:3128 1 2 3 4 5 6 7 8 9 10 11 12 13

Legenda de ações

1Usuário → Traefik: abre https://<slug>-staging.<suffix>.
2Gate (Traefik) → Odoo: forwardAuth consulta /paas_form/gate/verify (SSO/OTP) antes de liberar o app.
3paas_form → Gitea API: cria o repo atlasren/<slug> e commita site/ + Dockerfile + docker-compose.yml.
4paas_form → Coolify API: cria a app (dockercompose, deploy key) e dispara o deploy.
5Coolify → Gitea: clona o repo via git@10.0.x.250 (chave SSH / deploy key).
6Coolify build → ghcr: docker build FROM ghcr.io/.../static-web + COPY site.
7Coolify → App: sobe o container web-<uuid> na rede coolify.
8Traefik → App: roteia o host do app, com o cert wildcard + o gate.
9wildcard-sync → acme-dns + LE: lego pede *.<suffix> (DNS-01), grava o TXT no acme-dns.
10Let's Encrypt → acme-dns (53): valida o TXT do desafio (porta 53 pública).
11DNS → acme-dns: o _acme-challenge.<suffix> é um CNAME para a zona do acme-dns.
12VM → ghcr: todo o stack de infra puxa do mirror privado (postgres/redis/traefik/gitea).
13Dev/Admin → VM: make deploy (rsync+SSH) ou sudo -E make bootstrap na VM.

3. Fluxo de um deploy (staging)

1
Usuário no Odoo preenche o form (nome, descrição), faz upload de index.html/.zip e clica Deploy staging.
2
paas_form → Gitea: cria atlasren/<slug>, garante a branch staging e commita site/* + Dockerfile (FROM ghcr static-web) + docker-compose.yml.
3
paas_form → Coolify: cria as apps (staging+produção, dockercompose, deploy key SSH) e trigger_deploy na staging.
4
Coolify clona o repo (git@10.0.x.250), faz docker build (puxa a base do ghcr) e sobe o container web-<uuid>.
5
gate-sync (timer) detecta o app e escreve o router no Traefik com forwardAuth (gate) + TLS. Com o wildcard ativo, o router usa tls:{} (sem cert per-app → nada vaza no Certificate Transparency).
6
Usuário acessa https://<slug>-staging.<suffix> → Traefik → gate (SSO/OTP no Odoo) → app servindo o HTML.
Promoção → produção: o paas_form abre um PR staging→main no Gitea; ao mergear, o webhook do Gitea dispara o deploy da app de produção no Coolify (<slug>.<suffix>).

4. Tutorial — aplicar na VM Azure (PaasMachine), passo a passo

4.1 Pré-requisitos

ItemDetalhe
VMUbuntu 24.04, x86/amd64 (o mirror ghcr é amd64). Azure PaasMachine = 20.106.196.125, user paasuser, SSH porta 31847.
Portas (NSG Azure)liberar 22/SSH, 80, 443 e 53 (UDP+TCP) para o acme-dns (origem Any — o LE valida de IPs variáveis).
PAT GitHubtoken só com read:packages → o daemon loga no ghcr e puxa o mirror privado.
Domínio + DNScontrole do DNS de atlasren.com (GoDaddy ou Cloudflare) para criar A wildcard + os 3 registros do acme-dns.
EgressAzure NSG fechado → setar EGRESS_PROXY_URL=http://10.3.0.28:3128 (Squid). Se aberto → deixar vazio.

4.2 Preparar a VM

# liberar a porta 53 do systemd-resolved (acme-dns usa a 53)
sudo sed -i 's/^#\?DNSStubListener=.*/DNSStubListener=no/' /etc/systemd/resolved.conf
sudo ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf
sudo systemctl restart systemd-resolved

4.3 Preencher o config.env

Na sua máquina, no repo paas-devops: cp config.env.example config.env e edite (ver tabela completa de variáveis). Exemplo de produção (Azure):

PROFILE=prod
DOMAIN_SUFFIX=paas.atlasren.com
COOLIFY_DOMAIN=coolify.paas.atlasren.com
GITEA_DOMAIN=gitea.paas.atlasren.com
SSH_PORT=31847
ACME_EMAIL=devops@atlasren.com

COOLIFY_VERSION=4.1.2
COOLIFY_ADMIN_EMAIL=admin@atlasren.com
COOLIFY_ADMIN_PASSWORD=<senha-forte>

EGRESS_PROXY_URL=http://10.3.0.28:3128   # Azure egress fechado (vazio se aberto)

WILDCARD_TLS_ENABLED=1
ACME_DNS_ZONE=acme.atlasren.com
VM_PUBLIC_IP=20.106.196.125
ACME_CA_SERVER=https://acme-staging-v02.api.letsencrypt.org/directory  # staging no 1º teste

GHCR_NS=ghcr.io/atlasren-partners
GHCR_USER=<login-github>
GHCR_TOKEN=<PAT read:packages>

GITEA_ORG=atlasren
GITEA_ADMIN_USER=gitea_admin
GITEA_ADMIN_PASS=<senha-forte>
GITEA_BOT_USER=paas-bot
GITEA_BOT_PASS=<senha-forte>

GITEA_HTTP_PUBLISH_PORT=          # VAZIO em prod (gitea interno). 3000 só p/ teste por IP
GATE_VERIFY_URL=https://apphub-staging.atlasren.com/paas_form/gate/verify

COOLIFY_SSO_AZURE_CLIENT_ID=<...>     # SSO opcional no Coolify
COOLIFY_SSO_AZURE_CLIENT_SECRET=<...>
COOLIFY_SSO_AZURE_TENANT=<...>
Em produção, nunca re-rode a bootstrap inteira às cegas. Aplique por step, em janela. O step 10 (instalador do Coolify) puxa postgres/redis/traefik do docker.io no primeiro install (antes do step 15 re-apontar). Em uma VM já viva, prefira make deploy STEP=NN.

4.4 Rodar o bootstrap

# da sua máquina (rsync do repo + roda na VM por SSH):
make deploy                 # bootstrap completo na VM
make deploy STEP=15         # só um step (em janela)

# OU diretamente na VM:
sudo -E make bootstrap

Ordem dos steps: 00 vm-prep · 05 egress (direto ou Squid) · 10 Coolify (+setup+proxy) · 15 re-point infra→ghcr · 20 Coolify config (admin, token, fqdn, SSO) · 30 Gitea · 40 Gitea config (admin/bot/deploy-key) · 50 gate · 55 wildcard (acme-dns) · 60 dns/tls · 70 credenciais.

4.5 DNS — os registros

Base (uma vez): A *.paas → IP (e A coolify, A gitea se não usar wildcard de 1 nível). O step 55 imprime os 3 registros do acme-dns — crie no DNS:

TipoNomeConteúdo
AacmeIP da VM
NSacmeacme.atlasren.com (delega a subzona)
CNAME_acme-challenge.paas<fulldomain>.acme.atlasren.com (o step 55 imprime)
No Cloudflare: todos esses registros DNS only (nuvem cinza) — proxied quebra a 53/LE/Traefik.

4.6 Emitir o wildcard

O wildcard-sync.timer re-tenta sozinho quando o CNAME propaga. Para validar e depois trocar pra produção:

sudo systemctl start wildcard-sync.service
sudo openssl x509 -in /data/coolify/proxy/certs/wildcard.crt -noout -ext subjectAltName   # DNS:*.paas...
# validado no staging → trocar ACME_CA_SERVER= (vazio) no config.env e re-rodar step 55

4.7 Verificação

5. config.env — todas as variáveis de entrada

VariávelO quêExemplo / prod
PROFILEprod (LE, domínios reais) ou test (DinD, nip.io)prod
DOMAIN_SUFFIXsufixo dos apps; injetado no gatepaas.atlasren.com
COOLIFY_DOMAIN / GITEA_DOMAINFQDN do dashboard Coolify / do Giteacoolify.paas… / gitea.paas…
SSH_PORT · ACME_EMAILporta SSH (UFW) · conta LE (lego)31847 · devops@…
COOLIFY_VERSIONpinar (:latest muda API/proxy)4.1.2
COOLIFY_ADMIN_EMAIL / _PASSWORDadmin logável do Coolify (UI)login
EGRESS_PROXY_URLvazio=direto · setado=Squid (Azure fechado)http://10.3.0.28:3128
WILDCARD_TLS_ENABLED1 ativa o wildcard via acme-dns1
ACME_DNS_ZONE · VM_PUBLIC_IPzona do acme-dns · IP públicoacme.atlasren.com · IP
ACME_CA_SERVERvazio=LE prod · staging p/ testarstaging→vazio
GHCR_NS / GHCR_USER / GHCR_TOKENmirror privado + PAT read:packagesghcr.io/atlasren-partners
GITEA_ORG · GITEA_ADMIN_* · GITEA_BOT_*org + admin + bot (token do paas_form)atlasren · login
GITEA_HTTP_PUBLISH_PORTvazio=interno (prod) · 3000=acesso por IP (teste)vazio em prod
GATE_VERIFY_URLendpoint do gate no Odoohttps://apphub…/paas_form/gate/verify
COOLIFY_SSO_AZURE_*SSO Azure AD no Coolify (opcional)client_id / secret / tenant

O config.env é gitignored. Na VM vive em /opt/paas-devops/config.env (chmod 600). No CI ele viaja dentro do secret PAAS_CONFIG_ENV.

6. Saídas → onde configurar no Odoo (Settings → PaaS)

O step 70 imprime, e o arquivo paas-devops/out/credentials.env guarda, os valores gerados. Copie para Settings → PaaS do apphub (Odoo):

Settings → PaaS (Odoo)Vem de (credentials.env)Modo domínio (prod)Modo IP (teste)
Gitea URLhttps://gitea.paas.atlasren.comhttp://<IP>:3000
Gitea URL (internal)GITEA_URL_INTERNALgit@10.0.x.250 (o Coolify clona — sempre interno)
Gitea tokenGITEA_API_TOKENtoken do paas-bot
Gitea orgGITEA_ORGatlasren
Coolify URLhttps://coolify.paas.atlasren.comhttp://<IP>:8000
Coolify tokenCOOLIFY_API_TOKENid|plain (tem |)
Deploy Key UUID (SSH)COOLIFY_PRIVATE_KEY_UUIDchave que o Coolify usa pra clonar o Gitea
Default Project UUIDCOOLIFY_PROJECT_UUIDprojeto atlasren
Default Server UUIDCOOLIFY_SERVER_UUIDservidor localhost
Domain suffixDOMAIN_SUFFIXpaas.atlasren.com
A tela de Settings do Odoo só persiste ao clicar "Salvar" (topo). Se editar via API/shell com o servidor no ar, reinicie o Odoo (cache de get_param por processo) e dê Ctrl+Shift+R. A "Static base image" foi removida — a base é fixa no código (ghcr.io/.../static-web).

7. Operação do dia a dia

🔄 Atualizar (preserva dados)

sudo make update — re-pull das imagens (Coolify + mirror) e up -d sem -v; re-aplica steps 15/30/40/50/55/60. Nunca toca em volumes/bancos. Pula installer e rotação de token (10/20).

🧹 Limpar tudo

sudo make teardown — remove Coolify/Gitea/acme-dns/apps/volumes/units (confirma digitando o hostname). ARGS=--purge-docker remove até o Docker+swap. Irreversível.

8. Especificidades da VM Azure (vs. a de teste)

AspectoAzure (prod)Teste (Contabo/Hetzner)
Egressfechado no NSG → EGRESS_PROXY_URL=http://10.3.0.28:3128 (Squid)aberto → EGRESS_PROXY_URL= vazio
GiteainternoGITEA_HTTP_PUBLISH_PORT= vazio (acesso só via domínio)3000 p/ acesso por IP (Odoo fora da VM)
TLSLE produção (cert válido) → ACME_CA_SERVER= vazioLE staging no 1º teste
Odooapphub real (apphub-staging.atlasren.com) aponta pros domínios httpsOdoo dev (WSL) aponta pros IPs http
SSO CoolifyAzure AD (COOLIFY_SSO_AZURE_*)
Por que esconder os nomes dos apps (wildcard): com cert per-app, cada <slug>.atlasren.com aparece no Certificate Transparency (crt.sh) e scanners sondam. O wildcard *.paas.atlasren.com publica só o coringa — os nomes dos apps nunca vazam.

Gerado a partir do estado validado em VM real · repos atlasren-partners/paas-devops + paas-form · branch feat/offline-coolify-deploy.