
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.
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.692ZO 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.
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:emports: 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 lanapesar de tudo: no contentor,loopbacksignificaria «ninguém me pode contactar, nem mesmo o Docker». É a publicação da porta em127.0.0.1do anfitrião que faz o trabalho de restrição, não o modo de ligação interno.cap_drop: [ALL]: o compose oficial só retiraNET_RAWeNET_ADMIN. Retirar tudo também funciona, verificação feita mais abaixo.- O serviço
cliatrás de umprofiles: partilha a pilha de rede da gateway (network_mode: "service:gateway"), logo está dentro da fronteira de confiança. O perfil evita que umdocker compose updistraí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_fileem vez deenvironment: 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.
umask 077
printf 'OPENCLAW_GATEWAY_TOKEN=%s\n' "$(openssl rand -hex 32)" > .env
chmod 600 .env
echo '.env' >> .gitignorePrimeiro arranque: a configuração em falta
Lançar a pilha tal como está não basta. O contentor arranca, falha, reinicia, e recomeça.
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.
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 gatewayO --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.
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.
[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 itRepara 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.
{
"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
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: 1Nenhuma 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.
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"
# 200O 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.
# 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.
- Um fornecedor remoto: a chave vai para
.env(ANTHROPIC_API_KEY,OPENAI_API_KEY), lida porenv_file. Nunca entra emopenclaw.json. - Um modelo local na máquina anfitriã: num contentor,
127.0.0.1designa o contentor. A documentação impõehttp://host.docker.internal:11434para o Ollama, e o anfitrião deve escutar para além da sua loopback (OLLAMA_HOST=0.0.0.0:11434 ollama serve). - O binário Claude Code no contentor: possível, mas é preciso persistir
/home/nodenum 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.
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 gatewayEscolher 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.
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.
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 servidorDois 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.
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 clawA reter
- Usa o
ghcr.io/openclaw/openclaw: o espelhoalpine/openclawtinha 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=localantes do primeiroup. - Publicar em
127.0.0.1fecha as interfaces da máquina, não a rede Docker: um contentor vizinho contactava a gateway em HTTP 200. internal: truesuprime silenciosamente a porta publicada e o acesso ahost.docker.internal: a reservar para gateways pilotadas porexec.- O token vive em
.env, nunca no compose.state/openclaw.sqliteconté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
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.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.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.ghcr.io/openclaw/openclaw ou openclaw/openclaw.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.

