4f3e959805
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>
404 lines
15 KiB
Markdown
404 lines
15 KiB
Markdown
# 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).
|