Começar
Aparência

Vale na hora e fica lembrada neste navegador.

A plataforma

13 · Containers, implantação e segredos

Docker empacota cada aplicação; Docker Compose monta o ambiente de desenvolvimento; Kamal implanta na VPS; Bitwarden Secrets Manager guarda os segredos. Deploy é sempre do mantenedor.

Do código à produção

Do código à produção. Na estação de trabalho: build e testes, docker build com o contexto na raiz e o compose.yaml com o ambiente local. O mantenedor faz o push, e a CI roda build, testes e verificações. Para implantar, o mantenedor exporta o acesso ao Bitwarden e roda kamal deploy: o Kamal constrói a imagem, envia ao ghcr.io e sobe o container na VPS, atrás do kamal-proxy, com os segredos resolvidos na hora; banco e backup são accessories. estação de trabalhobuild + testesdocker buildcontexto: a raizcompose.yamlbanco · Keycloak · apps, localCI · GitHub Actionsbuild, testese verificaçõespush (mantenedor)mantenedorexport BWS_ACCESS_TOKEN BWS_PROJECT_IDkamal deployde deploy/<app>/ghcr.ioas imagensVPSkamal-proxyTLScontainer da appdbaccessorybackupaccessoryconstrói e enviaa imagemsegredos, na hora

Docker: a imagem de cada aplicação

Regras para todo deploy/<app>/Dockerfile:

RegraPor quê
Multi-stage: um estágio de build, um de runtime enxutoPor quê: imagem pequena, sem ferramentas de build em produção
Contexto de build na raiz do repositório; o build compila só a aplicaçãoPor quê: módulos compartilhados ficam acessíveis; imagem não carrega o resto
Imagens base com tag completa (e digest nas de runtime)Por quê: versões fixas (ORG-0010)
Usuário não-rootPor quê: segurança
EXPOSE só a porta da aplicação; a porta de métricas não é expostaPor quê: métricas ficam internas
Cache de dependências do build montado (--mount=type=cache)Por quê: build rápido
Configuração por variável de ambiente, sem arquivo por ambiente na imagemPor quê: a mesma imagem roda local e em produção
.dockerignore exclui docs/, e2e/, openspec/, build/Por quê: contexto pequeno; acervo de capturas fora

Site estático: nginx com tag completa, HTML + CSS, variáveis por envsubst no entrypoint, sem JavaScript se o produto assim decidir.

Docker Compose: o ambiente local

O ambiente local no compose.yaml: o banco, com credenciais óbvias e sem volume nomeado; o Keycloak, que importa os realms de desenvolvimento; as aplicações, construídas localmente; o site estático, se houver. Num perfil à parte, observability, um Tempo e um Grafana mínimos recebem os traces das aplicações. Tudo sobe do zero e é descartável. compose.yaml · projeto <produto>app-abuild localapp-bbuild localestáticose houverdbcredenciais óbvias, sem volumekeycloakimporta eng/auth/dev-realmsperfil observabilitytempografanaOTLPdocker compose up: tudo do zero, descartável

Dois caminhos, ambos documentados em docs/guias/desenvolvimento-local.md:

CaminhoComandoQuando
1 · Apps nativas + infraestrutura em container (recomendado)Comando: eng/dev-db.sh up · docker compose up -d keycloak · rodar cada app nativamenteQuando: dia a dia: recarga rápida, depurador
2 · Tudo em containerComando: docker compose up --buildQuando: validar a imagem antes de um deploy

Regras do Compose:

  • descartável: sem volume nomeado; eng/dev-db.sh reset zera;
  • derrubar e zerar alcançam todos os perfis: o down do eng/dev-db.sh é docker compose --profile '*' down --volumes --remove-orphans. Com só os perfis padrão, um container parado de outro perfil sobrevive preso à rede apagada, e o up seguinte falha com network … not found;
  • credenciais óbvias e falsas (<produto>/<produto>, admin/admin, dev-secret-<client>), que só existem aqui;
  • mesmas imagens e tags do deploy.yml e dos testes (conferido pela CI);
  • portas locais fixas e documentadas na ficha (uma por aplicação, mais a de métricas publicada só no local);
  • observabilidade local num perfil (--profile observability), que não sobe por padrão.

Kamal: a implantação

