Lo script deploy-noclone.sh ora carica automaticamente come ultimo -f un eventuale $DEPLOY_DIR/docker-compose.local.yml se esiste. Utile per fix specifici macchina (es. DOCKER_API_VERSION watchtower su daemon vecchi). Gitignored per design — non versionato nel repo. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
16 KiB
Deployment Cerbero_mcp
Guida operativa per il deploy della suite MCP su un VPS pubblico. L'architettura è: Gitea ospita codice + container registry; le immagini vengono buildate e pushate dalla macchina di sviluppo (laptop) verso il registry; il VPS produzione non builda nulla, fa solo pull dei container già pronti e usa Watchtower per il rollover automatico.
┌──────────────────────────┐ ┌─────────────────────────┐ ┌──────────────────────────────────┐
│ Laptop dev │ │ Gitea git.tielogic.xyz │ │ VPS produzione │
│ │ │ │ │ cerbero-mcp.tielogic.xyz │
│ build-push.sh ──push──▶ │───▶│ ┌────────────────────┐ │ │ │
│ (8 image) │ │ │ Container registry │ │ │ ┌────────────────────────────┐ │
│ git push ─────────────▶ │───▶│ └────────────────────┘ │◀──┼──┤ docker compose │ │
│ │ │ ┌────────────────────┐ │ pull │ (docker-compose.prod.yml) │ │
│ │ │ │ Cerbero-mcp repo │ │ │ │ gateway, mcp-* │ │
│ │ │ └────────────────────┘ │ │ │ watchtower (poll 5min) │ │
│ │ │ │ │ └────────────────────────────┘ │
└──────────────────────────┘ └─────────────────────────┘ └──────────────────────────────────┘
Niente CI/CD su Gitea — qualità e build sono responsabilità del laptop
prima del push (lint/test in locale, poi scripts/build-push.sh).
1. Build & push image (dal laptop)
Lo script scripts/build-push.sh builda e pusha le 8 image al registry
Gitea, replicando il vecchio job CI ma in locale. Pre-requisiti:
docker+buildxsul laptop.- Personal Access Token Gitea con scope
write:package(User Settings → Applications → Generate Token).
export GITEA_PAT='<PAT_write:package>'
export GITEA_USER=adriano
# Tutte le 8 image (base + gateway + 6 mcp-*)
./scripts/build-push.sh
# Solo specifiche (es. dopo modifica a un singolo servizio)
./scripts/build-push.sh base mcp-bybit
Lo script:
- Fa
docker login git.tielogic.xyz. - Builda con
docker buildx build --push(cache buildx locale del laptop, niente cache registry: build successivi rapidi senza pesare sul registry). - Tagga
:latest+:sha-<short_HEAD>. - Per le mcp-* passa
BASE_IMAGE/BASE_TAGcome build-arg in modo da ereditare dall'imagebaseappena pushata.
Ordine consigliato: builda base prima delle mcp-* (lo script lo fa
di default se chiamato senza argomenti).
1b. Quality gate locale (consigliato prima del push)
Prima di build-push.sh esegui in locale i check che prima girava il CI:
uv run ruff check services/
uv run mypy services/common/src/mcp_common
uv run pytest services/ --tb=short
docker compose -f docker-compose.prod.yml config -q
Tutti devono essere verdi prima di pushare image al registry.
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 no-clone)
Il modo più rapido è scripts/deploy-noclone.sh, idempotente. Sul VPS
non viene clonato il repo: lo script scarica via raw HTTP solo i
file strettamente necessari al runtime (compose, Caddyfile, public
assets). Esegui sul VPS:
# Prerequisiti
export GITEA_PAT="<PAT con scope read:package>"
export GITEA_USER=adriano
# Crea la dir di deploy e mettici i secrets via scp dal posto sicuro
sudo mkdir -p /docker/cerbero_mcp/secrets
sudo chown -R "$USER" /docker/cerbero_mcp
# scp deribit.json bybit.json hyperliquid.json alpaca.json \
# macro.json sentiment.json core.token observer.token \
# vps:/docker/cerbero_mcp/secrets/
# Behind Traefik (opzionale, solo se VPS condiviso con Gitea o altri)
# export BEHIND_TRAEFIK=true
# export TRAEFIK_NETWORK=gitea_traefik-public
curl -sL -o /tmp/deploy-noclone.sh \
https://git.tielogic.xyz/Adriano/Cerbero-mcp/raw/branch/main/scripts/deploy-noclone.sh
chmod +x /tmp/deploy-noclone.sh
/tmp/deploy-noclone.sh
Lo script esegue: docker login registry → scarica docker-compose.prod.yml,
docker-compose.traefik.yml, gateway/Caddyfile, gateway/public/* in
/docker/cerbero_mcp/ → chmod 600 sui secrets → 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
e secrets, ricarica config dal branch main aggiornato).
Override paths: DEPLOY_DIR (default /docker/cerbero_mcp),
SECRETS_SRC (default $DEPLOY_DIR/secrets), AUDIT_LOG_DIR (default
/var/log/cerbero-mcp).
Override compose locale (docker-compose.local.yml): lo script
include automaticamente come ultimo -f un eventuale
$DEPLOY_DIR/docker-compose.local.yml. Utile per fix specifici della
macchina (es. forzare DOCKER_API_VERSION su watchtower se il daemon
del VPS è più vecchio dell'API attesa). File gitignored per design —
non viene scaricato dal repo, lo crei a mano sul VPS. Esempio:
# /docker/cerbero_mcp/docker-compose.local.yml
services:
watchtower:
environment:
DOCKER_API_VERSION: "1.44"
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:
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:
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 conEnvironmentMismatchError. - 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):
- Edita
secrets/bybit.json: aggiungi"environment": "mainnet". - Modifica
.env:BYBIT_TESTNET=false. 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 retentionAUDIT_LOG_BACKUP_DAYS(default 30 giorni).
Esempio record:
{
"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:
# 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:
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) Crea dir di deploy e scarica i file di config
Sul VPS NON serve clonare il repo. Bastano i file di compose, il
Caddyfile e i public assets del gateway:
sudo mkdir -p /docker/cerbero_mcp/{secrets,gateway/public}
sudo chown -R "$USER" /docker/cerbero_mcp
cd /docker/cerbero_mcp
BASE=https://git.tielogic.xyz/Adriano/Cerbero-mcp/raw/branch/main
curl -fsSL -o docker-compose.prod.yml $BASE/docker-compose.prod.yml
curl -fsSL -o docker-compose.traefik.yml $BASE/docker-compose.traefik.yml
curl -fsSL -o gateway/Caddyfile $BASE/gateway/Caddyfile
curl -fsSL -o gateway/public/index.html $BASE/gateway/public/index.html
curl -fsSL -o gateway/public/status.js $BASE/gateway/public/status.js
curl -fsSL -o gateway/public/style.css $BASE/gateway/public/style.css
Il VPS NON ha bisogno di buildare; usa docker-compose.prod.yml che fa solo
pull dal registry.
c) Prepara secrets
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 /docker/cerbero_mcp/.env:
# 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
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:
docker pulldella nuova imagedocker stopgraceful del container vecchiodocker rm+ start del nuovo container con stessa config + secret + volumi- 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:
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
# 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
# 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. fail2bansu 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:
# 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 docker-compose.prod.yml né
Caddyfile. Se cambi struttura (nuovi servizi, nuove env var, modifiche
al gateway), ri-esegui sul VPS lo script no-clone, che ri-scarica i file
di config dal branch main di Gitea e applica:
/tmp/deploy-noclone.sh
Lo script è idempotente: preserva .env e secrets/, aggiorna solo i
file di config + fa pull + up -d.