Files
easymcp-release/docker-compose.yml
T
2026-07-07 13:46:12 +00:00

292 lines
11 KiB
YAML

# SAP-MCP-Stack (C#) — RELEASE-Compose für Kunden-Deployments (liegt via CI als
# docker-compose.yml im öffentlichen Repo kraemerweb/easymcp-release). Referenziert
# NUR vorgebaute Images aus der öffentlichen Gitea-Registry (kein build:) —
# der Kunde zieht Updates per 'docker compose pull && docker compose up -d'.
# APP_VERSION setzt setup.sh in der .env (Default: latest, pinbar auf Versionstags).
#
# Härtung wie im Entwicklungs-Compose: alle Dienste no-new-privileges; die
# .NET-Dienste + Hydra zusätzlich cap_drop ALL; login/mcp/admin read_only
# (nur /tmp als tmpfs). Postgres behält Default-Caps und bleibt schreibbar.
name: sapb1mcp
# Log-Rotation für die Dauerläufer (Docker-Default rotiert nicht). Max ~30 MB/Dienst.
x-logging: &default-logging
driver: json-file
options:
max-size: "10m"
max-file: "3"
services:
postgres:
image: postgres:17-alpine
container_name: sapb1mcp-postgres
restart: unless-stopped
logging: *default-logging
security_opt:
- no-new-privileges:true
environment:
POSTGRES_USER: mcp
POSTGRES_PASSWORD: ${PG_PASSWORD:?PG_PASSWORD fehlt in .env — setup.sh ausführen}
POSTGRES_DB: mcp
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U mcp -d mcp"]
interval: 5s
timeout: 3s
retries: 12
# App-Schema (sap_credentials, mcp_user_access, audit_entries) via EF Core.
# Nutzt das mcp-Image, ruft es aber im --migrate-Modus auf und beendet sich.
db-migrate:
image: git.kraemerweb.de/kraemerweb/sapb1mcp-mcp:${APP_VERSION:-latest}
container_name: sapb1mcp-db-migrate
restart: "no"
security_opt:
- no-new-privileges:true
depends_on:
postgres:
condition: service_healthy
environment:
POSTGRES_CONNECTION: "Host=postgres;Port=5432;Database=mcp;Username=mcp;Password=${PG_PASSWORD}"
entrypoint: ["dotnet", "SapB1McpServer.dll", "--migrate"]
# Hydra-Schema (eigene Tabellen in derselben DB).
hydra-migrate:
image: oryd/hydra:v2.3.0
container_name: sapb1mcp-hydra-migrate
restart: "no"
security_opt:
- no-new-privileges:true
depends_on:
postgres:
condition: service_healthy
environment:
DSN: postgres://mcp:${PG_PASSWORD}@postgres:5432/mcp?sslmode=disable
volumes:
- ./hydra.yml:/etc/hydra/hydra.yml:ro
command: migrate sql -e --yes --config /etc/hydra/hydra.yml
hydra:
image: oryd/hydra:v2.3.0
container_name: sapb1mcp-hydra
restart: unless-stopped
logging: *default-logging
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
depends_on:
hydra-migrate:
condition: service_completed_successfully
environment:
DSN: postgres://mcp:${PG_PASSWORD}@postgres:5432/mcp?sslmode=disable
SECRETS_SYSTEM: ${SECRETS_SYSTEM:?SECRETS_SYSTEM fehlt in .env — setup.sh ausführen}
URLS_SELF_ISSUER: ${PUBLIC_ISSUER:?PUBLIC_ISSUER fehlt in .env}
URLS_LOGIN: ${LOGIN_URL}/login
URLS_CONSENT: ${LOGIN_URL}/consent
# Hydra leitet registration_endpoint NICHT automatisch ab — ohne diesen
# Eintrag fehlt es im Discovery-Dokument und Clients halten DCR für
# nicht unterstützt.
WEBFINGER_OIDC_DISCOVERY_CLIENT_REGISTRATION_URL: ${PUBLIC_ISSUER}/oauth2/register
volumes:
- ./hydra.yml:/etc/hydra/hydra.yml:ro
command: serve all --config /etc/hydra/hydra.yml
ports:
# Nur an die interne Host-IP (BIND_ADDR) binden, die der Reverse Proxy
# erreicht — nie 0.0.0.0. Default 127.0.0.1 = sicher (Proxy auf demselben Host).
- "${BIND_ADDR:-127.0.0.1}:${HYDRA_PUBLIC_HOST_PORT:-4444}:4444" # öffentlich (OAuth-Issuer)
# Admin-API 4445 bleibt intern im Compose-Netz (nur login/admin-Container).
login:
image: git.kraemerweb.de/kraemerweb/sapb1mcp-login:${APP_VERSION:-latest}
container_name: sapb1mcp-login
restart: unless-stopped
logging: *default-logging
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
read_only: true
tmpfs:
- /tmp
depends_on:
db-migrate:
condition: service_completed_successfully
hydra:
condition: service_started
environment:
ASPNETCORE_URLS: http://0.0.0.0:8081
POSTGRES_CONNECTION: "Host=postgres;Port=5432;Database=mcp;Username=mcp;Password=${PG_PASSWORD}"
Hydra__AdminUrl: http://hydra:4445
SAPServiceLayer__BaseUrl: ${SL_BASE_URL:?SL_BASE_URL fehlt in .env}
SAPServiceLayer__CompanyDB: ${SL_COMPANYDB:?SL_COMPANYDB fehlt in .env}
SAPServiceLayer__IgnoreSslErrors: ${SL_INSECURE:-false}
CREDS_KEY_FILE: /run/secrets/creds_key
# MCP-Zugriff: immer erlaubte SAP-Benutzer (Allowlist, default-deny für den Rest).
MCP_ALLOWED_USERS: ${MCP_ALLOWED_USERS:-manager}
secrets:
- creds_key
ports:
- "${BIND_ADDR:-127.0.0.1}:${LOGIN_HOST_PORT:-8081}:8081"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8081/healthz"]
interval: 10s
timeout: 3s
retries: 6
mcp:
image: git.kraemerweb.de/kraemerweb/sapb1mcp-mcp:${APP_VERSION:-latest}
container_name: sapb1mcp-mcp
restart: unless-stopped
logging: *default-logging
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
read_only: true
tmpfs:
- /tmp
depends_on:
db-migrate:
condition: service_completed_successfully
hydra:
condition: service_started
environment:
McpHttp__Port: "8080"
# Öffentliche Resource-URL dieses MCP-Servers (RFC-9728-Metadaten + WWW-Authenticate).
MCP_RESOURCE_URL: ${MCP_RESOURCE_URL:?MCP_RESOURCE_URL fehlt in .env}
# Hydra-Issuer für JWT-Validierung (Authority). MUSS per HTTPS erreichbar sein
# (RequireHttpsMetadata=true) — im Betrieb der Reverse-Proxy-vHost.
Hydra__PublicIssuer: ${PUBLIC_ISSUER}
SAPServiceLayer__BaseUrl: ${SL_BASE_URL}
SAPServiceLayer__CompanyDB: ${SL_COMPANYDB}
SAPServiceLayer__IgnoreSslErrors: ${SL_INSECURE:-false}
POSTGRES_CONNECTION: "Host=postgres;Port=5432;Database=mcp;Username=mcp;Password=${PG_PASSWORD}"
CREDS_KEY_FILE: /run/secrets/creds_key
MCP_ALLOWED_USERS: ${MCP_ALLOWED_USERS:-manager}
secrets:
- creds_key
ports:
- "${BIND_ADDR:-127.0.0.1}:${MCP_HOST_PORT:-8080}:8080"
healthcheck:
# Der MCP-Server hat kein /healthz; die anonyme RFC-9728-Metadaten-Route
# antwortet ohne Token mit 200 und dient als Health-Signal.
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/.well-known/oauth-protected-resource"]
interval: 10s
timeout: 3s
retries: 6
# Docker-Socket-Proxy: EINZIGER Container, der den Docker-Socket sieht. Filtert die
# Engine-API auf das Minimum, das das Admin-UI braucht (Container listen/Logs/Restart).
docker-socket-proxy:
image: tecnativa/docker-socket-proxy:0.3.0
container_name: sapb1mcp-docker-proxy
restart: unless-stopped
logging: *default-logging
security_opt:
- no-new-privileges:true
# Kein read_only: der tecnativa-Entrypoint generiert seine haproxy.cfg zur Laufzeit
# aus der im Image liegenden Template — read_only dort crasht den Proxy.
environment:
# Nur Container-Bereich + POST (für /restart) freigeben, alles andere bleibt 0.
CONTAINERS: 1
POST: 1
# Explizit geschlossen halten (Defaults, zur Klarheit dokumentiert):
EXEC: 0
IMAGES: 0
VOLUMES: 0
NETWORKS: 0
INFO: 0
SERVICES: 0
TASKS: 0
SWARM: 0
SYSTEM: 0
AUTH: 0
SECRETS: 0
CONFIGS: 0
NODES: 0
PLUGINS: 0
DISTRIBUTION: 0
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- dockerproxy
admin:
image: git.kraemerweb.de/kraemerweb/sapb1mcp-admin:${APP_VERSION:-latest}
container_name: sapb1mcp-admin
restart: unless-stopped
logging: *default-logging
security_opt:
- no-new-privileges:true
# Kein Docker-Socket-Mount: der Admin erreicht die Engine-API ausschließlich
# über den gefilterten Proxy (siehe DOCKER_HOST).
cap_drop:
- ALL
read_only: true
tmpfs:
- /tmp
depends_on:
db-migrate:
condition: service_completed_successfully
docker-socket-proxy:
condition: service_started
environment:
ASPNETCORE_URLS: http://0.0.0.0:8082
POSTGRES_CONNECTION: "Host=postgres;Port=5432;Database=mcp;Username=mcp;Password=${PG_PASSWORD}"
Hydra__AdminUrl: http://hydra:4445
# Login-Gate: nur dieser SAP-User darf das Admin-UI öffnen (SL-verifiziert).
# Pflicht (fail-closed) — der Dienst lässt sonst niemanden hinein.
ADMIN_SAP_USER: ${ADMIN_SAP_USER:?ADMIN_SAP_USER fehlt in .env}
SAPServiceLayer__BaseUrl: ${SL_BASE_URL}
SAPServiceLayer__CompanyDB: ${SL_COMPANYDB}
SAPServiceLayer__IgnoreSslErrors: ${SL_INSECURE:-false}
MCP_ALLOWED_USERS: ${MCP_ALLOWED_USERS:-manager}
# Container-Steuerung/Logs NUR über den gefilterten Proxy (nicht über den rohen Socket).
COMPOSE_PROJECT: sapb1mcp
DOCKER_HOST: tcp://docker-socket-proxy:2375
# Update-Seite („Pull & Update"): startet den Einweg-Updater-Container.
# PROJECT_DIR = Host-Pfad des Deployments (setzt setup.sh); der Updater mountet
# ihn pfad-identisch, damit die relativen Binds der Compose aufgehen.
APP_VERSION: ${APP_VERSION:-latest}
PROJECT_DIR: ${PROJECT_DIR:?PROJECT_DIR fehlt in .env — setup.sh ausführen}
UPDATES_VOLUME: sapb1mcp_updates
# Updater bewusst auf :latest — er ist kein Compose-Dienst; setup.sh pullt ihn
# initial, updater-run.sh hält ihn bei jedem Update selbst aktuell.
UPDATER_IMAGE: git.kraemerweb.de/kraemerweb/sapb1mcp-updater:latest
volumes:
# Geteilte Ablage für update.log/update.status — so kann auch der beim
# Update NEU erstellte Admin das Ergebnis noch anzeigen.
- updates:/updates
networks:
- default
- dockerproxy
ports:
# Admin-UI inkl. /audit liegt hinter dem SAP-Login-Gate (nur /login + /healthz frei).
# Trotzdem nur intern binden: Default loopback; ADMIN_BIND in .env auf die interne
# Host-IP setzen, die der Reverse Proxy erreicht — nie 0.0.0.0.
- "${ADMIN_BIND:-127.0.0.1}:${ADMIN_HOST_PORT:-8082}:8082"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8082/healthz"]
interval: 10s
timeout: 3s
retries: 6
volumes:
pgdata:
# Staging-Volume der Update-Funktion (Name auf dem Host: sapb1mcp_updates —
# muss zur UPDATES_VOLUME-Env des Admin passen, der Updater mountet es per Name).
updates:
networks:
# Standard-Netz für alle Dienste (Postgres, Hydra, login, mcp, admin).
default:
# Isoliertes, nach außen abgeschottetes Netz nur für admin ↔ docker-socket-proxy.
dockerproxy:
internal: true
secrets:
creds_key:
file: ./secrets/creds_key