Files
Cerbero-mcp/DEPLOYMENT.md
T
AdrianoDev 4f3e959805
ci / ruff lint (push) Failing after 13s
ci / mypy mcp_common (push) Successful in 22s
ci / pytest (push) Successful in 32s
ci / validate compose + Caddyfile (push) Failing after 2m23s
ci / build & push to registry (push) Has been skipped
feat(deploy): docker-compose.traefik.yml overlay per behind-Traefik
Per VPS condiviso (es. con Gitea) dove Traefik gestisce già 80/443.

- gateway/Caddyfile: env-aware listen + auto_https + trusted_proxies
  (defaults invariati per modalità standalone).
- docker-compose.traefik.yml: overlay che rimuove ports binding host,
  attacca gateway alla network esterna di Traefik, set labels per
  routing Host(cerbero-mcp.tielogic.xyz) + TLS via certresolver
  Traefik. Caddy ascolta plain HTTP :80 interno.
- scripts/deploy.sh: rileva BEHIND_TRAEFIK=true → aggiunge -f
  docker-compose.traefik.yml a tutti i docker compose call.
- DEPLOYMENT.md: nuova sezione 2a (topologia standalone vs behind-traefik)
  + sotto-sezione modalità behind-Traefik con env vars richieste.

Uso:
  docker compose -f docker-compose.prod.yml -f docker-compose.traefik.yml \
                 --env-file .env up -d

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 09:56:07 +02:00