Cada aplicação tem deploy/<app>/ e o Kamal roda de dentro dessa pasta. Há um ambiente implantado só, a produção (DEPLOYMENT_ENVIRONMENT=production); o outro é o local, no Compose. Um ambiente implantado de desenvolvimento ainda não existe na arquitetura (12).

Texto
deploy/<app>/
├── Dockerfile
├── config/deploy.yml     # serviço, host, proxy, env, accessories
└── .kamal/secrets        # SÓ referências ao Bitwarden

O esqueleto, pronto para copiar, está no deploy/README.md do modelo. Elementos fixos de todo deploy.yml; os valores da plataforma vêm do contrato:

ElementoValor
serviceValor: nome da aplicação
imageValor: hibex-solutions/<app> no ghcr.io
servers.web.hostsValor: a VPS (contrato; a lista, na ADR-0007 da stack)
servers.web.options.network-aliasValor: o nome da aplicação: o alvo do Prometheus não muda entre versões (o Kamal 2 muda o nome do container a cada uma)
proxy.hostValor: o endereço da aplicação (ORG-0017; a lista, na ADR-0005 da stack)
proxy.app_portValor: a porta da aplicação
proxy.sslValor: true
proxy.healthcheck.pathValor: /up
proxy.run.metrics_portValor: 9091 (contrato)
registryValor: ghcr.io, com KAMAL_REGISTRY_USERNAME e KAMAL_REGISTRY_PASSWORD (contrato)
builderValor: arch: amd64 (contrato), context: ../.., dockerfile: Dockerfile
ssh.userValor: ubuntu (contrato)
env.clear.DEPLOYMENT_ENVIRONMENTValor: production
env.clear da porta interna de métricasValor: a porta da ficha, nunca publicada
env.clear.OTEL_EXPORTER_OTLP_ENDPOINTValor: http://toolbox-observability-alloy:4318 (contrato)
env.clear do emissor OIDCValor: https://id.hibex.co/realms/<realm> (contrato)
env.secretValor: só nomes; valores vêm do .kamal/secrets
accessoriesValor: os armazenamentos e o backup, no deploy.yml da aplicação dona do schema

O contrato da plataforma

O que o toolbox garante a toda stack, para ela preencher o deploy.yml sem ler o toolbox. Cada valor tem a fonte lá. Mudar um deles muda todas as stacks: o toolbox não o faz sem uma versão nova da arquitetura que traga o valor novo, e o "Para as stacks" do CHANGELOG.md diz a troca.

ValorO quêFonte no toolbox
web.ovhcloudlab.hibex.netO quê: a VPS das aplicaçõesFonte no toolbox: servers de apps/*/config/deploy.yml
ubuntuO quê: ssh.user: o deploy entra por SSH sem rootFonte no toolbox: ssh de apps/*/config/deploy.yml
amd64O quê: builder.archFonte no toolbox: builder de apps/*/config/deploy.yml
ghcr.io, hibex-solutions/<app>O quê: o registry e a imagem; o acesso pelos segredos KAMAL_REGISTRY_USERNAME e KAMAL_REGISTRY_PASSWORDFonte no toolbox: registry de apps/*/config/deploy.yml
9091O quê: proxy.run.metrics_port, a configuração global do kamal-proxy do host, igual em todo deploy.ymlFonte no toolbox: apps/identity/config/deploy.yml e apps/observability/config/deploy.yml
https://id.hibex.co/realms/<realm>O quê: o emissor OIDC do Keycloak de produçãoFonte no toolbox: KC_HOSTNAME de apps/identity/config/deploy.yml
kamalO quê: a rede de todos os containers e accessories do host, a mesma do PrometheusFonte no toolbox: apps/observability/prometheus/prometheus.yml
<service>-<accessory>O quê: o nome de um accessory na rede (o db do atendimento-console é atendimento-console-db)Fonte no toolbox: apps/observability/config/deploy.yml
job_name: <app>, alvo <app>:<porta de métricas>O quê: como o Prometheus coleta a aplicação (static_configs)Fonte no toolbox: apps/observability/prometheus/prometheus.yml
http://toolbox-observability-alloy:4318O quê: o endereço OTLP sobre HTTP dos traces, que o Alloy entrega ao TempoFonte no toolbox: otelcol.receiver.otlp de apps/observability/alloy/config.alloy (ADR-0037)
traceIdO quê: o campo do log JSON, com o id do trace em hexadecimal minúsculo, que liga log e trace no Grafana, nos dois sentidosFonte no toolbox: derivedFields de apps/observability/grafana/provisioning/datasources/datasources.yml (ADR-0038)
prometheus, tempoO quê: os uids das fontes de dados que o painel da stack usaFonte no toolbox: apps/observability/grafana/provisioning/datasources/datasources.yml
quay.io/keycloak/keycloak:26.7.1O quê: a imagem do Keycloak de produção: a do compose.yaml e dos testes de integração, já na ficha do modeloFonte no toolbox: apps/identity/Dockerfile (ADR-0010)
bitwarden-sm, BWS_ACCESS_TOKEN, BWS_PROJECT_IDO quê: o adapter do .kamal/secrets e o que o mantenedor exporta antes do deployFonte no toolbox: docs/guias/gerenciar-segredos-de-aplicacoes.md

