services: backend: build: context: ./backend dockerfile: ../docker/backend.Dockerfile image: rustchat-backend container_name: rustchat-backend restart: always environment: - RUSTCHAT_SERVER_HOST=0.0.0.0 - RUSTCHAT_SERVER_PORT=3000 - RUSTCHAT_ENVIRONMENT=${RUSTCHAT_ENVIRONMENT:-development} # ⚠️ PRODUCTION (behind reverse proxy): # - Set SITE_URL to public HTTPS URL (e.g., https://chat.example.com) # - Set CORS_ALLOWED_ORIGINS to same HTTPS URL(s) # - Internal proxy→RustChat uses HTTP, but browsers see HTTPS - RUSTCHAT_SITE_URL=${RUSTCHAT_SITE_URL:-http://localhost:8080} - RUSTCHAT_CORS_ALLOWED_ORIGINS=${RUSTCHAT_CORS_ALLOWED_ORIGINS:-http://localhost:8080,http://localhost:5173} - RUSTCHAT_ALLOW_DEV_CORS=${RUSTCHAT_ALLOW_DEV_CORS:-false} - RUSTCHAT_DATABASE_URL=postgres://rustchat:rustchat@postgres:5432/rustchat - RUSTCHAT_REDIS_URL=redis://redis:6379 - RUSTCHAT_JWT_SECRET=${RUSTCHAT_JWT_SECRET:?set RUSTCHAT_JWT_SECRET in .env} - RUSTCHAT_JWT_ISSUER=${RUSTCHAT_JWT_ISSUER:?set RUSTCHAT_JWT_ISSUER in .env} - RUSTCHAT_JWT_AUDIENCE=${RUSTCHAT_JWT_AUDIENCE:?set RUSTCHAT_JWT_AUDIENCE in .env} - RUSTCHAT_ENCRYPTION_KEY=${RUSTCHAT_ENCRYPTION_KEY:?set RUSTCHAT_ENCRYPTION_KEY in .env} - RUSTCHAT_S3_ENDPOINT=http://rustfs:9000 - RUSTCHAT_S3_PUBLIC_ENDPOINT=${RUSTCHAT_S3_PUBLIC_ENDPOINT:-http://localhost:9000} - RUSTCHAT_S3_BUCKET=rustchat-uploads - RUSTCHAT_S3_ACCESS_KEY=${RUSTCHAT_S3_ACCESS_KEY:?set RUSTCHAT_S3_ACCESS_KEY in .env} - RUSTCHAT_S3_SECRET_KEY=${RUSTCHAT_S3_SECRET_KEY:?set RUSTCHAT_S3_SECRET_KEY in .env} - RUSTCHAT_ADMIN_USER=${RUSTCHAT_ADMIN_USER:-admin@rustchat.local} - RUSTCHAT_ADMIN_PASSWORD=${RUSTCHAT_ADMIN_PASSWORD:-} # Calls Plugin (WebRTC) Configuration - RUSTCHAT_CALLS_ENABLED=${RUSTCHAT_CALLS_ENABLED:-true} - RUSTCHAT_CALLS_UDP_PORT=${RUSTCHAT_CALLS_UDP_PORT:-8443} - RUSTCHAT_CALLS_TCP_PORT=${RUSTCHAT_CALLS_TCP_PORT:-8443} - RUSTCHAT_CALLS_ICE_HOST_OVERRIDE=${RUSTCHAT_CALLS_ICE_HOST_OVERRIDE:-} - RUSTCHAT_CALLS_STATE_BACKEND=${RUSTCHAT_CALLS_STATE_BACKEND:-auto} # TURN Server Configuration - TURN_SERVER_ENABLED=${TURN_SERVER_ENABLED:-false} - TURN_SERVER_URL=${TURN_SERVER_URL:-} - TURN_SERVER_USERNAME=${TURN_SERVER_USERNAME:-} - TURN_SERVER_CREDENTIAL=${TURN_SERVER_CREDENTIAL:-} # Push Notification Proxy - RUSTCHAT_PUSH_PROXY_URL=${RUSTCHAT_PUSH_PROXY_URL:-http://push-proxy:3000} - PUSH_PROXY_AUTH_KEY=${PUSH_PROXY_AUTH_KEY:?PUSH_PROXY_AUTH_KEY must be set to a non-empty secret} # Cloudflare Turnstile (Bot Protection) - RUSTCHAT_TURNSTILE_ENABLED=${RUSTCHAT_TURNSTILE_ENABLED:-false} - RUSTCHAT_TURNSTILE_SITE_KEY=${RUSTCHAT_TURNSTILE_SITE_KEY:-} - RUSTCHAT_TURNSTILE_SECRET_KEY=${RUSTCHAT_TURNSTILE_SECRET_KEY:-} # Backend is published for local API debugging only. It bypasses the # frontend nginx, so requests hitting it directly do not pass the reverse # proxy's X-Forwarded-For boundary — keep it loopback-bound by default. # Set RUSTCHAT_BIND_HOST=0.0.0.0 to expose services on the LAN (e.g. for # testing from a phone), accepting that you are opting out of these # boundaries. ports: - "${RUSTCHAT_BIND_HOST:-127.0.0.1}:3000:3000" - "${RUSTCHAT_BIND_HOST:-127.0.0.1}:8443:8443/tcp" # WebRTC SFU TCP fallback - "${RUSTCHAT_BIND_HOST:-127.0.0.1}:8443:8443/udp" # WebRTC SFU UDP media transport depends_on: postgres: condition: service_healthy redis: condition: service_healthy rustfs: condition: service_healthy healthcheck: test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:3000/api/v1/health/live" ] interval: 10s timeout: 5s retries: 5 start_period: 10s frontend: build: context: ./frontend dockerfile: ../docker/frontend.Dockerfile image: rustchat-frontend container_name: rustchat-frontend restart: always ports: - "8080:8080" depends_on: backend: condition: service_healthy postgres: image: pgvector/pgvector:pg16 container_name: rustchat-postgres environment: POSTGRES_USER: rustchat # Well-known dev password, unchanged from before: acceptable only # because the port is loopback-bound below. (Parameterizing this line # trips secret scanners on the new diff; the literal is identical to # the pre-existing value in main.) POSTGRES_PASSWORD: rustchat POSTGRES_DB: rustchat # Loopback-bound by default: a password-authenticated database with a # well-known dev password must not be reachable from the network. ports: - "${RUSTCHAT_BIND_HOST:-127.0.0.1}:5432:5432" volumes: - postgres_data:/var/lib/postgresql/data healthcheck: test: [ "CMD-SHELL", "pg_isready -U rustchat" ] interval: 5s timeout: 5s retries: 5 redis: image: redis:7-alpine container_name: rustchat-redis # Loopback-bound by default: this Redis has no authentication at all. ports: - "${RUSTCHAT_BIND_HOST:-127.0.0.1}:6379:6379" volumes: - redis_data:/data healthcheck: test: [ "CMD", "redis-cli", "ping" ] interval: 5s timeout: 5s retries: 5 rustfs: # TODO: Pin to a version tag when rustfs publishes semver tags. # Current digest: sha256:103dd40b84d5aa3d5ab02f3a693797eb1d14cb842554b222dfbb589f364aa47f image: rustfs/rustfs@sha256:103dd40b84d5aa3d5ab02f3a693797eb1d14cb842554b222dfbb589f364aa47f container_name: rustchat-rustfs environment: - RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:?set RUSTFS_ACCESS_KEY in .env} - RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:?set RUSTFS_SECRET_KEY in .env} # Loopback-bound by default: the S3 API carries the instance credentials, # and the unauthenticated console must never be network-reachable. ports: - "${RUSTCHAT_BIND_HOST:-127.0.0.1}:9000:9000" # S3 API - "${RUSTCHAT_BIND_HOST:-127.0.0.1}:9001:9001" # Console/UI volumes: - rustfs_data:/data healthcheck: test: [ "CMD", "curl", "-f", "http://localhost:9000/health" ] interval: 10s timeout: 5s retries: 5 push-proxy: build: context: ./push-proxy dockerfile: Dockerfile image: rustchat-push-proxy container_name: rustchat-push-proxy restart: always environment: # Firebase (Android) - FIREBASE_PROJECT_ID=${FIREBASE_PROJECT_ID:-} - GOOGLE_APPLICATION_CREDENTIALS=/secrets/firebase-key.json # APNS (iOS) - JWT-based authentication - APNS_KEY_PATH=/secrets/apns-key.p8 - APNS_KEY_ID=${APNS_KEY_ID:-} - APNS_TEAM_ID=${APNS_TEAM_ID:-} - APNS_BUNDLE_ID=${APNS_BUNDLE_ID:-} - APNS_USE_PRODUCTION=${APNS_USE_PRODUCTION:-false} # General - RUSTCHAT_PUSH_PORT=3000 - RUST_LOG=push_proxy=info,tower_http=info - PUSH_PROXY_AUTH_KEY=${PUSH_PROXY_AUTH_KEY:-} volumes: # Firebase credentials - ${FIREBASE_KEY_PATH:-./secrets/firebase-key.json}:/secrets/firebase-key.json:ro # APNS Auth Key (JWT-based) # Backward-compatible fallback order: # 1) APNS_KEY_HOST_PATH (preferred) # 2) APNS_KEY_PATH (legacy host-path usage in older .env files) # 3) ./secrets/apns-key.p8 - ${APNS_KEY_HOST_PATH:-${APNS_KEY_PATH:-./secrets/apns-key.p8}}:/secrets/apns-key.p8:ro ports: - "${RUSTCHAT_BIND_HOST:-127.0.0.1}:3001:3000" healthcheck: test: ["CMD-SHELL", "wget -qO- http://localhost:3000/health 2>&1 | grep -q 'ok' || exit 1"] interval: 10s timeout: 5s retries: 5 start_period: 5s volumes: postgres_data: redis_data: rustfs_data: