OpenClaw com Docker: instalar e blindar a gateway

O OpenClaw corre como gateway permanente e dezenas de milhares de instâncias estão abertas na Internet. Eis a instalação Docker Compose blindada, executada e medida a 7 de setembro de 2026.

OpenClaw com Docker: instalar e blindar a gateway
Resposta rápida

O OpenClaw é um assistente pessoal open source que corre como gateway permanente, escuta na porta 18789 e executa skills. Sob Docker, usa a imagem oficial ghcr.io/openclaw/openclaw, publica a porta só em 127.0.0.1, retira todas as capacidades ao contentor e guarda o token num ficheiro .env fora do compose. Atenção: a gateway recusa-se a arrancar enquanto gateway.mode não estiver escrito, e publicar na loopback local não a protege dos outros contentores da máquina.

O OpenClaw instala-se com um comando, e é exatamente esse o problema: a 11 de fevereiro de 2026, a SecurityScorecard contava mais de quarenta mil gateways abertas na Internet nas primeiras vinte e quatro horas do seu levantamento, das quais 35,4% consideradas vulneráveis a uma execução de código remota. O Docker não resolve nada sozinho, o que conta é o que o teu ficheiro compose publica, o que retira ao contentor, e onde vivem os segredos.

O que é o OpenClaw?

O OpenClaw é um assistente pessoal open source, sob licença MIT, conhecido anteriormente pelos nomes Clawdbot e depois Moltbot. Não é um agente de código no sentido do Claude Code ou do Codex, mas uma gateway, um processo que corre em permanência, expõe uma interface web (a Control UI) e uma API WebSocket na porta 18789, liga canais de mensagens (WhatsApp, Telegram, Discord) e executa «skills» em nome do seu proprietário.

O modelo de linguagem vem de fora: a imagem oficial traz plugins para Anthropic, OpenAI, xAI e Ollama, e tu forneces a chave ou o endereço do servidor local. A gateway é portanto uma máquina de executar instruções, sendo o modelo apenas um dos seus fornecedores. É isso que torna a sua exposição de rede perigosa, muito mais do que o modelo escolhido.

Porquê o Docker em vez de uma instalação local?

A documentação diz que o Docker é opcional, e tem razão do ponto de vista funcional. Do ponto de vista da segurança, a diferença é nítida. Uma instalação local dá à gateway a tua conta de utilizador: a tua pasta pessoal, as tuas chaves SSH, o teu chaveiro, os teus repositórios. Um contentor dá-lhe um utilizador sem privilégios, três volumes e nada mais. É o mesmo raciocínio que para os agentes de código, detalhado em isolar o Claude Code e o Codex: a única fronteira que se aguenta é a que o sistema operativo faz respeitar.

O preço a pagar é real. As dependências de sistema que certos skills exigem (ffmpeg, tmux, um navegador) não estão na imagem, e a documentação é categórica: instalar binários num contentor a correr é uma armadilha, é preciso cozê-los no momento da construção com OPENCLAW_IMAGE_APT_PACKAGES.

Que imagem escolher?

A documentação Docker do OpenClaw recenseia dois repositórios. O registo oficial é ghcr.io/openclaw/openclaw, com um espelho no Docker Hub sob openclaw/openclaw. Um terceiro, alpine/openclaw, é um espelho não oficial que a documentação pede explicitamente para evitar, porque não segue nem o calendário de publicação nem a política de retenção do projeto.

Descarreguei as duas a 7 de setembro de 2026 para verificar a diferença.

bash
docker pull ghcr.io/openclaw/openclaw:latest   # 2 min 30 s
docker pull alpine/openclaw:latest             # 1 min 28 s

docker run --rm ghcr.io/openclaw/openclaw:latest openclaw --version
# OpenClaw 2026.9.2 (3928bad)

docker run --rm alpine/openclaw:latest openclaw --version
# OpenClaw 2026.6.9

docker image inspect ghcr.io/openclaw/openclaw:latest \
  --format '{{index .Config.Labels "org.opencontainers.image.created"}}'
# 2026-09-05T15:21:16.692Z