Regras dos accessories

RegraPor quê
Nenhum accessory publica porta no hostPor quê: duas publicações iguais no mesmo host colidem; as aplicações os alcançam pelo nome, na rede kamal
Imagem que roda com outro UID leva directories com modePor quê: o deploy entra sem root e não consegue chown
Um arquivo montado (files:) só sobe de novo num kamal accessory boot ou reboot, nunca no kamal deployPor quê: mudou um script de backup ou uma configuração montada: kamal accessory reboot <nome>. Depois do reboot, o serviço leva uns segundos para responder (no Prometheus, uns 30 s até a primeira coleta)
A imagem de um armazenamento pode exigir um kernel ou uma CPU da VPSPor quê: o guia de implantação traz a conferência (por exemplo, cat /proc/version_signature e grep -c avx2 /proc/cpuinfo) antes do primeiro boot

Segredos: Bitwarden Secrets Manager

Segredos no deploy: o mantenedor exporta o token e o projeto do Bitwarden e roda kamal deploy. O Kamal avalia o .kamal/secrets, que só tem linhas de referência: elas buscam os segredos do projeto no Bitwarden e extraem cada um pelo nome com prefixo. O container sobe na VPS com os segredos como variáveis de ambiente. No repositório não há nenhum valor. export BWS_*kamal deploy.kamal/secretsSECRETS=$(kamal secrets fetch--adapter bitwarden-sm $BWS_PROJECT_ID/all)KAMAL_REGISTRY_PASSWORD=$(kamalsecrets extract <PREFIXO>_<NOME> $SECRETS)só referências: nenhum valorBitwarden SMum projeto por appfetchos segredosVPS: o containercom os segredos como variáveis de ambienteextracto agente escreve as referências;o valor, nunca: nos documentos, <VALOR>

Regras:

  • um projeto no Bitwarden para o produto, lido por todas as aplicações, e todo segredo com o prefixo do produto (ATENDIMENTO_). O que as aplicações compartilham, como a senha de um armazenamento, é um nome só; o que cada uma tem o seu leva o nome dela (ATENDIMENTO_CONSOLE_OIDC_CLIENT_SECRET);
  • o .kamal/secrets tem só referências; todo $(...) abre e fecha na mesma linha (toolbox ADR-0031);
  • segredos de terceiros usados por scripts (ex.: credenciais do IdP Google) também vêm do Bitwarden, exportados pelo mantenedor na hora;
  • o agente escreve as referências e os nomes, nunca valores; nos documentos, <VALOR>.

O mínimo no Bitwarden: três tipos de linha

O Kamal roda cada $(kamal secrets …) dentro do próprio processo, sem shell: não há || echo padrão, e um nome que falta no Bitwarden para o deploy inteiro. O .kamal/secrets é lido como um .env, que troca ${NOME} pelo valor de uma linha anterior, mas não conhece ${NOME:-padrão}. Com isso, cada variável é de um de três tipos:

TipoA linhaVai ao Bitwarden
ObrigatóriaA linha: NOME=$(kamal secrets extract <PREFIXO>_NOME $SECRETS)Vai ao Bitwarden: sim; sem ela, o deploy para com Could not find secret
OpcionalA linha: a mesma extração, com o padrão na linha de padrões, logo depois da buscaVai ao Bitwarden: só para trocar o padrão: o valor do Bitwarden ganha
CompostaA linha: NOME=<texto fixo com ${OUTRA}>, montada ali, como uma URI com a senhaVai ao Bitwarden: nunca

