
OpenClaw è un assistente personale open source che gira come gateway permanente, ascolta sulla porta 18789 ed esegue skill. Sotto Docker, prendi l'immagine ufficiale ghcr.io/openclaw/openclaw, pubblica la porta solo su 127.0.0.1, togli tutte le capability al container e tieni il token in un file .env fuori dal compose. Attenzione: il gateway rifiuta di avviarsi finché gateway.mode non è scritto, e pubblicare sul loopback non lo protegge dagli altri container della macchina.
OpenClaw si installa con un comando, ed è esattamente questo il problema: l’11 febbraio 2026, SecurityScorecard contava più di quarantamila gateway aperti su Internet nelle prime ventiquattro ore del suo rilevamento, di cui il 35,4% giudicati vulnerabili a un’esecuzione di codice remota. Docker non risolve nulla da solo, ciò che conta è cosa pubblica il tuo file compose, cosa toglie al container, e dove vivono i segreti.
OpenClaw, cos’è?
OpenClaw è un assistente personale open source, sotto licenza MIT, conosciuto in precedenza con i nomi Clawdbot e poi Moltbot. Non è un agente di codice nel senso di Claude Code o di Codex, ma un gateway, un processo che gira in permanenza, espone un’interfaccia web (la Control UI) e un’API WebSocket sulla porta 18789, collega canali di messaggistica (WhatsApp, Telegram, Discord) ed esegue «skill» a nome del suo proprietario.
Il modello linguistico viene da altrove: l’immagine ufficiale include plugin per Anthropic, OpenAI, xAI e Ollama, e fornisci tu la chiave o l’indirizzo del server locale. Il gateway è dunque una macchina per eseguire istruzioni, essendo il modello solo uno dei suoi fornitori. È questo che rende la sua esposizione di rete pericolosa, molto più del modello scelto.
Perché Docker invece di un’installazione locale?
La documentazione dice che Docker è opzionale, e ha ragione sul piano funzionale. Sul piano della sicurezza, la differenza è netta. Un’installazione locale dà al gateway il tuo account utente: la tua cartella personale, le tue chiavi SSH, il tuo portachiavi, i tuoi repository. Un container gli dà un utente senza privilegi, tre volumi e nient’altro. È lo stesso ragionamento degli agenti di codice, dettagliato in isolare Claude Code e Codex: l’unica frontiera che tiene è quella che il sistema operativo fa rispettare.
Il prezzo da pagare è reale. Le dipendenze di sistema richieste da certi skill (ffmpeg, tmux, un browser) non sono nell’immagine, e la documentazione è categorica: installare binari in un container che gira è una trappola, bisogna cuocerli al momento della costruzione con OPENCLAW_IMAGE_APT_PACKAGES.
Quale immagine scegliere?
La documentazione Docker di OpenClaw censisce due repository. Il registro ufficiale è ghcr.io/openclaw/openclaw, con un mirror Docker Hub sotto openclaw/openclaw. Un terzo, alpine/openclaw, è un mirror non ufficiale che la documentazione chiede esplicitamente di evitare, perché non segue né il calendario di pubblicazione né la politica di retention del progetto.
Ho scaricato entrambi il 7 settembre 2026 per verificare lo scarto.
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.692ZIl mirror alpine/openclaw era fermo alla versione 2026.6.9, costruita il 21 giugno 2026: quasi tre mesi di ritardo, correzioni di sicurezza Debian comprese. L’immagine ufficiale, invece, portava la versione 2026.9.2, costruita il 5 settembre, due giorni prima del mio test. La scelta non è una questione di gusto.
Prevedi lo spazio: docker image inspect annuncia 1,10 GB di contenuto per l’immagine ufficiale, ma la colonna DISK USAGE di docker images sotto Docker 29 ne contava 4,43 GB realmente occupati sul disco.
Il file compose blindato
La documentazione fornisce un docker-compose.yml completo, ma presuppone un repository clonato e uno script di installazione. Ecco la versione minima che ho scritto ed eseguito, con la sola immagine pre-costruita.
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 # il token vive qui, non nel 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:Sei decisioni meritano una spiegazione.
- Il prefisso
127.0.0.1:inports: senza di esso, Docker pubblica su tutte le interfacce della macchina, compresa quella della rete locale. È l’errore più comune, e spiega buona parte dei gateway trovati dagli scanner. --bind lannonostante tutto: nel container,loopbackvorrebbe dire «nessuno può raggiungermi, nemmeno Docker». È la pubblicazione della porta su127.0.0.1dell’host a fare il lavoro di restrizione, non la modalità di binding interna.cap_drop: [ALL]: il compose ufficiale toglie soloNET_RAWeNET_ADMIN. Toglierle tutte funziona anche, verifica fatta più sotto.- Il servizio
clidietro unprofiles: condivide la pila di rete del gateway (network_mode: "service:gateway"), quindi è dentro la frontiera di fiducia. Il profilo evita che undocker compose updistratto lo lasci girare in permanenza. - Volumi con nome, non bind mount: la documentazione insiste per montare lo stato come una directory, mai come un file isolato, pena la divergenza tra host e container dopo una scrittura di configurazione.
env_filepiuttosto cheenvironment: il token non compare né nel compose, né nel repository Git.
Una precisazione sulla porta: la 18789 era già occupata sulla mia macchina da un altro gateway, ho quindi pubblicato il laboratorio su 127.0.0.1:18889. Tutte le misurazioni che seguono riguardano questa porta, sostituiscila con 18789 da te, compreso in gateway.controlUi.allowedOrigins.
Il file .env si genera localmente e non si committa mai.
umask 077
printf 'OPENCLAW_GATEWAY_TOKEN=%s\n' "$(openssl rand -hex 32)" > .env
chmod 600 .env
echo '.env' >> .gitignorePrimo avvio: la configurazione mancante
Avviare la pila così com’è non basta. Il container si avvia, fallisce, riparte, e ricomincia.
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).Con restart: unless-stopped, Docker era arrivato a undici riavvii prima che guardassi i log, e il container era segnato unhealthy. Il gateway rifiuta di servire senza configurazione esplicita: è un buon comportamento predefinito, ma bisogna saperlo. La correzione non richiede nessuna chiave di fornitore.
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 gatewayIl --no-deps --entrypoint node non è decorativo: il servizio cli condivide la pila di rete del gateway e funziona quindi solo una volta creato il container gateway. Per scrivere la configurazione prima del primo avvio, bisogna passare dall’immagine del gateway stessa.
Il risultato, cronometrato: /healthz ha risposto 200 in 7,6 secondi, e Docker ha segnato il container healthy a 11,9 secondi. Un riavvio a caldo, con lo stato già inizializzato, scende a 5,0 secondi.
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}I log di avvio sono prolissi, e istruttivi.
[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 itNota l’ultima riga: senza nessuna chiave configurata, il gateway è andato a cercare un catalogo di modelli su Internet. Un gateway «a riposo» esce comunque sulla rete.
Il file di configurazione scritto è di 348 byte e non contiene nessun segreto: il token resta nella variabile d’ambiente.
{
"gateway": {
"mode": "local",
"bind": "lan",
"auth": { "mode": "token" },
"controlUi": { "allowedOrigins": ["http://127.0.0.1:18789"] }
},
"meta": { "lastTouchedVersion": "2026.9.2" }
}Accanto, state/openclaw.sqlite pesava già 1,5 MB. È lì che finiscono i token OAuth, in chiaro: la documentazione chiede di trattare questa directory e i suoi backup come credenziali.
Cosa blocca davvero la blindatura
Tre verifiche valgono più di una dichiarazione d’intenti.
Le capability e l’utente
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: 1Nessuna capability, utente non privilegiato, elevazione impossibile. L’immagine ufficiale fa già metà del lavoro: gira come node (uid 1000) e lancia tini come processo 1.
La porta pubblicata sul loopback non protegge da Docker
È il risultato che mi ha sorpreso di più. La porta è pubblicata solo su 127.0.0.1, eppure un container qualsiasi, anche su un’altra rete Docker, raggiunge il gateway tramite il suo indirizzo di bridge.
GWIP=$(docker inspect -f \
'{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' openclaw-lab-gateway)
echo "$GWIP" # 172.29.0.2
# dalla rete predefinita, non quella del 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"
# 200Il prefisso 127.0.0.1: chiude le interfacce della macchina, non la rete del demone Docker. Se ospiti altri container sulla stessa macchina, basta che uno solo di essi sia compromesso per parlare al gateway. Il rimedio è l’autenticazione del gateway e, su un server Linux, regole nella catena DOCKER-USER: la documentazione di blindatura di OpenClaw ne dà un set completo per UFW, perché le regole INPUT abituali non vedono mai il traffico pubblicato da Docker.
La rete interna costa più di quanto sembri
Il riflesso successivo è passare la rete a internal: true. Ho misurato cosa toglie.
# docker-compose.internal.yml
networks:
openclaw:
internal: true| Verifica | Rete bridge | Rete internal |
|---|---|---|
fetch('https://api.anthropic.com/') dal container | HTTP 404 (raggiungibile) | fetch failed |
curl http://127.0.0.1:18789/healthz dall’host | HTTP 200 | connessione rifiutata |
host.docker.internal risolto | sì | sì |
Servizio dell’host raggiunto via host.docker.internal | HTTP 200 | fetch failed |
Container segnato healthy | sì | sì |
Due conseguenze sono controintuitive. Anzitutto, Docker Compose ignora silenziosamente la sezione ports su una rete interna: docker compose ps mostra 18789/tcp invece di 127.0.0.1:18789->18789/tcp, senza il minimo avviso, e la Control UI diventa inaccessibile. Poi, host.docker.internal continua a risolversi ma non instrada più: anche un Ollama che gira sulla macchina host non è più raggiungibile.
La rete interna è quindi utilizzabile solo per un gateway pilotato interamente da docker compose exec, con un modello servito da un altro container della stessa rete. Per tutto il resto, la risposta giusta è un bridge dedicato più un tunnel SSH: la documentazione di OpenClaw raccomanda peraltro Tailscale Serve piuttosto che un collegamento LAN.
Collegare un modello senza scrivere una chiave nel compose
Esistono tre percorsi, e nessuno richiede di incollare una chiave in un file versionato.
- Un fornitore remoto: la chiave va in
.env(ANTHROPIC_API_KEY,OPENAI_API_KEY), letta daenv_file. Non entra mai inopenclaw.json. - Un modello locale sulla macchina host: in un container,
127.0.0.1designa il container. La documentazione imponehttp://host.docker.internal:11434per Ollama, e l’host deve ascoltare oltre il suo loopback (OLLAMA_HOST=0.0.0.0:11434 ollama serve). - Il binario Claude Code nel container: possibile, ma bisogna persistere
/home/nodein un volume con nome, altrimenti il prossimo aggiornamento dell’immagine cancella l’installazione e l’autenticazione.
Nessuna chiave è stata inserita durante questo test, e il plugin ollama è effettivamente caricato dal gateway. Il percorso del modello locale si verifica senza chiave: con un Ollama pubblicato sulla porta 11434 dell’host, http://host.docker.internal:11434/v1 risponde 200 e in formato OpenAI da un container della rete bridge. È esattamente l’indirizzo che impone la documentazione, e la ragione per cui 127.0.0.1 non funzionerebbe: in un container, designa il container.
Aggiornare l’immagine senza rompere lo stato
Le etichette mobili (latest, main, extended-stable) vengono ricostruite ogni settimana a partire dalla stessa fonte, per recuperare le correzioni di sicurezza del sistema di base tra due versioni di OpenClaw. Ogni ricostruzione pubblica anche un’etichetta datata immutabile, del tipo 2026.8.1-r20260820: è quella da fissare quando non vuoi che un deployment segua un’etichetta mobile.
Al cambio di immagine, il gateway applica le sue migrazioni all’avvio. Se non ci riesce, esce in errore invece di dichiararsi sano, e con una politica di riavvio, vedrai un loop. Il rimedio documentato è rilanciare la stessa immagine una volta con openclaw doctor --fix sugli stessi volumi, poi riavviare 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 gatewayScegliere i propri skill su ClawHub
L’immagine consegna 53 skill pronti all’uso: 51 nel pacchetto di base, 2 extra. È già molto, e basta per la maggior parte degli usi. Il resto viene da ClawHub, il registro pubblico, ed è lì che iniziano i guai. Il formato è lo stesso SKILL.md descritto nella guida Agent Skills: un frontmatter YAML, un corpo Markdown, file annessi.
La documentazione degli skill non usa mezzi termini: chiede di trattare ogni skill di terze parti come codice non affidabile e di leggerlo prima di attivarlo. I numeri le danno ragione. Un censimento pubblicato il 1° marzo 2026 attribuisce ad Antiy CERT 1.184 skill malevoli confermati su ClawHub, circa un pacchetto su cinque al picco della campagna.
skills verify interroga il registro senza installare nulla, e questo è apprezzabile. Il suo verdetto globale, invece, merita di essere aperto. Ecco cosa restituisce su uno skill Docker popolare, alla data del 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 file, di cui SKILL.md (24.283 byte)Due dei tre segnali dicono «sospetto», il pacchetto non è firmato, la sua provenienza GitHub non è registrata, e il verdetto aggregato resta «benigno, fiducia alta». Il riassunto leggibile parla di un assistente locale senza esfiltrazione rilevata, e non è questo skill a essere in causa. Tieni a mente lo scarto tra i segnali e la conclusione: un punteggio verde non è una lettura.
La regola che ne traggo sta in tre punti: leggere il SKILL.md prima di installare (skills info dà il percorso esatto del file), rifiutare ogni skill che esce sulla rete senza che la sua funzione lo richieda, e preferire i 53 skill consegnati con l’immagine finché bastano. Il campo security.installPolicy della configurazione permette di imporre questo paletto invece di contare sulla disciplina.
Aggiungere server MCP
Il gateway gestisce i suoi server MCP in mcp.servers, con una superficie di comandi completa: add (che sonda il server prima di registrarlo), probe, doctor, status, tools per filtrare gli strumenti esposti, e login / logout per i server OAuth.
docker compose run --rm -T cli mcp doctor # difetti di configurazione statici
docker compose run --rm -T cli mcp status # trasporti, senza connettersi
docker compose run --rm -T cli mcp probe # connessione reale, elenca le capacità
docker compose run --rm -T cli mcp tools # filtri include/exclude per serverDue riflessi vale la pena conservare. mcp tools esiste, usalo. Un server MCP espone spesso trenta strumenti quando ne vuoi tre, e ogni strumento supplementare è una descrizione che il modello legge come un’istruzione. E un server MCP in STDIO gira dentro il container del gateway, quindi con i suoi volumi e le sue variabili d’ambiente. La nota di ricerca della Cloud Security Alliance del 4 maggio 2026 raccomanda esattamente il contrario: un container dedicato per server, senza accesso alle credenziali dell’host. L’argomento è trattato in dettaglio in creare un server MCP in PHP.
OpenClaw, Claude Code o Hermes Agent?
I tre non giocano nello stesso campo, e confonderli porta a scelte sbagliate.
| OpenClaw | Claude Code | Hermes Agent | |
|---|---|---|---|
| Forma | Gateway permanente | Sessione da terminale | Agente containerizzato |
| Innesco | Messaggistica, cron, Control UI | Tu, da tastiera | Task e code |
| Superficie di rete | Una porta aperta in permanenza | Nessun ascolto in entrata | A seconda del deployment |
| Modello | Fornitore a scelta, Ollama compreso | Anthropic | A seconda del deployment |
Claude Code non ascolta nulla: chiudi il terminale, la superficie d’attacco scompare. OpenClaw ascolta in permanenza, per costruzione, perché è ciò che gli si chiede: rispondere a un messaggio Telegram alle tre del mattino. Hermes Agent, installato nello stesso modo in un articolo dedicato, occupa una terza posizione: un agente pensato per girare in container fin dall’inizio. Il panorama degli agenti a riga di comando colloca gli altri, e OpenCode copre il caso dell’agente di codice open source.
Eliminare tutto
Un laboratorio si smonta. docker compose down da solo lascia i volumi dietro di sé, con il database SQLite e i suoi token.
docker compose down --volumes --remove-orphans
docker rmi ghcr.io/openclaw/openclaw:2026.9.2 alpine/openclaw:latest
rm -f .env
# verifica: nulla deve comparire
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 clawCosa ricordare
- Prendi
ghcr.io/openclaw/openclaw: il mirroralpine/openclawaveva tre mesi di ritardo il giorno del test (2026.6.9 contro 2026.9.2). - Il gateway rifiuta di avviarsi senza configurazione ed entra in loop di riavvio: scrivi
gateway.mode=localprima del primoup. - Pubblicare su
127.0.0.1chiude le interfacce della macchina, non la rete Docker: un container vicino raggiungeva il gateway in HTTP 200. internal: trueelimina silenziosamente la porta pubblicata e l’accesso ahost.docker.internal: da riservare ai gateway pilotati daexec.- Il token vive in
.env, mai nel compose.state/openclaw.sqlitecontiene token OAuth in chiaro e si tratta come un segreto. - Su ClawHub, un verdetto «clean» può nascondere due segnali «suspicious», un pacchetto non firmato e una provenienza sconosciuta. Leggi il
SKILL.md.
Errori frequenti
ports: ["18789:18789"] pubblica su tutte le interfacce, rete locale compresa. Scrivi "127.0.0.1:18789:18789", e su un server Linux aggiungi regole nella catena DOCKER-USER: le regole INPUT non vedono il traffico pubblicato da Docker.172.29.0.2:18789/healthz in HTTP 200. Il prefisso chiude le interfacce della macchina, non il demone. Conta sull'autenticazione del gateway, non sulla pubblicazione.docker compose up senza configurazione Il container si blocca in loop su Missing config. Run openclaw setup or set gateway.mode=local e restart: unless-stopped maschera l'errore. Scrivi la configurazione con config set --batch-json e --no-deps --entrypoint node prima del primo avvio.alpine/openclaw per l'immagine ufficiale È un mirror non ufficiale, fermo alla 2026.6.9 del 21 giugno 2026 quando l'ufficiale era alla 2026.9.2. La documentazione chiede di usare ghcr.io/openclaw/openclaw o openclaw/openclaw.internal: true come semplice blindatura Compose ignora allora la sezione ports senza avviso, la Control UI diventa irraggiungibile, e host.docker.internal si risolve ma non instrada più: un Ollama sull'host diventa inaccessibile.

