name: opengym services: # Passkey auth + per-user data (Node, no framework). # image: prebuilt (docker compose pull). build: from source (docker compose up --build). api: image: ghcr.io/duartesantos8/opengym-api:latest # Default target: no AI runtime, no AI dependency — the image every instance has always had. # To opt into the AI Coach, build the other target instead: # API_TARGET=coach docker compose up --build api # `--build` matters: with an `image:` also set, compose would otherwise pull the prebuilt # default rather than build the coach one. build: context: ./api target: ${API_TARGET:-default} restart: unless-stopped env_file: .env environment: # The port the API listens on. web proxies to this same value, so the two # cannot drift — change it in .env and both follow. - PORT=${PORT:-3000} - DATA_DIR=/data # The API is reachable only through the web container here, and that container overwrites # X-Forwarded-For / X-Real-IP with the real peer — so the sign-in throttle may count # requests by those headers instead of lumping everyone in as "the web container". - TRUST_PROXY=${TRUST_PROXY:-1} volumes: - ./data:/data # users, passkeys, per-user state, session secret — BACK THIS UP # (and uploads/: the photos and videos of custom exercises) # Provider credential cache (Codex). A sibling of ./data on purpose: the documented backup # is `tar czf … data/`, and a live refresh token must not end up inside it. Nothing here # needs backing up — if you lose it, sign the provider in again. - ./coach-auth:/coach-auth # Builds the React frontend (multi-stage) and serves it, proxying /api → api # and the exercise animations, which are part of the image. Single origin for passkeys. web: image: ghcr.io/duartesantos8/opengym-web:latest build: context: . dockerfile: web/Dockerfile restart: unless-stopped depends_on: api: condition: service_started ports: - "${WEB_PORT:-8080}:${NGINX_PORT:-80}" # nginx renders its config at start-up from these (web/nginx.conf.template), # so the API service does not have to be called `api`. Defaults are the old # hardcoded values, so an existing .env and an existing stack are unaffected. environment: - NGINX_PORT=${NGINX_PORT:-80} - BACKEND=${BACKEND:-api} - PORT=${PORT:-3000} # Docker's embedded DNS. Only ever needs changing on a runtime that is not Docker. - RESOLVER=${RESOLVER:-127.0.0.11} # Empty drops the CF-Connecting-IP header, so a direct client cannot forge the address # the activity log records. Cloudflare users set CF_CONNECTING_IP='$http_cf_connecting_ip' # in .env, single quotes included, or Compose expands it to an empty string. - CF_CONNECTING_IP=${CF_CONNECTING_IP:-} # Subpath deployments behind a proxy that passes its prefix through ("/gym", no trailing # slash). Empty serves the app at the site root, which is what it has always done; a proxy # that strips the prefix needs nothing set (docs/SELF_HOSTING.md §5). - BASE_PATH=${BASE_PATH:-} # The largest photo or video upload nginx passes to the API. Keep it above # MEDIA_VIDEO_MAX_MB (40 by default), which the api service reads from .env. - MEDIA_UPLOAD_MAX=${MEDIA_UPLOAD_MAX:-48m}