services: postgres: image: postgres:16-alpine environment: POSTGRES_DB: production POSTGRES_USER: production # No fallback default — a missing .env value must stop the stack, not # silently start Postgres with a well-known password. POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set in .env} volumes: - pg_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U production"] interval: 5s timeout: 5s retries: 5 restart: unless-stopped ports: # Bound to TAILSCALE_IP (or loopback if unset), never 0.0.0.0 — useful # if you replicate/back up this database to another host over a # private network (e.g. Tailscale) instead of exposing Postgres # publicly. Safe to ignore if you don't need that; it just falls back # to 127.0.0.1. - "${TAILSCALE_IP:-127.0.0.1}:5433:5432" minio: image: minio/minio command: server /data --console-address ":9001" environment: # No fallback defaults — see POSTGRES_PASSWORD above for why. MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:?MINIO_ACCESS_KEY must be set in .env} MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:?MINIO_SECRET_KEY must be set in .env} volumes: - minio_data:/data healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] interval: 10s timeout: 5s retries: 5 restart: unless-stopped ports: # Same Tailscale-only binding as postgres above — useful for MinIO # bucket replication/backups to another host over a private network. - "${TAILSCALE_IP:-127.0.0.1}:9002:9000" backend: # Built locally from source by default so anyone can `docker compose up # --build` without needing access to a private image registry. If you # run this in production at scale, you may prefer to build images in CI # and push them to your own registry instead (see # .gitea/workflows/build-release.yml for an example of that pattern) and # swap this `build:` block for `image: your-registry/production-backend:latest`. build: context: ./backend dockerfile: Dockerfile env_file: .env # Optional deploy-agent self-update integration (see # deploy-agent/README.md) is NOT wired up by default — it needs a bind # mount to a Unix socket that doesn't exist on a fresh install, which # would otherwise fail `docker compose up` for everyone. If you want the # "check/apply update" button in Settings → Обновления, install # deploy-agent per its README first, then add: # volumes: # - ./deploy-agent/agent.sock:/run/deploy-agent/agent.sock:ro # here yourself. ports: # loopback-only — put a reverse proxy (Caddy, nginx, etc.) in front # for public access. - "127.0.0.1:18091:3000" networks: default: {} platform: aliases: - production depends_on: postgres: condition: service_healthy minio: condition: service_healthy restart: unless-stopped # Self-hosted Telegram Bot API server (internal/notify) — backend talks to # it over the docker network (TG_API_BASE_URL) instead of # api.telegram.org directly. This is optional: requires # TELEGRAM_API_ID/TELEGRAM_API_HASH from my.telegram.org (one-time manual # registration, needs a phone number) or it exits immediately on start; # restart: "no" so a missing registration doesn't crash-loop this into the # logs forever. backend has no depends_on for this service — # notify.Handler fails soft when it's down or unreachable, same as an # invalid token would. If you don't need Telegram notifications, leave # this service stopped. telegram-bot-api: image: aiogram/telegram-bot-api:latest environment: TELEGRAM_API_ID: ${TELEGRAM_API_ID:-} TELEGRAM_API_HASH: ${TELEGRAM_API_HASH:-} TELEGRAM_LOCAL: "1" volumes: - telegram_bot_api_data:/var/lib/telegram-bot-api restart: unless-stopped web: # Built locally from source, same reasoning as backend above. Vite bakes # VITE_CORE_URL/VITE_PRODUCTION_URL into the JS bundle at build time (the # browser's own URLs for core/production, not the container-network # addresses) — wired here from WEB_CORE_URL/WEB_PRODUCTION_URL in .env. # If you build/push images in CI instead (see .gitea/workflows/ # build-release.yml), pass the same two build args there and swap this # for `image: your-registry/production-web:latest`. build: context: ./web dockerfile: Dockerfile args: VITE_CORE_URL: ${WEB_CORE_URL} VITE_PRODUCTION_URL: ${WEB_PRODUCTION_URL} ports: - "127.0.0.1:18092:80" restart: unless-stopped volumes: pg_data: minio_data: telegram_bot_api_data: networks: # Shared with the companion "core" auth service (and any other modules # you run alongside this one) — lets this module reach core's # /api/modules/:name/heartbeat by the "core" alias. Create it once: # `docker network create platform_net`. platform: name: platform_net external: true