New Plane stable requires 3 frontend services: - plane-admin (nginx:80) for /god-mode/ routes - plane-space (node:3000) for /spaces/ routes - plane-web (nginx:80) for all other routes Also add APP/ADMIN/SPACE_BASE_URL env vars to plane-api so the setup wizard knows where to redirect. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
400 lines
14 KiB
Django/Jinja
400 lines
14 KiB
Django/Jinja
# 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
|
||
depends_on:
|
||
- plane-api
|
||
networks:
|
||
- backend
|
||
- plane-internal
|
||
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=80"
|
||
- "traefik.http.routers.plane.priority=1"
|
||
|
||
plane-admin:
|
||
image: {{ plane_admin_image }}
|
||
container_name: plane-admin
|
||
restart: unless-stopped
|
||
depends_on:
|
||
- plane-api
|
||
- plane-web
|
||
networks:
|
||
- backend
|
||
- plane-internal
|
||
labels:
|
||
- "traefik.enable=true"
|
||
- "traefik.http.routers.plane-admin.rule=Host(`{{ domain_plane }}`) && PathPrefix(`/god-mode/`)"
|
||
- "traefik.http.routers.plane-admin.entrypoints=websecure"
|
||
- "traefik.http.routers.plane-admin.tls.certresolver=letsencrypt"
|
||
- "traefik.http.services.plane-admin.loadbalancer.server.port=80"
|
||
- "traefik.http.routers.plane-admin.priority=10"
|
||
|
||
plane-space:
|
||
image: {{ plane_space_image }}
|
||
container_name: plane-space
|
||
restart: unless-stopped
|
||
depends_on:
|
||
- plane-api
|
||
- plane-web
|
||
networks:
|
||
- backend
|
||
- plane-internal
|
||
labels:
|
||
- "traefik.enable=true"
|
||
- "traefik.http.routers.plane-space.rule=Host(`{{ domain_plane }}`) && PathPrefix(`/spaces/`)"
|
||
- "traefik.http.routers.plane-space.entrypoints=websecure"
|
||
- "traefik.http.routers.plane-space.tls.certresolver=letsencrypt"
|
||
- "traefik.http.services.plane-space.loadbalancer.server.port=3000"
|
||
- "traefik.http.routers.plane-space.priority=10"
|
||
|
||
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/
|
||
- AMQP_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}
|
||
- GUNICORN_WORKERS=2
|
||
- APP_BASE_URL=https://{{ domain_plane }}
|
||
- ADMIN_BASE_URL=https://{{ domain_plane }}/god-mode
|
||
- SPACE_BASE_URL=https://{{ domain_plane }}/spaces
|
||
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/
|
||
- AMQP_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/
|
||
- AMQP_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=https://{{ domain_git }}
|
||
- 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
|