O espelho alpine/openclaw estava congelado na versão 2026.6.9, construída a 21 de junho de 2026: quase três meses de atraso, correções de segurança do Debian incluídas. A imagem oficial, essa, trazia a versão 2026.9.2, construída a 5 de setembro, dois dias antes do meu teste. A escolha não é uma questão de gosto.

Prevê o espaço: docker image inspect anuncia 1,10 GB de conteúdo para a imagem oficial, mas a coluna DISK USAGE de docker images sob o Docker 29 contava 4,43 GB realmente ocupados em disco.

O ficheiro compose blindado

A documentação fornece um docker-compose.yml completo, mas pressupõe um repositório clonado e um script de instalação. Eis a versão mínima que escrevi e executei, só com a imagem pré-construída.

yaml
name: openclaw-lab

x-openclaw-env: &openclaw-env
  HOME: /home/node
  OPENCLAW_HOME: /home/node
  OPENCLAW_STATE_DIR: /home/node/.openclaw
  OPENCLAW_CONFIG_DIR: /home/node/.openclaw
  OPENCLAW_CONFIG_PATH: /home/node/.openclaw/openclaw.json
  OPENCLAW_WORKSPACE_DIR: /home/node/.openclaw/workspace
  OPENCLAW_GATEWAY_PORT: "18789"
  OPENCLAW_DISABLE_BONJOUR: "1"
  TZ: Europe/Paris

services:
  gateway:
    image: ghcr.io/openclaw/openclaw:2026.9.2
    container_name: openclaw-lab-gateway
    init: true
    restart: unless-stopped
    env_file:
      - path: .env          # o token vive aqui, não no compose
        required: true
    environment: *openclaw-env
    command: ["node", "openclaw.mjs", "gateway", "--bind", "lan", "--port", "18789"]
    ports:
      - "127.0.0.1:18789:18789"
    volumes:
      - state:/home/node/.openclaw
      - workspace:/home/node/.openclaw/workspace
      - authsecrets:/home/node/.config/openclaw
    networks: [openclaw]
    cap_drop: [ALL]
    security_opt:
      - no-new-privileges:true
    healthcheck:
      test: ["CMD", "node", "dist/docker-healthcheck.js"]
      interval: 30s
      timeout: 5s
      retries: 5
      start_period: 20s

  cli:
    image: ghcr.io/openclaw/openclaw:2026.9.2
    profiles: ["cli"]
    network_mode: "service:gateway"
    init: true
    env_file:
      - path: .env
        required: true
    environment: *openclaw-env
    entrypoint: ["node", "openclaw.mjs"]
    volumes:
      - state:/home/node/.openclaw
      - workspace:/home/node/.openclaw/workspace
      - authsecrets:/home/node/.config/openclaw
    cap_drop: [ALL]
    security_opt:
      - no-new-privileges:true
    depends_on: [gateway]

networks:
  openclaw:
    name: openclaw-lab

volumes:
  state:
  workspace:
  authsecrets:

Seis decisões merecem uma explicação.

  • O prefixo 127.0.0.1: em ports: sem ele, o Docker publica em todas as interfaces da máquina, incluindo a da rede local. É o erro mais comum, e explica boa parte das gateways encontradas pelos scanners.
  • --bind lan apesar de tudo: no contentor, loopback significaria «ninguém me pode contactar, nem mesmo o Docker». É a publicação da porta em 127.0.0.1 do anfitrião que faz o trabalho de restrição, não o modo de ligação interno.
  • cap_drop: [ALL]: o compose oficial só retira NET_RAW e NET_ADMIN. Retirar tudo também funciona, verificação feita mais abaixo.
  • O serviço cli atrás de um profiles: partilha a pilha de rede da gateway (network_mode: "service:gateway"), logo está dentro da fronteira de confiança. O perfil evita que um docker compose up distraído o deixe a correr em permanência.
  • Volumes nomeados, não bind mounts: a documentação insiste em montar o estado como um diretório, nunca como um ficheiro isolado, sob pena de divergência entre o anfitrião e o contentor depois de uma escrita de configuração.
  • env_file em vez de environment: o token não aparece nem no compose, nem no repositório Git.

Uma precisão sobre a porta: a 18789 já estava ocupada na minha máquina por outra gateway, publiquei portanto o laboratório em 127.0.0.1:18889. Todas as medições que se seguem incidem sobre esta porta, substitui-a por 18789 em tua casa, incluindo em gateway.controlUi.allowedOrigins.

