Vai al contenuto

9router in Docker Compose, esposto con Cloudflare Tunnel

9router in Docker Compose, protetto e persistente, esposto via Cloudflare Tunnel con fallback automatico tra le tue subscription AI.

Testato il: 2026-09-13 Ambiente: Ubuntu 24.04.4 LTS · Docker Engine 29.8.0 · Docker Compose v5.5.1 · cloudflared 2026.9.0 · 9router 0.5.75

Prerequisiti

  • Un server Linux (qui Ubuntu 24.04) con Docker Engine e il plugin Docker Compose installati
  • Un dominio i cui nameserver puntano a Cloudflare, per il tunnel nominato (il quick tunnel non lo richiede)
  • Almeno una subscription AI da usare come upstream: Claude Pro, ChatGPT Business/Plus, OpenCode Go
  • cloudflared installato sul server: lo si installa al passo 6 se manca

Lo scopo è questo: un solo endpoint OpenAI-compatible, http://localhost:20128/v1, che inoltra le richieste alle subscription che già paghi e, quando una va in quota, passa automaticamente alla successiva. Niente più sessioni di coding ferme a mezzanotte per un rate limit.

1. Preparare la directory e generare i segreti

Crea la directory di lavoro:

mkdir -p ~/9router && cd ~/9router

9router legge i segreti dalle variabili d'ambiente. Genera tre valori casuali, uno per JWT_SECRET (firma dei cookie di sessione), uno per API_KEY_SECRET (firma delle API key generate) e uno per MACHINE_ID_SALT:

openssl rand -hex 32

Esegui il comando tre volte e conserva i tre output: ti serviranno al passo successivo. Poi scegli una password forte per INITIAL_PASSWORD: non è un dettaglio, vedi il passo 4.

2. Creare .env e docker-compose.yml

Crea il file .env nella directory:

cat > .env <<'EOF'
JWT_SECRET=INSERISCI_Q1
API_KEY_SECRET=INSERISCI_Q2
MACHINE_ID_SALT=INSERISCI_Q3
INITIAL_PASSWORD=INSERISCI_UNA_PASSWORD_FORTE
DATA_DIR=/app/data
PORT=20128
NODE_ENV=production
EOF

Sostituisci i placeholder con i valori generati al passo 1. DATA_DIR=/app/data è il punto in cui il container scrive il database, che ora mappiamo su un volume. Le variabili di sicurezza AUTH_COOKIE_SECURE e REQUIRE_API_KEY le attiviamo al passo 7, quando il servizio diventa pubblico: in locale non servono e complicherebbero solo il primo accesso.

Poi il file Compose. Uso un bind mount invece del named volume del repo ufficiale: $HOME/.9router resta visibile sul host, il database db/data.sqlite lo puoi ispezionare e fare backup con un semplice cp.

services:
  9router:
    image: decolua/9router:0.5.75
    container_name: 9router
    restart: always
    ports:
      - "20128:20128"
    volumes:
      - "$HOME/.9router:/app/data"
    env_file:
      - .env
    environment:
      DATA_DIR: /app/data
      PORT: "20128"
      HOSTNAME: "0.0.0.0"
      NODE_ENV: production

Nota la versione dell'immagine, 0.5.75, fissata esplicitamente: non usare :latest su un servizio che esponi, così un aggiornamento è una decisione tua (docker compose pull && docker compose up -d), non un evento casuale al prossimo riavvio.

Verifica che il file sia valido:

docker compose config --quiet

Nessun output e codice di uscita 0 significa che la sintassi è corretta.

3. Avviare lo stack e verificare l'endpoint in locale

docker compose up -d

Atteso: il container parte e resta attivo. Controlla:

docker compose ps

Atteso: 9router in stato Up. Poi i log di avvio:

docker logs -f 9router

Atteso: una riga che indica il driver del database, ad esempio [DB] Driver: node:sqlite | file: /app/data/db/data.sqlite, e il server in ascolto sulla porta 20128. Premi Ctrl+C per uscire dal follow.

Conferma che l'endpoint risponde:

curl -s -o /dev/null -w "%{http_code}\n" http://localhost:20128/v1/models

Atteso: 200. Se vedi 000, il container non è raggiungibile: controlla docker compose ps e che la porta non sia già occupata da un altro processo (ss -ltnp | grep 20128).

4. Primo accesso alla dashboard e cambio password

Apri http://localhost:20128/dashboard e accedi con la password che hai messo in INITIAL_PASSWORD.

Qui c'è il punto su cui non transigo: cambia subito la password.

Password di default, la porta d'ingresso — Con `INITIAL_PASSWORD` non valorizzata, 9router accetta `123456` come password di login, qualunque sia la versione. CVE-2026-63732 (CVSS 9.4) ha dimostrato che un'istanza fresca esposta su internet è controllabile da chiunque acceda con la password di default, spoofi l'header Host e registri un plugin MCP malevolo.

La prima cosa da fare dopo il login è Dashboard → Settings → Change Password, con la nuova password forte. Poi, se vuoi, riduci il rischio residuo disattivando i plugin MCP che non usi.

5. Creare l'API key e collegare i provider

L'endpoint /v1 si autentica con una API key che crei tu dal dashboard. Vai in Dashboard → Settings → API Keys, genera una chiave e copia il valore (ha il formato 9r_...): ti serve per qualsiasi tool che punti all'endpoint.

Ora collega le subscription. I percorsi nel dashboard sono questi:

  • Claude Pro: Dashboard → Providers → Connect Claude Code → OAuth login. Modelli disponibili: cc/claude-opus-4-7, cc/claude-sonnet-4-6
  • ChatGPT (Codex): Dashboard → Providers → Connect Codex → OAuth login (callback sulla porta 1455). Modelli: cx/gpt-5.5, cx/gpt-5.4
  • OpenCode Go: Dashboard → Providers → Connect OpenCode Free, nessun login richiesto. Modello generico: oc/<auto>