A linha de padrões usa o Ruby que o Kamal já exige:

Terminal
SECRETS=$(ruby -rjson -rshellwords -e 'defaults = {"<PREFIXO>_NATS_USER" => "<padrão>"}; print JSON.dump(defaults.merge(JSON.parse(ARGV[0]))).shellescape' $SECRETS)
  • Todo valor que se deduz de partes fixas e de outros segredos é composto, e não se cadastra: a URI do banco se monta com a senha;
  • só um nome de usuário pode ter padrão, porque um padrão está no repositório;
  • uma senha que entra numa URI é gerada com openssl rand -hex: um @, : ou / quebraria a URI composta;
  • o eng/deploy-secrets.sh confere tudo isso, e que cada nome do deploy.yml tem a sua linha, na CI.

O guia de implantação da stack cataloga todas as variáveis, em quatro tabelas: obrigatórias (o nome completo no Bitwarden, pronto para copiar, quem usa e como gerar), opcionais (o padrão e quando trocar), compostas (como se montam) e fixas (no deploy.yml ou na imagem). Para conferir o que o Kamal monta sem expor valores: kamal secrets print | cut -d= -f1, de dentro de deploy/<app>/.

Endereços

Política de domínios do toolbox, aplicada a cada aplicação pela ORG-0017:

Endereços pela política de domínios do toolbox: o usuário final, cliente ou público, usa <app>.hibex.app; os colaboradores da plataforma usam <app>.<plataforma>.hibex.net; uma porta de entrada comercial, por exceção e com ADR, usa <nome>.hibex.co. Públicos diferentes ficam em domínios registráveis diferentes, sem cookie compartilhado. quem usa?<app>.hibex.appusuário finalcliente, público<app>.<plataforma>.hibex.netcolaboradoresda plataforma<nome>.hibex.coporta comercialexceção, com ADRpúblicos diferentes, domínios registráveis diferentes: nenhum cookie compartilhado

Aplicações para públicos diferentes ficam em domínios registráveis diferentes: nenhum cookie de sessão é compartilhado entre elas por construção.

Quem faz o quê

AçãoAgenteMantenedor
Escrever Dockerfile, deploy.yml, .kamal/secrets (referências)Agente: ✓Mantenedor: revisa
docker build, docker compose up localAgente: ✓Mantenedor: ✓
Criar projeto e segredos no BitwardenAgente: —Mantenedor: ✓
kamal setup, kamal deploy, kamal accessory …Agente: nuncaMantenedor: ✓
ssh na VPSAgente: nuncaMantenedor: ✓
DNSAgente: —Mantenedor: ✓ (guia do toolbox)
Instruções de deploy ao fim da changeAgente: ✓ escreveMantenedor: ✓ executa

O primeiro deploy (portão 3)

Checklist do guia docs/guias/implantacao.md, que o mantenedor marca. A ordem importa: cada passo depende do anterior.

  • Ferramentas e acesso: Kamal e bws nas versões da ficha; BWS_ACCESS_TOKEN e BWS_PROJECT_ID do projeto do produto exportados, sem eco;
  • DNS de cada endereço apontado para a VPS;
  • Identidade, antes de qualquer deploy: o realm existe no Keycloak real; provision.sh rodado; o segredo de cada client no Bitwarden. Sem ele, o kamal deploy para com Could not find secret;
  • Os demais segredos obrigatórios no projeto, conferidos pelos nomes (kamal secrets print | cut -d= -f1);
  • A VPS: o kernel e a CPU que as imagens dos armazenamentos exigem, como o guia confere;
  • Os accessories da dona do schema (kamal accessory boot all) e os preparos de uma vez só que o guia lista;
  • O deploy da dona do schema, depois o das demais; /up respondendo em cada uma;
  • O toolbox: o job_name de cada aplicação no Prometheus e o painel, com os blocos prontos do 14; os traces conferidos no Tempo;
  • login com um usuário de cada perfil; logs no Grafana;
  • o primeiro backup agora, sem esperar a agenda, e a restauração testada localmente com ele.