O ficheiro .env gera-se localmente e nunca se comita.

bash
umask 077
printf 'OPENCLAW_GATEWAY_TOKEN=%s\n' "$(openssl rand -hex 32)" > .env
chmod 600 .env
echo '.env' >> .gitignore

Primeiro arranque: a configuração em falta

Lançar a pilha tal como está não basta. O contentor arranca, falha, reinicia, e recomeça.

bash
docker compose up -d gateway
docker compose logs gateway | tail -3
# [gateway] loading configuration…
# [gateway] resolving authentication…
# Missing config. Run `openclaw setup` or set gateway.mode=local (or pass --allow-unconfigured).

Com restart: unless-stopped, o Docker já ia em onze reinícios antes de eu olhar para os registos, e o contentor estava marcado unhealthy. A gateway recusa-se a servir sem configuração explícita: é um bom comportamento por defeito, mas é preciso sabê-lo. A correção não exige nenhuma chave de fornecedor.

bash
docker compose stop gateway

docker compose run --rm -T --no-deps --entrypoint node gateway openclaw.mjs \
  config set --batch-json '[
    {"path":"gateway.mode","value":"local"},
    {"path":"gateway.bind","value":"lan"},
    {"path":"gateway.auth.mode","value":"token"},
    {"path":"gateway.controlUi.allowedOrigins","value":["http://127.0.0.1:18789"]}
  ]'
# Updated 4 config paths. Restart the gateway to apply.

docker compose up -d --force-recreate gateway

O --no-deps --entrypoint node não é decorativo: o serviço cli partilha a pilha de rede da gateway e por isso só funciona depois de criado o contentor gateway. Para escrever a configuração antes do primeiro arranque, é preciso passar pela própria imagem da gateway.

O resultado, cronometrado: /healthz respondeu 200 em 7,6 segundos, e o Docker marcou o contentor healthy aos 11,9 segundos. Um reinício a quente, com o estado já inicializado, cai para 5,0 segundos.

bash
curl -s http://127.0.0.1:18789/healthz   # {"ok":true,"status":"live"}
curl -s http://127.0.0.1:18789/startupz  # {"ok":true,"status":"started"}
curl -s http://127.0.0.1:18789/readyz    # {"ready":true}

Os registos de arranque são tagarelas, e instrutivos.

bash
[gateway] ⚠️  Gateway is binding to a non-loopback address. Ensure authentication
          is configured before exposing to public networks.
[gateway] agent model: openai/gpt-5.6-sol (thinking=medium, fast=off)
[gateway] http server listening (13 plugins: anthropic, browser, canvas,
          cua-computer, device-pair, file-transfer, geolocation, linux-node,
          memory-core, ollama, openai, talk-voice, xai; 2.7s)
[gateway] log file: /tmp/openclaw/openclaw-2026-09-07.log
[gateway] remote model catalog updated; restart the Gateway to apply it

Repara na última linha: sem nenhuma chave configurada, a gateway foi buscar um catálogo de modelos à Internet. Uma gateway «em repouso» sai à mesma para a rede.

O ficheiro de configuração escrito tem 348 bytes e não contém nenhum segredo: o token fica na variável de ambiente.

json
{
  "gateway": {
    "mode": "local",
    "bind": "lan",
    "auth": { "mode": "token" },
    "controlUi": { "allowedOrigins": ["http://127.0.0.1:18789"] }
  },
  "meta": { "lastTouchedVersion": "2026.9.2" }
}

Ao lado, state/openclaw.sqlite já pesava 1,5 MB. É aí que acabam os tokens OAuth, em claro: a documentação pede para tratar esta pasta e as suas cópias de segurança como credenciais.

O que a blindagem bloqueia mesmo

Três verificações valem mais do que uma declaração de intenções.

As capacidades e o utilizador

bash
docker compose exec -T gateway sh -lc \
  'grep -E "^Cap(Prm|Eff|Bnd)" /proc/1/status; id; grep NoNewPrivs /proc/self/status'
