Files
Cerbero-mcp/DEPLOYMENT.md
T
AdrianoDev 7fa269de14 feat(deploy): auto-include docker-compose.local.yml override
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>
2026-04-29 22:44:01 +02:00

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 + buildx sul 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_TAG come build-arg in modo da ereditare dall'image base appena 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 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:

{
  "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:

  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:

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.
  • fail2ban su SSH e (opz) sul log Caddy 401.
  • Secret rotation manuale: aggiorna i file secrets/*.tokendocker 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.ymlCaddyfile. 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.