# Full stack. `docker compose up --build` brings everything up; only docker is needed. # # Edge (Traefik) → / → frontend (landing: presentation vs demo) # /slides → slides (Slidev presentation) # /api → api (load-balanced across replicas) # /mock → mock-smtp (third-party email; demo inbox) # # Stateless by design: no volumes, so every `docker compose up` is a clean start. # Host ports default to random (set *_PORT in .env to pin). Find them: docker compose ps. # API + worker run multiple replicas by default to showcase horizontal scaling. name: dailymotion-registration # Shared definition for the roles that run our single app image (api/worker/mock). x-app: &app build: context: . dockerfile: docker/Dockerfile target: runtime image: dailymotion-registration-app:local env_file: .env restart: unless-stopped services: # ───────────────────────── edge ───────────────────────── traefik: # File provider only (no Docker socket); config baked in via a tiny inline build. build: context: . dockerfile_inline: | FROM traefik:v3.7.4 COPY docker/traefik/traefik.yml /etc/traefik/traefik.yml COPY docker/traefik/dynamic.yml /etc/traefik/dynamic.yml image: dailymotion-registration-traefik:local ports: - "${HTTP_PORT:-0}:80" # web entrypoint (random host port unless pinned in .env) - "${TRAEFIK_DASHBOARD_PORT:-0}:8080" # dashboard (dev only) depends_on: api: condition: service_healthy restart: unless-stopped # ──────────────────────── backing services ─────────────────────── postgres: # Schema is baked into the image and applied by Postgres' initdb on first start # (no volumes → every `up` is a fresh DB → schema is always applied). A tiny # inline build keeps the demo to a single step; see docs/03-data-model.md for the # numbered-migrations + runner strategy we'd use in production. build: context: . dockerfile_inline: | FROM postgres:18.4-alpine COPY docker/postgres/initdb/ /docker-entrypoint-initdb.d/ image: dailymotion-registration-postgres:local env_file: .env # provides POSTGRES_USER / PASSWORD / DB healthcheck: test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] interval: 5s timeout: 5s retries: 10 start_period: 10s restart: unless-stopped redis: image: redis/redis-stack-server:7.4.0-v8 # RedisJSON + RediSearch for redis-om healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 3s retries: 10 restart: unless-stopped rabbitmq: image: rabbitmq:4.3.1-management env_file: .env # provides RABBITMQ_DEFAULT_USER / PASS ports: - "${RABBITMQ_UI_PORT:-0}:15672" # management UI healthcheck: test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"] interval: 10s timeout: 10s retries: 10 start_period: 20s restart: unless-stopped # ──────────────────────── inspection UIs ─────────────────────── adminer: image: adminer:5.4.2 ports: ["${ADMINER_PORT:-0}:8080"] environment: ADMINER_DEFAULT_SERVER: postgres depends_on: postgres: { condition: service_healthy } restart: unless-stopped redisinsight: image: redis/redisinsight:3.4.2 ports: ["${REDISINSIGHT_PORT:-0}:5540"] environment: # Pre-add our Redis so RedisInsight opens straight to it (no manual "Add database"). RI_ACCEPT_TERMS_AND_CONDITIONS: "true" RI_REDIS_HOST: redis RI_REDIS_PORT: "6379" RI_REDIS_ALIAS: dailymotion-redis depends_on: redis: { condition: service_healthy } restart: unless-stopped # ──────────────────────── application ─────────────────────── # The single app image runs every role; its default CMD is the API (proxy-aware), # so only the worker / mock-smtp roles need a `command`. Schema is applied by # Postgres itself (initdb), so there is no migration step to gate on. api: <<: *app expose: ["8000"] deploy: replicas: ${API_REPLICAS:-3} # horizontal scale, load-balanced by Traefik depends_on: postgres: { condition: service_healthy } redis: { condition: service_healthy } rabbitmq: { condition: service_healthy } healthcheck: test: ["CMD", "python", "-m", "app.entrypoints.healthcheck", "http://localhost:8000/health/ready"] interval: 10s timeout: 5s retries: 10 start_period: 20s worker: <<: *app command: python -m app.entrypoints.worker_main deploy: replicas: ${WORKER_REPLICAS:-2} # competing consumers on the email queue depends_on: postgres: { condition: service_healthy } rabbitmq: { condition: service_healthy } mock-smtp: { condition: service_healthy } mock-smtp: <<: *app command: uvicorn mock_smtp.server:app --host 0.0.0.0 --port 8000 expose: ["8000"] healthcheck: test: ["CMD", "python", "-m", "app.entrypoints.healthcheck", "http://localhost:8000/health"] interval: 10s timeout: 5s retries: 5 start_period: 10s # ──────────────────────── web ─────────────────────── # Landing page (chooser) + interactive demo. Tiny image → inline build. frontend: # Two-stage: fetch the OPAQUE browser client (wire-compatible with opaquepy), then # serve it + the static UI from nginx. The password never leaves the browser. build: context: . dockerfile_inline: | FROM node:22.22.3-alpine AS opaque RUN cd /tmp && npm pack @tiptenbrink/opaquewasm@0.6.0 && tar xzf *.tgz FROM nginx:1.31.1-alpine COPY docker/nginx/nginx.conf /etc/nginx/conf.d/default.conf COPY frontend/ /usr/share/nginx/html/ COPY --from=opaque /tmp/package/opaquewasm.js /tmp/package/opaquewasm_bg.wasm /usr/share/nginx/html/vendor/opaquewasm/ image: dailymotion-registration-frontend:local depends_on: api: { condition: service_healthy } restart: unless-stopped # Slidev presentation, built to static HTML (see docker/slides.Dockerfile). slides: build: context: . dockerfile: docker/slides.Dockerfile image: dailymotion-registration-slides:local restart: unless-stopped