feat(ci/cd): Gitea Actions + registry + Watchtower auto-update
CI pipeline (.gitea/workflows/ci.yml): - Job lint (ruff), typecheck (mypy mcp_common gating + servizi warn-only), test (pytest 455). - Job build-and-push solo su main: builda gateway + 6 image MCP via docker/build-push-action@v6, login al registry Gitea con docker/login-action@v3 + secrets.GITEA_TOKEN auto-iniettato. - Cache distribuita type=gha per layer Docker → run successivi 5-10x più veloci. Tag :latest + :sha-XXXXXXX per ogni image. Deploy VPS (docker-compose.prod.yml): - Niente build locale: solo `image:` da git.tielogic.xyz/adriano/ cerbero-mcp/<service>:latest. Variabile IMAGE_TAG per pin a sha specifico. - Servizio Watchtower containerizzato che polla ogni 5min (configurabile via WATCHTOWER_POLL_INTERVAL) e auto-aggiorna i container con label com.centurylinklabs.watchtower.enable=true. Auth registry riusa ~/.docker/config.json bind-mounted readonly. DEPLOYMENT.md: runbook completo per setup VPS, login registry, secrets, .env, smoke test post-deploy, rollback (pin a sha), disable auto-update, nota Traefik upload limit. README aggiornato con link. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+224
@@ -0,0 +1,224 @@
|
||||
# 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, in sequenza:
|
||||
|
||||
1. **lint** (`ruff check`) — gating
|
||||
2. **typecheck** (`mypy mcp_common`) — gating su mcp_common, warn-only sui servizi
|
||||
3. **test** (`pytest services/`) — gating, 455 test
|
||||
4. **build-and-push** — solo su push a `main`:
|
||||
- Logga al registry `git.tielogic.xyz` con `secrets.GITEA_TOKEN`
|
||||
- Builda `docker/base.Dockerfile` (cache)
|
||||
- Builda e pusha `gateway` + 6 servizi MCP con tag:
|
||||
- `:latest` (mobile, Watchtower polla questo)
|
||||
- `:sha-XXXXXXX` (immutabile, per rollback puntuali)
|
||||
|
||||
Le PR fanno girare solo lint+typecheck+test, niente build/push.
|
||||
|
||||
## 2. Setup iniziale del VPS
|
||||
|
||||
**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`.
|
||||
|
||||
## 3. 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.
|
||||
|
||||
## 4. 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.
|
||||
|
||||
## 5. 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
|
||||
```
|
||||
|
||||
## 6. 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.
|
||||
|
||||
## 7. 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.
|
||||
|
||||
## 8. 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).
|
||||
Reference in New Issue
Block a user