# Whole platform on one host, one public port (EDGE_PORT, default 8080): # # browser ──► edge (nginx) ──► frontend (nginx, prebuilt SPA: terminal + client area) # ├─► gateway (Go, MT5 Manager API gateway) ─┐ # └─► demomarket (real market data via Yahoo ├─► postgres # Finance/Binance + demo broker/CRM with │ (users, accounts, # user management; no real broker) ───────┘ sessions; the # gateway's alerts # and workspaces) # # Run from this directory on the host: docker compose up -d --build # gateway.env and .env (database password, admin token) are created by # deploy.sh with fresh random secrets — see gateway.env.example. name: tradeplatform services: edge: image: nginx:1.27-alpine restart: unless-stopped ports: - "${EDGE_PORT:-8080}:80" # The DIRECTORY is mounted, not the file: rsync replaces nginx.conf with a # new inode on every deploy, and a single-file bind mount would keep the # container on the old one (an upstream rename then 502s until a restart). volumes: - ./edge:/etc/nginx/edge:ro # The image's entrypoint renders templates/*.template into conf.d with # the two host names below; nginx.conf includes the result. - ./edge/templates:/etc/nginx/templates:ro environment: # The subdomains, when there are any. `.invalid` never matches a real # request, so an unset host leaves only the default server in play. TERMINAL_HOST: ${TERMINAL_HOST:-terminal.invalid} CLIENT_AREA_HOST: ${CLIENT_AREA_HOST:-client-area.invalid} NGINX_ENVSUBST_FILTER: ^(TERMINAL_HOST|CLIENT_AREA_HOST)$$ command: ["nginx", "-c", "/etc/nginx/edge/nginx.conf", "-g", "daemon off;"] depends_on: frontend: condition: service_healthy gateway: condition: service_started networks: [platform] frontend: build: context: ./frontend-build # assembled by deploy.sh: dist/ + nginx.conf + entrypoint.sh dockerfile: Dockerfile restart: unless-stopped env_file: frontend.env networks: [platform] gateway: build: context: ../backend dockerfile: deploy/docker/Dockerfile restart: unless-stopped env_file: gateway.env environment: # Price alerts and saved workspaces persist here (internal/store/appdata). POSTGRES_DSN: postgres://tradeplatform:${POSTGRES_PASSWORD}@postgres:5432/tradeplatform?sslmode=disable depends_on: demomarket: condition: service_started postgres: condition: service_healthy networks: [platform] demomarket: build: context: ../backend dockerfile: deploy/docker/Dockerfile.demomarket restart: unless-stopped environment: USERS_DSN: postgres://tradeplatform:${POSTGRES_PASSWORD}@postgres:5432/tradeplatform?sslmode=disable ADMIN_TOKEN: ${ADMIN_TOKEN} # The demo broker's book (positions, orders, history, deals) lives in # PostgreSQL alongside the users. The file is only read once, to import a # book left by an earlier file-backed release. BROKER_STATE_FILE: /data/broker-state.json volumes: - brokerdata:/data depends_on: postgres: condition: service_healthy networks: [platform] postgres: image: postgres:16-alpine restart: unless-stopped environment: POSTGRES_USER: tradeplatform POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: tradeplatform volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U tradeplatform -d tradeplatform"] interval: 5s timeout: 3s retries: 20 # Not published on any host port: only the compose network reaches it. networks: [platform] volumes: pgdata: brokerdata: networks: platform: driver: bridge ipam: config: # Pinned so gateway.env can trust X-Forwarded-For from `edge` only # (RATE_LIMIT_TRUSTED_PROXIES). - subnet: 172.30.0.0/24