release 2026.07.07-24520d4
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user