#version: "3.9" # Supported by both podman-compose and Docker Compose v2+ ############################################################################### # NETWORKS + VOLUMES - declared first so they can be referenced later ############################################################################### networks: mcpnet: # Single user-defined bridge network keeps traffic private driver: bridge volumes: # Named volumes survive podman-compose down/up pgdata: # pgdata18: # Enable for postgres 18+ mariadbdata: mysqldata: mongodata: pgadmindata: redisinsight_data: nginx_cache: ############################################################################### # CORE SERVICE - MCP Gateway ############################################################################### services: # ────────────────────────────────────────────────────────────────────── # Nginx Caching Proxy - High-performance reverse proxy with CDN-like caching # ────────────────────────────────────────────────────────────────────── nginx: build: context: ./nginx dockerfile: Dockerfile image: mcpgateway/nginx-cache:latest restart: unless-stopped ports: - "8080:80" # HTTP caching proxy (public-facing) networks: [mcpnet] depends_on: gateway: condition: service_healthy volumes: - nginx_cache:/var/cache/nginx # Persistent cache storage - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro # Mount config as read-only healthcheck: test: ["CMD", "curl", "-f", "http://localhost/health"] interval: 30s timeout: 5s retries: 3 start_period: 10s # ────────────────────────────────────────────────────────────────────── # MCP Gateway - the main API server for the MCP stack # ────────────────────────────────────────────────────────────────────── gateway: image: ${IMAGE_LOCAL:-mcpgateway/mcpgateway:latest} # Use the local latest image. Run `make docker-prod` to build it. #image: ghcr.io/ibm/mcp-context-forge:0.9.0 # Use the release MCP Context Forge image #image: ghcr.io/ibm/mcp-context-forge:0.7.0 # Testing migration from 0.7.0 build: context: . dockerfile: Containerfile.lite # Same one the Makefile builds restart: unless-stopped ports: - "4444:4444" # HTTP (or HTTPS if SSL=true is set) networks: [mcpnet] # ────────────────────────────────────────────────────────────────────── # Environment - pick ONE database URL line, comment the rest # ────────────────────────────────────────────────────────────────────── environment: - HOST=0.0.0.0 - PORT=4444 - DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD:-mysecretpassword}@postgres:5432/mcp # - DATABASE_URL=mysql+pymysql://mysql:${MYSQL_PASSWORD:-changeme}@mariadb:3306/mcp # - DATABASE_URL=mysql+pymysql://admin:${MARIADB_PASSWORD:-changeme}@mariadb:3306/mcp # - DATABASE_URL=mongodb://admin:${MONGO_PASSWORD:-changeme}@mongodb:27017/mcp - CACHE_TYPE=redis # backend for caching (memory, redis, database, or none) - REDIS_URL=redis://redis:6379/0 # JWT Configuration - Choose ONE approach: # Option 1: HMAC (Default - Simple deployments) - JWT_ALGORITHM=HS256 - JWT_SECRET_KEY=my-test-key # Option 2: RSA (Production - Asymmetric, uncomment and generate certs) # - JWT_ALGORITHM=RS256 # - JWT_PUBLIC_KEY_PATH=/app/certs/jwt/public.pem # - JWT_PRIVATE_KEY_PATH=/app/certs/jwt/private.pem - JWT_AUDIENCE=mcpgateway-api - JWT_ISSUER=mcpgateway - EMAIL_AUTH_ENABLED=true - PLATFORM_ADMIN_EMAIL=admin@example.com - PLATFORM_ADMIN_PASSWORD=changeme - REQUIRE_TOKEN_EXPIRATION=false - MCPGATEWAY_UI_ENABLED=true - MCPGATEWAY_ADMIN_API_ENABLED=true # Security configuration (using defaults) - ENVIRONMENT=development - SECURITY_HEADERS_ENABLED=true - CORS_ALLOW_CREDENTIALS=true - SECURE_COOKIES=false # - SSL=true # - CERT_FILE=/app/certs/cert.pem # - KEY_FILE=/app/certs/key.pem # Uncomment to enable plugins - PLUGINS_ENABLED=true # Uncomment to enable catalog - MCPGATEWAY_CATALOG_ENABLED=true - MCPGATEWAY_CATALOG_FILE=/app/mcp-catalog.yml # Authentication configuration - AUTH_REQUIRED=true - MCP_CLIENT_AUTH_ENABLED=true - TRUST_PROXY_AUTH=false # Logging configuration - LOG_LEVEL=ERROR # Default to ERROR for production performance - DISABLE_ACCESS_LOG=true # Disable uvicorn access logs for performance (massive I/O overhead) # Phoenix Observability Integration (uncomment when using Phoenix) # - PHOENIX_ENDPOINT=${PHOENIX_ENDPOINT:-http://phoenix:6006} # - OTEL_EXPORTER_OTLP_ENDPOINT=${OTEL_EXPORTER_OTLP_ENDPOINT:-http://phoenix:4317} # - OTEL_SERVICE_NAME=${OTEL_SERVICE_NAME:-mcp-gateway} # - OTEL_TRACES_EXPORTER=${OTEL_TRACES_EXPORTER:-otlp} # - OTEL_METRICS_EXPORTER=${OTEL_METRICS_EXPORTER:-otlp} # - OTEL_RESOURCE_ATTRIBUTES=${OTEL_RESOURCE_ATTRIBUTES:-deployment.environment=docker,service.namespace=mcp} depends_on: # Default stack: Postgres + Redis + Alembic migration postgres: condition: service_healthy # ▶ wait for DB redis: condition: service_started # migration: # condition: service_completed_successfully healthcheck: test: ["CMD", "python3", "-c", "import urllib.request; import json; resp = urllib.request.urlopen('http://localhost:4444/health', timeout=5); data = json.loads(resp.read()); exit(0 if data.get('status') == 'healthy' else 1)"] #test: ["CMD", "curl", "-f", "https://localhost:4444/health"] interval: 30s timeout: 10s retries: 5 start_period: 30s # volumes: # - ./mcp-catalog.yml:/app/mcp-catalog.yml:ro # mount catalog configuration # - ./certs:/app/certs:ro # mount certs folder read-only (includes both SSL and JWT keys) # # For JWT asymmetric keys: # 1. Generate keys: make certs-jwt # 2. Uncomment volumes mount above # 3. Switch JWT_ALGORITHM to RS256 and uncomment JWT_*_KEY_PATH variables ############################################################################### # DATABASES - enable ONE of these blocks and adjust DATABASE_URL ############################################################################### postgres: image: postgres:17 environment: - POSTGRES_USER=postgres - POSTGRES_PASSWORD=mysecretpassword - POSTGRES_DB=mcp volumes: - pgdata:/var/lib/postgresql/data # - pgdata18:/var/lib/postgresql # Enable for postgres 18+ networks: [mcpnet] healthcheck: test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER"] interval: 30s timeout: 5s retries: 5 start_period: 20s mariadb: image: mariadb:10.6 environment: MARIADB_ROOT_PASSWORD: mysecretpassword MARIADB_DATABASE: mcp MARIADB_USER: mariadb MARIADB_PASSWORD: mysecretpassword volumes: - mariadbdata:/var/lib/mysql networks: [mcpnet] ports: - "3306:3306" healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] interval: 30s timeout: 5s retries: 5 start_period: 20s # mariadb: # image: mariadb:11 # environment: # - MARIADB_ROOT_PASSWORD=mysecretpassword # - MARIADB_DATABASE=mcp # - MARIADB_USER=admin # - MARIADB_PASSWORD=changeme # volumes: [mariadbdata:/var/lib/mysql] # networks: [mcpnet] # mariadb: # image: registry.redhat.io/rhel9/mariadb-106:12.0.2-ubi10 # environment: # - MYSQL_ROOT_PASSWORD=mysecretpassword # - MYSQL_DATABASE=mcp # - MYSQL_USER=mysql # - MYSQL_PASSWORD=changeme # volumes: ["mariadbdata:/var/lib/mysql"] # networks: [mcpnet] # ports: # - "3306:3306" # healthcheck: # test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-pmysecretpassword"] # interval: 30s # timeout: 10s # retries: 5 # start_period: 30s # mongodb: # image: mongo:7 # environment: # - MONGO_INITDB_ROOT_USERNAME=admin # - MONGO_INITDB_ROOT_PASSWORD=changeme # - MONGO_INITDB_DATABASE=mcp # volumes: [mongodata:/data/db] # networks: [mcpnet] # migration: # #image: ghcr.io/ibm/mcp-context-forge:0.7.0 # Testing migration from 0.7.0 # image: mcpgateway/mcpgateway:latest # Use the local latest image. Run `make docker-prod` to build it. # build: # context: . # dockerfile: Containerfile # environment: # - DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD:-mysecretpassword}@postgres:5432/mcp # command: alembic -c mcpgateway/alembic.ini upgrade head # depends_on: # postgres: # condition: service_healthy # networks: [mcpnet] ############################################################################### # CACHE ############################################################################### redis: image: redis:latest ports: - "6379:6379" # expose only if you want host access networks: [mcpnet] ############################################################################### # OPTIONAL ADMIN TOOLS - handy web UIs for DB & cache (disabled by default) ############################################################################### pgadmin: # 🔧 Postgres admin UI image: dpage/pgadmin4:latest environment: - PGADMIN_DEFAULT_EMAIL=admin@example.com - PGADMIN_DEFAULT_PASSWORD=changeme ports: - "5050:80" # http://localhost:5050 volumes: - pgadmindata:/var/lib/pgadmin networks: [mcpnet] depends_on: postgres: condition: service_healthy # ────────────────────────────────────────────────────────────────────── # Redis Insight - a powerful Redis GUI (recently updated) # ────────────────────────────────────────────────────────────────────── redis_insight: # 🔧 Redis Insight GUI image: redis/redisinsight:latest container_name: redisinsight restart: unless-stopped networks: [mcpnet] ports: - "5540:5540" # Redis Insight UI (default 5540) depends_on: # Default stack: Postgres + Redis redis: condition: service_started # ────────────────────────────────────────────────────────────────────── # Persist data (config, logs, history) between restarts # ────────────────────────────────────────────────────────────────────── # volumes: # - ./redisinsight_data:/data volumes: - redisinsight_data:/data # <- persist data in named volume # ────────────────────────────────────────────────────────────────────── # Preconfigure Redis connection(s) via env vars # ────────────────────────────────────────────────────────────────────── environment: # Single connection (omit "*" since only one): - RI_REDIS_HOST=redis # <- your Redis hostname - RI_REDIS_PORT=6379 # <- your Redis port - RI_REDIS_USERNAME=default # <- ACL/username (Redis 6+) #- RI_REDIS_PASSWORD=changeme # <- Redis AUTH password #- RI_REDIS_TLS=true # <- enable TLS # Optional: validate self-signed CA instead of trusting all: # - RI_REDIS_TLS_CA_PATH=/certs/selfsigned.crt # - RI_REDIS_TLS_CERT_PATH=/certs/client.crt # - RI_REDIS_TLS_KEY_PATH=/certs/client.key # - RI_REDIS_TLS=true # (already set above) # ────────────────────────────────────────────────────────────────── # Core Redis Insight settings # ────────────────────────────────────────────────────────────────── - RI_APP_HOST=0.0.0.0 # <- listen on all interfaces - RI_APP_PORT=5540 # <- UI port (container-side) # ────────────────────────────────────────────────────────────────── # (Optional) Enable HTTPS for the UI # ────────────────────────────────────────────────────────────────── # - RI_SERVER_TLS_KEY=/certs/tls.key # - RI_SERVER_TLS_CERT=/certs/tls.crt # ────────────────────────────────────────────────────────────────────── # Redis Commander - a web-based Redis GUI # ────────────────────────────────────────────────────────────────────── # redis_commander: # 🔧 Redis key browser # image: rediscommander/redis-commander:latest # restart: unless-stopped # networks: [mcpnet] # depends_on: # redis: # condition: service_started # ports: # - "8081:8081" # <- change if you want a different host port # # ───────────────────────────────────────────────────────────────────────── # # Mount your local certs directory (only needed if you want real cert validation) # # ───────────────────────────────────────────────────────────────────────── # # volumes: # # - ./certs:/certs:ro # <- put your selfsigned.crt (PEM) in ./certs # environment: # # ────────────────────────────────────────────────────────────────────── # # LEGACY HOST LIST (for showing in UI - not used for TLS) # # ────────────────────────────────────────────────────────────────────── # - REDIS_HOSTS=local:redis:6379 # # ────────────────────────────────────────────────────────────────────── # # CORE REDIS/TLS # # ────────────────────────────────────────────────────────────────────── # - REDIS_HOST=redis # <- your Redis hostname or IP # - REDIS_PORT=6379 # <- your Redis port # - REDIS_USERNAME=admin # ← REQUIRED when Redis has users/ACLs # - REDIS_PASSWORD=${REDIS_PASSWORD}# <- if you need a Redis auth password # # - REDIS_TLS=true # <- turn on TLS # - CLUSTER_NO_TLS_VALIDATION=true # <- skip SNI/hostname checks in clusters # # ────────────────────────────────────────────────────────────────────── # # SELF-SIGNED: trust no-CA by default # # ────────────────────────────────────────────────────────────────────── # - NODE_TLS_REJECT_UNAUTHORIZED=0 # <- Node.js will accept your self-signed cert # # ────────────────────────────────────────────────────────────────────── # # HTTP BASIC-AUTH FOR THE WEB UI # # ────────────────────────────────────────────────────────────────────── # - HTTP_USER=admin # <- change your UI username # - HTTP_PASSWORD=changeme # <- change your UI password # # ────────────────────────────────────────────────────────────────────── # # OPTIONAL: ENABLE REAL CERT VALIDATION (instead of skipping checks) # # ────────────────────────────────────────────────────────────────────── # # - REDIS_TLS_CA_CERT_FILE=/certs/selfsigned.crt # # - REDIS_TLS_SERVER_NAME=redis.example.com # mongo_express: # 🔧 MongoDB GUI (works if mongodb service is enabled) # image: mongo-express:1 # environment: # - ME_CONFIG_MONGODB_ADMINUSERNAME=admin # - ME_CONFIG_MONGODB_ADMINPASSWORD=changeme # - ME_CONFIG_MONGODB_SERVER=mongodb # ports: # - "8082:8081" # http://localhost:8082 # networks: [mcpnet] # depends_on: # mongodb: # condition: service_started # phpmyadmin: # 🔧 MySQL / MariaDB GUI # image: phpmyadmin:latest # environment: # - PMA_HOST=mysql # or mariadb # - PMA_USER=mysql # - PMA_PASSWORD=changeme # - PMA_ARBITRARY=1 # allow login to any host if you switch DBs # ports: # - "8083:80" # http://localhost:8083 # networks: [mcpnet] # depends_on: # mysql: # condition: service_started ############################################################################### # OPTIONAL MCP SERVERS - drop-in helpers the Gateway can call ############################################################################### ############################################################################### # Fast Time Server - High-performance time/timezone service for MCP # Note: This is an amd64-only image. On ARM platforms (Apple Silicon), # emulation may not work properly. Use profiles to disable: # docker compose --profile with-fast-time up -d ############################################################################### fast_time_server: image: ghcr.io/ibm/fast-time-server:latest restart: unless-stopped networks: [mcpnet] ports: - "8888:8080" # Map host port 8888 to container port 8080 command: ["-transport=sse", "-listen=0.0.0.0", "-port=8080", "-log-level=info"] profiles: ["with-fast-time"] # Optional: enable with --profile with-fast-time ############################################################################### # Auto-registration service - registers fast_time_server with gateway ############################################################################### register_fast_time: image: ${IMAGE_LOCAL:-mcpgateway/mcpgateway:latest} networks: [mcpnet] depends_on: gateway: condition: service_healthy fast_time_server: condition: service_started environment: - JWT_SECRET_KEY=my-test-key # This is a one-shot container that exits after registration restart: "no" profiles: ["with-fast-time"] # Optional: enable with --profile with-fast-time entrypoint: ["/bin/sh", "-c"] command: - | echo "Using latest gateway image with current JWT utility..." echo "Waiting for services to be ready..." # Wait for gateway to be ready using Python python3 -c " import time import urllib.request import urllib.error for i in range(1, 61): try: with urllib.request.urlopen('http://gateway:4444/health', timeout=2) as response: if response.status == 200: print('✅ gateway is healthy') break except: pass print(f'Waiting for gateway... ({i}/60)') time.sleep(2) else: print('❌ Gateway failed to become healthy') exit(1) " # Wait for fast_time_server to be ready using Python python3 -c " import time import urllib.request import urllib.error for i in range(1, 31): try: with urllib.request.urlopen('http://fast_time_server:8080/health', timeout=2) as response: if response.status == 200: print('✅ fast_time_server is healthy') break except: pass print(f'Waiting for fast_time_server... ({i}/30)') time.sleep(2) else: print('❌ Fast time server failed to become healthy') exit(1) " echo "Generating JWT token..." echo "Environment: JWT_SECRET_KEY=$$JWT_SECRET_KEY" echo "Running: python3 -m mcpgateway.utils.create_jwt_token --username admin@example.com --exp 10080 --secret my-test-key --algo HS256" # Only capture stdout (the token), let warnings go to stderr export MCPGATEWAY_BEARER_TOKEN=$$(python3 -m mcpgateway.utils.create_jwt_token --username admin@example.com --exp 10080 --secret my-test-key --algo HS256 2>/dev/null) echo "Generated token: $$MCPGATEWAY_BEARER_TOKEN" # Decode the token to verify it has expiration echo "Decoding token to verify claims..." python3 -m mcpgateway.utils.create_jwt_token --decode "$$MCPGATEWAY_BEARER_TOKEN" 2>/dev/null || echo "Failed to decode token" # Test authentication first echo "Testing authentication..." # Use Python to make HTTP requests python3 -c " import urllib.request import urllib.error import json import sys import os token = os.environ.get('MCPGATEWAY_BEARER_TOKEN', '') # Test version endpoint without auth print('Checking gateway config...') try: with urllib.request.urlopen('http://gateway:4444/version') as response: data = response.read().decode('utf-8') print(f'Gateway version response (no auth): {data[:200]}') except Exception as e: print(f'Version check failed: {e}') # Test version endpoint with auth print('Testing authentication...') try: req = urllib.request.Request('http://gateway:4444/version') req.add_header('Authorization', f'Bearer {token}') with urllib.request.urlopen(req) as response: data = response.read().decode('utf-8') print(f'Auth test response: SUCCESS') print(f'Response preview: {data[:200]}...') auth_success = True except Exception as e: print(f'Auth test response: FAILED - {e}') auth_success = False # Register fast_time_server with gateway print('Registering fast_time_server with gateway...') try: payload = json.dumps({'name': 'fast_time', 'url': 'http://fast_time_server:8080/sse'}) req = urllib.request.Request('http://gateway:4444/gateways', data=payload.encode('utf-8'), method='POST') req.add_header('Authorization', f'Bearer {token}') req.add_header('Content-Type', 'application/json') with urllib.request.urlopen(req) as response: data = response.read().decode('utf-8') print(f'Registration response: {data}') # Check if registration was successful response_data = json.loads(data) if 'id' in response_data: print('✅ Successfully registered fast_time_server') registration_success = True else: print('❌ Registration failed - no ID in response') registration_success = False except Exception as e: print(f'❌ Registration failed: {e}') registration_success = False # Exit with error code if registration failed if not registration_success: sys.exit(1) " echo "✅ Setup complete!" ############################################################################### # Hashicorp Terraform MCP Server # https://hub.docker.com/r/hashicorp/terraform-mcp-server # https://github.com/hashicorp/terraform-mcp-server/blob/main/README.md ############################################################################### # terraform-mcp-server: # image: docker.io/hashicorp/terraform-mcp-server:dev # container_name: terraform-mcp-server # networks: [mcpnet] # ports: # - "8001:8080" # Map host port 8888 to container port 8080 # restart: unless-stopped # environment: # - TRANSPORT_MODE=streamable-http # - TRANSPORT_HOST=0.0.0.0 # - TRANSPORT_PORT=8080 # - MCP_CORS_MODE=disabled # healthcheck: # test: ["CMD", "curl", "-f", "http://localhost:8080/health"] # interval: 30s # timeout: 10s # retries: 5 # start_period: 20s