name: crypto_trading services: backend: build: context: ./backend args: BUILD_VERSION: ${CRYPTO_BUILD_VERSION:-unknown} image: crypto_trading-backend container_name: crypto-trading-backend restart: unless-stopped mem_limit: ${CRYPTO_MEM_LIMIT:-1g} cpus: ${CRYPTO_CPUS:-2} logging: driver: json-file options: max-size: "10m" max-file: "5" security_opt: - no-new-privileges:true # Reachable only on the internal network; the frontend nginx proxies to it. expose: - "8000" environment: - MODE=${CRYPTO_MODE:-paper} - EXCHANGE=${CRYPTO_EXCHANGE:-binance} - SYMBOL=${CRYPTO_SYMBOL:-BTC/USDT} - TIMEFRAME=${CRYPTO_TIMEFRAME:-5m} - POLL_INTERVAL=${CRYPTO_POLL_INTERVAL:-5} - STARTING_BALANCE=${CRYPTO_STARTING_BALANCE:-10000} - FEE_BPS=${CRYPTO_FEE_BPS:-10} - SLIPPAGE_BPS=${CRYPTO_SLIPPAGE_BPS:-5} - DATABASE_URL=sqlite:////data/trader.db - CORS_ORIGINS=${CRYPTO_CORS_ORIGINS:-*} - EXCHANGE_API_KEY=${CRYPTO_EXCHANGE_API_KEY:-} - EXCHANGE_API_SECRET=${CRYPTO_EXCHANGE_API_SECRET:-} - EXCHANGE_SANDBOX=${CRYPTO_EXCHANGE_SANDBOX:-false} - MAX_DAILY_LOSS=${CRYPTO_MAX_DAILY_LOSS:-0} - MAX_DRAWDOWN_PCT=${CRYPTO_MAX_DRAWDOWN_PCT:-0} - MAX_POSITION_PCT=${CRYPTO_MAX_POSITION_PCT:-1.0} - MAX_TRADES_PER_DAY=${CRYPTO_MAX_TRADES_PER_DAY:-0} - TELEGRAM_BOT_TOKEN=${CRYPTO_TELEGRAM_BOT_TOKEN:-} - TELEGRAM_CHAT_ID=${CRYPTO_TELEGRAM_CHAT_ID:-} - NOTIFY_WEBHOOK_URL=${CRYPTO_NOTIFY_WEBHOOK_URL:-} - NOTIFY_LEVELS=${CRYPTO_NOTIFY_LEVELS:-trade,error} - NOTIFY_LIFECYCLE=${CRYPTO_NOTIFY_LIFECYCLE:-true} - SUMMARY_INTERVAL_MINUTES=${CRYPTO_SUMMARY_INTERVAL_MINUTES:-0} - LIVE_CONFIRMATION=${CRYPTO_LIVE_CONFIRMATION:-} - MIN_LIVE_BALANCE=${CRYPTO_MIN_LIVE_BALANCE:-100} - API_TOKEN=${CRYPTO_API_TOKEN:-} - API_TOKEN_HEADER=${CRYPTO_API_TOKEN_HEADER:-X-API-Token} volumes: # Persist the SQLite database across releases. Prefer an absolute host # path (CRYPTO_DATA_DIR / PERSISTENT_MOUNT); the repo-relative default # can be cleaned by runner maintenance. - ${CRYPTO_DATA_DIR:-./data}:/data healthcheck: test: ["CMD-SHELL", "python -c \"import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/api/health', timeout=5).status==200 else 1)\""] interval: 30s timeout: 10s retries: 3 start_period: 20s networks: - internal frontend: build: context: ./frontend args: BUILD_VERSION: ${CRYPTO_BUILD_VERSION:-unknown} image: crypto_trading-frontend container_name: crypto-trading-frontend restart: unless-stopped mem_limit: ${CRYPTO_MEM_LIMIT:-1g} cpus: ${CRYPTO_CPUS:-2} logging: driver: json-file options: max-size: "10m" max-file: "5" security_opt: - no-new-privileges:true depends_on: backend: condition: service_healthy environment: - API_TOKEN=${CRYPTO_API_TOKEN:-} - API_TOKEN_HEADER=${CRYPTO_API_TOKEN_HEADER:-X-API-Token} healthcheck: test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:80/ || exit 1"] interval: 30s timeout: 5s retries: 3 start_period: 10s networks: - internal - docker_default labels: - "traefik.enable=true" - "traefik.docker.network=docker_default" - "traefik.http.routers.crypto-trading.rule=Host(`crypto.${BASE_DOMAIN:-matitos.org}`)" - "traefik.http.routers.crypto-trading.entrypoints=websecure" - "traefik.http.routers.crypto-trading.tls.certresolver=myresolvercd" - "traefik.http.services.crypto-trading.loadbalancer.server.port=80" # Basic auth on the WebUI (htpasswd), required: the stack refuses to # start when CRYPTO_FRONTEND_AUTH_USERS is unset (no admin/admin # fallback). A per-source rate limit reduces credential brute-forcing. - "traefik.http.routers.crypto-trading.middlewares=crypto-trading-auth@docker,crypto-trading-sec@docker,crypto-trading-authlimit@docker" - "traefik.http.middlewares.crypto-trading-auth.basicauth.users=${CRYPTO_FRONTEND_AUTH_USERS:?CRYPTO_FRONTEND_AUTH_USERS is required (htpasswd string, e.g. htpasswd -nbB user pass)}" # Browsers do not reliably replay Basic-auth credentials on a WebSocket # upgrade, which left the live feed stuck "reconnecting". Give /api/ws its # own higher-priority router that skips Basic auth so the feed connects. # If CRYPTO_API_TOKEN is set, the backend still requires it (nginx injects # it server-side); if it is unset, /api/ws is as open as the rest of /api. - "traefik.http.routers.crypto-trading-ws.rule=Host(`crypto.${BASE_DOMAIN:-matitos.org}`) && PathPrefix(`/api/ws`)" - "traefik.http.routers.crypto-trading-ws.entrypoints=websecure" - "traefik.http.routers.crypto-trading-ws.tls.certresolver=myresolvercd" - "traefik.http.routers.crypto-trading-ws.priority=100" - "traefik.http.routers.crypto-trading-ws.middlewares=crypto-trading-sec@docker,crypto-trading-authlimit@docker" - "traefik.http.routers.crypto-trading-ws.service=crypto-trading" - "traefik.http.middlewares.crypto-trading-authlimit.ratelimit.average=${CRYPTO_AUTH_RATE_LIMIT_AVERAGE:-10}" - "traefik.http.middlewares.crypto-trading-authlimit.ratelimit.burst=${CRYPTO_AUTH_RATE_LIMIT_BURST:-5}" - "traefik.http.middlewares.crypto-trading-sec.headers.stsSeconds=31536000" - "traefik.http.middlewares.crypto-trading-sec.headers.stsIncludeSubdomains=true" - "traefik.http.middlewares.crypto-trading-sec.headers.forceSTSHeader=true" - "traefik.http.middlewares.crypto-trading-sec.headers.contentTypeNosniff=true" - "traefik.http.middlewares.crypto-trading-sec.headers.browserXssFilter=true" - "traefik.http.middlewares.crypto-trading-sec.headers.frameDeny=true" - "traefik.http.middlewares.crypto-trading-sec.headers.referrerPolicy=strict-origin-when-cross-origin" networks: internal: docker_default: external: true