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
Docker: a imagem de cada aplicação
Regras para todo deploy/<app>/Dockerfile:
| Regra | Por quê |
|---|---|
| Multi-stage: um estágio de build, um de runtime enxuto | Por quê: imagem pequena, sem ferramentas de build em produção |
| Contexto de build na raiz do repositório; o build compila só a aplicação | Por 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-root | Por quê: segurança |
EXPOSE só a porta da aplicação; a porta de métricas não é exposta | Por 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 imagem | Por 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
Dois caminhos, ambos documentados em docs/guias/desenvolvimento-local.md:
| Caminho | Comando | Quando |
|---|---|---|
| 1 · Apps nativas + infraestrutura em container (recomendado) | Comando: eng/dev-db.sh up · docker compose up -d keycloak · rodar cada app nativamente | Quando: dia a dia: recarga rápida, depurador |
| 2 · Tudo em container | Comando: docker compose up --build | Quando: validar a imagem antes de um deploy |
Regras do Compose:
- descartável: sem volume nomeado;
eng/dev-db.sh resetzera; - derrubar e zerar alcançam todos os perfis: o
downdoeng/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 oupseguinte falha comnetwork … not found; - credenciais óbvias e falsas (
<produto>/<produto>,admin/admin,dev-secret-<client>), que só existem aqui; - mesmas imagens e tags do
deploy.ymle 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).
deploy/<app>/
├── Dockerfile
├── config/deploy.yml # serviço, host, proxy, env, accessories
└── .kamal/secrets # SÓ referências ao BitwardenO 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:
| Elemento | Valor |
|---|---|
service | Valor: nome da aplicação |
image | Valor: hibex-solutions/<app> no ghcr.io |
servers.web.hosts | Valor: a VPS (contrato; a lista, na ADR-0007 da stack) |
servers.web.options.network-alias | Valor: 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.host | Valor: o endereço da aplicação (ORG-0017; a lista, na ADR-0005 da stack) |
proxy.app_port | Valor: a porta da aplicação |
proxy.ssl | Valor: true |
proxy.healthcheck.path | Valor: /up |
proxy.run.metrics_port | Valor: 9091 (contrato) |
registry | Valor: ghcr.io, com KAMAL_REGISTRY_USERNAME e KAMAL_REGISTRY_PASSWORD (contrato) |
builder | Valor: arch: amd64 (contrato), context: ../.., dockerfile: Dockerfile |
ssh.user | Valor: ubuntu (contrato) |
env.clear.DEPLOYMENT_ENVIRONMENT | Valor: production |
env.clear da porta interna de métricas | Valor: a porta da ficha, nunca publicada |
env.clear.OTEL_EXPORTER_OTLP_ENDPOINT | Valor: http://toolbox-observability-alloy:4318 (contrato) |
env.clear do emissor OIDC | Valor: https://id.hibex.co/realms/<realm> (contrato) |
env.secret | Valor: só nomes; valores vêm do .kamal/secrets |
accessories | Valor: 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.
| Valor | O quê | Fonte no toolbox |
|---|---|---|
web.ovhcloudlab.hibex.net | O quê: a VPS das aplicações | Fonte no toolbox: servers de apps/*/config/deploy.yml |
ubuntu | O quê: ssh.user: o deploy entra por SSH sem root | Fonte no toolbox: ssh de apps/*/config/deploy.yml |
amd64 | O quê: builder.arch | Fonte 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_PASSWORD | Fonte no toolbox: registry de apps/*/config/deploy.yml |
9091 | O quê: proxy.run.metrics_port, a configuração global do kamal-proxy do host, igual em todo deploy.yml | Fonte 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ção | Fonte no toolbox: KC_HOSTNAME de apps/identity/config/deploy.yml |
kamal | O quê: a rede de todos os containers e accessories do host, a mesma do Prometheus | Fonte 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:4318 | O quê: o endereço OTLP sobre HTTP dos traces, que o Alloy entrega ao Tempo | Fonte no toolbox: otelcol.receiver.otlp de apps/observability/alloy/config.alloy (ADR-0037) |
traceId | O quê: o campo do log JSON, com o id do trace em hexadecimal minúsculo, que liga log e trace no Grafana, nos dois sentidos | Fonte no toolbox: derivedFields de apps/observability/grafana/provisioning/datasources/datasources.yml (ADR-0038) |
prometheus, tempo | O quê: os uids das fontes de dados que o painel da stack usa | Fonte no toolbox: apps/observability/grafana/provisioning/datasources/datasources.yml |
quay.io/keycloak/keycloak:26.7.1 | O quê: a imagem do Keycloak de produção: a do compose.yaml e dos testes de integração, já na ficha do modelo | Fonte no toolbox: apps/identity/Dockerfile (ADR-0010) |
bitwarden-sm, BWS_ACCESS_TOKEN, BWS_PROJECT_ID | O quê: o adapter do .kamal/secrets e o que o mantenedor exporta antes do deploy | Fonte no toolbox: docs/guias/gerenciar-segredos-de-aplicacoes.md |
Regras dos accessories
| Regra | Por quê |
|---|---|
| Nenhum accessory publica porta no host | Por 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 mode | Por 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 deploy | Por 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 VPS | Por 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
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/secretstem 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:
| Tipo | A linha | Vai ao Bitwarden |
|---|---|---|
| Obrigatória | A linha: NOME=$(kamal secrets extract <PREFIXO>_NOME $SECRETS) | Vai ao Bitwarden: sim; sem ela, o deploy para com Could not find secret |
| Opcional | A linha: a mesma extração, com o padrão na linha de padrões, logo depois da busca | Vai ao Bitwarden: só para trocar o padrão: o valor do Bitwarden ganha |
| Composta | A linha: NOME=<texto fixo com ${OUTRA}>, montada ali, como uma URI com a senha | Vai ao Bitwarden: nunca |
A linha de padrões usa o Ruby que o Kamal já exige:
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.shconfere tudo isso, e que cada nome dodeploy.ymltem 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:
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ção | Agente | Mantenedor |
|---|---|---|
Escrever Dockerfile, deploy.yml, .kamal/secrets (referências) | Agente: ✓ | Mantenedor: revisa |
docker build, docker compose up local | Agente: ✓ | Mantenedor: ✓ |
| Criar projeto e segredos no Bitwarden | Agente: — | Mantenedor: ✓ |
kamal setup, kamal deploy, kamal accessory … | Agente: nunca | Mantenedor: ✓ |
ssh na VPS | Agente: nunca | Mantenedor: ✓ |
| DNS | Agente: — | Mantenedor: ✓ (guia do toolbox) |
| Instruções de deploy ao fim da change | Agente: ✓ escreve | Mantenedor: ✓ 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
bwsnas versões da ficha;BWS_ACCESS_TOKENeBWS_PROJECT_IDdo 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.shrodado; o segredo de cada client no Bitwarden. Sem ele, okamal deploypara comCould 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;
/uprespondendo em cada uma; - O
toolbox: ojob_namede 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.