Migrar docker-compose pro runner

Este guia cobre o caminho completo de tirar um projeto que roda via docker compose up direto no servidor e colocar ele sob o runner — preservando a capacidade de regenerar os .deploy.yml quando o compose mudar upstream (via runner reconcile).

Aplica-se a v2.26.10+. Versoes anteriores tem gaps conhecidos — veja changelog (v2.26.1 a v2.26.10) para a lista completa de fixes. Para projetos sem compose, veja Wizard CLI.

Antes de comecar

Precondicoes:

  • Runner v2.26.10+ instalado no servidor de deploy (runner self-update)
  • Repo com docker-compose.yml (e opcionalmente docker-compose.override.yml / .prod.yml)
  • Acesso de escrita no repo (vai commitar os .deploy.yml gerados + .runner/wizard-answers.yml)

Se o repo tem multiplos compose files (caso de overrides por ambiente), o runner faz merge per compose-spec semantics — exatamente igual ao docker compose -f base.yml -f override.yml.

Capacidades cobertas (v2.26.10)

Tudo abaixo e traduzido automaticamente pelo wizard --from-compose:

Compose Manifest Notas
command: (string ou array) command: [...] Args apos entrypoint. v2.26.3+
entrypoint: entrypoint: [...] Override de ENTRYPOINT. v2.24.0+
environment: [VAR=${X:-default}] Expandido pra valor literal Le shell env no parse. v2.26.3+
volumes: ['./X:/etc/X'] Resolve a src/<inst>/<v>/X se existir Bind paths matching compose. v2.26.2+
networks: [meet] (user-defined) networks: [<prefix>_meet] + alias Apps do mesmo prefix se veem. v2.26.3+
networks: [mysql, redis] (shared infra) passa unchanged Cross-project. v2.26.3+
deploy.resources.limits.memory resources.memory Cpus, memory, swap. v2.24.0+
cap_add, cap_drop, devices security.*, devices v2.24.0+
logging.driver logging.driver v2.24.0+
tmpfs, expose, dns, hostname mesmo nome no manifest v2.24.0+

Nao cobertos (halt no wizard, ou drop com notice): privileged: true, network_mode: host, sysctls, pid/ipc/uts: host, userns_mode, group_add, extra_hosts (use hosts: no manifest), profiles, extends, deploy.replicas, deploy.placement.

Mental model: 3 camadas de configuracao

Antes de mexer em qualquer arquivo, entenda como o runner separa responsabilidades:

Camada Arquivo Conteudo Quem edita
Estrutural docker-compose.yml (upstream) Port real, healthcheck endpoint real, env vars do app, volumes, networks Dev + Ops
Runner-specific .runner/wizard-answers.yml Domain por instance, system label, instance name, ckey, branch Ops (via prompt do wizard)
Gerado <service>/.deploy.yml Combinacao das 2 camadas acima NUNCA edita a mao

Esse split eh o que permite runner reconcile funcionar — o reconcile re-roda o mapper com a combinacao compose + answers e regenera o .deploy.yml. Se voce editar .deploy.yml direto, o reconcile sobrescreve suas edits.

Fluxo recomendado

1. Ajustar o `docker-compose.yml` upstream pra ter declaracoes estruturais corretas

Antes de rodar o wizard, garanta que o compose tem:

services:
  agent:
    expose: ["8787"]                    # Porta real do app (default 80 se ausente)
    healthcheck:
      test: ["CMD", "curl", "-f", "http://127.0.0.1:8787/healthz"]
      interval: 30s
      timeout: 5s
      retries: 3

Por que adicionar healthcheck no compose:

  • Beneficia dev local — docker compose up ja mostra (healthy) no docker ps
  • Wizard traduz pro .deploy.yml automaticamente
  • Reconcile preserva (vem do compose, nao das answers)

Atencao com localhost: em imagens Alpine (que muitos services usam), localhost resolve pra ::1 (IPv6) e o healthcheck falha. Sempre prefira 127.0.0.1 em healthchecks.

2. Rodar o wizard SEM `--accept-defaults`

cd /opt/meu-projeto

runner wizard --path . --from-compose \
  -f docker-compose.yml \
  -f docker-compose.prod.yml

(Sem -f o runner auto-detecta docker-compose.override.{yml,yaml} ao lado da base.)

O wizard pergunta service-por-service:

Service 'frontend' (build: ./frontend):
  domain (empty = port-only) [default: empty]: meet.example.com
  system (sys|front|api|worker) [default: sys]: front
  instance name [default: production]:

Service 'agent' (build: ./agent):
  domain (empty = port-only) [default: empty]:    # internal, leave blank
  system [default: sys]: worker
  instance name [default: production]:

Service 'livekit-server' (image: livekit/livekit-server:v1.8.3):
  domain [default: empty]: livekit.example.com
  system [default: sys]: sys
  instance name [default: production]:

Resultado salvo em .runner/wizard-answers.yml:

schema_version: 1
project_prefix: meu-projeto
services:
  frontend:
    wizardize: true
    domain: meet.example.com
    system: front
    instance_name: production
  agent:
    wizardize: true
    domain: null
    system: worker
    instance_name: production
  livekit-server:
    wizardize: true
    domain: livekit.example.com
    system: sys
    instance_name: production

Por que --accept-defaults eh ruim aqui: pula os prompts e gera instance.domain: null pra todos os services. Se voce edita o domain direto no .deploy.yml, o reconcile vai apagar.

3. Conferir os `.deploy.yml` gerados

ls */.deploy.yml
# agent/.deploy.yml
# frontend/.deploy.yml
# livekit-server/.deploy.yml

grep -E "port|healthcheck|domain|resources" agent/.deploy.yml

Verifique:

  • port: bateu com o expose: do compose (ou foi inferido de ports:)
  • healthcheck.path ou healthcheck.cmd reflete o endpoint real
  • instance.production.domain esta correto (do wizard-answers)
  • resources.* (se compose tinha deploy.resources.limits.* ou mem_limit/cpus) foi traduzido

Se algo estiver estruturalmente errado (port errado, healthcheck path errado), volte ao passo 1: ajuste o compose, re-rode o wizard. Nao edite .deploy.yml.

Se algo for runner-specific errado (domain, instance), edite .runner/wizard-answers.yml e rode runner reconcile <app> apos o registro.

4. Commit tudo

git add docker-compose.yml docker-compose.prod.yml          # mudancas estruturais
git add agent/.deploy.yml frontend/.deploy.yml livekit-server/.deploy.yml
git add .runner/wizard-answers.yml                          # critico — reconcile depende
git commit -m "feat: runner-managed deploy via wizard --from-compose"
git push origin dist                                        # ou main, dependendo do flow

.runner/wizard-answers.yml deve ser commitado. Sem ele, ninguem mais consegue rodar runner reconcile consistentemente.

5. Registrar cada app no runner (`--force-duplicate` do 2o em diante)

runner add --repo user/repo --branch dist --ckey \
  --manifest-path agent/.deploy.yml \
  --instance staging

runner add --repo user/repo --branch dist --ckey \
  --manifest-path frontend/.deploy.yml \
  --instance staging \
  --force-duplicate

runner add --repo user/repo --branch dist --ckey \
  --manifest-path livekit-server/.deploy.yml \
  --instance staging \
  --force-duplicate

Comece em staging. Nao registre direto em production. Valide pelo menos 1 semana.

6. Setar secrets

runner secrets set agent LIVEKIT_API_KEY=...
runner secrets set agent OPENAI_API_KEY=...
# ... etc

VITE_* / build args ja foram capturados pelo wizard em build.args do .deploy.yml. Esses sao build-time, nao runtime — nao precisam ir em secrets.

7. Deploy

runner deploy agent --instance staging --force
runner deploy frontend --instance staging --force
runner deploy livekit-server --instance staging --force

Manutencao continua (reconcile)

Quando o time mexer no docker-compose.yml (adicionar service, mudar port, ajustar env), regere os manifests:

git pull origin main
runner reconcile agent --apply
runner reconcile frontend --apply
runner reconcile livekit-server --apply
git diff   # confere o que mudou nos .deploy.yml
git commit -am "chore: reconcile after compose update"

Reconcile preserva o que esta em wizard-answers.yml (domains, instance config). Soh muda o que veio do compose.

Erros comuns

"wizard halted on service X"

Service usa feature compose que o runner nao mapeia (privileged, sysctls, network_mode: host, etc).

Opcoes:

  1. Fail-soft default (v2.26.0+): os outros services geram normal, soh esse foi pulado. Veja o output do wizard — ele lista quais services foram skipped e por que.
  2. --ignore-eccentric <field> ou --ignore-eccentric all: forca a geracao mesmo com a feature. Use com cuidado — o .deploy.yml resultante ignora silenciosamente a feature.
  3. Editar o compose: remover a feature problematica, talvez substituir por algo que o runner suporte.

"no compose file found"

Wizard nao encontrou docker-compose.yml ou compose.yml no --path. Passe explicito:

runner wizard --from-compose -f path/to/my-compose.yml

"service '' missing from merged compose"

Aconteceu durante runner reconcile. Significa que o compose mudou e o service que o app tinha base nao existe mais. Resolva:

  • Restaure o service no compose, OU
  • Migre a app pra um .deploy.yml autorado a mao: runner unregister <name> e re-crie sem o --from-compose.

Domains ficaram `null` apos reconcile

Voce rodou wizard com --accept-defaults originalmente, dominios nunca foram pra wizard-answers.yml. Solucao:

  1. Edite .runner/wizard-answers.yml a mao, adicionando os domains
  2. Rode runner reconcile <app> --apply

Deploy haltou com "manifest drift detected" (v2.26.7+)

Sintoma:

Error: manifest drift detected for service 'agent' — re-running the wizard
mapper with the current `wizard-answers.yml` would produce a DIFFERENT
`.deploy.yml` than what's on disk.

Fields divergentes (current → expected):
  networks:
    current:  ["meet"]
    expected: ["meet_meet"]
  ...

Fix: runner reconcile meet-agent --apply

O que isso significa: o manifest no disco (e no upstream) esta stale relativo ao wizard-answers.yml. Causas tipicas:

  1. Manifest foi gerado por wizard antigo (pre-v2.26.3, antes do project-prefix em networks). Solucao: runner reconcile <app> --apply regenera; commitar upstream pra resolver permanentemente.
  2. wizard-answers.yml foi editado (project_prefix, system, domain) mas reconcile nunca rodou.
  3. Algum outro service do mesmo wizard run foi reconciliado (atualizando wizard-answers.yml) mas este nao.

Fix:

runner reconcile <app> --apply
runner deploy <app>

Pra evitar reincidencia: commitar os manifests regenerados upstream:

# No dev (que tem o repo clonado):
git pull
# Manifestos regenerados estao em /data/apps/<app>/.deploy.yml — copie pro repo:
scp prod-server:/data/apps/<app>/.deploy.yml ./<service>/.deploy.yml
git add */.deploy.yml && git commit -m "chore: reconcile post-v2.26.7"
git push

Apos o commit, qualquer outro servidor que rodar runner add ja vem com o manifest correto — sem reconcile recorrente.

Frontend Vite/Next/CRA crasha com "Invalid URL" ou bundle sem URLs

Sintoma: console do browser mostra erros tipo Uncaught Error: Invalid supabaseUrl: Must be a valid HTTP or HTTPS URL. apos deploy. Bundle JS no container nao tem as URLs esperadas:

docker exec meet_frontend_production_<hash> sh -c \
  'grep -ohE "https://[a-z0-9]+\\.supabase\\.co" /usr/share/caddy/assets/*.js | sort -u'
# (vazio)

Causa: build.args do manifest contem placeholders ${VAR} que nao foram expandidos no docker build. Vite (e similares) precisam dos valores em tempo de build, nao em tempo de runtime — bundles ja saem compilados.

Fix v2.26.10+: runner expande ${VAR} em 3 camadas:

  1. Shell env no wizard time (runner wizard --from-compose): se voce setar a env var antes, ela e bake-in no manifest.
  2. runner env set apos wizard: valor vai pro .env do app. Runtime expansion no deploy time pega via .secrets > .env > shell env.
  3. Literal preservado: se nenhuma fonte tem o valor, manifest mantem ${VAR} literal (em vez de virar empty string) — operador detecta no manifest ao inves de bundle silenciosamente vazio.

Workflow recomendado (Vite/Next/CRA):

# Defina os valores via runner env set (ou direto no .env do app)
runner env set <app> --key VITE_SUPABASE_URL --value "https://x.supabase.co"
runner env set <app> --key VITE_SUPABASE_ANON_KEY --value "eyJ..."
runner env set <app> --key VITE_LIVEKIT_WS_URL --value "wss://livekit.example.com"

# Reconcile garante o manifest e consistente com state (drift check)
runner reconcile <app> --apply

# Deploy --force pra bypass docker layer cache (build args mudam → bundle re-compila)
runner deploy <app> --force

Validacao:

docker exec <container> sh -c \
  'grep -ohE "https://[a-z0-9]+\\.supabase\\.co" /usr/share/caddy/assets/*.js | sort -u'
# https://x.supabase.co  ✓

Container nao resolve sibling por service-name

Sintoma: agent app loga gaierror: Temporary failure in name resolution ao tentar ws://livekit:7880.

Em v2.26.4+, sibling apps devem se resolver por nome curto (igual docker compose). Causa comum:

  1. Apps registrados antes da v2.26.3network_aliases: nao foi populado nos manifests. Rode runner reconcile <app> --apply em cada app + runner deploy <app> pra picar a nova network compartilhada.
  2. Apps em runner v2.26.3 — primeiro deploy entrou na network nova, mas deploys subsequentes pegaram a velha (Bug 12 re-open). Atualize pra v2.26.4+ (runner self-update) e re-deploy cada app:
    runner self-update
    runner fetch <app>
    runner reconcile <app> --apply
    runner deploy <app>
  3. project_prefix divergente — apps com prefixes diferentes em wizard-answers.yml nao compartilham network. Confira que todos os manifests do mesmo monorepo tem o mesmo project_prefix (regenere se necessario com wizard --from-compose do mesmo --path).
  4. Network e reserved/sharedmysql, redis, etc. nao recebem aliases (sao cross-project). Use networks: [meu_app] no compose pra forcar uma network user-defined que ganhe scoping.

`runner reconcile` falha com "compose file missing" apos um deploy

Sintoma (pre-v2.26.4):

$ runner reconcile <app> --apply
Error: compose file missing: /data/apps/<app>/pull/docker-compose.yml
Run `runner fetch <app>` first to update pull/.

Em v2.26.4+, runner deploy deixou de limpar pull/ no fim. Se voce esta em v2.26.4+ e ainda ve esse erro, e porque a app foi deployada em uma versao mais antiga e o pull/ ja tinha sido removido. Solucao: runner fetch <app> (com bootstrap automatico de pull/ desde v2.26.1+).

`runner deploy` reclama de "Another deploy is in progress" mas nao tem deploy rodando

Pre-v2.26.4, sem flag de recovery. Em v2.26.4+:

# 1. Confirme que nao ha deploy real rodando
ps -ef | grep "runner deploy <app>"

# 2. Force a remocao do lock
runner deploy <app> --force-unlock

Stale-detection (PID gone OR > 30min) deve cobrir o caso comum automaticamente. --force-unlock e pra quando a heuristica nao dispara (e.g., PID foi reciclado).

First deploy falhou e a porta ficou alocada

Sintoma (pre-v2.26.4): tentando docker compose up apos um runner deploy falho retorna:

Error response from daemon: Bind for 0.0.0.0:7880 failed: port is already allocated

Em v2.26.4+, o container quebrado e removido automaticamente no failure path. Se voce esta em v2.26.4+ e ainda ve isso, e porque o container ja existia de uma versao anterior do runner. Limpe manualmente:

docker ps -a --filter "name=<project>_<instance>_" --format "{{.Names}}" | xargs -r docker rm -f

`runner env set` nao tem efeito

Pre-v2.26.3, manifest environment: sempre vencia. Atualize pra v2.26.3+ e re-deploy.

Em v2.26.3+, ordem e: .env/.secrets (operador) > environment: (manifest). Verifique se runner env set escreveu de fato:

runner secrets show <app>   # mostra env + secrets atuais
docker inspect <container> --format '{{range .Config.Env}}{{println .}}{{end}}' | grep <KEY>

Variaveis `${...}` aparecem literais no container

Pre-v2.26.3, wizard copiava ${VAR:-default} literal pro manifest. Atualize e regenere os manifests:

git pull   # pega o compose upstream
runner reconcile <app> --apply
runner deploy <app>

Wizard agora expande ${VAR}, ${VAR:-default}, ${VAR-default}, ${VAR:?error} lendo do shell env no momento da geracao. Se quiser forcar valores no wizard, exporte antes:

LIVEKIT_PORT=7880 runner wizard --from-compose ...

Quando NAO usar `--from-compose`

  • App nao tem compose (Dockerfile direto) → use runner wizard interativo normal
  • Compose tem features que o runner nao suporta E sao essenciais (privileged + GPU advanced + multi-host) → autore .deploy.yml a mao
  • App eh single-service e simples → runner wizard direto poupa o ceremony do multi-file

Ver tambem

By Borlot.com.br on 05/07/2026