404 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Deployment Cerbero_mcp
Guida operativa per il deploy della suite MCP su un VPS pubblico.
L'architettura è: Gitea ospita codice + container registry; il VPS produzione
non builda nulla, ma fa pull dei container già pronti dalla registry e usa
Watchtower per il rollover automatico delle versioni.
```
┌─────────────────────────┐ ┌──────────────────────────────────┐
│ Gitea git.tielogic.xyz │ │ VPS produzione │
│ │ │ cerbero-mcp.tielogic.xyz │
│ ┌──────────────────┐ │ push │ │
│ │ Cerbero-mcp repo │───┼─CI/CD──▶│ ┌────────────────────────────┐ │
│ └──────────────────┘ │ image │ │ docker compose │ │
│ ┌──────────────────┐ │ │ │ (docker-compose.prod.yml) │ │
│ │ Container reg. │◀──┼─ pull ──┤ │ gateway, mcp-* │ │
│ └──────────────────┘ │ │ │ watchtower (poll 5min) │ │
│ ┌──────────────────┐ │ │ └────────────────────────────┘ │
│ │ Actions runner │ │ │ │
│ └──────────────────┘ │ │ │
└─────────────────────────┘ └──────────────────────────────────┘
```
## 1. Pipeline CI/CD (Gitea Actions)
`.gitea/workflows/ci.yml` ad ogni push su `main` esegue 5 job:
1. **lint** (`ruff check services/`) — gating, 0 violations.
2. **typecheck** (`mypy services/common/src/mcp_common`) — gating sul
modulo comune, warn-only sui servizi.
3. **test** (`pytest services/`) — gating, 478 test verdi.
4. **validate-config**`docker compose config -q` su `docker-compose.yml`
e `docker-compose.prod.yml` + `caddy validate --config Caddyfile`.
5. **build-and-push** — solo su push a `main` (skip su PR):
- `docker login git.tielogic.xyz` con `${{ secrets.REGISTRY_TOKEN }}`
(PAT scope `write:package`, configurato user-level su Gitea).
- Builda e pusha **8 image** al registry con tag `:latest` + `:sha-X`:
- `git.tielogic.xyz/adriano/cerbero-mcp/base`
- `git.tielogic.xyz/adriano/cerbero-mcp/gateway`
- `git.tielogic.xyz/adriano/cerbero-mcp/mcp-{deribit,bybit,hyperliquid,alpaca,macro,sentiment}`
- Cache Docker buildx via registry stesso (`buildcache:<name>` per ognuna)
→ run successivi 5-10× più veloci.
Le PR fanno girare solo `lint`+`typecheck`+`test`+`validate-config`,
niente build/push (gating per merge).
### Setup runner Gitea
Il runner act_runner deve girare:
- Sulla stessa Docker network di Gitea (`gitea_gitea-internal` o equivalente)
altrimenti `actions/checkout@v4` non risolve `gitea:3000` per il clone.
- Con bind di `/var/run/docker.sock` per buildare image dai workflow.
- Image dei job mappata a `docker.gitea.com/runner-images:ubuntu-22.04`
(default Gitea, contiene docker CLI + buildx + git + curl + node).
- Label: almeno `ubuntu-latest` (usato dal workflow). Altre label
(`tielogic-ci`, `ubuntu-22.04`, ecc.) opzionali.
### Setup secret REGISTRY_TOKEN
Crea un Personal Access Token Gitea con scope `write:package` (User
Settings → Applications → Generate Token). Aggiungilo come secret a
livello user (User Settings → Secrets → New Secret) con nome
`REGISTRY_TOKEN`. Tutti i tuoi repo ereditano il secret automaticamente.
## 2a. Topologia: standalone vs behind-Traefik
Cerbero_mcp supporta due topologie di deploy:
### Standalone (Caddy gestisce TLS direttamente)
```
Internet ──[443]──► Caddy gateway ──► mcp-* services
(ACME Let's Encrypt)
```
Setto: `docker-compose.prod.yml` da solo. Caddy bind sulle porte
80/443 host, fa cert auto via ACME. Adatto a un VPS dedicato senza
altri servizi sulle 80/443.
### Behind-Traefik (Traefik termina TLS)
```
Internet ──[443]──► Traefik ──[traefik network]──► Caddy gateway ──► mcp-* services
(TLS+ACME) (rate-limit, IP allowlist)
```
Setto: `docker-compose.prod.yml` + `docker-compose.traefik.yml` overlay.
Caddy non bind su host, ascolta plain HTTP `:80` interno alla
`traefik` network. Traefik fa routing per `Host(cerbero-mcp.tielogic.xyz)`,
TLS, ACME. Adatto a VPS condiviso con altri servizi (Gitea, ecc.).
## 2. Deploy automatizzato (script)
Il modo più rapido è `scripts/deploy.sh`, idempotente. Esegui sul VPS:
```bash
# Prerequisiti
export GITEA_PAT="<PAT con scope read:package>"
export GITEA_USER=adriano
mkdir -p ~/cerbero-secrets
# Copia (via scp dal posto sicuro) i secret in ~/cerbero-secrets/:
# deribit.json bybit.json hyperliquid.json alpaca.json
# macro.json sentiment.json core.token observer.token
# Clone temporaneo solo per lo script
curl -sL -o /tmp/deploy.sh \
https://git.tielogic.xyz/Adriano/Cerbero-mcp/raw/branch/main/scripts/deploy.sh
chmod +x /tmp/deploy.sh
# Run (richiede sudo per /opt/cerbero-mcp e /var/log/cerbero-mcp)
/tmp/deploy.sh
```
Lo script esegue: docker login registry → clone/pull repo in
`/opt/cerbero-mcp` → copia secrets con `chmod 600` → genera `.env`
iniziale (testnet) → crea `/var/log/cerbero-mcp` con permessi `1000:1000`
→ pull image dal registry → `docker compose up -d` → smoke test pubblico.
Per aggiornare in seguito: ri-esegui lo stesso script (preserva `.env`).
### Modalità behind-Traefik
Se sul VPS gira già un Traefik (es. lo stesso VPS di Gitea), prima di
lanciare lo script aggiungi al tuo `.env`:
```bash
BEHIND_TRAEFIK=true
TRAEFIK_NETWORK=gitea_traefik-public # nome network esterna di Traefik
TRAEFIK_CERTRESOLVER=letsencrypt # nome resolver in Traefik
TRAEFIK_ENTRYPOINT=websecure # entrypoint HTTPS Traefik
# Porte gateway non più necessarie (Traefik bind 80/443):
# GATEWAY_HTTP_PORT, GATEWAY_HTTPS_PORT non vengono usate.
```
Lo script rileva `BEHIND_TRAEFIK=true` e usa
`docker compose -f docker-compose.prod.yml -f docker-compose.traefik.yml`.
Il gateway Caddy NON bind su 80/443 host; viene esposto via Traefik con
labels per `Host(cerbero-mcp.tielogic.xyz)`.
Verifica della network Traefik:
```bash
docker network ls | grep -i traefik
# Tipicamente vedrai: gitea_traefik-public, traefik_default, ecc.
# Usa il nome ESATTO come TRAEFIK_NETWORK in .env.
```
## 3. Safety: switch testnet → mainnet
`mcp_common.environment.consistency_check` (richiamato dal boot
`run_exchange_main`) PREVIENE switch accidentali:
- Se l'ambiente risolto è **mainnet** ma il secret JSON corrispondente
non contiene `"environment": "mainnet"` esplicito → boot abort con
`EnvironmentMismatchError`.
- Se il secret dichiara un environment diverso da quello risolto (es.
`creds["environment"]="mainnet"` ma env var setta testnet) → boot abort.
**Per passare a mainnet su un exchange specifico** (es. bybit):
1. Edita `secrets/bybit.json`: aggiungi `"environment": "mainnet"`.
2. Modifica `.env`: `BYBIT_TESTNET=false`.
3. `docker compose -f docker-compose.prod.yml --env-file .env restart mcp-bybit`.
Senza il flag esplicito nel secret, il container mcp-bybit fallirà al
boot e Watchtower NON aggiornerà su versioni con cred mainnet rotti.
Override `STRICT_MAINNET=false` in `.env` permette mainnet senza la
conferma esplicita (downgrade safety, sconsigliato in produzione).
## 4. Audit log persistente
Tutti i write endpoint (`place_order`, `place_combo_order`, `cancel_*`,
`set_*`, `close_*`, `transfer_*`, `amend_*`, `switch_*`) emettono un
record JSON strutturato sul logger `mcp.audit`.
**Sink**:
- stdout/stderr container (sempre, visibile via `docker logs`).
- File JSONL persistente su volume host:
`${AUDIT_LOG_DIR:-/var/log/cerbero-mcp}/<service>.audit.jsonl`.
Rotation a mezzanotte UTC con retention `AUDIT_LOG_BACKUP_DAYS`
(default 30 giorni).
**Esempio record**:
```json
{
"audit_event": "write_op",
"action": "place_order",
"exchange": "bybit",
"principal": "core",
"target": "BTCUSDT",
"payload": {"side": "Buy", "qty": 0.01, "price": 60000, "leverage": 3},
"result": {"order_id": "abc123", "status": "submitted"}
}
```
**Query operative**:
```bash
# Tutto l'audit log oggi
tail -f /var/log/cerbero-mcp/*.audit.jsonl
# Solo place_order su bybit
jq -c 'select(.action=="place_order" and .exchange=="bybit")' \
/var/log/cerbero-mcp/bybit.audit.jsonl
# Errori
jq -c 'select(.error)' /var/log/cerbero-mcp/*.audit.jsonl
# Operazioni di un principal
jq -c 'select(.principal=="core")' /var/log/cerbero-mcp/*.audit.jsonl
```
I secret (api_key, password) sono filtrati automaticamente da
`SecretsFilter` prima di arrivare al sink.
## 5. Setup iniziale del VPS (manuale, alternativa allo script)
**Pre-requisiti**: Docker Engine ≥ 24, `docker compose` plugin, accesso SSH
sudo, dominio DNS A record `cerbero-mcp.tielogic.xyz` → IP del VPS, porte 80
e 443 aperte sul firewall (per ACME challenge + traffico HTTPS).
### a) Login al registry Gitea
Crea un Personal Access Token su Gitea (`Settings → Applications →
Generate new token`) con scope `read:package`. Quindi sul VPS:
```bash
echo "$GITEA_PAT" | docker login git.tielogic.xyz -u <gitea-username> --password-stdin
```
Le credenziali vengono salvate in `~/.docker/config.json`. Watchtower lo
bind-monta in sola lettura per fare i pull autenticati.
### b) Clona repository (solo per i file di compose, secret e Caddyfile)
```bash
sudo mkdir -p /opt/cerbero-mcp && sudo chown $USER /opt/cerbero-mcp
cd /opt/cerbero-mcp
git clone ssh://git@git.tielogic.xyz:222/Adriano/Cerbero-mcp.git .
```
Il VPS NON ha bisogno di buildare; usa `docker-compose.prod.yml` che fa solo
pull dal registry.
### c) Prepara secrets
```bash
mkdir -p secrets
# Copia (via scp) i file JSON con cred reali:
# secrets/deribit.json, bybit.json, alpaca.json, hyperliquid.json,
# secrets/macro.json, sentiment.json
# secrets/core.token, observer.token
chmod 600 secrets/*
```
### d) `.env` con configurazione runtime
Crea `/opt/cerbero-mcp/.env`:
```bash
# Gateway
ACME_EMAIL=adrianodalpastro@tielogic.com
GATEWAY_HTTP_PORT=80
GATEWAY_HTTPS_PORT=443
WRITE_ALLOWLIST="127.0.0.1/32 ::1/128 172.16.0.0/12"
# Image tag — `latest` per auto-update Watchtower, oppure pin a sha-XXXXXXX
IMAGE_TAG=latest
IMAGE_PREFIX=git.tielogic.xyz/adriano/cerbero-mcp
# Environment exchange (true=testnet, false=mainnet)
DERIBIT_TESTNET=true
BYBIT_TESTNET=true
HYPERLIQUID_TESTNET=true
ALPACA_PAPER=true
# Watchtower polling interval (sec). 300=5min default.
WATCHTOWER_POLL_INTERVAL=300
```
### e) Avvio
```bash
docker compose -f docker-compose.prod.yml --env-file .env pull
docker compose -f docker-compose.prod.yml --env-file .env up -d
docker compose -f docker-compose.prod.yml logs -f gateway
```
Caddy chiede automaticamente il certificato Let's Encrypt al primo
contatto su `https://cerbero-mcp.tielogic.xyz`.
## 6. Auto-update via Watchtower
Watchtower (servizio `watchtower` nel compose) polla il registry ogni
`WATCHTOWER_POLL_INTERVAL` secondi. Se trova un nuovo digest dietro al tag
`:latest` di un container etichettato `com.centurylinklabs.watchtower.enable=true`,
fa:
1. `docker pull` della nuova image
2. `docker stop` graceful del container vecchio
3. `docker rm` + start del nuovo container con stessa config + secret + volumi
4. Cleanup image vecchia (`WATCHTOWER_CLEANUP=true`)
I container con label sono: `gateway`, `mcp-deribit`, `mcp-bybit`,
`mcp-hyperliquid`, `mcp-alpaca`, `mcp-macro`, `mcp-sentiment`. Il container
`watchtower` stesso non si auto-aggiorna (per evitare loop).
### Disabilitare auto-update temporaneamente
Pin a uno SHA specifico nel `.env`:
```bash
IMAGE_TAG=sha-6b7b3f7
docker compose -f docker-compose.prod.yml --env-file .env up -d
```
In questo modo `:latest` non viene più seguito; per riattivare il rollover
automatico ripristina `IMAGE_TAG=latest`.
### Disabilitare auto-update per un singolo servizio
Rimuovi la label `com.centurylinklabs.watchtower.enable=true` per quel
servizio nel compose (oppure imposta `=false`). Watchtower lo ignora ma
continua a tenere aggiornati gli altri.
## 7. Rollback
```bash
# Trova lo SHA della versione precedente
docker images "git.tielogic.xyz/adriano/cerbero-mcp/*" --format "{{.Tag}}"
# Pin nel .env
IMAGE_TAG=sha-XXXXXXX
docker compose -f docker-compose.prod.yml --env-file .env up -d
```
Watchtower NON downgraderà perché il digest del tag pin corrisponde a quello
locale.
## 8. Smoke test post-deploy
```bash
# Da fuori VPS (laptop)
curl -s https://cerbero-mcp.tielogic.xyz/mcp-macro/health
# {"status":"ok",...}
# Test write endpoint allowlist (deve rispondere 403 da IP esterno):
curl -X POST https://cerbero-mcp.tielogic.xyz/mcp-deribit/tools/place_order \
-H "Authorization: Bearer $(cat secrets/core.token)" \
-d '{"instrument_name":"BTC-PERPETUAL","side":"buy","amount":1}'
# 403 forbidden: source ip not in allowlist ← OK
# Sul VPS:
GATEWAY=http://localhost bash tests/smoke/run.sh
```
## 9. Sicurezza VPS
- Firewall `ufw`: `allow 22, 80, 443`. Tutto il resto deny in.
- `fail2ban` su SSH e (opz) sul log Caddy 401.
- Secret rotation manuale: aggiorna i file `secrets/*.token`
`docker compose restart` (i token vengono ricaricati al boot di ogni
servizio MCP).
- Audit log in `docker compose logs <service> | grep audit_event` — per
produzione meglio redirezionare a syslog o a un servizio dedicato.
## 10. Note Traefik / reverse proxy davanti a Gitea
Gitea è esposto via Traefik (ROOT_URL `https://git.tielogic.xyz`). Per il push
di image Docker il reverse proxy deve consentire upload di body grossi (un
singolo layer può superare i 100MB).
Traefik default va bene, ma se vedi `413 Request Entity Too Large` durante
`docker push` aumenta il limite nel middleware:
```yaml
# traefik dynamic config
http:
middlewares:
gitea-upload:
buffering:
maxRequestBodyBytes: 524288000 # 500MB
```
Applicalo come middleware al router Gitea.
## 11. Aggiornamento del compose stesso (file YAML)
Watchtower aggiorna le **image**, non il `docker-compose.prod.yml`. Se cambi
struttura (nuovi servizi, nuove env var) devi:
```bash
cd /opt/cerbero-mcp
git pull
docker compose -f docker-compose.prod.yml --env-file .env up -d
```
Per automatizzare anche questo serve un cron job o uno step CD push-based
(vedi backlog).