Initial commit
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
a1b97f3e4b
28 changed files with 1220 additions and 0 deletions
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
inventory/group_vars/all/vault.yml
|
||||
.vault-password-file
|
||||
*.retry
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.DS_Store
|
||||
.ansible_cache/
|
||||
61
CLAUDE.md
Normal file
61
CLAUDE.md
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Prerequisites (once, on operator machine)
|
||||
ansible-galaxy collection install community.general community.docker ansible.posix
|
||||
echo "yourpassword" > ~/.vault-password-file && chmod 600 ~/.vault-password-file
|
||||
|
||||
# First-time server setup (run as root)
|
||||
ansible-playbook playbooks/bootstrap.yml -u root
|
||||
|
||||
# Idempotent deploy (all subsequent runs)
|
||||
ansible-playbook playbooks/deploy.yml
|
||||
|
||||
# Edit secrets
|
||||
ansible-vault edit inventory/group_vars/all.vault.yml
|
||||
|
||||
# Check syntax without connecting
|
||||
ansible-playbook playbooks/deploy.yml --syntax-check
|
||||
|
||||
# Dry run
|
||||
ansible-playbook playbooks/deploy.yml --check
|
||||
|
||||
# Run only specific role
|
||||
ansible-playbook playbooks/deploy.yml --tags base
|
||||
ansible-playbook playbooks/deploy.yml --tags docker
|
||||
ansible-playbook playbooks/deploy.yml --tags services
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
**Traffic flow:** Internet → Traefik (ports 80/443, TLS via Let's Encrypt ACME) → services. Ports 80 and 443 are open on the server.
|
||||
|
||||
**Secrets:** All secrets live in `inventory/group_vars/all.vault.yml` (Ansible Vault, AES-256). The file `all.yml` references them via `"{{ vault_* }}"` aliases. The vault password must exist at `~/.vault-password-file` on the operator machine — this path is in `.gitignore` and never committed.
|
||||
|
||||
**Roles:**
|
||||
- `base` — OS hardening: UFW (allow SSH + 80 + 443), fail2ban, sshd config, deploy user
|
||||
- `docker` — Docker CE + Compose plugin via official apt repo
|
||||
- `services` — renders Jinja2 templates → `/opt/services/`, then runs `docker compose up`
|
||||
|
||||
**Templates → server files:**
|
||||
- `roles/services/templates/docker-compose.yml.j2` → `/opt/services/docker-compose.yml`
|
||||
- `roles/services/templates/env.j2` → `/opt/services/.env` (mode 0600)
|
||||
- `roles/services/templates/traefik/traefik.yml.j2` → `/opt/services/traefik/traefik.yml`
|
||||
- `acme.json` created at `/opt/services/traefik/acme.json` (mode 0600, mounted into Traefik)
|
||||
|
||||
**Docker networks:**
|
||||
- `backend` (internal) — traefik ↔ user-facing services
|
||||
- `forgejo-db` (internal) — forgejo ↔ its postgres
|
||||
- `plane-internal` (internal) — all plane components (api, worker, beat, db, redis, minio)
|
||||
|
||||
**Adding a new service:** add container to `docker-compose.yml.j2` on the `backend` network with `traefik.enable=true` and `traefik.http.routers.X.tls.certresolver=letsencrypt` labels, add its domain variable to `all.yml`.
|
||||
|
||||
## Deployment
|
||||
|
||||
DNS: add A-records for each subdomain → server IP (or wildcard `*` → IP).
|
||||
|
||||
Fill `all.vault.yml` → set `domain_base` in `all.yml` → run bootstrap + deploy. Traefik obtains TLS certificates automatically on first request to each domain.
|
||||
168
README.md
Normal file
168
README.md
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
# Infra
|
||||
|
||||
Ansible + Docker инфраструктура для команды. Все сервисы доступны через HTTPS — трафик принимается напрямую на портах 80/443, TLS-сертификаты выдаются автоматически через Let's Encrypt.
|
||||
|
||||
**Сервисы:**
|
||||
- `vault.csrx.ru` — Vaultwarden (менеджер паролей)
|
||||
- `git.csrx.ru` — Forgejo (Git)
|
||||
- `plane.csrx.ru` — Plane (управление проектами)
|
||||
- `sync.csrx.ru` — Syncthing (синхронизация Obsidian)
|
||||
- `traefik.csrx.ru` — Traefik dashboard
|
||||
|
||||
---
|
||||
|
||||
## Что нужно перед запуском
|
||||
|
||||
### 1. На машине оператора
|
||||
|
||||
```bash
|
||||
# Ansible
|
||||
pip install ansible
|
||||
|
||||
# Коллекции
|
||||
ansible-galaxy collection install community.general community.docker ansible.posix
|
||||
|
||||
# SSH-ключ (если нет)
|
||||
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Шаг 1 — DNS
|
||||
|
||||
Добавить A-записи у DNS-провайдера: каждый субдомен → `87.249.49.32`.
|
||||
|
||||
Или wildcard (если провайдер поддерживает): `*` → `87.249.49.32`.
|
||||
|
||||
| Запись | Значение |
|
||||
|--------|----------|
|
||||
| `vault.csrx.ru` | `87.249.49.32` |
|
||||
| `git.csrx.ru` | `87.249.49.32` |
|
||||
| `plane.csrx.ru` | `87.249.49.32` |
|
||||
| `sync.csrx.ru` | `87.249.49.32` |
|
||||
| `traefik.csrx.ru` | `87.249.49.32` |
|
||||
|
||||
---
|
||||
|
||||
## Шаг 2 — Заполнить секреты
|
||||
|
||||
Отредактировать `inventory/group_vars/all.vault.yml`:
|
||||
|
||||
```yaml
|
||||
vault_acme_email: "you@example.com" # email для Let's Encrypt уведомлений
|
||||
|
||||
vault_vaultwarden_admin_token: "..." # придумать длинный пароль
|
||||
|
||||
vault_forgejo_db_password: "..." # придумать пароль для PostgreSQL
|
||||
vault_plane_db_password: "..." # придумать пароль для PostgreSQL
|
||||
vault_plane_secret_key: "..." # сгенерировать: openssl rand -hex 32
|
||||
vault_plane_minio_password: "..." # придумать пароль для MinIO
|
||||
|
||||
# Генерировать командой: htpasswd -nb admin 'yourpassword'
|
||||
# Знак $ нужно удваивать: $apr1$ → $$apr1$
|
||||
vault_traefik_dashboard_htpasswd: "admin:$$apr1$$..."
|
||||
vault_syncthing_basic_auth_htpasswd: "admin:$$apr1$$..."
|
||||
```
|
||||
|
||||
Сгенерировать нужные значения:
|
||||
|
||||
```bash
|
||||
# plane_secret_key
|
||||
openssl rand -hex 32
|
||||
|
||||
# htpasswd (нужен apache2-utils или httpd-tools)
|
||||
htpasswd -nb admin 'yourpassword'
|
||||
# macOS без установки:
|
||||
python3 -c "import crypt; print('admin:' + crypt.crypt('yourpassword', crypt.mksalt(crypt.METHOD_MD5)))"
|
||||
```
|
||||
|
||||
Затем зашифровать файл:
|
||||
|
||||
```bash
|
||||
# Создать файл с паролем vault
|
||||
echo "придумать-пароль-для-vault" > ~/.vault-password-file
|
||||
chmod 600 ~/.vault-password-file
|
||||
|
||||
# Зашифровать
|
||||
ansible-vault encrypt inventory/group_vars/all.vault.yml
|
||||
```
|
||||
|
||||
> `~/.vault-password-file` — только на машине оператора, никогда не коммитить.
|
||||
|
||||
---
|
||||
|
||||
## Шаг 3 — Указать домен
|
||||
|
||||
В `inventory/group_vars/all.yml` установить:
|
||||
|
||||
```yaml
|
||||
domain_base: "csrx.ru" # уже стоит, изменить если нужно
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Шаг 4 — Первый запуск (от root)
|
||||
|
||||
```bash
|
||||
# Создаёт пользователя deploy, устанавливает sudo
|
||||
ansible-playbook playbooks/bootstrap.yml -u root
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Шаг 5 — Деплой
|
||||
|
||||
```bash
|
||||
ansible-playbook playbooks/deploy.yml
|
||||
```
|
||||
|
||||
Устанавливает Docker, настраивает UFW/fail2ban (открывает 22, 80, 443), поднимает все контейнеры.
|
||||
Traefik автоматически получит TLS-сертификаты при первом обращении к каждому домену.
|
||||
|
||||
---
|
||||
|
||||
## Проверка
|
||||
|
||||
```bash
|
||||
# На сервере
|
||||
ssh deploy@87.249.49.32
|
||||
docker compose -f /opt/services/docker-compose.yml ps
|
||||
```
|
||||
|
||||
Все сервисы должны быть в статусе `Up`. Затем открыть в браузере:
|
||||
|
||||
- `https://vault.csrx.ru` — Vaultwarden
|
||||
- `https://git.csrx.ru` — Forgejo (первичная настройка через веб)
|
||||
- `https://plane.csrx.ru` — Plane
|
||||
- `https://sync.csrx.ru` — Syncthing (логин/пароль из `syncthing_basic_auth_htpasswd`)
|
||||
- `https://traefik.csrx.ru` — Traefik dashboard (логин/пароль из `traefik_dashboard_htpasswd`)
|
||||
|
||||
---
|
||||
|
||||
## Первичная настройка сервисов
|
||||
|
||||
### Vaultwarden
|
||||
- Открыть `https://vault.csrx.ru/admin` → ввести `vault_vaultwarden_admin_token`
|
||||
- Создать пользователей через admin-панель (регистрация отключена)
|
||||
|
||||
### Forgejo
|
||||
- Открыть `https://git.csrx.ru` → пройти wizard установки
|
||||
- Первый зарегистрированный пользователь становится администратором
|
||||
|
||||
### Plane
|
||||
- Открыть `https://plane.csrx.ru` → создать workspace
|
||||
|
||||
### Syncthing
|
||||
- Открыть `https://sync.csrx.ru`
|
||||
- Скопировать Device ID сервера
|
||||
- На каждом устройстве команды: добавить сервер как remote device, расшарить папку Obsidian vault
|
||||
|
||||
---
|
||||
|
||||
## Обновление
|
||||
|
||||
```bash
|
||||
ansible-playbook playbooks/deploy.yml
|
||||
```
|
||||
|
||||
Идемпотентно — можно запускать сколько угодно раз.
|
||||
24
ansible.cfg
Normal file
24
ansible.cfg
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
[defaults]
|
||||
timeout = 60
|
||||
inventory = inventory/hosts.ini
|
||||
roles_path = roles
|
||||
vault_password_file = ~/.vault-password-file
|
||||
remote_user = deploy
|
||||
private_key_file = ~/.ssh/id_ed25519
|
||||
host_key_checking = True
|
||||
deprecation_warnings = False
|
||||
stdout_callback = default
|
||||
result_format = yaml
|
||||
callbacks_enabled = profile_tasks
|
||||
fact_caching = jsonfile
|
||||
fact_caching_connection = .ansible_cache
|
||||
fact_caching_timeout = 3600
|
||||
|
||||
[ssh_connection]
|
||||
retries = 5
|
||||
ssh_args = -o ServerAliveInterval=30 -o ServerAliveCountMax=10 -o ConnectTimeout=15
|
||||
|
||||
[privilege_escalation]
|
||||
become = true
|
||||
become_method = sudo
|
||||
become_user = root
|
||||
23
dns-zone.zone
Normal file
23
dns-zone.zone
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
$ORIGIN csrx.ru.
|
||||
$TTL 3600
|
||||
|
||||
; ── A-записи сервисов ────────────────────────────────────────────────────────
|
||||
vault IN A 87.249.49.32
|
||||
git IN A 87.249.49.32
|
||||
plane IN A 87.249.49.32
|
||||
sync IN A 87.249.49.32
|
||||
traefik IN A 87.249.49.32
|
||||
mail IN A 87.249.49.32
|
||||
|
||||
; ── Почта ────────────────────────────────────────────────────────────────────
|
||||
@ IN MX 10 mail.csrx.ru.
|
||||
|
||||
; SPF — разрешаем отправку только с нашего mail-сервера
|
||||
@ IN TXT "v=spf1 mx ~all"
|
||||
|
||||
; DMARC — мониторинг без блокировки (p=none), отчёты на admin@csrx.ru
|
||||
_dmarc IN TXT "v=DMARC1; p=none; rua=mailto:admin@csrx.ru"
|
||||
|
||||
; DKIM — добавить после первого запуска Stalwart (взять ключ из mail.csrx.ru → DKIM)
|
||||
; Пример как будет выглядеть:
|
||||
; mail._domainkey IN TXT "v=DKIM1; k=rsa; p=<ключ из Stalwart>"
|
||||
25
inventory/group_vars/all/main.yml
Normal file
25
inventory/group_vars/all/main.yml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
# Non-secret variables
|
||||
domain_base: "csrx.ru"
|
||||
|
||||
# Derived domains
|
||||
domain_vault: "vault.{{ domain_base }}"
|
||||
domain_git: "git.{{ domain_base }}"
|
||||
domain_plane: "plane.{{ domain_base }}"
|
||||
domain_sync: "sync.{{ domain_base }}"
|
||||
domain_traefik: "traefik.{{ domain_base }}"
|
||||
|
||||
# Service paths
|
||||
services_root: /opt/services
|
||||
deploy_user: deploy
|
||||
deploy_group: deploy
|
||||
|
||||
# Secrets (from vault)
|
||||
acme_email: "{{ vault_acme_email }}"
|
||||
vaultwarden_admin_token: "{{ vault_vaultwarden_admin_token }}"
|
||||
forgejo_db_password: "{{ vault_forgejo_db_password }}"
|
||||
plane_db_password: "{{ vault_plane_db_password }}"
|
||||
plane_secret_key: "{{ vault_plane_secret_key }}"
|
||||
plane_minio_password: "{{ vault_plane_minio_password }}"
|
||||
traefik_dashboard_htpasswd: "{{ vault_traefik_dashboard_htpasswd }}"
|
||||
syncthing_basic_auth_htpasswd: "{{ vault_syncthing_basic_auth_htpasswd }}"
|
||||
5
inventory/hosts.ini
Normal file
5
inventory/hosts.ini
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
[servers]
|
||||
main ansible_host=87.249.49.32
|
||||
|
||||
[servers:vars]
|
||||
ansible_python_interpreter=/usr/bin/python3
|
||||
51
playbooks/bootstrap.yml
Normal file
51
playbooks/bootstrap.yml
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
---
|
||||
# First-run playbook executed as root before deploy user exists
|
||||
# ansible-playbook playbooks/bootstrap.yml -u root
|
||||
- name: Bootstrap server
|
||||
hosts: servers
|
||||
become: false
|
||||
remote_user: root
|
||||
|
||||
tasks:
|
||||
- name: Update apt cache
|
||||
ansible.builtin.apt:
|
||||
update_cache: true
|
||||
cache_valid_time: 3600
|
||||
|
||||
- name: Install essential packages
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- python3
|
||||
- python3-pip
|
||||
- sudo
|
||||
- curl
|
||||
- git
|
||||
state: present
|
||||
|
||||
- name: Create deploy group
|
||||
ansible.builtin.group:
|
||||
name: deploy
|
||||
state: present
|
||||
|
||||
- name: Create deploy user
|
||||
ansible.builtin.user:
|
||||
name: deploy
|
||||
group: deploy
|
||||
groups: sudo
|
||||
shell: /bin/bash
|
||||
create_home: true
|
||||
state: present
|
||||
|
||||
- name: Set up authorized keys for deploy user
|
||||
ansible.posix.authorized_key:
|
||||
user: deploy
|
||||
state: present
|
||||
key: "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}"
|
||||
|
||||
- name: Allow deploy user passwordless sudo
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/sudoers.d/deploy
|
||||
line: "deploy ALL=(ALL) NOPASSWD:ALL"
|
||||
create: true
|
||||
mode: "0440"
|
||||
validate: "visudo -cf %s"
|
||||
12
playbooks/deploy.yml
Normal file
12
playbooks/deploy.yml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
# Idempotent deploy playbook
|
||||
# ansible-playbook playbooks/deploy.yml
|
||||
- name: Deploy all services
|
||||
hosts: servers
|
||||
roles:
|
||||
- role: base
|
||||
tags: base
|
||||
- role: docker
|
||||
tags: docker
|
||||
- role: services
|
||||
tags: services
|
||||
10
playbooks/site.yml
Normal file
10
playbooks/site.yml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
# Master playbook — for reference only.
|
||||
# Do NOT run this directly: bootstrap.yml requires `-u root`,
|
||||
# deploy.yml runs as the deploy user. Run them separately:
|
||||
#
|
||||
# ansible-playbook playbooks/bootstrap.yml -u root # first time only
|
||||
# ansible-playbook playbooks/deploy.yml # all subsequent runs
|
||||
#
|
||||
# - import_playbook: bootstrap.yml
|
||||
# - import_playbook: deploy.yml
|
||||
24
roles/base/defaults/main.yml
Normal file
24
roles/base/defaults/main.yml
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
# SSH hardening
|
||||
sshd_port: 22
|
||||
sshd_permit_root_login: "no"
|
||||
sshd_password_authentication: "no"
|
||||
sshd_pubkey_authentication: "yes"
|
||||
sshd_x11_forwarding: "no"
|
||||
sshd_max_auth_tries: 3
|
||||
sshd_client_alive_interval: 300
|
||||
sshd_client_alive_count_max: 2
|
||||
|
||||
# Packages to install
|
||||
base_packages:
|
||||
- ufw
|
||||
- fail2ban
|
||||
- curl
|
||||
- wget
|
||||
- git
|
||||
- htop
|
||||
- vim
|
||||
- unzip
|
||||
- ca-certificates
|
||||
- gnupg
|
||||
- lsb-release
|
||||
10
roles/base/handlers/main.yml
Normal file
10
roles/base/handlers/main.yml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
- name: Restart sshd
|
||||
ansible.builtin.systemd:
|
||||
name: sshd
|
||||
state: restarted
|
||||
|
||||
- name: Restart fail2ban
|
||||
ansible.builtin.systemd:
|
||||
name: fail2ban
|
||||
state: restarted
|
||||
79
roles/base/tasks/firewall.yml
Normal file
79
roles/base/tasks/firewall.yml
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
---
|
||||
- name: Allow SSH
|
||||
community.general.ufw:
|
||||
rule: allow
|
||||
port: "{{ sshd_port }}"
|
||||
proto: tcp
|
||||
comment: "SSH"
|
||||
|
||||
- name: Allow HTTP
|
||||
community.general.ufw:
|
||||
rule: allow
|
||||
port: "80"
|
||||
proto: tcp
|
||||
comment: "HTTP (ACME challenge)"
|
||||
|
||||
- name: Allow HTTPS
|
||||
community.general.ufw:
|
||||
rule: allow
|
||||
port: "443"
|
||||
proto: tcp
|
||||
comment: "HTTPS"
|
||||
|
||||
- name: Allow Syncthing sync TCP
|
||||
community.general.ufw:
|
||||
rule: allow
|
||||
port: "22000"
|
||||
proto: tcp
|
||||
comment: "Syncthing sync"
|
||||
|
||||
- name: Allow Syncthing sync UDP
|
||||
community.general.ufw:
|
||||
rule: allow
|
||||
port: "22000"
|
||||
proto: udp
|
||||
comment: "Syncthing sync"
|
||||
|
||||
- name: Allow Syncthing discovery UDP
|
||||
community.general.ufw:
|
||||
rule: allow
|
||||
port: "21027"
|
||||
proto: udp
|
||||
comment: "Syncthing discovery"
|
||||
|
||||
- name: Set UFW default deny incoming
|
||||
community.general.ufw:
|
||||
direction: incoming
|
||||
policy: deny
|
||||
|
||||
- name: Set UFW default allow outgoing
|
||||
community.general.ufw:
|
||||
direction: outgoing
|
||||
policy: allow
|
||||
|
||||
- name: Enable UFW
|
||||
community.general.ufw:
|
||||
state: enabled
|
||||
|
||||
- name: Ensure fail2ban is configured for SSH
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/fail2ban/jail.local
|
||||
content: |
|
||||
[DEFAULT]
|
||||
bantime = 3600
|
||||
findtime = 600
|
||||
maxretry = 5
|
||||
|
||||
[sshd]
|
||||
enabled = true
|
||||
port = {{ sshd_port }}
|
||||
logpath = %(sshd_log)s
|
||||
backend = %(sshd_backend)s
|
||||
mode: "0644"
|
||||
notify: Restart fail2ban
|
||||
|
||||
- name: Ensure fail2ban is started and enabled
|
||||
ansible.builtin.systemd:
|
||||
name: fail2ban
|
||||
state: started
|
||||
enabled: true
|
||||
5
roles/base/tasks/main.yml
Normal file
5
roles/base/tasks/main.yml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
- import_tasks: packages.yml
|
||||
- import_tasks: users.yml
|
||||
- import_tasks: sshd.yml
|
||||
- import_tasks: firewall.yml
|
||||
18
roles/base/tasks/packages.yml
Normal file
18
roles/base/tasks/packages.yml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
- name: Update apt cache
|
||||
ansible.builtin.apt:
|
||||
update_cache: true
|
||||
cache_valid_time: 3600
|
||||
retries: 3
|
||||
delay: 10
|
||||
register: apt_cache
|
||||
until: apt_cache is succeeded
|
||||
|
||||
- name: Install base packages
|
||||
ansible.builtin.apt:
|
||||
name: "{{ base_packages }}"
|
||||
state: present
|
||||
retries: 3
|
||||
delay: 10
|
||||
register: apt_packages
|
||||
until: apt_packages is succeeded
|
||||
10
roles/base/tasks/sshd.yml
Normal file
10
roles/base/tasks/sshd.yml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
- name: Configure SSH daemon
|
||||
ansible.builtin.template:
|
||||
src: sshd_config.j2
|
||||
dest: /etc/ssh/sshd_config
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
validate: /usr/sbin/sshd -t -f %s
|
||||
notify: Restart sshd
|
||||
22
roles/base/tasks/users.yml
Normal file
22
roles/base/tasks/users.yml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
- name: Ensure deploy group exists
|
||||
ansible.builtin.group:
|
||||
name: "{{ deploy_group }}"
|
||||
state: present
|
||||
|
||||
- name: Ensure deploy user exists
|
||||
ansible.builtin.user:
|
||||
name: "{{ deploy_user }}"
|
||||
group: "{{ deploy_group }}"
|
||||
groups: sudo
|
||||
shell: /bin/bash
|
||||
create_home: true
|
||||
state: present
|
||||
|
||||
- name: Ensure deploy user has passwordless sudo
|
||||
ansible.builtin.lineinfile:
|
||||
path: "/etc/sudoers.d/{{ deploy_user }}"
|
||||
line: "{{ deploy_user }} ALL=(ALL) NOPASSWD:ALL"
|
||||
create: true
|
||||
mode: "0440"
|
||||
validate: "visudo -cf %s"
|
||||
33
roles/base/templates/sshd_config.j2
Normal file
33
roles/base/templates/sshd_config.j2
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# Managed by Ansible — do not edit manually
|
||||
|
||||
Port {{ sshd_port }}
|
||||
AddressFamily inet
|
||||
ListenAddress 0.0.0.0
|
||||
|
||||
# Authentication
|
||||
PermitRootLogin {{ sshd_permit_root_login }}
|
||||
PasswordAuthentication {{ sshd_password_authentication }}
|
||||
PubkeyAuthentication {{ sshd_pubkey_authentication }}
|
||||
AuthorizedKeysFile .ssh/authorized_keys
|
||||
PermitEmptyPasswords no
|
||||
ChallengeResponseAuthentication no
|
||||
UsePAM yes
|
||||
|
||||
# Forwarding
|
||||
AllowAgentForwarding no
|
||||
AllowTcpForwarding no
|
||||
X11Forwarding {{ sshd_x11_forwarding }}
|
||||
PrintMotd no
|
||||
|
||||
# Timeouts and limits
|
||||
LoginGraceTime 30
|
||||
MaxAuthTries {{ sshd_max_auth_tries }}
|
||||
MaxSessions 5
|
||||
ClientAliveInterval {{ sshd_client_alive_interval }}
|
||||
ClientAliveCountMax {{ sshd_client_alive_count_max }}
|
||||
|
||||
# Subsystems
|
||||
Subsystem sftp /usr/lib/openssh/sftp-server
|
||||
|
||||
# Only allow the deploy user
|
||||
AllowUsers {{ deploy_user }}
|
||||
5
roles/docker/handlers/main.yml
Normal file
5
roles/docker/handlers/main.yml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
- name: Restart Docker
|
||||
ansible.builtin.systemd:
|
||||
name: docker
|
||||
state: restarted
|
||||
81
roles/docker/tasks/main.yml
Normal file
81
roles/docker/tasks/main.yml
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
---
|
||||
- name: Remove old Docker versions
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- docker
|
||||
- docker-engine
|
||||
- docker.io
|
||||
- containerd
|
||||
- runc
|
||||
state: absent
|
||||
purge: true
|
||||
|
||||
- name: Create keyrings directory
|
||||
ansible.builtin.file:
|
||||
path: /etc/apt/keyrings
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Add Docker GPG key
|
||||
ansible.builtin.get_url:
|
||||
url: https://download.docker.com/linux/ubuntu/gpg
|
||||
dest: /etc/apt/keyrings/docker.asc
|
||||
mode: "0644"
|
||||
retries: 5
|
||||
delay: 10
|
||||
register: gpg_key
|
||||
until: gpg_key is succeeded
|
||||
|
||||
- name: Add Docker repository
|
||||
ansible.builtin.apt_repository:
|
||||
repo: >-
|
||||
deb [arch={{ ansible_facts['architecture'] | replace('x86_64', 'amd64') }}
|
||||
signed-by=/etc/apt/keyrings/docker.asc]
|
||||
https://download.docker.com/linux/ubuntu
|
||||
{{ ansible_facts['distribution_release'] }} stable
|
||||
filename: docker
|
||||
state: present
|
||||
retries: 3
|
||||
delay: 10
|
||||
register: docker_repo
|
||||
until: docker_repo is succeeded
|
||||
|
||||
- name: Install Docker Engine and Compose plugin
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- docker-ce
|
||||
- docker-ce-cli
|
||||
- containerd.io
|
||||
- docker-buildx-plugin
|
||||
- docker-compose-plugin
|
||||
state: present
|
||||
update_cache: true
|
||||
retries: 3
|
||||
delay: 10
|
||||
register: docker_install
|
||||
until: docker_install is succeeded
|
||||
notify: Restart Docker
|
||||
|
||||
- name: Configure Docker daemon (registry mirrors)
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/docker/daemon.json
|
||||
content: |
|
||||
{
|
||||
"registry-mirrors": [
|
||||
"https://dockerhub.timeweb.cloud"
|
||||
]
|
||||
}
|
||||
mode: "0644"
|
||||
notify: Restart Docker
|
||||
|
||||
- name: Ensure Docker is started and enabled
|
||||
ansible.builtin.systemd:
|
||||
name: docker
|
||||
state: started
|
||||
enabled: true
|
||||
|
||||
- name: Add deploy user to docker group
|
||||
ansible.builtin.user:
|
||||
name: "{{ deploy_user }}"
|
||||
groups: docker
|
||||
append: true
|
||||
19
roles/services/defaults/main.yml
Normal file
19
roles/services/defaults/main.yml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
services_root: /opt/services
|
||||
|
||||
# Image versions
|
||||
# IMPORTANT: pin each image to a specific version tag.
|
||||
# Check Docker Hub for the latest stable release before updating.
|
||||
traefik_image: "traefik:v3.3" # https://hub.docker.com/_/traefik/tags
|
||||
vaultwarden_image: "vaultwarden/server:1.32.7" # https://hub.docker.com/r/vaultwarden/server/tags
|
||||
forgejo_image: "codeberg.org/forgejo/forgejo:9"
|
||||
forgejo_db_image: "postgres:16-alpine"
|
||||
plane_frontend_image: "makeplane/plane-frontend:stable" # https://hub.docker.com/r/makeplane/plane-frontend/tags
|
||||
plane_backend_image: "makeplane/plane-backend:stable" # https://hub.docker.com/r/makeplane/plane-backend/tags
|
||||
plane_db_image: "postgres:16-alpine"
|
||||
plane_redis_image: "redis:7-alpine"
|
||||
# ВАЖНО: MinIO прекратил публикацию образов на Docker Hub с октября 2025.
|
||||
# Последний стабильный тег на Docker Hub: RELEASE.2025-04-22T22-12-26Z
|
||||
# Рекомендуется перейти на alpine/minio или собирать из исходников.
|
||||
plane_minio_image: "minio/minio:RELEASE.2025-04-22T22-12-26Z" # https://hub.docker.com/r/minio/minio/tags
|
||||
syncthing_image: "syncthing/syncthing:1.27" # https://hub.docker.com/r/syncthing/syncthing/tags
|
||||
10
roles/services/handlers/main.yml
Normal file
10
roles/services/handlers/main.yml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
- name: Restart stack
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ services_root }}"
|
||||
state: present
|
||||
pull: never
|
||||
|
||||
- name: Stack deployed
|
||||
ansible.builtin.debug:
|
||||
msg: "Stack deployed/updated successfully"
|
||||
37
roles/services/tasks/configs.yml
Normal file
37
roles/services/tasks/configs.yml
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
- name: Deploy .env file
|
||||
ansible.builtin.template:
|
||||
src: env.j2
|
||||
dest: "{{ services_root }}/.env"
|
||||
owner: "{{ deploy_user }}"
|
||||
group: "{{ deploy_group }}"
|
||||
mode: "0600"
|
||||
notify: Restart stack
|
||||
|
||||
- name: Deploy docker-compose.yml
|
||||
ansible.builtin.template:
|
||||
src: docker-compose.yml.j2
|
||||
dest: "{{ services_root }}/docker-compose.yml"
|
||||
owner: "{{ deploy_user }}"
|
||||
group: "{{ deploy_group }}"
|
||||
mode: "0644"
|
||||
notify: Restart stack
|
||||
|
||||
- name: Deploy Traefik static config
|
||||
ansible.builtin.template:
|
||||
src: traefik/traefik.yml.j2
|
||||
dest: "{{ services_root }}/traefik/traefik.yml"
|
||||
owner: "{{ deploy_user }}"
|
||||
group: "{{ deploy_group }}"
|
||||
mode: "0644"
|
||||
notify: Restart stack
|
||||
|
||||
- name: Create acme.json for Let's Encrypt certificates
|
||||
ansible.builtin.file:
|
||||
path: "{{ services_root }}/traefik/acme.json"
|
||||
state: touch
|
||||
owner: "{{ deploy_user }}"
|
||||
group: "{{ deploy_group }}"
|
||||
mode: "0600"
|
||||
modification_time: preserve
|
||||
access_time: preserve
|
||||
26
roles/services/tasks/directories.yml
Normal file
26
roles/services/tasks/directories.yml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
- name: Create services root directory
|
||||
ansible.builtin.file:
|
||||
path: "{{ services_root }}"
|
||||
state: directory
|
||||
owner: "{{ deploy_user }}"
|
||||
group: "{{ deploy_group }}"
|
||||
mode: "0755"
|
||||
|
||||
- name: Create service subdirectories
|
||||
ansible.builtin.file:
|
||||
path: "{{ services_root }}/{{ item }}"
|
||||
state: directory
|
||||
owner: "{{ deploy_user }}"
|
||||
group: "{{ deploy_group }}"
|
||||
mode: "0755"
|
||||
loop:
|
||||
- traefik
|
||||
- traefik/dynamic
|
||||
- vaultwarden/data
|
||||
- forgejo/data
|
||||
- forgejo/db
|
||||
- plane/pgdata
|
||||
- plane/media
|
||||
- syncthing/config
|
||||
- syncthing/data
|
||||
67
roles/services/tasks/main.yml
Normal file
67
roles/services/tasks/main.yml
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
---
|
||||
- import_tasks: directories.yml
|
||||
- import_tasks: configs.yml
|
||||
|
||||
- name: Pull Docker images one by one
|
||||
ansible.builtin.command: docker pull {{ item }}
|
||||
loop:
|
||||
- "{{ traefik_image }}"
|
||||
- "{{ vaultwarden_image }}"
|
||||
- "{{ forgejo_image }}"
|
||||
- "{{ forgejo_db_image }}"
|
||||
- "{{ plane_frontend_image }}"
|
||||
- "{{ plane_backend_image }}"
|
||||
- "{{ plane_db_image }}"
|
||||
- "{{ plane_redis_image }}"
|
||||
- "{{ plane_minio_image }}"
|
||||
- "{{ syncthing_image }}"
|
||||
register: pull_result
|
||||
changed_when: "'Status: Downloaded newer image' in pull_result.stdout"
|
||||
retries: 5
|
||||
delay: 30
|
||||
until: pull_result.rc == 0
|
||||
|
||||
- name: Deploy Docker Compose stack
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ services_root }}"
|
||||
state: present
|
||||
pull: never
|
||||
retries: 3
|
||||
delay: 15
|
||||
register: compose_result
|
||||
until: compose_result is succeeded
|
||||
notify: Stack deployed
|
||||
|
||||
- name: Wait for MinIO to be ready
|
||||
ansible.builtin.command: docker exec plane-minio curl -sf http://localhost:9000/minio/health/live
|
||||
register: minio_ready
|
||||
changed_when: false
|
||||
retries: 15
|
||||
delay: 10
|
||||
until: minio_ready.rc == 0
|
||||
|
||||
- name: Get plane-internal network name
|
||||
ansible.builtin.shell: >
|
||||
docker inspect plane-minio |
|
||||
python3 -c "import sys,json; d=json.load(sys.stdin)[0];
|
||||
print([k for k in d['NetworkSettings']['Networks'] if 'plane-internal' in k][0])"
|
||||
register: plane_internal_network
|
||||
changed_when: false
|
||||
|
||||
- name: Create MinIO uploads bucket via mc container
|
||||
# minio/mc entrypoint = mc, поэтому нужен --entrypoint sh
|
||||
# access-key = имя пользователя MinIO (plane-minio), secret-key = пароль
|
||||
ansible.builtin.shell: |
|
||||
docker run --rm \
|
||||
--entrypoint sh \
|
||||
--network "{{ plane_internal_network.stdout | trim }}" \
|
||||
-e MC_ACCESS="{{ plane_minio_password }}" \
|
||||
minio/mc:RELEASE.2025-05-21T01-59-54Z \
|
||||
-c 'mc alias set local http://plane-minio:9000 plane-minio "{{ plane_minio_password }}" 2>/dev/null \
|
||||
&& mc mb --ignore-existing local/uploads \
|
||||
&& echo "Bucket created or already exists"'
|
||||
register: minio_bucket
|
||||
changed_when: "'Bucket created' in minio_bucket.stdout"
|
||||
retries: 5
|
||||
delay: 10
|
||||
until: minio_bucket.rc == 0
|
||||
331
roles/services/templates/docker-compose.yml.j2
Normal file
331
roles/services/templates/docker-compose.yml.j2
Normal file
|
|
@ -0,0 +1,331 @@
|
|||
# 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
|
||||
plane-internal:
|
||||
driver: bridge
|
||||
internal: true
|
||||
|
||||
volumes:
|
||||
vaultwarden_data:
|
||||
forgejo_data:
|
||||
forgejo_db_data:
|
||||
plane_pgdata:
|
||||
plane_redis_data:
|
||||
plane_minio_data:
|
||||
plane_media:
|
||||
syncthing_config:
|
||||
syncthing_data:
|
||||
|
||||
services:
|
||||
|
||||
# ── Traefik ────────────────────────────────────────────────────────────────
|
||||
# proxy — для ACME (исходящий интернет), backend — для маршрутизации к сервисам
|
||||
traefik:
|
||||
image: {{ traefik_image }}
|
||||
container_name: traefik
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
environment:
|
||||
- DOCKER_API_VERSION=1.45
|
||||
networks:
|
||||
- proxy
|
||||
- backend
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- {{ 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
|
||||
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__service__DISABLE_REGISTRATION=true
|
||||
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"
|
||||
12
roles/services/templates/env.j2
Normal file
12
roles/services/templates/env.j2
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# Generated by Ansible — do not edit manually
|
||||
VAULTWARDEN_ADMIN_TOKEN={{ vaultwarden_admin_token }}
|
||||
FORGEJO_DB_PASSWORD={{ forgejo_db_password }}
|
||||
PLANE_DB_PASSWORD={{ plane_db_password }}
|
||||
PLANE_SECRET_KEY={{ plane_secret_key }}
|
||||
PLANE_MINIO_PASSWORD={{ plane_minio_password }}
|
||||
DOMAIN_BASE={{ domain_base }}
|
||||
DOMAIN_VAULT={{ domain_vault }}
|
||||
DOMAIN_GIT={{ domain_git }}
|
||||
DOMAIN_PLANE={{ domain_plane }}
|
||||
DOMAIN_SYNC={{ domain_sync }}
|
||||
DOMAIN_TRAEFIK={{ domain_traefik }}
|
||||
45
roles/services/templates/traefik/traefik.yml.j2
Normal file
45
roles/services/templates/traefik/traefik.yml.j2
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# Traefik v3 static configuration
|
||||
# Generated by Ansible
|
||||
|
||||
global:
|
||||
checkNewVersion: false
|
||||
sendAnonymousUsage: false
|
||||
|
||||
log:
|
||||
level: INFO
|
||||
|
||||
accessLog: {}
|
||||
|
||||
api:
|
||||
dashboard: true
|
||||
insecure: false
|
||||
|
||||
entryPoints:
|
||||
web:
|
||||
address: ":80"
|
||||
http:
|
||||
redirections:
|
||||
entryPoint:
|
||||
to: websecure
|
||||
scheme: https
|
||||
websecure:
|
||||
address: ":443"
|
||||
|
||||
certificatesResolvers:
|
||||
letsencrypt:
|
||||
acme:
|
||||
email: "{{ acme_email }}"
|
||||
storage: /acme/acme.json
|
||||
httpChallenge:
|
||||
entryPoint: web
|
||||
|
||||
providers:
|
||||
docker:
|
||||
exposedByDefault: false
|
||||
network: backend
|
||||
file:
|
||||
directory: /etc/traefik/dynamic
|
||||
watch: true
|
||||
|
||||
serversTransport:
|
||||
insecureSkipVerify: false
|
||||
Loading…
Reference in a new issue