# CapPrm: 0000000000000000
# CapEff: 0000000000000000
# CapBnd: 0000000000000000
# uid=1000(node) gid=1000(node) groups=1000(node)
# NoNewPrivs: 1

Nenhuma capacidade, utilizador sem privilégios, elevação impossível. A imagem oficial já faz metade do trabalho: corre como node (uid 1000) e lança tini como processo 1.

A porta publicada na loopback local não protege do Docker

É o resultado que mais me surpreendeu. A porta só é publicada em 127.0.0.1, mas um contentor qualquer, mesmo noutra rede Docker, contacta na mesma a gateway pelo seu endereço de bridge.

bash
GWIP=$(docker inspect -f \
  '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' openclaw-lab-gateway)
echo "$GWIP"   # 172.29.0.2

# a partir da rede por defeito, não a do lab
docker run --rm alpine:3.22 sh -lc \
  "apk add --no-cache curl >/dev/null; \
   curl -s -o /dev/null -w '%{http_code}\n' http://$GWIP:18789/healthz"
# 200

O prefixo 127.0.0.1: fecha as interfaces da máquina, não a rede do daemon Docker. Se hospedas outros contentores na mesma máquina, basta um deles estar comprometido para falar com a gateway. A defesa é a autenticação da gateway e, num servidor Linux, regras na cadeia DOCKER-USER: a documentação de blindagem do OpenClaw dá um conjunto completo para o UFW, porque as regras INPUT habituais nunca veem o tráfego publicado pelo Docker.

A rede interna custa mais caro do que parece

O reflexo seguinte é passar a rede para internal: true. Medi o que isso retira.

yaml
# docker-compose.internal.yml
networks:
  openclaw:
    internal: true
Verificação Rede bridge Rede internal
fetch('https://api.anthropic.com/') a partir do contentor HTTP 404 (contactável) fetch failed
curl http://127.0.0.1:18789/healthz a partir do anfitrião HTTP 200 ligação recusada
host.docker.internal resolvido sim sim
Serviço do anfitrião contactado via host.docker.internal HTTP 200 fetch failed
Contentor marcado healthy sim sim

Duas consequências são contraintuitivas. Primeiro, o Docker Compose ignora silenciosamente a secção ports numa rede interna: docker compose ps mostra 18789/tcp em vez de 127.0.0.1:18789->18789/tcp, sem o mínimo aviso, e a Control UI torna-se inacessível. Depois, host.docker.internal continua a resolver-se mas deixa de encaminhar: um Ollama que corre na máquina anfitriã também deixa de ser contactável.

A rede interna só é portanto utilizável para uma gateway pilotada inteiramente por docker compose exec, com um modelo servido por outro contentor da mesma rede. Para tudo o resto, a boa resposta é um bridge dedicado mais um túnel SSH: a documentação do OpenClaw recomenda aliás o Tailscale Serve em vez de uma ligação LAN.

Ligar um modelo sem escrever uma chave no compose

Existem três caminhos, e nenhum exige colar uma chave num ficheiro versionado.

  1. Um fornecedor remoto: a chave vai para .env (ANTHROPIC_API_KEY, OPENAI_API_KEY), lida por env_file. Nunca entra em openclaw.json.
  2. Um modelo local na máquina anfitriã: num contentor, 127.0.0.1 designa o contentor. A documentação impõe http://host.docker.internal:11434 para o Ollama, e o anfitrião deve escutar para além da sua loopback (OLLAMA_HOST=0.0.0.0:11434 ollama serve).
  3. O binário Claude Code no contentor: possível, mas é preciso persistir /home/node num volume nomeado, senão a próxima atualização de imagem apaga a instalação e a autenticação.

Nenhuma chave foi introduzida durante este teste, e o plugin ollama é de facto carregado pela gateway. O caminho do modelo local verifica-se sem chave: com um Ollama publicado na porta 11434 do anfitrião, http://host.docker.internal:11434/v1 responde 200 e em formato OpenAI a partir de um contentor da rede bridge. É exatamente o endereço que a documentação impõe, e a razão pela qual 127.0.0.1 não funcionaria: dentro de um contentor, designa o contentor.

Atualizar a imagem sem quebrar o estado

