name: opengym services: # One-time: download the exercise images + GIFs into ./media (skipped if already there). # ~140 MB, from github.com/hasaneyldrm/exercises-dataset. That dataset is MIT for the # metadata and instruction text, but the images and GIFs are © Gym visual # (https://gymvisual.com/) and are used under the dataset's terms — openGym neither # redistributes nor relicenses them. See NOTICE.md. media: image: alpine/git volumes: - ./media/img:/out/img - ./media/gif:/out/gif entrypoint: ["/bin/sh", "-c"] command: - | if [ -z "$$(ls -A /out/img 2>/dev/null)" ]; then echo "↓ Downloading exercise media (~140 MB, one time)…" echo " Source: github.com/hasaneyldrm/exercises-dataset" echo " Metadata and instruction text: MIT." echo " Images and animations: © Gym visual — https://gymvisual.com/" echo " They are used under that dataset's terms, not openGym's AGPL, and are" echo " downloaded from upstream — openGym does not redistribute them." echo " Terms: https://gymvisual.com/content/3-terms-and-conditions-of-use" echo " Reusing this media yourself, commercially or not, needs your own licence" echo " from Gym visual. Details in NOTICE.md." git clone --depth 1 https://github.com/hasaneyldrm/exercises-dataset /tmp/ds cp /tmp/ds/images/*.jpg /out/img/ && cp /tmp/ds/videos/*.gif /out/gif/ echo "✓ Exercise media ready ($$(ls /out/img | wc -l) images)." else echo "✓ Exercise media already present — skipping download." fi restart: "no" # 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 volumes: - ./data:/data # users, passkeys, per-user state, session secret — BACK THIS UP # 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 media from the shared volume. Single origin for passkeys. web: image: ghcr.io/duartesantos8/opengym-web:latest build: context: . dockerfile: web/Dockerfile restart: unless-stopped depends_on: media: condition: service_completed_successfully 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. - 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:-} volumes: - ./media/img:/usr/share/nginx/html/img:ro - ./media/gif:/usr/share/nginx/html/gif:ro