Il valore di 9router è il fallback a tier: subscription come primario, free come ultima spiaggia. Crea un combo in Dashboard → Combos → Create New:

Name: personal-stack
Models:
  1. cc/claude-opus-4-7
  2. cx/gpt-5.5
  3. oc/<auto>

Quando la quota di Claude si esaurisce, la richiesta passa a Codex; se anche quello va in limite, finisce su OpenCode. Nei tool, invece di un modello singolo, punti a personal-stack.

Verifica un provider prima di proseguire: Dashboard → Providers → il pulsante Test accanto alla connessione deve rispondere con esito positivo.

6. Quick tunnel per il primo test esterno

Il quick tunnel non richiede account né dominio: è il modo più rapido per vedere il servizio dall'esterno, ma l'URL cambia a ogni riavvio. Usalo solo come test.

cloudflared tunnel --url http://localhost:20128

Atteso: cloudflared apre un URL https://<identificatore-casuale>.trycloudflare.com. Aprilo dal browser: devi vedere la dashboard, e da qui puoi già usare 9router da qualsiasi dispositivo. Per l'uso stabile però serve il tunnel nominato del passo successivo.

7. Tunnel nominato e attivazione delle protezioni pubbliche

Installazione di cloudflared

Installa cloudflared, se non l'hai già fatto (repo ufficiale Cloudflare):

sudo mkdir -p --mode=0755 /usr/share/keyrings
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared any main" | sudo tee /etc/apt/sources.list.d/cloudflared.list
sudo apt-get update && sudo apt-get install cloudflared

Verifica la versione:

cloudflared --version

Login e creazione del tunnel

Autentica il tunnel col tuo account Cloudflare:

cloudflared tunnel login

Atteso: si apre un browser, scegli il dominio da gestire e cloudflared genera il certificato cert.pem in ~/.cloudflared/. Crea il tunnel:

cloudflared tunnel create 9router

Atteso: un messaggio con l'UUID del tunnel e il percorso del file di credenziali, ~/.cloudflared/<UUID>.json. Copia l'UUID. Ora crea la configurazione:

cat > ~/.cloudflared/config.yml <<EOF
url: http://localhost:20128
tunnel: INSERISCI_L_UUID
credentials-file: /root/.cloudflared/INSERISCI_L_UUID.json
EOF

Sostituisci l'UUID in entrambi i punti (se l'utente non è root, il percorso del file di credenziali è ~/.cloudflared/INSERISCI_L_UUID.json). Associa il DNS:

cloudflared tunnel route dns 9router router.tuodominio.com

Atteso: un record CNAME che punta a <UUID>.cfargotunnel.com.

Esposizione e protezioni pubbliche

A questo punto il servizio ha un indirizzo stabile, quindi è il momento di alzare le protezioni. Modifica .env:

cat >> .env <<'EOF'
REQUIRE_API_KEY=true
AUTH_COOKIE_SECURE=true
BASE_URL=https://router.tuodominio.com
EOF
Le env non si applicano con un restart — `env_file` viene letto alla creazione del container: dopo aver modificato `.env` serve la ricreazione con `docker compose up -d`, un semplice `docker restart` non basta.
docker compose up -d

Atteso: il container viene ricreato con le nuove variabili. Avvia il tunnel:

cloudflared tunnel run 9router

Atteso: il tunnel si connette agli edge Cloudflare e resta in attesa. Per la persistenza dopo un riavvio del server, Cloudflare mette a disposizione l'installazione come servizio:

cloudflared service install

che legge la configurazione da /etc/cloudflared/config.yml (sposta lì il file se usi questa strada).

Verifica

Dall'esterno, con l'API key creata al passo 5:

curl -s https://router.tuodominio.com/v1/models -H "Authorization: Bearer INSERISCI_LA_API_KEY"

Atteso: 200 e il JSON con la lista dei modelli. Senza la chiave:

curl -s -o /dev/null -w "%{http_code}\n" https://router.tuodominio.com/v1/models

Atteso: 401: con REQUIRE_API_KEY=true l'endpoint rifiuta le richieste anonime. Controlla anche che la dashboard sia servita in HTTPS:

curl -sI https://router.tuodominio.com/dashboard | head -1

Atteso: HTTP/2 200. Infine, da un tool client (Claude Code, Codex o Cursor) punta l'endpoint a https://router.tuodominio.com/v1 con la tua API key e il modello personal-stack: la prima richiesta deve rispondere senza errori.

Limiti noti

  • Il quick tunnel genera un URL nuovo a ogni avvio: per l'uso quotidiano serve il tunnel nominato, non sostituirlo.
  • REQUIRE_API_KEY=true protegge l'endpoint /v1, ma la dashboard resta protetta solo dalla password. Se vuoi uno strato in più, metti davanti al tunnel un accesso Cloudflare Access con il tuo account come unico utente.
  • La password di fallback 123456 (CVE-2026-63732) è insita nel codice anche nelle versioni recenti se INITIAL_PASSWORD non è valorizzata: la copertura è la tua password forte e l'aggiornamento dell'immagine, non altro.
  • I nomi dei modelli (es. cc/claude-opus-4-7, cx/gpt-5.5) seguono il catalogo del momento: se un modello sparisce dal README del progetto, il combo va aggiornato dal dashboard.
  • L'immagine decolua/9router:0.5.75 è quella verificata per questa guida: prima di esporre un nuovo tag, controlla i changelog del progetto.
Condividi