# Docker Compose stack — generated by Ansible # Do not edit manually; re-run ansible-playbook deploy.yml networks: # proxy — публичная сеть только для Traefik: нужна для исходящего интернет-доступа # (ACME Let's Encrypt, внешние сервисы). backend — internal: true, поэтому # сервисы не имеют прямого исходящего доступа в интернет. proxy: driver: bridge backend: driver: bridge internal: true forgejo-db: driver: bridge internal: true forgejo-ssh: driver: bridge plane-internal: driver: bridge internal: true runner-jobs: driver: bridge volumes: vaultwarden_data: forgejo_data: forgejo_db_data: plane_pgdata: plane_redis_data: plane_minio_data: plane_media: syncthing_config: syncthing_data: act_runner_data: services: # ── Traefik ──────────────────────────────────────────────────────────────── # proxy — для ACME (исходящий интернет), backend — для маршрутизации к сервисам traefik: image: {{ traefik_image }} container_name: traefik restart: unless-stopped ports: - "80:80" - "443:443" networks: - proxy - backend volumes: - {{ services_root }}/traefik/traefik.yml:/etc/traefik/traefik.yml:ro - {{ services_root }}/traefik/dynamic:/etc/traefik/dynamic:ro - {{ services_root }}/traefik/acme.json:/acme/acme.json labels: - "traefik.enable=true" - "traefik.http.routers.traefik-dashboard.rule=Host(`{{ domain_traefik }}`)" - "traefik.http.routers.traefik-dashboard.entrypoints=websecure" - "traefik.http.routers.traefik-dashboard.tls.certresolver=letsencrypt" - "traefik.http.routers.traefik-dashboard.service=api@internal" - "traefik.http.routers.traefik-dashboard.middlewares=traefik-auth" - "traefik.http.middlewares.traefik-auth.basicauth.users={{ traefik_dashboard_htpasswd }}" # ── Vaultwarden ──────────────────────────────────────────────────────────── vaultwarden: image: {{ vaultwarden_image }} container_name: vaultwarden restart: unless-stopped networks: - backend volumes: - vaultwarden_data:/data environment: - ADMIN_TOKEN=${VAULTWARDEN_ADMIN_TOKEN} - DOMAIN=https://{{ domain_vault }} - SIGNUPS_ALLOWED=false - INVITATIONS_ALLOWED=true - LOG_LEVEL=warn - EXTENDED_LOGGING=true - TZ=UTC labels: - "traefik.enable=true" - "traefik.http.routers.vaultwarden.rule=Host(`{{ domain_vault }}`)" - "traefik.http.routers.vaultwarden.entrypoints=websecure" - "traefik.http.routers.vaultwarden.tls.certresolver=letsencrypt" - "traefik.http.services.vaultwarden.loadbalancer.server.port=80" # ── Forgejo ──────────────────────────────────────────────────────────────── forgejo: image: {{ forgejo_image }} container_name: forgejo restart: unless-stopped depends_on: forgejo-db: condition: service_healthy networks: - backend - forgejo-db - forgejo-ssh volumes: - forgejo_data:/data - /etc/timezone:/etc/timezone:ro - /etc/localtime:/etc/localtime:ro environment: - USER_UID=1000 - USER_GID=1000 - FORGEJO__database__DB_TYPE=postgres - FORGEJO__database__HOST=forgejo-db:5432 - FORGEJO__database__NAME=forgejo - FORGEJO__database__USER=forgejo - FORGEJO__database__PASSWD=${FORGEJO_DB_PASSWORD} - FORGEJO__server__DOMAIN={{ domain_git }} - FORGEJO__server__ROOT_URL=https://{{ domain_git }} - FORGEJO__server__SSH_DOMAIN={{ domain_git }} - FORGEJO__server__SSH_PORT=2222 - FORGEJO__service__DISABLE_REGISTRATION=true ports: - "2222:22" labels: - "traefik.enable=true" - "traefik.http.routers.forgejo.rule=Host(`{{ domain_git }}`)" - "traefik.http.routers.forgejo.entrypoints=websecure" - "traefik.http.routers.forgejo.tls.certresolver=letsencrypt" - "traefik.http.services.forgejo.loadbalancer.server.port=3000" forgejo-db: image: {{ forgejo_db_image }} container_name: forgejo-db restart: unless-stopped networks: - forgejo-db volumes: - forgejo_db_data:/var/lib/postgresql/data environment: - POSTGRES_USER=forgejo - POSTGRES_PASSWORD=${FORGEJO_DB_PASSWORD} - POSTGRES_DB=forgejo - PGDATA=/var/lib/postgresql/data/pgdata healthcheck: test: ["CMD-SHELL", "pg_isready -U forgejo"] interval: 10s timeout: 5s retries: 5 mem_limit: 512m # ── Plane ────────────────────────────────────────────────────────────────── # Маршрутизация через Traefik: # /api/* и /auth/* → plane-api:8000 (Django, на backend + plane-internal) # остальное → plane-web:3000 (Next.js, на backend + plane-internal) # Правило с PathPrefix длиннее → более высокий приоритет у Traefik автоматически. plane-web: image: {{ plane_frontend_image }} container_name: plane-web restart: unless-stopped command: node web/server.js depends_on: - plane-api networks: - backend - plane-internal environment: - NEXT_PUBLIC_API_BASE_URL=https://{{ domain_plane }} labels: - "traefik.enable=true" - "traefik.http.routers.plane.rule=Host(`{{ domain_plane }}`)" - "traefik.http.routers.plane.entrypoints=websecure" - "traefik.http.routers.plane.tls.certresolver=letsencrypt" - "traefik.http.services.plane.loadbalancer.server.port=3000" plane-api: image: {{ plane_backend_image }} container_name: plane-api restart: unless-stopped mem_limit: 512m command: ./bin/docker-entrypoint-api.sh depends_on: plane-db: condition: service_healthy plane-redis: condition: service_started plane-minio: condition: service_healthy networks: - backend - plane-internal volumes: - plane_media:/app/media environment: - DATABASE_URL=postgresql://plane:${PLANE_DB_PASSWORD}@plane-db:5432/plane - REDIS_URL=redis://plane-redis:6379/ - SECRET_KEY=${PLANE_SECRET_KEY} - DEBUG=0 - DJANGO_SETTINGS_MODULE=plane.settings.production - WEB_URL=https://{{ domain_plane }} - FILE_SIZE_LIMIT=5242880 - USE_MINIO=1 - AWS_REGION=us-east-1 - AWS_ACCESS_KEY_ID=plane-minio - AWS_SECRET_ACCESS_KEY=${PLANE_MINIO_PASSWORD} - AWS_S3_ENDPOINT_URL=http://plane-minio:9000 - AWS_S3_BUCKET_NAME=uploads - MINIO_ROOT_USER=plane-minio - MINIO_ROOT_PASSWORD=${PLANE_MINIO_PASSWORD} labels: - "traefik.enable=true" - "traefik.http.routers.plane-api.rule=Host(`{{ domain_plane }}`) && (PathPrefix(`/api/`) || PathPrefix(`/auth/`))" - "traefik.http.routers.plane-api.entrypoints=websecure" - "traefik.http.routers.plane-api.tls.certresolver=letsencrypt" - "traefik.http.services.plane-api.loadbalancer.server.port=8000" plane-worker: image: {{ plane_backend_image }} container_name: plane-worker restart: unless-stopped command: ./bin/docker-entrypoint-worker.sh depends_on: - plane-api networks: - plane-internal volumes: - plane_media:/app/media environment: - DATABASE_URL=postgresql://plane:${PLANE_DB_PASSWORD}@plane-db:5432/plane - REDIS_URL=redis://plane-redis:6379/ - SECRET_KEY=${PLANE_SECRET_KEY} - DEBUG=0 - DJANGO_SETTINGS_MODULE=plane.settings.production - USE_MINIO=1 - AWS_REGION=us-east-1 - AWS_ACCESS_KEY_ID=plane-minio - AWS_SECRET_ACCESS_KEY=${PLANE_MINIO_PASSWORD} - AWS_S3_ENDPOINT_URL=http://plane-minio:9000 - AWS_S3_BUCKET_NAME=uploads - MINIO_ROOT_USER=plane-minio - MINIO_ROOT_PASSWORD=${PLANE_MINIO_PASSWORD} plane-beat: image: {{ plane_backend_image }} container_name: plane-beat restart: unless-stopped command: ./bin/docker-entrypoint-beat.sh depends_on: - plane-api networks: - plane-internal environment: - DATABASE_URL=postgresql://plane:${PLANE_DB_PASSWORD}@plane-db:5432/plane - REDIS_URL=redis://plane-redis:6379/ - SECRET_KEY=${PLANE_SECRET_KEY} - DEBUG=0 - DJANGO_SETTINGS_MODULE=plane.settings.production - USE_MINIO=1 - AWS_REGION=us-east-1 - AWS_ACCESS_KEY_ID=plane-minio - AWS_SECRET_ACCESS_KEY=${PLANE_MINIO_PASSWORD} - AWS_S3_ENDPOINT_URL=http://plane-minio:9000 - AWS_S3_BUCKET_NAME=uploads - MINIO_ROOT_USER=plane-minio - MINIO_ROOT_PASSWORD=${PLANE_MINIO_PASSWORD} plane-db: image: {{ plane_db_image }} container_name: plane-db restart: unless-stopped mem_limit: 512m networks: - plane-internal volumes: - plane_pgdata:/var/lib/postgresql/data environment: - POSTGRES_USER=plane - POSTGRES_PASSWORD=${PLANE_DB_PASSWORD} - POSTGRES_DB=plane - PGDATA=/var/lib/postgresql/data/pgdata healthcheck: test: ["CMD-SHELL", "pg_isready -U plane"] interval: 10s timeout: 5s retries: 5 plane-redis: image: {{ plane_redis_image }} container_name: plane-redis restart: unless-stopped networks: - plane-internal volumes: - plane_redis_data:/data command: redis-server --appendonly yes plane-minio: image: {{ plane_minio_image }} container_name: plane-minio restart: unless-stopped mem_limit: 512m networks: - plane-internal volumes: - plane_minio_data:/data environment: - MINIO_ROOT_USER=plane-minio - MINIO_ROOT_PASSWORD=${PLANE_MINIO_PASSWORD} command: server /data --console-address ":9001" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] interval: 30s timeout: 20s retries: 3 # ── Syncthing ────────────────────────────────────────────────────────────── # Порты 22000 и 21027 нужны для синхронизации между устройствами (не только UI). # backend — internal: true, но Syncthing на published ports выходит наружу через host. syncthing: image: {{ syncthing_image }} container_name: syncthing restart: unless-stopped networks: - backend ports: - "22000:22000/tcp" - "22000:22000/udp" - "21027:21027/udp" volumes: - syncthing_config:/var/syncthing/config - syncthing_data:/var/syncthing/data environment: - PUID=1000 - PGID=1000 - TZ=UTC labels: - "traefik.enable=true" - "traefik.http.routers.syncthing.rule=Host(`{{ domain_sync }}`)" - "traefik.http.routers.syncthing.entrypoints=websecure" - "traefik.http.routers.syncthing.tls.certresolver=letsencrypt" - "traefik.http.routers.syncthing.middlewares=syncthing-auth" - "traefik.http.middlewares.syncthing-auth.basicauth.users={{ syncthing_basic_auth_htpasswd }}" - "traefik.http.services.syncthing.loadbalancer.server.port=8384" # ── Forgejo Actions Runner ───────────────────────────────────────────────── # backend — для связи с Forgejo по внутренней сети (http://forgejo:3000) # runner-jobs — сеть с интернет-доступом для job-контейнеров act_runner: image: {{ act_runner_image }} container_name: act_runner restart: unless-stopped depends_on: - forgejo environment: - GITEA_INSTANCE_URL=http://forgejo:3000 - GITEA_RUNNER_REGISTRATION_TOKEN=${FORGEJO_RUNNER_TOKEN} - GITEA_RUNNER_NAME=vps-runner - CONFIG_FILE=/data/config.yaml volumes: - act_runner_data:/data - /var/run/docker.sock:/var/run/docker.sock - {{ services_root }}/act_runner/config.yaml:/data/config.yaml:ro networks: - backend - runner-jobs