
OpenClaw is een open source persoonlijke assistent die als permanente gateway draait, op poort 18789 luistert en skills uitvoert. Onder Docker neem je de officiële image ghcr.io/openclaw/openclaw, publiceer je de poort uitsluitend op 127.0.0.1, haal je alle capabilities van de container weg en houd je het token in een bestand .env buiten de compose. Let op: de gateway weigert te starten zolang gateway.mode niet is geschreven, en publiceren op de loopback beschermt hem niet tegen de andere containers van de machine.
OpenClaw installeer je met één commando, en dat is precies het probleem: op 11 februari 2026 telde SecurityScorecard meer dan veertigduizend open gateways op internet in de eerste vierentwintig uur van zijn meting, waarvan 35,4% als kwetsbaar voor remote code execution werd beoordeeld. Docker lost op zichzelf niets op, wat telt, is wat jouw compose-bestand publiceert, wat het van de container weghaalt, en waar de secrets leven.
Wat is OpenClaw?
OpenClaw is een open source persoonlijke assistent, onder MIT-licentie, voorheen bekend onder de namen Clawdbot en daarna Moltbot. Het is geen code-agent in de zin van Claude Code of Codex, maar een gateway, een proces dat permanent draait, een webinterface blootstelt (de Control UI) en een WebSocket-API op poort 18789, berichtenkanalen koppelt (WhatsApp, Telegram, Discord) en “skills” uitvoert namens zijn eigenaar.
Het taalmodel komt van elders: de officiële image bevat plugins voor Anthropic, OpenAI, xAI en Ollama, en jij levert de sleutel of het adres van de lokale server. De gateway is dus een machine die instructies uitvoert, waarbij het model maar één van zijn leveranciers is. Dat maakt zijn netwerkblootstelling gevaarlijk, veel meer dan het gekozen model.
Waarom Docker in plaats van een lokale installatie?
De documentatie zegt dat Docker optioneel is, en functioneel heeft ze gelijk. Op het vlak van beveiliging is het verschil duidelijk. Een lokale installatie geeft de gateway jouw gebruikersaccount: je persoonlijke map, je SSH-sleutels, je sleutelbos, je repositories. Een container geeft hem een gebruiker zonder privileges, drie volumes en verder niets. Dat is dezelfde redenering als bij code-agents, uitgewerkt in Claude Code en Codex afschermen: de enige grens die standhoudt, is die welke het besturingssysteem afdwingt.
De prijs die je betaalt, is reëel. De systeemdependencies die sommige skills vereisen (ffmpeg, tmux, een browser) zitten niet in de image, en de documentatie is categoriek: binaries installeren in een draaiende container is een valstrik, je moet ze inbakken tijdens het bouwen met OPENCLAW_IMAGE_APT_PACKAGES.
Welke image kies je?
De Docker-documentatie van OpenClaw telt twee repositories. Het officiële register is ghcr.io/openclaw/openclaw, met een Docker Hub-spiegel onder openclaw/openclaw. Een derde, alpine/openclaw, is een niet-officiële spiegel die de documentatie expliciet vraagt te vermijden, omdat hij noch het publicatieschema noch het retentiebeleid van het project volgt.
Ik heb beide op 7 september 2026 gedownload om het verschil te controleren.
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.692ZDe spiegel alpine/openclaw zat vast op versie 2026.6.9, gebouwd op 21 juni 2026: bijna drie maanden achterstand, Debian-beveiligingspatches inbegrepen. De officiële image droeg dan weer versie 2026.9.2, gebouwd op 5 september, twee dagen vóór mijn test. De keuze is geen kwestie van smaak.
Voorzie de ruimte: docker image inspect geeft 1,10 GB aan inhoud aan voor de officiële image, maar de kolom DISK USAGE van docker images onder Docker 29 telde er 4,43 GB die werkelijk op de schijf in beslag werden genomen.
Het geharde compose-bestand
De documentatie levert een volledig docker-compose.yml, maar dat veronderstelt een gekloonde repository en een installatiescript. Hier is de minimale versie die ik heb geschreven en uitgevoerd, met alleen de voorgebouwde image.
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 # het token leeft hier, niet in de 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:Zes beslissingen verdienen uitleg.
- Het voorvoegsel
127.0.0.1:inports: zonder dat publiceert Docker op alle interfaces van de machine, inclusief die van het lokale netwerk. Dat is de meest voorkomende fout, en ze verklaart een groot deel van de gateways die scanners vinden. --bind lanondanks alles: in de container zouloopbackbetekenen “niemand kan me bereiken, zelfs Docker niet”. Het is de publicatie van de poort op127.0.0.1van de host die het beperkingswerk doet, niet de interne bindingsmodus.cap_drop: [ALL]: de officiële compose haalt alleenNET_RAWenNET_ADMINweg. Alles weghalen werkt ook, verderop geverifieerd.- De service
cliachter eenprofiles: hij deelt de netwerkstack van de gateway (network_mode: "service:gateway"), en zit dus binnen de vertrouwensgrens. Het profiel voorkomt dat een onoplettendedocker compose uphem permanent laat draaien. - Named volumes, geen bind mounts: de documentatie dringt erop aan de staat als een map te monteren, nooit als een geïsoleerd bestand, op straffe van divergentie tussen host en container na een configuratieschrijving.
env_filein plaats vanenvironment: het token verschijnt noch in de compose, noch in de Git-repository.
Een precisering over de poort: 18789 was op mijn machine al bezet door een andere gateway, dus heb ik het laboratorium gepubliceerd op 127.0.0.1:18889. Alle metingen hierna hebben betrekking op die poort, vervang hem bij jou door 18789, ook in gateway.controlUi.allowedOrigins.
Het bestand .env genereer je lokaal en commit je nooit.
umask 077
printf 'OPENCLAW_GATEWAY_TOKEN=%s\n' "$(openssl rand -hex 32)" > .env
chmod 600 .env
echo '.env' >> .gitignoreEerste start: de ontbrekende configuratie
De stack zomaar starten is niet genoeg. De container start, faalt, herstart, en begint opnieuw.
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).Met restart: unless-stopped zat Docker al aan elf herstarts voordat ik naar de logs keek, en de container was gemarkeerd als unhealthy. De gateway weigert te dienen zonder expliciete configuratie: dat is een goede standaardinstelling, maar je moet het weten. De correctie vereist geen enkele leverancierssleutel.
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 gatewayDe --no-deps --entrypoint node is niet decoratief: de service cli deelt de netwerkstack van de gateway en werkt dus pas zodra de container gateway is aangemaakt. Om de configuratie te schrijven vóór de eerste start, moet je via de image van de gateway zelf gaan.
Het resultaat, gekronometreerd: /healthz antwoordde 200 in 7,6 seconden, en Docker markeerde de container als healthy op 11,9 seconden. Een warme herstart, met de staat al geïnitialiseerd, valt terug tot 5,0 seconden.
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}De opstartlogs zijn spraakzaam, en leerzaam.
[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 itLet op de laatste regel: zonder enige geconfigureerde sleutel is de gateway een modellencatalogus op internet gaan ophalen. Een “rustende” gateway gaat toch het netwerk op.
Het geschreven configuratiebestand is 348 bytes groot en bevat geen enkel geheim: het token blijft in de omgevingsvariabele.
{
"gateway": {
"mode": "local",
"bind": "lan",
"auth": { "mode": "token" },
"controlUi": { "allowedOrigins": ["http://127.0.0.1:18789"] }
},
"meta": { "lastTouchedVersion": "2026.9.2" }
}Daarnaast woog state/openclaw.sqlite al 1,5 MB. Daar komen de OAuth-tokens terecht, in platte tekst: de documentatie vraagt om deze map en zijn back-ups als identifiers te behandelen.
Wat de verharding echt blokkeert
Drie controles zijn meer waard dan een intentieverklaring.
De capabilities en de gebruiker
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: 1Geen enkele capability, gebruiker zonder privileges, escalatie onmogelijk. De officiële image doet al het halve werk: ze draait als node (uid 1000) en start tini als proces 1.
De poort gepubliceerd op de loopback beschermt niet tegen Docker
Dit is het resultaat dat me het meest verraste. De poort is alleen gepubliceerd op 127.0.0.1, en toch bereikt een willekeurige container, zelfs op een ander Docker-netwerk, de gateway via zijn bridge-adres.
GWIP=$(docker inspect -f \
'{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' openclaw-lab-gateway)
echo "$GWIP" # 172.29.0.2
# vanuit het standaardnetwerk, niet dat van het 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"
# 200Het voorvoegsel 127.0.0.1: sluit de interfaces van de machine af, niet het netwerk van de Docker-daemon. Als je andere containers op dezelfde machine host, volstaat één gecompromitteerde container om met de gateway te praten. Het tegenmiddel is de authenticatie van de gateway en, op een Linux-server, regels in de keten DOCKER-USER: de hardeningsdocumentatie van OpenClaw geeft er een complete set van voor UFW, omdat de gebruikelijke INPUT-regels het door Docker gepubliceerde verkeer nooit zien.
Het interne netwerk kost meer dan het lijkt
De volgende reflex is om het netwerk op internal: true te zetten. Ik heb gemeten wat dat wegneemt.
# docker-compose.internal.yml
networks:
openclaw:
internal: true| Controle | Bridge-netwerk | Netwerk internal |
|---|---|---|
fetch('https://api.anthropic.com/') vanuit de container | HTTP 404 (bereikbaar) | fetch failed |
curl http://127.0.0.1:18789/healthz vanaf de host | HTTP 200 | verbinding geweigerd |
host.docker.internal opgelost | ja | ja |
Dienst van de host bereikt via host.docker.internal | HTTP 200 | fetch failed |
Container gemarkeerd als healthy | ja | ja |
Twee gevolgen zijn contra-intuïtief. Ten eerste negeert Docker Compose stilzwijgend de sectie ports op een intern netwerk: docker compose ps toont 18789/tcp in plaats van 127.0.0.1:18789->18789/tcp, zonder de minste waarschuwing, en de Control UI wordt onbereikbaar. Ten tweede blijft host.docker.internal zich oplossen maar routeert niet meer: een Ollama die op de hostmachine draait, is dan ook niet bereikbaar.
Het interne netwerk is dus alleen bruikbaar voor een gateway die volledig wordt aangestuurd door docker compose exec, met een model dat door een andere container op hetzelfde netwerk wordt geserveerd. Voor al de rest is het juiste antwoord een eigen bridge plus een SSH-tunnel: de documentatie van OpenClaw beveelt trouwens Tailscale Serve aan in plaats van een LAN-verbinding.
Een model koppelen zonder een sleutel in de compose te schrijven
Er bestaan drie wegen, en geen enkele vraagt om een sleutel in een geversioneerd bestand te plakken.
- Een externe leverancier: de sleutel gaat in
.env(ANTHROPIC_API_KEY,OPENAI_API_KEY), gelezen doorenv_file. Ze komt nooit inopenclaw.jsonterecht. - Een lokaal model op de hostmachine: in een container duidt
127.0.0.1de container zelf aan. De documentatie legthttp://host.docker.internal:11434op voor Ollama, en de host moet verder luisteren dan zijn loopback (OLLAMA_HOST=0.0.0.0:11434 ollama serve). - De Claude Code-binary in de container: mogelijk, maar je moet
/home/nodepersisteren in een named volume, anders wist de volgende image-update de installatie en de authenticatie.
Er is tijdens deze test geen enkele sleutel ingevoerd, en de ollama-plugin wordt wel degelijk door de gateway geladen. Het pad naar het lokale model laat zich zonder sleutel controleren: met een Ollama gepubliceerd op poort 11434 van de host antwoordt http://host.docker.internal:11434/v1 met 200 en in OpenAI-formaat vanuit een container op het bridge-netwerk. Dat is precies het adres dat de documentatie voorschrijft, en de reden waarom 127.0.0.1 niet zou werken: in een container wijst het naar de container.
De image bijwerken zonder de staat te breken
De bewegende tags (latest, main, extended-stable) worden elke week herbouwd vanuit dezelfde bron, om de beveiligingspatches van het basissysteem op te halen tussen twee OpenClaw-versies in. Elke herbouw publiceert ook een onveranderlijke gedateerde tag, van het type 2026.8.1-r20260820: die moet je vastpinnen als je niet wilt dat een implementatie een bewegende tag volgt.
Bij het wisselen van image past de gateway zijn migraties toe bij het opstarten. Lukt dat niet, dan sluit hij af met een fout in plaats van zich gezond te verklaren, en met een herstartbeleid zie je een lus. De gedocumenteerde remedie is dezelfde image één keer opnieuw te draaien met openclaw doctor --fix op dezelfde volumes, en daarna normaal te herstarten.
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 gatewayJe skills kiezen op ClawHub
De image levert 53 kant-en-klare skills: 51 in het basispakket, 2 extra. Dat is al veel, en het volstaat voor de meeste toepassingen. De rest komt van ClawHub, het publieke register, en daar beginnen de problemen. Het formaat is hetzelfde SKILL.md als beschreven in de gids Agent Skills: een YAML-frontmatter, een Markdown-hoofdtekst, bijkomende bestanden.
De documentatie over skills neemt geen blad voor de mond: ze vraagt om elke externe skill als onbetrouwbare code te behandelen en hem te lezen voordat je hem activeert. De cijfers geven haar gelijk. Een telling gepubliceerd op 1 maart 2026 schrijft Antiy CERT 1.184 bevestigde kwaadaardige skills op ClawHub toe, oftewel ongeveer één pakket op vijf op het hoogtepunt van de campagne.
skills verify bevraagt het register zonder iets te installeren, en dat is meegenomen. Zijn totaaloordeel daarentegen verdient het om opengemaakt te worden. Hier is wat het teruggeeft op een populaire Docker-skill, op de datum van de test.
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 bestanden, waaronder SKILL.md (24.283 bytes)Twee van de drie signalen zeggen “suspect”, het pakket is niet ondertekend, zijn GitHub-herkomst is niet geregistreerd, en het samengevatte oordeel blijft “benign, confidence high”. De leesbare samenvatting spreekt van een lokale assistent zonder gedetecteerde exfiltratie, en het gaat hier niet om deze specifieke skill. Onthoud het verschil tussen de signalen en de conclusie: een groene score is geen lezing.
De regel die ik onthoud, past in drie punten: lees het SKILL.md voor je installeert (skills info geeft het exacte pad van het bestand), weiger elke skill die het netwerk op gaat zonder dat zijn functie dat vereist, en geef de voorkeur aan de 53 skills die met de image worden meegeleverd zolang ze volstaan. Het veld security.installPolicy van de configuratie laat toe deze vangrail af te dwingen in plaats van op discipline te rekenen.
MCP-servers toevoegen
De gateway beheert zijn MCP-servers in mcp.servers, met een volledige commando-oppervlakte: add (die de server aftast voor het registreren), probe, doctor, status, tools om de blootgestelde tools te filteren, en login / logout voor de OAuth-servers.
docker compose run --rm -T cli mcp doctor # statische configuratiefouten
docker compose run --rm -T cli mcp status # transporten, zonder te verbinden
docker compose run --rm -T cli mcp probe # echte verbinding, somt de mogelijkheden op
docker compose run --rm -T cli mcp tools # include/exclude-filters per serverTwee reflexen zijn het bewaren waard. mcp tools bestaat, gebruik het. Een MCP-server stelt vaak dertig tools bloot terwijl jij er drie wilt, en elke extra tool is een beschrijving die het model als een instructie leest. En een MCP-server in STDIO draait in de container van de gateway, dus met zijn volumes en zijn omgevingsvariabelen. De onderzoeksnota van de Cloud Security Alliance van 4 mei 2026 beveelt precies het omgekeerde aan: een eigen container per server, zonder toegang tot de identifiers van de host. Het onderwerp wordt in detail behandeld in een MCP-server bouwen in PHP.
OpenClaw, Claude Code of Hermes Agent?
De drie spelen niet op dezelfde plek, en ze door elkaar halen leidt tot verkeerde keuzes.
| OpenClaw | Claude Code | Hermes Agent | |
|---|---|---|---|
| Vorm | Permanente gateway | Sessie in terminal | Gecontaineriseerde agent |
| Trigger | Berichten, cron, Control UI | Jij, aan het toetsenbord | Taken en wachtrijen |
| Netwerkoppervlak | Een permanent open poort | Geen enkele inkomende listener | Afhankelijk van de implementatie |
| Model | Leverancier naar keuze, Ollama inbegrepen | Anthropic | Afhankelijk van de implementatie |
Claude Code luistert nergens naar: sluit de terminal, en het aanvalsoppervlak verdwijnt. OpenClaw luistert permanent, door zijn ontwerp, omdat dat is wat je hem vraagt: om drie uur ’s nachts op een Telegram-bericht antwoorden. Hermes Agent, op dezelfde manier geïnstalleerd in een apart artikel, neemt een derde positie in: een agent die vanaf het begin bedacht is om in een container te draaien. Het overzicht van command line-agents situeert de andere, en OpenCode behandelt het geval van de open source code-agent.
Alles verwijderen
Een laboratorium haal je weer uit elkaar. docker compose down alleen laat de volumes achter, met de SQLite-database en zijn tokens.
docker compose down --volumes --remove-orphans
docker rmi ghcr.io/openclaw/openclaw:2026.9.2 alpine/openclaw:latest
rm -f .env
# controle: er mag niets overblijven
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 clawWat je moet onthouden
- Neem
ghcr.io/openclaw/openclaw: de spiegelalpine/openclawhad op de dag van de test drie maanden achterstand (2026.6.9 tegenover 2026.9.2). - De gateway weigert te starten zonder configuratie en gaat in een herstartlus: schrijf
gateway.mode=localvóór de eersteup. - Publiceren op
127.0.0.1sluit de interfaces van de machine af, niet het Docker-netwerk: een naburige container bereikte de gateway met HTTP 200. internal: trueverwijdert stilzwijgend de gepubliceerde poort en de toegang tothost.docker.internal: voorbehouden aan gateways die viaexecworden aangestuurd.- Het token leeft in
.env, nooit in de compose.state/openclaw.sqlitebevat OAuth-tokens in platte tekst en wordt behandeld als een geheim. - Op ClawHub kan een oordeel “clean” twee signalen “suspect”, een niet-ondertekend pakket en een onbekende herkomst verbergen. Lees het
SKILL.md.
Veelgemaakte fouten
ports: ["18789:18789"] publiceert op alle interfaces, lokaal netwerk inbegrepen. Schrijf "127.0.0.1:18789:18789", en voeg op een Linux-server regels toe in de keten DOCKER-USER: de INPUT-regels zien het door Docker gepubliceerde verkeer niet.172.29.0.2:18789/healthz met HTTP 200. Het voorvoegsel sluit de interfaces van de machine af, niet de daemon. Reken op de authenticatie van de gateway, niet op de publicatie.restart: unless-stopped verbergt de fout. Schrijf de configuratie met config set --batch-json en --no-deps --entrypoint node vóór de eerste start.ghcr.io/openclaw/openclaw of openclaw/openclaw te gebruiken.ports zonder waarschuwing, de Control UI wordt onbereikbaar, en host.docker.internal lost zich nog op maar routeert niet meer: een Ollama op de host wordt onbereikbaar.

