name: opengym services: # One-time: download the exercise images + GIFs into ./media (skipped if already there). # ~140 MB, from github.com/hasaneyldrm/exercises-dataset (CC). 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)…" 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 build: ./api restart: unless-stopped env_file: .env environment: - PORT=3000 # internal port (nginx proxies here) — do not change - DATA_DIR=/data volumes: - ./data:/data # users, passkeys, per-user state, session secret — BACK THIS UP # 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}:80" volumes: - ./media/img:/usr/share/nginx/html/img:ro - ./media/gif:/usr/share/nginx/html/gif:ro