name: garabato services: frontend: image: ghcr.io/nonetss/garabato-frontend:main container_name: garabato-frontend init: true # Reached only through the gateway, which publishes the site expose: - "4321" environment: BACKEND_URL: http://backend:3000 LOKI_URL: http://loki:3100 depends_on: backend: condition: service_healthy env_file: .env healthcheck: test: [ "CMD", "bun", "-e", "fetch('http://localhost:4321/login').then((r) => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))" ] interval: 10s timeout: 5s retries: 5 start_period: 15s restart: unless-stopped networks: - garabato backend: image: ghcr.io/nonetss/garabato-backend:main container_name: garabato-backend init: true expose: - "3000" environment: NODE_ENV: production DATABASE_URL: ${DATABASE_URL} # Better Auth's base URL is the public URL: OAuth callbacks are built # from it. It reuses CORS_ORIGIN from .env so both always match. BETTER_AUTH_URL: ${CORS_ORIGIN} BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET} # CORS_ORIGIN (the public URL) comes from .env. `environment` # overrides `env_file`. LOKI_URL: http://loki:3100 env_file: .env depends_on: db: condition: service_healthy healthcheck: test: [ "CMD", "bun", "-e", "fetch('http://localhost:3000/').then((r) => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))" ] interval: 10s timeout: 5s retries: 5 start_period: 15s restart: unless-stopped networks: - garabato # Gateway (apps/gateway/Caddyfile): the only published service. Its :80 # site routes /rpc, /api, /scalar and /openapi.json to the backend and the # rest to the frontend. gateway: image: ghcr.io/nonetss/garabato-gateway:main container_name: garabato-gateway ports: # Host port comes from .env (scripts/bootstrap.sh prompts for it); the # default keeps `docker compose up` working with no FRONTEND_PORT set. - "${FRONTEND_PORT:-4444}:80" depends_on: frontend: condition: service_healthy healthcheck: test: [ "CMD", "wget", "-q", "--spider", "http://localhost/health" ] interval: 10s timeout: 5s retries: 5 start_period: 5s restart: unless-stopped networks: - garabato # Optional: backend/frontend ship structured logs here when LOKI_URL is # set, but neither depends on it at startup — the app works without it. loki: image: grafana/loki:3.7.7 container_name: garabato-loki expose: - "3100" volumes: - loki_data:/loki command: - -config.file=/etc/loki/local-config.yaml - -validation.allow-structured-metadata=true - -store.retention=720h - -compactor.working-directory=/loki/compactor - -compactor.delete-request-store=filesystem - -compactor.retention-enabled=true healthcheck: test: [ "CMD", "wget", "--spider", "-q", "http://localhost:3100/ready" ] interval: 10s timeout: 5s retries: 5 start_period: 10s restart: unless-stopped networks: - garabato db: image: postgres:17 container_name: garabato-db environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: stack volumes: - db_data:/var/lib/postgresql/data healthcheck: test: [ "CMD-SHELL", "pg_isready -U postgres -d stack" ] interval: 10s timeout: 5s retries: 5 start_period: 10s restart: unless-stopped networks: - garabato # Bundled object store, started only with the `minio` profile # (COMPOSE_PROFILES=minio in .env, written by bootstrap.sh). Without it the # backend uses the external S3-compatible store set in .env. Nothing # depends on it at startup, so both setups start the same way. # # Document store: pgsty/silo, a MinIO build (its entrypoint maps `server` # to the `silo` binary). Its root credentials are the backend's S3 # credentials from .env. It publishes no port: the S3 API stays on the # Docker network, and MinIO is administered with `mc` inside it # (`docker exec garabato-minio mc ...`). # # The container creates S3_BUCKET itself (Bun's S3 client cannot create # buckets): it starts the server in the background, runs an idempotent # `mc mb` once it answers, then marks itself ready for the healthcheck. # The trap forwards compose's stop signal to the server. minio: image: pgsty/silo:latest container_name: garabato-minio profiles: - minio entrypoint: [ "sh", "-c" ] command: - | rm -f /tmp/minio-bucket-ready /usr/bin/docker-entrypoint.sh server /data --console-address :9001 & pid=$$! trap 'kill -TERM "$$pid"; wait "$$pid"; exit $$?' TERM INT until mc alias set local http://127.0.0.1:9000 "$$MINIO_ROOT_USER" "$$MINIO_ROOT_PASSWORD" >/dev/null 2>&1 && mc mb --ignore-existing "local/$$S3_BUCKET"; do kill -0 "$$pid" 2>/dev/null || exit 1 sleep 1 done touch /tmp/minio-bucket-ready wait "$$pid" environment: MINIO_ROOT_USER: ${S3_ACCESS_KEY_ID} MINIO_ROOT_PASSWORD: ${S3_SECRET_ACCESS_KEY} S3_BUCKET: ${S3_BUCKET} expose: - "9000" volumes: - minio_data:/data # Healthy only once the server is live and the bucket exists. healthcheck: test: [ "CMD-SHELL", "test -f /tmp/minio-bucket-ready && curl -fsS -o /dev/null http://localhost:9000/minio/health/live" ] interval: 10s timeout: 5s retries: 5 start_period: 10s restart: unless-stopped networks: - garabato volumes: db_data: loki_data: minio_data: networks: garabato: driver: bridge