OpenClaw con Docker: installare e blindare il gateway

OpenClaw gira come gateway permanente e decine di migliaia di istanze sono aperte su Internet. Ecco l'installazione Docker Compose blindata, eseguita e misurata il 7 settembre 2026.

OpenClaw con Docker: installare e blindare il gateway
Risposta rapida

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.

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

Il 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.

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          # 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: in ports: 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 lan nonostante tutto: nel container, loopback vorrebbe dire «nessuno può raggiungermi, nemmeno Docker». È la pubblicazione della porta su 127.0.0.1 dell’host a fare il lavoro di restrizione, non la modalità di binding interna.
  • cap_drop: [ALL]: il compose ufficiale toglie solo NET_RAW e NET_ADMIN. Toglierle tutte funziona anche, verifica fatta più sotto.
  • Il servizio cli dietro un profiles: condivide la pila di rete del gateway (network_mode: "service:gateway"), quindi è dentro la frontiera di fiducia. Il profilo evita che un docker compose up distratto 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_file piuttosto che environment: 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.

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

Primo avvio: la configurazione mancante

Avviare la pila così com’è non basta. Il container si avvia, fallisce, riparte, e ricomincia.

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).

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.

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

Il --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.

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}

I log di avvio sono prolissi, e istruttivi.

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

Nota 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.

json
{
  "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

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

Nessuna 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.

bash
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"
# 200

Il 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.

yaml
# 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
Servizio dell’host raggiunto via host.docker.internal HTTP 200 fetch failed
Container segnato healthy

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.

  1. Un fornitore remoto: la chiave va in .env (ANTHROPIC_API_KEY, OPENAI_API_KEY), letta da env_file. Non entra mai in openclaw.json.
  2. Un modello locale sulla macchina host: in un container, 127.0.0.1 designa il container. La documentazione impone http://host.docker.internal:11434 per Ollama, e l’host deve ascoltare oltre il suo loopback (OLLAMA_HOST=0.0.0.0:11434 ollama serve).
  3. Il binario Claude Code nel container: possibile, ma bisogna persistere /home/node in 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.

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

Scegliere 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.

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 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.

bash
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 server

Due 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.

bash
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 claw

Cosa ricordare

  • Prendi ghcr.io/openclaw/openclaw: il mirror alpine/openclaw aveva 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=local prima del primo up.
  • Pubblicare su 127.0.0.1 chiude le interfacce della macchina, non la rete Docker: un container vicino raggiungeva il gateway in HTTP 200.
  • internal: true elimina silenziosamente la porta pubblicata e l’accesso a host.docker.internal: da riservare ai gateway pilotati da exec.
  • Il token vive in .env, mai nel compose. state/openclaw.sqlite contiene 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

Pubblicare la porta senza prefisso di indirizzo 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.
Credere che 127.0.0.1 isoli il gateway Un container di un'altra rete Docker ha raggiunto 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.
Lanciare 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.
Prendere 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.
Aggiungere 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.

DockerMCPOllamaOpenClawSécuritéSkills

Damien Flandrin Sviluppatore web dal 2010, creatore di Gekkode e di Email Impact. Ogni articolo è testato su un progetto reale prima della pubblicazione. Contatti
Newsletter

I nuovi test, tutorial e progetti, via e-mail.

Test riproducibili, codice versionato, risultati datati. Mai spam.