services: # Infrastructure Services nats: image: nats:2.10-alpine container_name: bridge-nats restart: always ports: - "4222:4222" - "8222:8222" command: "-js -m 8222 --store_dir /data -c /etc/nats/nats.conf" volumes: - nats-data:/data - ./config/nats/mainnet.conf:/etc/nats/nats.conf:ro networks: - bridge-network healthcheck: test: ["CMD", "nc", "-z", "localhost", "4222"] interval: 5s timeout: 3s retries: 5 consul: image: hashicorp/consul:1.17 container_name: bridge-consul restart: always ports: - "8500:8500" - "8600:8600/udp" command: "agent -server -bootstrap-expect=3 -ui -client=0.0.0.0 -datacenter=mainnet" volumes: - consul-data:/consul/data - ./config/consul/mainnet.json:/consul/config/mainnet.json:ro networks: - bridge-network environment: - CONSUL_BIND_INTERFACE=eth0 - CONSUL_DATACENTER=mainnet healthcheck: test: ["CMD", "consul", "members"] interval: 5s timeout: 3s retries: 5 # Database with replication support postgres: image: ghcr.io/hanzoai/sql:latest container_name: bridge-postgres restart: always environment: - POSTGRES_USER=bridge - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - POSTGRES_DB=bridge - POSTGRES_INITDB_ARGS=--encoding=UTF-8 --locale=en_US.utf8 - POSTGRES_HOST_AUTH_METHOD=scram-sha-256 volumes: - postgres-data:/var/lib/postgresql/data - ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql - ./config/postgres/mainnet.conf:/etc/postgresql/postgresql.conf:ro ports: - "5432:5432" networks: - bridge-network healthcheck: test: ["CMD-SHELL", "pg_isready -U bridge"] interval: 5s timeout: 3s retries: 5 # MPC Nodes - Distributed across different availability zones mpc-node-0: image: ghcr.io/luxfi/lux-mpc:latest container_name: bridge-mpc-0 restart: always environment: - NODE_ID=mpc-node-0 - NODE_PORT=6000 - NATS_URL=nats://nats:4222 - CONSUL_URL=http://consul:8500 - DATA_DIR=/data/mpc - LOG_LEVEL=info - MPC_THRESHOLD=2 - MPC_TOTAL_NODES=3 - MPC_KEY_PREFIX=bridge/mpc/mainnet - NETWORK=mainnet - ENABLE_TLS=true - TLS_CERT_PATH=/certs/tls.crt - TLS_KEY_PATH=/certs/tls.key volumes: - mpc-data-0:/data/mpc - ./certs/mpc-0:/certs:ro ports: - "6000:6000" - "9090:9090" networks: - bridge-network depends_on: nats: condition: service_healthy consul: condition: service_healthy healthcheck: test: ["CMD", "curl", "-f", "https://localhost:6000/health", "--insecure"] interval: 10s timeout: 5s retries: 3 deploy: resources: limits: cpus: '2' memory: 4G reservations: cpus: '1' memory: 2G mpc-node-1: image: ghcr.io/luxfi/lux-mpc:latest container_name: bridge-mpc-1 restart: always environment: - NODE_ID=mpc-node-1 - NODE_PORT=6000 - NATS_URL=nats://nats:4222 - CONSUL_URL=http://consul:8500 - DATA_DIR=/data/mpc - LOG_LEVEL=info - MPC_THRESHOLD=2 - MPC_TOTAL_NODES=3 - MPC_KEY_PREFIX=bridge/mpc/mainnet - NETWORK=mainnet - ENABLE_TLS=true - TLS_CERT_PATH=/certs/tls.crt - TLS_KEY_PATH=/certs/tls.key volumes: - mpc-data-1:/data/mpc - ./certs/mpc-1:/certs:ro ports: - "6001:6000" - "9091:9090" networks: - bridge-network depends_on: nats: condition: service_healthy consul: condition: service_healthy healthcheck: test: ["CMD", "curl", "-f", "https://localhost:6000/health", "--insecure"] interval: 10s timeout: 5s retries: 3 deploy: resources: limits: cpus: '2' memory: 4G reservations: cpus: '1' memory: 2G mpc-node-2: image: ghcr.io/luxfi/lux-mpc:latest container_name: bridge-mpc-2 restart: always environment: - NODE_ID=mpc-node-2 - NODE_PORT=6000 - NATS_URL=nats://nats:4222 - CONSUL_URL=http://consul:8500 - DATA_DIR=/data/mpc - LOG_LEVEL=info - MPC_THRESHOLD=2 - MPC_TOTAL_NODES=3 - MPC_KEY_PREFIX=bridge/mpc/mainnet - NETWORK=mainnet - ENABLE_TLS=true - TLS_CERT_PATH=/certs/tls.crt - TLS_KEY_PATH=/certs/tls.key volumes: - mpc-data-2:/data/mpc - ./certs/mpc-2:/certs:ro ports: - "6002:6000" - "9092:9090" networks: - bridge-network depends_on: nats: condition: service_healthy consul: condition: service_healthy healthcheck: test: ["CMD", "curl", "-f", "https://localhost:6000/health", "--insecure"] interval: 10s timeout: 5s retries: 3 deploy: resources: limits: cpus: '2' memory: 4G reservations: cpus: '1' memory: 2G # Bridge Server with HA bridge-server: image: ghcr.io/luxfi/bridge-server:latest container_name: bridge-server restart: always environment: - NODE_ENV=production - PORT=3000 - DATABASE_URL=postgresql://bridge:${POSTGRES_PASSWORD}@postgres:5432/bridge?sslmode=require - NATS_URL=nats://nats:4222 - CONSUL_URL=http://consul:8500 - NODE_ID=bridge-server-0 - MPC_THRESHOLD=2 - MPC_TOTAL_NODES=3 - MPC_KEY_PREFIX=bridge/mpc/mainnet - NETWORK=mainnet - ENABLE_METRICS=true - METRICS_PORT=9090 - LOG_LEVEL=info - RATE_LIMIT_ENABLED=true - RATE_LIMIT_MAX=100 - RATE_LIMIT_WINDOW=60 ports: - "3000:3000" - "9093:9090" networks: - bridge-network depends_on: postgres: condition: service_healthy nats: condition: service_healthy consul: condition: service_healthy mpc-node-0: condition: service_healthy mpc-node-1: condition: service_healthy mpc-node-2: condition: service_healthy healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3000/health"] interval: 10s timeout: 5s retries: 3 deploy: resources: limits: cpus: '4' memory: 8G reservations: cpus: '2' memory: 4G # Bridge UI - Mainnet — Vite SPA (ghcr.io/hanzoai/spa serves /dist on :80). # BRIDGE_* env documents what the image was built with; the values # become real only when baked at build time (app/bridge/Dockerfile). bridge-ui: image: ghcr.io/luxfi/bridge-ui:latest container_name: bridge-ui restart: always environment: - BRIDGE_API_HOST=https://bridge-api.lux.network - BRIDGE_ENV=mainnet - BRIDGE_CLIENT_ID=lux-bridge - BRIDGE_IAM_ORG=lux-network ports: - "3001:80" networks: - bridge-network depends_on: - bridge-server deploy: resources: limits: cpus: '2' memory: 2G reservations: cpus: '1' memory: 1G # Bridge unified Go binary (cmd/bridge). Mainnet equivalent of the # bridge-go service in compose.testnet.yml and the Deployment in # k8s/bridge-deployment.yaml — same image, same env contract, same # mainnet chain set (networks.mainnet.yaml mirrors the k8s ConfigMap). # # Build locally before `docker compose up bridge-go`: # docker build -f cmd/bridge/Dockerfile -t ghcr.io/luxfi/bridge:dev . # Then export BRIDGE_TAG=dev. For production, pin to a released tag # (e.g. BRIDGE_TAG=v1.2.3) rather than :latest so a registry retag # can't roll the running stack out from under you. # # Runs alongside bridge-server + bridge-ui during the §13.6 cutover # soak. Once the Go binary handles real traffic without regressions, # DNS at bridge.lux.network repoints to this service and the legacy # bridge-server + bridge-ui can be retired. bridge-go: image: ghcr.io/luxfi/bridge:${BRIDGE_TAG:-latest} container_name: bridge-go restart: always ports: - "8080:8080" environment: - BRIDGE_ADDR=:8080 # The operator listener: /metrics, the swap list, the deposit poll. # Deliberately absent from `ports:` — it is reachable on the compose # network (prometheus scrapes bridge-go:8081/metrics) and not from # the host. - BRIDGE_ADMIN_ADDR=:8081 - BRIDGE_CONFIG=/etc/bridge/networks.yaml - BRIDGE_DATA_DIR=/var/lib/lux-bridge # Empty disables the legacy backend proxy — all /v1/bridge/* # routes resolve through native handlers. - BRIDGE_BACKEND_URL= # BridgeVM (b-chain) JSON-RPC. Leave unset until BridgeVM exposes # its public API; native handlers serve quote + swap state in the # meantime. - BRIDGE_BCHAIN_URL= # MPC daemon. The lux-mpc image exposes its HTTP API on NODE_PORT # (default 6000 per the in-compose mpc-node-* env). Override # BRIDGE_MPC_URL to point at an external mpcd cluster when not # running the in-compose MPC services. - BRIDGE_MPC_URL=${BRIDGE_MPC_URL:-http://mpc-node-0:6000} - BRIDGE_MPC_TOKEN=${BRIDGE_MPC_TOKEN:-} - BRIDGE_MPC_ORG_ID=bridge # Mainnet source-chain RPC overrides. Public RPC endpoints are # fine for low-volume soak; pin to a private provider (Alchemy / # Infura / Quicknode) before high-volume cutover so rate limits # don't drop deposit-watcher polls. - BRIDGE_SOURCE_RPC_OVERRIDES=ETHEREUM_MAINNET=https://eth.llamarpc.com,LUX_MAINNET=https://api.lux.network/v1/chain/C/rpc,ARBITRUM_MAINNET=https://arb1.arbitrum.io/rpc,OPTIMISM_MAINNET=https://mainnet.optimism.io,BASE_MAINNET=https://mainnet.base.org,POLYGON_MAINNET=https://polygon-rpc.com,BSC_MAINNET=https://bsc-dataseed.binance.org,AVAX_MAINNET=https://api.avax.network/ext/bc/C/rpc volumes: # Networks + tokens config. Read-only mount so an accidental write # inside the container can't corrupt the operator-checked source- # of-truth file. Keep this in sync with the inline YAML in # k8s/bridge-deployment.yaml's ConfigMap. - ./cmd/bridge/networks.mainnet.yaml:/etc/bridge/networks.yaml:ro # zapdb-backed swap store + release-wallets.json. Bind-mounted to # a host SSD path so the data survives compose down/up and is # visible to host-side backup tooling. - bridge-go-data:/var/lib/lux-bridge networks: - bridge-network depends_on: mpc-node-0: condition: service_healthy mpc-node-1: condition: service_healthy mpc-node-2: condition: service_healthy deploy: resources: limits: cpus: '4' memory: 8G reservations: cpus: '2' memory: 4G # Enhanced Monitoring prometheus: image: prom/prometheus:latest container_name: bridge-prometheus restart: always volumes: - ./monitoring/prometheus/mainnet.yml:/etc/prometheus/prometheus.yml - prometheus-data:/prometheus ports: - "9094:9090" networks: - bridge-network command: - '--config.file=/etc/prometheus/prometheus.yml' - '--storage.tsdb.path=/prometheus' - '--storage.tsdb.retention.time=30d' - '--web.enable-lifecycle' deploy: resources: limits: cpus: '1' memory: 2G grafana: image: grafana/grafana:latest container_name: bridge-grafana restart: always ports: - "3002:3000" networks: - bridge-network environment: - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD} - GF_USERS_ALLOW_SIGN_UP=false - GF_INSTALL_PLUGINS=grafana-clock-panel,grafana-simple-json-datasource - GF_SERVER_ROOT_URL=https://metrics.bridge.lux.network volumes: - grafana-data:/var/lib/grafana - ./monitoring/grafana/dashboards:/etc/grafana/provisioning/dashboards - ./monitoring/grafana/datasources:/etc/grafana/provisioning/datasources deploy: resources: limits: cpus: '1' memory: 1G # Backup Service backup: image: ghcr.io/luxfi/bridge-backup:latest container_name: bridge-backup restart: always environment: - BACKUP_INTERVAL=3600 - BACKUP_RETENTION_DAYS=30 - S3_BUCKET=${BACKUP_S3_BUCKET} - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} volumes: - postgres-data:/data/postgres:ro - mpc-data-0:/data/mpc-0:ro - mpc-data-1:/data/mpc-1:ro - mpc-data-2:/data/mpc-2:ro networks: - bridge-network volumes: nats-data: driver: local consul-data: driver: local mpc-data-0: driver: local driver_opts: type: none o: bind device: /mnt/ssd/bridge/mpc-0 mpc-data-1: driver: local driver_opts: type: none o: bind device: /mnt/ssd/bridge/mpc-1 mpc-data-2: driver: local driver_opts: type: none o: bind device: /mnt/ssd/bridge/mpc-2 postgres-data: driver: local driver_opts: type: none o: bind device: /mnt/ssd/bridge/postgres prometheus-data: driver: local grafana-data: driver: local bridge-go-data: driver: local driver_opts: type: none o: bind device: /mnt/ssd/bridge/bridge-go networks: bridge-network: driver: bridge ipam: config: - subnet: 172.20.0.0/16