As etiquetas móveis (latest, main, extended-stable) são reconstruídas todas as semanas a partir da mesma origem, para recuperar as correções de segurança do sistema base entre duas versões do OpenClaw. Cada reconstrução também publica uma etiqueta datada imutável, do tipo 2026.8.1-r20260820: é essa que se deve fixar quando não queres que um deployment siga uma etiqueta móvel.

Na mudança de imagem, a gateway aplica as suas migrações no arranque. Se não conseguir, sai em erro em vez de se declarar saudável, e com uma política de reinício, vais ver um ciclo. O remédio documentado é relançar a mesma imagem uma vez com openclaw doctor --fix sobre os mesmos volumes, depois reiniciar normalmente.

bash
docker compose pull
docker run --rm -v openclaw-lab_state:/home/node/.openclaw \
  ghcr.io/openclaw/openclaw:2026.9.2 openclaw doctor --fix
docker compose up -d gateway

Escolher os teus skills no ClawHub

A imagem traz 53 skills prontos a usar: 51 no pacote base, 2 em extra. Já é muito, e chega para a maioria dos usos. O resto vem do ClawHub, o registo público, é aí que os problemas começam. O formato é o mesmo SKILL.md descrito no guia Agent Skills: um frontmatter YAML, um corpo Markdown, ficheiros anexos.

A documentação dos skills não tem rodeios: pede para tratar qualquer skill de terceiros como código não fiável e para o ler antes de o ativar. Os números dão-lhe razão. Um levantamento publicado a 1 de março de 2026 atribui à Antiy CERT 1 184 skills maliciosos confirmados no ClawHub, ou seja cerca de um pacote em cinco no pico da campanha.

skills verify interroga o registo sem instalar nada, e isso agradece-se. O seu veredito global, esse, merece ser aberto. Eis o que devolve sobre um skill Docker popular, à data do teste.

bash
docker compose run --rm -T cli skills verify @ivangdavila/docker

# decision            : pass
# security.status     : clean
# security.verdict    : benign      (confidence: high)
# signature.status    : unsigned
# provenance.source   : unavailable
# signals.staticScan  : suspicious  -> suspicious.exposed_secret_literal
# signals.skillSpector: suspicious
# signals.virusTotal  : clean
# artifact.files      : 16 ficheiros, incluindo SKILL.md (24 283 bytes)

Dois dos três sinais dizem «suspeito», o pacote não está assinado, a sua proveniência GitHub não está registada, e o veredito agregado continua «benigno, confiança elevada». O resumo legível fala de um assistente local sem exfiltração detetada, e não é este skill que está em causa. Retém o desfasamento entre os sinais e a conclusão: uma pontuação verde não é uma leitura.

A regra que retenho cabe em três pontos: ler o SKILL.md antes de instalar (skills info dá o caminho exato do ficheiro), recusar qualquer skill que saia para a rede sem que a sua função o exija, e preferir os 53 skills entregues com a imagem enquanto chegarem. O campo security.installPolicy da configuração permite impor esta salvaguarda em vez de contar com a disciplina.

Acrescentar servidores MCP

A gateway gere os seus servidores MCP em mcp.servers, com uma superfície de comandos completa: add (que sonda o servidor antes de registar), probe, doctor, status, tools para filtrar as ferramentas expostas, e login / logout para os servidores OAuth.

bash
docker compose run --rm -T cli mcp doctor     # falhas de configuração estáticas
docker compose run --rm -T cli mcp status     # transportes, sem se ligar
docker compose run --rm -T cli mcp probe      # ligação real, lista as capacidades
docker compose run --rm -T cli mcp tools      # filtros include/exclude por servidor

Dois reflexos valem a pena guardar. mcp tools existe, usa-o. Um servidor MCP expõe muitas vezes trinta ferramentas quando só queres três, e cada ferramenta a mais é uma descrição que o modelo lê como uma instrução. E um servidor MCP em STDIO corre dentro do contentor da gateway, portanto com os seus volumes e as suas variáveis de ambiente. A nota de investigação da Cloud Security Alliance de 4 de maio de 2026 recomenda exatamente o inverso: um contentor dedicado por servidor, sem acesso às credenciais do anfitrião. O assunto é tratado em detalhe em criar um servidor MCP em PHP.

OpenClaw, Claude Code ou Hermes Agent?

Os três não jogam no mesmo campo, e confundi-los leva a más decisões.

OpenClaw Claude Code Hermes Agent
Forma Gateway permanente Sessão em terminal Agente contentorizado
Gatilho Mensagens, cron, Control UI Tu, ao teclado Tarefas e filas
Superfície de rede Uma porta aberta em permanência Nenhuma escuta de entrada Consoante o deployment
Modelo Fornecedor à escolha, Ollama incluído Anthropic Consoante o deployment

O Claude Code não escuta nada: fecha o terminal, a superfície de ataque desaparece. O OpenClaw escuta em permanência, por construção, porque é isso que lhe é pedido: responder a uma mensagem de Telegram às três da manhã. O Hermes Agent, instalado da mesma forma num artigo dedicado, ocupa uma terceira posição: um agente pensado para correr em contentor desde o início. O panorama dos agentes em linha de comandos situa os outros, e o OpenCode cobre o caso do agente de código open source.

Apagar tudo

Um laboratório desmonta-se. Um docker compose down sozinho deixa os volumes para trás, com a base SQLite e os seus tokens.

bash
docker compose down --volumes --remove-orphans
docker rmi ghcr.io/openclaw/openclaw:2026.9.2 alpine/openclaw:latest
rm -f .env

# verificação: nada deve aparecer
docker ps -a --format '{{.Names}}' | grep -i claw
docker volume ls --format '{{.Name}}' | grep -i claw
docker images --format '{{.Repository}}' | grep -i claw
docker network ls --format '{{.Name}}' | grep -i claw

A reter

  • Usa o ghcr.io/openclaw/openclaw: o espelho alpine/openclaw tinha três meses de atraso no dia do teste (2026.6.9 contra 2026.9.2).
  • A gateway recusa-se a arrancar sem configuração e entra num ciclo de reinício: escreve gateway.mode=local antes do primeiro up.
  • Publicar em 127.0.0.1 fecha as interfaces da máquina, não a rede Docker: um contentor vizinho contactava a gateway em HTTP 200.
  • internal: true suprime silenciosamente a porta publicada e o acesso a host.docker.internal: a reservar para gateways pilotadas por exec.
  • O token vive em .env, nunca no compose. state/openclaw.sqlite contém tokens OAuth em claro e trata-se como um segredo.
  • No ClawHub, um veredito «clean» pode encobrir dois sinais «suspect», um pacote não assinado e uma proveniência desconhecida. Lê o SKILL.md.

Erros frequentes

Publicar a porta sem prefixo de endereço ports: ["18789:18789"] publica em todas as interfaces, rede local incluída. Escreve "127.0.0.1:18789:18789", e num servidor Linux acrescenta regras na cadeia DOCKER-USER: as regras INPUT não veem o tráfego publicado pelo Docker.
Achar que 127.0.0.1 isola a gateway Um contentor de outra rede Docker contactou 172.29.0.2:18789/healthz em HTTP 200. O prefixo fecha as interfaces da máquina, não o daemon. Conta com a autenticação da gateway, não com a publicação.
Lançar docker compose up sem configuração O contentor entra em ciclo sobre Missing config. Run openclaw setup or set gateway.mode=local e restart: unless-stopped disfarça o erro. Escreve a configuração com config set --batch-json e --no-deps --entrypoint node antes do primeiro arranque.
Confundir alpine/openclaw com a imagem oficial É um espelho não oficial, congelado em 2026.6.9 de 21 de junho de 2026 quando a oficial estava em 2026.9.2. A documentação pede para usar ghcr.io/openclaw/openclaw ou openclaw/openclaw.
Acrescentar internal: true como uma simples blindagem O Compose ignora então a secção ports sem aviso, a Control UI torna-se incontactável, e host.docker.internal resolve-se mas deixa de encaminhar: um Ollama no anfitrião torna-se inacessível.

DockerMCPOllamaOpenClawSécuritéSkills

Damien Flandrin Programador web desde 2010, criador da Gekkode e do Email Impact. Cada artigo é testado num projeto real antes de ser publicado. Contacto
Newsletter

Os novos testes, tutoriais e projetos, por e-mail.

Testes reproduzíveis, código versionado, resultados datados. Nunca spam.