version: '3.8' services: postgres: image: postgres:16-alpine environment: POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB} # Production security settings POSTGRES_INITDB_ARGS: "--auth-host=scram-sha-256 --auth-local=scram-sha-256" ports: # Only expose internally, not to host - "5432" volumes: - postgres-data:/var/lib/postgresql/data - ./docker/services/postgres:/docker-entrypoint-initdb.d # TLS certificates for PostgreSQL - ./config/ssl/postgres:/var/lib/postgresql/ssl:ro networks: - meldestelle-network healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] interval: 10s timeout: 5s retries: 5 start_period: 20s restart: unless-stopped # Security: Run as non-root user user: postgres # Resource limits deploy: resources: limits: memory: 1G cpus: '0.5' reservations: memory: 512M cpus: '0.25' redis: image: redis:7-alpine environment: # Redis with authentication REDIS_PASSWORD: ${REDIS_PASSWORD} ports: # Only expose internally - "6379" volumes: - redis-data:/data - ./config/redis/redis.conf:/usr/local/etc/redis/redis.conf:ro # TLS certificates for Redis - ./config/ssl/redis:/tls:ro command: > redis-server /usr/local/etc/redis/redis.conf --requirepass ${REDIS_PASSWORD} --appendonly yes --appendfsync everysec --save 900 1 --save 300 10 --save 60 10000 --maxmemory 256mb --maxmemory-policy allkeys-lru --tcp-keepalive 300 --timeout 0 --tcp-backlog 511 --databases 16 --stop-writes-on-bgsave-error yes --rdbcompression yes --rdbchecksum yes --dir /data networks: - meldestelle-network healthcheck: test: ["CMD", "redis-cli", "--no-auth-warning", "-a", "${REDIS_PASSWORD}", "ping"] interval: 10s timeout: 5s retries: 3 start_period: 10s restart: unless-stopped # Security: Run as non-root user user: redis # Resource limits deploy: resources: limits: memory: 512M cpus: '0.25' reservations: memory: 256M cpus: '0.1' keycloak: image: quay.io/keycloak/keycloak:23.0 environment: KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN} KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD} KC_DB: ${KC_DB} KC_DB_URL: ${KC_DB_URL} KC_DB_USERNAME: ${KC_DB_USERNAME} KC_DB_PASSWORD: ${KC_DB_PASSWORD} # Production settings KC_HOSTNAME: ${KC_HOSTNAME} KC_HOSTNAME_STRICT: true KC_HOSTNAME_STRICT_HTTPS: true KC_HTTP_ENABLED: false KC_HTTPS_PORT: 8443 KC_HTTPS_CERTIFICATE_FILE: /opt/keycloak/conf/server.crt.pem KC_HTTPS_CERTIFICATE_KEY_FILE: /opt/keycloak/conf/server.key.pem KC_PROXY: edge KC_LOG_LEVEL: WARN KC_METRICS_ENABLED: true KC_HEALTH_ENABLED: true # Security headers KC_HTTP_RELATIVE_PATH: /auth ports: # HTTPS only in production - "8443:8443" depends_on: postgres: condition: service_healthy volumes: - ./docker/services/keycloak:/opt/keycloak/data/import # TLS certificates - ./config/ssl/keycloak:/opt/keycloak/conf:ro command: start --import-realm --optimized networks: - meldestelle-network healthcheck: test: ["CMD", "curl", "--fail", "--insecure", "https://localhost:8443/auth/health/ready"] interval: 10s timeout: 5s retries: 5 start_period: 60s restart: unless-stopped # Resource limits deploy: resources: limits: memory: 1G cpus: '0.5' reservations: memory: 512M cpus: '0.25' zookeeper: image: confluentinc/cp-zookeeper:7.5.0 environment: ZOOKEEPER_CLIENT_PORT: ${ZOOKEEPER_CLIENT_PORT} ZOOKEEPER_TICK_TIME: 2000 ZOOKEEPER_SYNC_LIMIT: 2 ZOOKEEPER_INIT_LIMIT: 5 ZOOKEEPER_MAX_CLIENT_CNXNS: 60 ZOOKEEPER_AUTOPURGE_SNAP_RETAIN_COUNT: 3 ZOOKEEPER_AUTOPURGE_PURGE_INTERVAL: 24 # Security settings ZOOKEEPER_AUTH_PROVIDER_SASL: org.apache.zookeeper.server.auth.SASLAuthenticationProvider ZOOKEEPER_REQUIRE_CLIENT_AUTH_SCHEME: sasl KAFKA_OPTS: "-Djava.security.auth.login.config=/etc/kafka/secrets/zookeeper_jaas.conf" ports: # Only expose internally - "2181" volumes: - zookeeper-data:/var/lib/zookeeper/data - zookeeper-logs:/var/lib/zookeeper/log - ./config/kafka/secrets:/etc/kafka/secrets:ro networks: - meldestelle-network healthcheck: test: ["CMD", "nc", "-z", "localhost", "2181"] interval: 10s timeout: 5s retries: 3 start_period: 10s restart: unless-stopped # Resource limits deploy: resources: limits: memory: 512M cpus: '0.25' reservations: memory: 256M cpus: '0.1' kafka: image: confluentinc/cp-kafka:7.5.0 depends_on: zookeeper: condition: service_healthy ports: # Only expose internally - "9092" - "9093" environment: KAFKA_BROKER_ID: ${KAFKA_BROKER_ID} KAFKA_ZOOKEEPER_CONNECT: ${KAFKA_ZOOKEEPER_CONNECT} # Production security settings with SASL/SSL KAFKA_ADVERTISED_LISTENERS: SASL_SSL://kafka:9093,SASL_PLAINTEXT://kafka:9092 KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: SASL_SSL:SASL_SSL,SASL_PLAINTEXT:SASL_PLAINTEXT KAFKA_INTER_BROKER_LISTENER_NAME: SASL_SSL KAFKA_SECURITY_INTER_BROKER_PROTOCOL: SASL_SSL KAFKA_SASL_MECHANISM_INTER_BROKER_PROTOCOL: PLAIN KAFKA_SASL_ENABLED_MECHANISMS: PLAIN # SSL Configuration KAFKA_SSL_KEYSTORE_FILENAME: kafka.server.keystore.jks KAFKA_SSL_KEYSTORE_CREDENTIALS: kafka_keystore_creds KAFKA_SSL_KEY_CREDENTIALS: kafka_ssl_key_creds KAFKA_SSL_TRUSTSTORE_FILENAME: kafka.server.truststore.jks KAFKA_SSL_TRUSTSTORE_CREDENTIALS: kafka_truststore_creds KAFKA_SSL_CLIENT_AUTH: required # Performance and reliability settings KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3 KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 3 KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 2 KAFKA_DEFAULT_REPLICATION_FACTOR: 3 KAFKA_MIN_INSYNC_REPLICAS: 2 KAFKA_NUM_PARTITIONS: 3 KAFKA_LOG_RETENTION_HOURS: 168 KAFKA_LOG_SEGMENT_BYTES: 1073741824 KAFKA_LOG_RETENTION_CHECK_INTERVAL_MS: 300000 KAFKA_AUTO_CREATE_TOPICS_ENABLE: false # JVM settings KAFKA_HEAP_OPTS: "-Xmx512M -Xms512M" KAFKA_JVM_PERFORMANCE_OPTS: "-server -XX:+UseG1GC -XX:MaxGCPauseMillis=20 -XX:InitiatingHeapOccupancyPercent=35" # Security KAFKA_OPTS: "-Djava.security.auth.login.config=/etc/kafka/secrets/kafka_jaas.conf" volumes: - kafka-data:/var/lib/kafka/data - ./config/kafka/secrets:/etc/kafka/secrets:ro networks: - meldestelle-network healthcheck: test: ["CMD", "kafka-topics", "--bootstrap-server", "localhost:9092", "--list"] interval: 10s timeout: 5s retries: 3 start_period: 30s restart: unless-stopped # Resource limits deploy: resources: limits: memory: 1G cpus: '0.5' reservations: memory: 512M cpus: '0.25' zipkin: image: openzipkin/zipkin:2 ports: # Only expose internally - "9411" environment: # Production settings JAVA_OPTS: "-Xms256m -Xmx512m" STORAGE_TYPE: elasticsearch ES_HOSTS: http://elasticsearch:9200 networks: - meldestelle-network healthcheck: test: ["CMD", "wget", "-q", "-O", "-", "http://localhost:9411/health"] interval: 10s timeout: 5s retries: 3 start_period: 15s restart: unless-stopped # Resource limits deploy: resources: limits: memory: 512M cpus: '0.25' reservations: memory: 256M cpus: '0.1' # Production monitoring services prometheus: image: prom/prometheus:v2.48.1 volumes: - ./config/monitoring/prometheus.prod.yml:/etc/prometheus/prometheus.yml:ro - prometheus-data:/prometheus # TLS certificates - ./config/ssl/prometheus:/etc/ssl/prometheus:ro command: - '--config.file=/etc/prometheus/prometheus.yml' - '--storage.tsdb.path=/prometheus' - '--storage.tsdb.retention.time=30d' - '--storage.tsdb.retention.size=10GB' - '--web.console.libraries=/etc/prometheus/console_libraries' - '--web.console.templates=/etc/prometheus/consoles' - '--web.enable-lifecycle' - '--web.enable-admin-api' - '--web.external-url=https://${PROMETHEUS_HOSTNAME}' - '--web.config.file=/etc/ssl/prometheus/web.yml' ports: # Only expose internally - "9090" networks: - meldestelle-network healthcheck: test: ["CMD", "wget", "-q", "-O", "-", "http://localhost:9090/-/healthy"] interval: 10s timeout: 5s retries: 3 start_period: 15s restart: unless-stopped # Security: Run as non-root user user: "65534:65534" # Resource limits deploy: resources: limits: memory: 1G cpus: '0.5' reservations: memory: 512M cpus: '0.25' grafana: image: grafana/grafana:10.2.3 volumes: - ./config/monitoring/grafana/provisioning:/etc/grafana/provisioning:ro - ./config/monitoring/grafana/dashboards:/var/lib/grafana/dashboards:ro - grafana-data:/var/lib/grafana # TLS certificates - ./config/ssl/grafana:/etc/ssl/grafana:ro environment: # Security settings - GF_SECURITY_ADMIN_USER=${GF_SECURITY_ADMIN_USER} - GF_SECURITY_ADMIN_PASSWORD=${GF_SECURITY_ADMIN_PASSWORD} - GF_USERS_ALLOW_SIGN_UP=false - GF_USERS_ALLOW_ORG_CREATE=false - GF_AUTH_ANONYMOUS_ENABLED=false # HTTPS settings - GF_SERVER_PROTOCOL=https - GF_SERVER_CERT_FILE=/etc/ssl/grafana/server.crt - GF_SERVER_CERT_KEY=/etc/ssl/grafana/server.key - GF_SERVER_DOMAIN=${GRAFANA_HOSTNAME} - GF_SERVER_ROOT_URL=https://${GRAFANA_HOSTNAME} # Security headers - GF_SECURITY_STRICT_TRANSPORT_SECURITY=true - GF_SECURITY_STRICT_TRANSPORT_SECURITY_MAX_AGE_SECONDS=31536000 - GF_SECURITY_CONTENT_TYPE_PROTECTION=true - GF_SECURITY_X_CONTENT_TYPE_OPTIONS=nosniff - GF_SECURITY_X_XSS_PROTECTION=true # Session settings - GF_SESSION_PROVIDER=redis - GF_SESSION_PROVIDER_CONFIG=addr=redis:6379,pool_size=100,db=2,password=${REDIS_PASSWORD} - GF_SESSION_COOKIE_SECURE=true - GF_SESSION_COOKIE_SAMESITE=strict # Logging - GF_LOG_MODE=console - GF_LOG_LEVEL=warn ports: # HTTPS only - "3443:3000" networks: - meldestelle-network depends_on: prometheus: condition: service_healthy redis: condition: service_healthy healthcheck: test: ["CMD", "wget", "-q", "-O", "-", "--no-check-certificate", "https://localhost:3000/api/health"] interval: 10s timeout: 5s retries: 3 start_period: 20s restart: unless-stopped # Security: Run as non-root user user: "472:472" # Resource limits deploy: resources: limits: memory: 512M cpus: '0.25' reservations: memory: 256M cpus: '0.1' # Reverse proxy for production nginx: image: nginx:alpine ports: - "80:80" - "443:443" volumes: - ./config/nginx/nginx.prod.conf:/etc/nginx/nginx.conf:ro - ./config/nginx/conf.d:/etc/nginx/conf.d:ro - ./config/ssl/nginx:/etc/ssl/nginx:ro - ./logs/nginx:/var/log/nginx networks: - meldestelle-network depends_on: - keycloak - grafana healthcheck: test: ["CMD", "wget", "-q", "-O", "-", "http://localhost/health"] interval: 10s timeout: 5s retries: 3 start_period: 10s restart: unless-stopped # Resource limits deploy: resources: limits: memory: 256M cpus: '0.1' reservations: memory: 128M cpus: '0.05' volumes: postgres-data: driver: local redis-data: driver: local kafka-data: driver: local zookeeper-data: driver: local zookeeper-logs: driver: local prometheus-data: driver: local grafana-data: driver: local networks: meldestelle-network: driver: bridge ipam: config: - subnet: 172.20.0.0/16