> ## Content Index
> Fetch the complete content index at: https://adrianoamalfi.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# 9router in Docker Compose, esposto con Cloudflare Tunnel
- URL: https://adrianoamalfi.com/tech-scratchpad/9router-docker-compose-cloudflare-tunnel/
- Published: 2026-09-13T19:39:07.000Z
- Updated: 2026-09-28T12:12:08.000Z
- Description: 9router in Docker Compose, protetto e persistente, esposto via Cloudflare Tunnel con fallback automatico tra le tue subscription AI.
- Author: Adriano Amalfi
- Tags: AI Agentica, Scratchpad, AI Tools, LLM, Tutorial, #Tech Scratchpad: Appunti avanzati e esperimenti per esperti e amatoriali

**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](https://adrianoamalfi.com/opencode-go-guida-abbonamento/)
- cloudflared installato sul server: lo si installa al passo 6 se manca

Lo scopo è questo: [un solo endpoint OpenAI-compatible](https://adrianoamalfi.com/arbr-selfhost-docker-compose/), `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:

```bash
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`:

```bash
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:

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

```yaml
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:

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

```bash
docker compose up -d
```

Atteso: il container parte e resta attivo. Controlla:

```bash
docker compose ps
```

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

```bash
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:

```bash
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](https://adrianoamalfi.com/sicurezza-mcp-ansi-escape/).

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:

```text
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](https://adrianoamalfi.com/economia-sommersa-token-ai/), 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.

```bash
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):

```bash
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:

```bash
cloudflared --version
```

### Login e creazione del tunnel

Autentica il tunnel col tuo account Cloudflare:

```bash
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:

```bash
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:

```bash
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:

```bash
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`:

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

```bash
docker compose up -d
```

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

```bash
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:

```bash
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:

```bash
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:

```bash
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:

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