Compare commits
36 Commits
b7495e1d6b
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
bc6fff68bb
|
|||
|
512f31b350
|
|||
|
d25a28c611
|
|||
|
472c7a984f
|
|||
|
df3a37e610
|
|||
|
8efab2002f
|
|||
|
6edb72077a
|
|||
|
07eacad003
|
|||
|
3b1736534d
|
|||
|
4d92b3bd3e
|
|||
|
27834c6711
|
|||
|
89f46566c8
|
|||
|
7f1809b4ca
|
|||
|
6784381833
|
|||
|
7c42acf893
|
|||
|
452f7973a9
|
|||
|
303aefb75f
|
|||
|
22307d81c9
|
|||
|
cc811f954d
|
|||
|
f17c4ac227
|
|||
|
25d20df5a9
|
|||
|
7e1a8e2e99
|
|||
|
b90b87caa1
|
|||
|
75ce60d8a0
|
|||
|
0aa34efd00
|
|||
|
b7a18f1296
|
|||
|
6bfb362b20
|
|||
|
5f619eaccc
|
|||
|
362d6d8710
|
|||
|
5e6df110c8
|
|||
|
a0543e13f4
|
|||
|
41fe116dd7
|
|||
|
e34f8505a2
|
|||
|
53f43264cc
|
|||
|
c1f5eaeca0
|
|||
|
fb9c754461
|
@@ -1,69 +1,137 @@
|
||||
# AGENTS GUIDE
|
||||
|
||||
## Overview
|
||||
Ansible-based server automation for personal services. Playbooks provision Dockerized apps (e.g., gitea, authelia, homepage, miniflux, wakapi, memos) via per-app users, Caddy proxy, and Yandex Docker Registry. Secrets are managed with Ansible Vault.
|
||||
## Обзор
|
||||
|
||||
## Project Layout
|
||||
- Playbooks: `playbook-*.yml` (per service), `playbook-all-*.yml` for grouped actions.
|
||||
- Inventory: `production.yml` (ungrouped host `server`).
|
||||
- Variables: `vars/*.yml` (app configs, images), secrets in `vars/secrets.yml` (vault-encrypted).
|
||||
- Roles: custom roles under `roles/` (e.g., `eget`, `owner`, `secrets`) plus galaxy roles fetched to `galaxy.roles/`.
|
||||
- Files/templates: service docker-compose and backup templates under `files/`, shared templates under `templates/`.
|
||||
- Scripts: helper Python scripts in `scripts/` (SMTP utilities) and `files/backups/backup-all.py`.
|
||||
- CI: `.gitea/workflows/lint.yml` runs yamllint and ansible-lint.
|
||||
- Hooks: `lefthook.yml` references local hooks in `/home/av/projects/private/git-hooks` (gitleaks, vault check).
|
||||
- Formatting: `.editorconfig` enforces LF, trailing newline, 4-space indent; YAML/Jinja use 2-space indent.
|
||||
Ansible-проект для автоматизации личного сервера. Плейбуки разворачивают докеризированные приложения (gitea, authelia, miniflux, wakapi, memos, outline, gramps, calibre, wanderer, remembos, transcriber и др.) через выделенных системных пользователей, Caddy-прокси и Yandex Docker Registry. Секреты управляются через Ansible Vault.
|
||||
|
||||
## Setup
|
||||
- Copy vault password sample: `cp ansible-vault-password-file.dist ansible-vault-password-file` (needed for ansible and CI).
|
||||
- Install galaxy roles: `ansible-galaxy role install --role-file requirements.yml --force` (or `task install-roles`).
|
||||
- Ensure `yq`, `task`, `ansible` installed per README requirements.
|
||||
## Структура проекта
|
||||
|
||||
## Tasks (taskfile)
|
||||
- `task install-roles` — install galaxy roles into `galaxy.roles/`.
|
||||
- `task ssh` — SSH to target using inventory (`production.yml`).
|
||||
- `task btop` — run `btop` on remote.
|
||||
- `task encrypt|decrypt -- <files>` — ansible-vault helpers.
|
||||
- Authelia helpers:
|
||||
- `task authelia-cli -- <args>` — run authelia CLI in docker.
|
||||
- `task authelia-validate-config` — render `files/authelia/configuration.template.yml` with secrets and validate via authelia docker image.
|
||||
- `task authelia-gen-random-string LEN=64` — generate random string.
|
||||
- `task authelia-gen-secret-and-hash LEN=72` — generate hashed secret.
|
||||
- `task format-py-files` — run Black via docker (pyfound/black).
|
||||
- `playbook-*.yml` — плейбуки по одному на сервис, `playbook-all-*.yml` для групповых запусков.
|
||||
- `production.yml` — инвентарь с единственным хостом `server`.
|
||||
- `vars/*.yml` — переменные приложений и образов, `vars/secrets.yml` — зашифрованные секреты (vault).
|
||||
- `roles/` — кастомные роли (`eget`, `owner`, `secrets`), галактические роли в `galaxy.roles/`.
|
||||
- `files/<app>/` — docker-compose шаблоны, конфиги, скрипты бэкапов для каждого сервиса.
|
||||
- `templates/` — общие шаблоны (например `env.template`).
|
||||
- `scripts/` — вспомогательные Python-скрипты (SMTP-утилиты для Yandex Cloud Postbox).
|
||||
- `.gitea/workflows/lint.yml` — CI: yamllint + ansible-lint.
|
||||
- `lefthook.yml` — pre-commit хуки (ruff, mypy, yamllint, ansible-lint, gitleaks, проверка vault).
|
||||
- `tasks.py` — задачи через invoke (`inv <task>`).
|
||||
- `pyproject.toml` — зависимости Python, управляются через `uv`.
|
||||
|
||||
## Ansible Usage
|
||||
- Inventory: `production.yml` with `server` host. `ansible.cfg` points to `./ansible-vault-password-file` and `./galaxy.roles` for roles path.
|
||||
- Typical deploy example (from README): `ansible-playbook -i production.yml --diff playbook-gitea.yml`.
|
||||
- Per-app playbooks: `playbook-<app>.yml`; grouped runs: `playbook-all-setup.yml`, `playbook-all-applications.yml`, `playbook-upgrade.yml`, etc.
|
||||
- Secrets: encrypted `vars/secrets.yml`; additional `files/<app>/secrets.yml` used for templating (e.g., Authelia). Respect `.crushignore` ignoring vault files.
|
||||
- Templates: many `docker-compose.template.yml` and `*.template.sh` files under `files/*` plus shared `templates/env.j2`. Use `vars/*.yml` to supply values.
|
||||
- Custom roles:
|
||||
- `roles/eget`: installs `eget` tool; see defaults/vars for version/source.
|
||||
- `roles/owner`: manages user/group and env template.
|
||||
- `roles/secrets`: manages vault-related items.
|
||||
## Настройка окружения
|
||||
|
||||
## Linting & CI
|
||||
- Local lint configs: `.yamllint.yml`, `.ansible-lint.yml` (excludes `.ansible/`, `.gitea/`, `galaxy.roles/`, `Taskfile.yml`).
|
||||
- CI (.gitea/workflows/lint.yml) installs `yamllint` and `ansible-lint` and runs `yamllint .` then `ansible-lint .`; creates dummy vault file if missing.
|
||||
- Pre-commit via lefthook (local hooks path): runs `gitleaks git --staged` and secret-file vault check script.
|
||||
```bash
|
||||
uv sync
|
||||
cp ansible-vault-password-file.dist ansible-vault-password-file
|
||||
uv run ansible-galaxy install --role-file requirements.yml
|
||||
```
|
||||
|
||||
## Coding/Templating Conventions
|
||||
- Indentation: 2 spaces for YAML/Jinja (`.editorconfig`), 4 spaces default elsewhere.
|
||||
- End-of-line: LF; ensure final newline.
|
||||
- Template suffixes `.template.yml`, `.yml.j2`, `.template.sh` are rendered via Ansible `template` module.
|
||||
- Avoid committing real secrets; `.crushignore` excludes `ansible-vault-password-file` and `*secrets.yml`.
|
||||
- Service directories under `files/` hold docker-compose and backup templates; ensure per-app users and registry settings align with `vars/*.yml`.
|
||||
Требуется: `uv`, `ansible`, `yq`.
|
||||
|
||||
## Testing/Validation
|
||||
- YAML lint: `yamllint .` (CI default).
|
||||
- Ansible lint: `ansible-lint .` (CI default).
|
||||
- Authelia config validation: `task authelia-validate-config` (renders with secrets then validates via docker).
|
||||
- Black formatting for Python helpers: `task format-py-files`.
|
||||
- Python types validation with mypy: `mypy <file.py>`.
|
||||
## Задачи (invoke)
|
||||
|
||||
## Operational Notes
|
||||
- Deployments rely on `production.yml` inventory and per-app playbooks; run with `--diff` for visibility.
|
||||
- Yandex Docker Registry auth helper: `files/yandex-docker-registry-auth.sh`.
|
||||
- Backups: templates and scripts under `files/backups/` per service; `backup-all.py` orchestrates.
|
||||
- Home network/DNS reference in README (Yandex domains).
|
||||
- Ensure `ansible-vault-password-file` present for vault operations and CI.
|
||||
Таск-раннер — `invoke` (файл `tasks.py`), вызывается через `inv`:
|
||||
|
||||
- `inv pl -- <app> [app2 ...]` — запуск плейбука (`ansible-playbook -i production.yml --diff`).
|
||||
- `inv install-roles` — установка галактических ролей.
|
||||
- `inv ssh` — SSH на сервер.
|
||||
- `inv zj` — zellij на удалённом сервере.
|
||||
- `inv btop` — btop на удалённом сервере.
|
||||
- `inv encrypt -- <file>` / `inv decrypt -- <file>` — шифрование/дешифрование через ansible-vault.
|
||||
- `inv authelia-cli -- <args>` — запуск Authelia CLI в docker.
|
||||
- `inv authelia-validate-config` — рендер и валидация конфига Authelia через docker.
|
||||
- `inv authelia-gen-random-string LEN=10` — генерация случайной строки.
|
||||
- `inv authelia-gen-secret-and-hash LEN=72` — генерация секрета и его хэша (pbkdf2-sha512).
|
||||
- `inv format-py-files` — форматирование Python-файлов через Black (docker).
|
||||
|
||||
## Плейбуки
|
||||
|
||||
### Системные
|
||||
|
||||
- `playbook-system.yml` — базовая настройка системы (apt-пакеты, безопасность, fail2ban, монтирование хранилища).
|
||||
- `playbook-docker.yml` — установка Docker CE, создание сетей (web_proxy_network, monitoring_network), cron очистки образов.
|
||||
- `playbook-eget.yml` — установка eget и инструментов через него (rclone, restic, resticprofile, btop, gobackup, task, dust, zellij).
|
||||
- `playbook-ufw.yml` — настройка файрвола UFW (SSH/22, Gitea SSH/2222, HTTP/80, HTTPS/443).
|
||||
- `playbook-upgrade.yml` — обновление системных пакетов, очистка Docker.
|
||||
- `playbook-backups.yml` — настройка restic-бэкапов и оркестратора backup-all.py с cron-расписанием.
|
||||
- `playbook-caddyproxy.yml` — Caddy reverse proxy.
|
||||
|
||||
### Приложения
|
||||
|
||||
- `playbook-gitea.yml` — Git-сервер.
|
||||
- `playbook-authelia.yml` — аутентификация/SSO.
|
||||
- `playbook-miniflux.yml` — RSS-ридер.
|
||||
- `playbook-wakapi.yml` — трекинг времени.
|
||||
- `playbook-memos.yml` — заметки.
|
||||
- `playbook-outline.yml` — вики/база знаний.
|
||||
- `playbook-homepage.yml` — дашборд (образ из Yandex Registry).
|
||||
- `playbook-rssbridge.yml` — RSS-агрегатор.
|
||||
- `playbook-netdata.yml` — мониторинг.
|
||||
- `playbook-dozzle.yml` — просмотр Docker-логов.
|
||||
- `playbook-goaccess.yml` — аналитика веб-логов Caddy в реальном времени.
|
||||
- `playbook-gramps.yml` — генеалогия.
|
||||
- `playbook-calibre.yml` — управление электронными книгами.
|
||||
- `playbook-transcriber.yml` — транскрибация (образ из Yandex Registry).
|
||||
- `playbook-wanderer.yml` — пешие маршруты.
|
||||
- `playbook-remembos.yml` — интервальное повторение.
|
||||
- `playbook-tuwunel.yml` — Matrix-сервер (Tuwunel) с federation-делегацией на apex-домен.
|
||||
|
||||
### Агрегатные и служебные
|
||||
|
||||
- `playbook-all-setup.yml` — системная настройка целиком (system + docker + eget + backups).
|
||||
- `playbook-all-applications.yml` — деплой всех приложений.
|
||||
- `playbook-homepage-registry.yml` / `playbook-transcriber-registry.yml` — загрузка образов в Yandex Registry.
|
||||
- `playbook-remove-user-and-app.yml` — удаление пользователя и приложения (`--extra-vars user_name=<name>`).
|
||||
|
||||
## Роли
|
||||
|
||||
- `roles/owner` — создаёт системного пользователя/группу для приложения, настраивает SSH-ключи, переменные окружения (~/.env, ~/.bashrc).
|
||||
- `roles/eget` — скачивает и устанавливает утилиту eget.
|
||||
- `roles/secrets` — управляет vault-зашифрованными файлами секретов для приложений.
|
||||
|
||||
Галактические роли (`galaxy.roles/`): `geerlingguy.security`, `geerlingguy.docker`, `yatesr.timezone`.
|
||||
|
||||
## Шаблоны и переменные
|
||||
|
||||
- Суффиксы шаблонов: `.template.yml`, `.template.sh`, `.template.cfg`, `.template.conf`, `.template.toml`, `.template` (для файлов без естественного расширения) — рендерятся Ansible модулем `template`. Расширение оригинального формата сохраняется после `.template.` ради подсветки синтаксиса в редакторе.
|
||||
- Большинство приложений определяют переменные inline в плейбуке. Отдельные файлы переменных только у homepage и transcriber (`vars/homepage.yml`, `vars/transcriber.yml` + `vars/transcriber.images.yml`).
|
||||
- Общие переменные из `vars/secrets.yml`: `application_dir`, `bin_prefix`, `primary_user` и др.
|
||||
- Каждое приложение: `app_name`, `app_user`, `app_owner_uid`, `app_owner_gid`, `base_dir`, `data_dir`.
|
||||
|
||||
## Линтинг и CI
|
||||
|
||||
- CI (`.gitea/workflows/lint.yml`): два параллельных job — yamllint и ansible-lint.
|
||||
- Конфиги: `.yamllint.yml` (макс. длина строки 120), `.ansible-lint.yml` (профиль production, offline).
|
||||
- Pre-commit хуки через lefthook:
|
||||
- `ruff format` + `ruff check` — форматирование и линтинг Python.
|
||||
- `mypy` — проверка типов Python.
|
||||
- `yamllint` — линтинг YAML.
|
||||
- `ansible-lint` — линтинг Ansible (профиль production).
|
||||
- `gitleaks` — поиск секретов в staged-файлах.
|
||||
- Проверка что секретные файлы зашифрованы vault.
|
||||
|
||||
## Соглашения по коду
|
||||
|
||||
- Отступы: 2 пробела для YAML/Jinja, 4 пробела в остальных файлах (`.editorconfig`).
|
||||
- Окончания строк: LF, завершающий перевод строки обязателен.
|
||||
- Не коммитить незашифрованные секреты; `.crushignore` исключает `ansible-vault-password-file` и `*secrets.yml`.
|
||||
- Директории в `files/<app>/` содержат docker-compose и шаблоны бэкапов; пользователи и настройки реестра должны соответствовать `vars/*.yml`.
|
||||
|
||||
## Деплой
|
||||
|
||||
```bash
|
||||
# Один сервис
|
||||
inv pl -- gitea
|
||||
|
||||
# Несколько сервисов
|
||||
inv pl -- gitea miniflux wakapi
|
||||
|
||||
# Напрямую через ansible-playbook
|
||||
ansible-playbook -i production.yml --diff playbook-gitea.yml
|
||||
```
|
||||
|
||||
## Бэкапы
|
||||
|
||||
- Шаблоны скриптов бэкапов в `files/<app>/` (backup.template.sh, gobackup.template.yml и др.).
|
||||
- `files/backups/backup-all.py` — оркестратор, запускает все бэкапы через restic.
|
||||
- Cron-расписание настраивается в `playbook-backups.yml`.
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
|
||||
- [uv](https://docs.astral.sh/uv/)
|
||||
- [ansible](https://docs.ansible.com/ansible/latest/getting_started/index.html)
|
||||
- [task](https://taskfile.dev/)
|
||||
- [yq](https://github.com/mikefarah/yq)
|
||||
|
||||
## Установка
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
services:
|
||||
|
||||
apprise_app:
|
||||
image: caronc/apprise:v1.3.3
|
||||
container_name: apprise_app
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "127.0.0.1:{{ apprise_external_port }}:8000"
|
||||
networks:
|
||||
web_proxy_network:
|
||||
aliases:
|
||||
- "apprise"
|
||||
volumes:
|
||||
- "{{ config_dir }}:/config"
|
||||
environment:
|
||||
PUID: "{{ owner_create_result.uid }}"
|
||||
PGID: "{{ owner_create_result.group }}"
|
||||
APPRISE_STATEFUL_MODE: simple
|
||||
APPRISE_WORKER_COUNT: 1
|
||||
|
||||
networks:
|
||||
web_proxy_network:
|
||||
external: true
|
||||
@@ -0,0 +1,2 @@
|
||||
tgram://{{ notifications_tg_bot_token }}/{{ notifications_tg_chat_id }}
|
||||
mailtos://{{ postbox_user }}:{{ postbox_pass }}@{{ postbox_host }}:{{ postbox_port }}/?from=notifications@vakhrushev.me&to={{ notifications_email }}
|
||||
@@ -731,6 +731,10 @@ access_control:
|
||||
subject: 'group:admins'
|
||||
policy: 'two_factor'
|
||||
|
||||
- domain: 'goaccess.vakhrushev.me'
|
||||
subject: 'group:admins'
|
||||
policy: 'two_factor'
|
||||
|
||||
- domain: 'wanderbase.vakhrushev.me'
|
||||
subject: 'group:admins'
|
||||
policy: 'two_factor'
|
||||
|
||||
@@ -2,7 +2,7 @@ services:
|
||||
|
||||
authelia_app:
|
||||
container_name: 'authelia_app'
|
||||
image: 'docker.io/authelia/authelia:4.39.16'
|
||||
image: 'docker.io/authelia/authelia:4.39.19'
|
||||
user: '{{ owner_create_result.uid }}:{{ owner_create_result.group }}'
|
||||
restart: 'unless-stopped'
|
||||
networks:
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
$ANSIBLE_VAULT;1.1;AES256
|
||||
39343035656562656632323766356561386665373036383564616331333333613765353737663632
|
||||
3531663835303562393063343231623464663232333532380a663838663938316566616532623065
|
||||
66336463643862626538366462346231386333366464323131363836326436373563623164336632
|
||||
6234353437383432380a396136653136616335343936343335633236373363353766666539396334
|
||||
36613836663831333838633231363731323234323761306630646632616238363662376462333039
|
||||
32373938343562313064663334383766653161613032623936646361316561666532356465623133
|
||||
32303663313834663834366363383265653939316336356239313364623366386631626536643439
|
||||
31333362353961353434333636343336323239363461663937313931616262316330376165393263
|
||||
63366665396431323034383939633365316134356564656136393032393864393636616234316231
|
||||
37616336396435626264643232343766616364306264376338313238356261653863336535363237
|
||||
34653638316161636431653465343536323331656230633332333139386132653433626662343837
|
||||
35396437633233363637376561303338386432643039626336376366373334613463663465613637
|
||||
36643734626163623738336435383032353837366532316566613864306430653336616637383262
|
||||
65646131643533323563393133373964633863636666633338616236386531323064396137376232
|
||||
37653333666566386563383235356232663338643161313635643661326339333661393135643030
|
||||
62356662623365376662646166316262353964383936373463393339623961376232653664306439
|
||||
36336231393434356661316336653033346430386366663138323832613532303265343136373836
|
||||
64666561616535623732326464643831363866326265343165356330646561653066393764336134
|
||||
30326436663066633163393163306265383834306634663639336437303965373063323335333537
|
||||
38643234623061376565636536323563623739313165343464316466363364613963636437363830
|
||||
33306632313839373132636130326331363538323763326333316165363633336561373030373963
|
||||
38313135343464303331343866646634393162393361333962356133376163393865373239323763
|
||||
31303336613937303031343532333036653133363439643864663661373639646566643831313662
|
||||
35613430333861376565
|
||||
+258
-205
@@ -11,6 +11,7 @@ import sys
|
||||
import subprocess
|
||||
import logging
|
||||
import pwd
|
||||
import time
|
||||
from abc import ABC
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
@@ -43,16 +44,38 @@ logger = logging.getLogger(__name__)
|
||||
@dataclass
|
||||
class Config:
|
||||
host_name: str
|
||||
roots: List[Path]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Application:
|
||||
path: Path
|
||||
owner: str
|
||||
backup_script: Optional[Path]
|
||||
backup_targets: List[Path]
|
||||
|
||||
|
||||
@dataclass
|
||||
class StorageRunResult:
|
||||
name: str
|
||||
success: bool
|
||||
duration: float
|
||||
|
||||
|
||||
def format_duration(seconds: float) -> str:
|
||||
if seconds < 60:
|
||||
return f"{seconds:.1f}s"
|
||||
minutes = int(seconds // 60)
|
||||
secs = int(seconds % 60)
|
||||
if minutes < 60:
|
||||
return f"{minutes}m{secs:02d}s"
|
||||
hours = minutes // 60
|
||||
minutes = minutes % 60
|
||||
return f"{hours}h{minutes:02d}m{secs:02d}s"
|
||||
|
||||
|
||||
class Storage(ABC):
|
||||
name: str
|
||||
|
||||
def backup(self, backup_dirs: List[str]) -> bool:
|
||||
"""Backup directories"""
|
||||
raise NotImplementedError()
|
||||
@@ -65,19 +88,15 @@ class ResticStorage(Storage):
|
||||
self.name = name
|
||||
self.restic_repository = str(params.get("restic_repository", ""))
|
||||
self.restic_password = str(params.get("restic_password", ""))
|
||||
self.aws_access_key_id = str(params.get("aws_access_key_id", ""))
|
||||
self.aws_secret_access_key = str(params.get("aws_secret_access_key", ""))
|
||||
self.aws_default_region = str(params.get("aws_default_region", ""))
|
||||
|
||||
if not all(
|
||||
[
|
||||
self.restic_repository,
|
||||
self.restic_password,
|
||||
self.aws_access_key_id,
|
||||
self.aws_secret_access_key,
|
||||
self.aws_default_region,
|
||||
]
|
||||
):
|
||||
env_raw = params.get("env") or {}
|
||||
if not isinstance(env_raw, dict):
|
||||
raise ValueError(
|
||||
f"'env' must be a table for storage backend ResticStorage: '{self.name}'"
|
||||
)
|
||||
self.env: Dict[str, str] = {str(k): str(v) for k, v in env_raw.items()}
|
||||
|
||||
if not self.restic_repository or not self.restic_password:
|
||||
raise ValueError(
|
||||
f"Missing storage configuration values for backend ResticStorage: '{self.name}'"
|
||||
)
|
||||
@@ -93,19 +112,13 @@ class ResticStorage(Storage):
|
||||
return False
|
||||
|
||||
def __backup_internal(self, backup_dirs: List[str]) -> bool:
|
||||
logger.info("Starting restic backup")
|
||||
logger.info("Starting restic backup for storage '%s'", self.name)
|
||||
logger.info("Destination: %s", self.restic_repository)
|
||||
|
||||
env = os.environ.copy()
|
||||
env.update(
|
||||
{
|
||||
"RESTIC_REPOSITORY": self.restic_repository,
|
||||
"RESTIC_PASSWORD": self.restic_password,
|
||||
"AWS_ACCESS_KEY_ID": self.aws_access_key_id,
|
||||
"AWS_SECRET_ACCESS_KEY": self.aws_secret_access_key,
|
||||
"AWS_DEFAULT_REGION": self.aws_default_region,
|
||||
}
|
||||
)
|
||||
env["RESTIC_REPOSITORY"] = self.restic_repository
|
||||
env["RESTIC_PASSWORD"] = self.restic_password
|
||||
env.update(self.env)
|
||||
|
||||
backup_cmd = ["restic", "backup", "--verbose"] + backup_dirs
|
||||
result = subprocess.run(backup_cmd, env=env, capture_output=True, text=True)
|
||||
@@ -154,63 +167,47 @@ class ResticStorage(Storage):
|
||||
|
||||
|
||||
class Notifier(ABC):
|
||||
def send(self, html_message: str) -> None:
|
||||
def send(self, title: str, html_message: str) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class TelegramNotifier(Notifier):
|
||||
TYPE_NAME = "telegram"
|
||||
class AppriseNotifier(Notifier):
|
||||
TYPE_NAME = "apprise"
|
||||
|
||||
def __init__(self, name: str, params: Dict[str, Any]):
|
||||
self.name = name
|
||||
self.telegram_bot_token = str(params.get("telegram_bot_token", ""))
|
||||
self.telegram_chat_id = str(params.get("telegram_chat_id", ""))
|
||||
if not all(
|
||||
[
|
||||
self.telegram_bot_token,
|
||||
self.telegram_chat_id,
|
||||
]
|
||||
):
|
||||
self.api_url = str(params.get("api_url", "")).rstrip("/")
|
||||
self.tag = str(params.get("tag", ""))
|
||||
if not self.api_url or not self.tag:
|
||||
raise ValueError(
|
||||
f"Missing notification configuration values for backend {name}"
|
||||
)
|
||||
|
||||
def send(self, html_message: str) -> None:
|
||||
url = f"https://api.telegram.org/bot{self.telegram_bot_token}/sendMessage"
|
||||
data = {
|
||||
"chat_id": self.telegram_chat_id,
|
||||
"parse_mode": "HTML",
|
||||
"text": html_message,
|
||||
def send(self, title: str, html_message: str) -> None:
|
||||
url = f"{self.api_url}/notify/{self.tag}/"
|
||||
payload = {
|
||||
"title": title,
|
||||
"body": html_message,
|
||||
"format": "html",
|
||||
}
|
||||
|
||||
response = requests.post(url, data=data, timeout=30)
|
||||
response = requests.post(url, json=payload, timeout=30)
|
||||
|
||||
if response.status_code == 200:
|
||||
logger.info("Telegram notification sent successfully")
|
||||
if response.ok:
|
||||
logger.info("Apprise notification sent successfully")
|
||||
else:
|
||||
logger.error(
|
||||
f"Failed to send Telegram notification: {response.status_code} - {response.text}"
|
||||
f"Failed to send Apprise notification: {response.status_code} - {response.text}"
|
||||
)
|
||||
|
||||
|
||||
class BackupManager:
|
||||
def __init__(
|
||||
self,
|
||||
config: Config,
|
||||
roots: List[Path],
|
||||
storages: List[Storage],
|
||||
notifiers: List[Notifier],
|
||||
):
|
||||
self.errors: List[str] = []
|
||||
class ApplicationFinder:
|
||||
def __init__(self, roots: List[Path]):
|
||||
self.roots = roots
|
||||
self.warnings: List[str] = []
|
||||
self.successful_backups: List[str] = []
|
||||
self.config = config
|
||||
self.roots: List[Path] = roots
|
||||
self.storages = storages
|
||||
self.notifiers = notifiers
|
||||
|
||||
def find_applications(self) -> List[Application]:
|
||||
"""Get all application directories and their owners."""
|
||||
"""Discover all applications with their backup scripts and targets."""
|
||||
applications: List[Application] = []
|
||||
source_dirs = itertools.chain(*(root.iterdir() for root in self.roots))
|
||||
|
||||
@@ -221,32 +218,182 @@ class BackupManager:
|
||||
try:
|
||||
stat_info = app_dir.stat()
|
||||
owner = pwd.getpwuid(stat_info.st_uid).pw_name
|
||||
applications.append(Application(path=app_dir, owner=owner))
|
||||
backup_script = self._find_backup_script(app_dir)
|
||||
backup_targets = self._find_backup_targets(app_dir)
|
||||
applications.append(
|
||||
Application(
|
||||
path=app_dir,
|
||||
owner=owner,
|
||||
backup_script=backup_script,
|
||||
backup_targets=backup_targets,
|
||||
)
|
||||
)
|
||||
except (KeyError, OSError) as e:
|
||||
logger.warning(f"Could not get owner for {app_dir}: {e}")
|
||||
|
||||
applications.sort(key=lambda app: app.path.name)
|
||||
return applications
|
||||
|
||||
def find_backup_script(self, app_dir: str) -> Optional[str]:
|
||||
"""Find backup script in user's home directory"""
|
||||
possible_scripts = [
|
||||
os.path.join(app_dir, "backup.sh"),
|
||||
os.path.join(app_dir, "backup"),
|
||||
]
|
||||
|
||||
for script_path in possible_scripts:
|
||||
if os.path.exists(script_path):
|
||||
# Check if file is executable
|
||||
def _find_backup_script(self, app_dir: Path) -> Optional[Path]:
|
||||
"""Find executable backup script in application directory."""
|
||||
for name in ("backup.sh", "backup"):
|
||||
script_path = app_dir / name
|
||||
if script_path.exists():
|
||||
if os.access(script_path, os.X_OK):
|
||||
return script_path
|
||||
else:
|
||||
logger.warning(
|
||||
f"Backup script {script_path} exists but is not executable"
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
def run_app_backup(self, script_path: str, app_dir: str, username: str) -> bool:
|
||||
def _find_backup_targets(self, app_dir: Path) -> List[Path]:
|
||||
"""Resolve backup target directories for an application."""
|
||||
targets_file = app_dir / BACKUP_TARGETS_FILE
|
||||
resolved_targets: List[Path] = []
|
||||
|
||||
if targets_file.exists():
|
||||
for target_line in self._parse_targets_file(targets_file):
|
||||
target_path = Path(target_line)
|
||||
if not target_path.is_absolute():
|
||||
target_path = (app_dir / target_path).resolve()
|
||||
else:
|
||||
target_path = target_path.resolve()
|
||||
if target_path.exists():
|
||||
resolved_targets.append(target_path)
|
||||
else:
|
||||
warning_msg = (
|
||||
f"Backup target does not exist for {app_dir}: {target_path}"
|
||||
)
|
||||
logger.warning(warning_msg)
|
||||
self.warnings.append(warning_msg)
|
||||
else:
|
||||
default_target = (app_dir / BACKUP_DEFAULT_DIR).resolve()
|
||||
if default_target.exists():
|
||||
resolved_targets.append(default_target)
|
||||
else:
|
||||
warning_msg = f"Default backup path does not exist for {app_dir}: {default_target}"
|
||||
logger.warning(warning_msg)
|
||||
self.warnings.append(warning_msg)
|
||||
|
||||
return resolved_targets
|
||||
|
||||
def _parse_targets_file(self, targets_file: Path) -> List[str]:
|
||||
"""Parse backup-targets file, skipping comments and empty lines."""
|
||||
targets: List[str] = []
|
||||
try:
|
||||
for raw_line in targets_file.read_text(encoding="utf-8").splitlines():
|
||||
line = raw_line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
targets.append(line)
|
||||
except OSError as e:
|
||||
warning_msg = f"Could not read backup targets file {targets_file}: {e}"
|
||||
logger.warning(warning_msg)
|
||||
self.warnings.append(warning_msg)
|
||||
return targets
|
||||
|
||||
|
||||
class BackupManager:
|
||||
def __init__(
|
||||
self,
|
||||
config: Config,
|
||||
storages: List[Storage],
|
||||
notifiers: List[Notifier],
|
||||
):
|
||||
self.errors: List[str] = []
|
||||
self.warnings: List[str] = []
|
||||
self.successful_backups: List[str] = []
|
||||
self.config = config
|
||||
self.storages = storages
|
||||
self.notifiers = notifiers
|
||||
self.archive_duration: float = 0.0
|
||||
self.storage_results: List[StorageRunResult] = []
|
||||
|
||||
def run_backup_process(self, applications: List[Application]) -> bool:
|
||||
"""Main backup process"""
|
||||
logger.info("Starting backup process")
|
||||
logger.info(f"Found {len(applications)} application directories")
|
||||
|
||||
archive_start = time.monotonic()
|
||||
# Process each user's backup
|
||||
for app in applications:
|
||||
app_dir = str(app.path)
|
||||
username = app.owner
|
||||
logger.info(f"Processing backup for app: {app_dir} (user {username})")
|
||||
|
||||
if app.backup_script is None:
|
||||
warning_msg = (
|
||||
f"No backup script found for app: {app_dir} (user {username})"
|
||||
)
|
||||
logger.warning(warning_msg)
|
||||
self.warnings.append(warning_msg)
|
||||
continue
|
||||
|
||||
self._run_app_backup(str(app.backup_script), app_dir, username)
|
||||
self.archive_duration = time.monotonic() - archive_start
|
||||
logger.info(
|
||||
"Archive phase finished in %s", format_duration(self.archive_duration)
|
||||
)
|
||||
|
||||
# Collect backup directories from applications
|
||||
backup_dirs: List[str] = []
|
||||
for app in applications:
|
||||
for target in app.backup_targets:
|
||||
target_str = str(target)
|
||||
if target_str not in backup_dirs:
|
||||
backup_dirs.append(target_str)
|
||||
logger.info(f"Found backup directories: {backup_dirs}")
|
||||
|
||||
overall_success = True
|
||||
|
||||
# Each storage is processed independently: a failure in one storage
|
||||
# must not prevent the others from being attempted.
|
||||
for storage in self.storages:
|
||||
storage_start = time.monotonic()
|
||||
try:
|
||||
backup_result = storage.backup(backup_dirs)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.error(
|
||||
"Storage '%s' raised an unexpected error: %s", storage.name, exc
|
||||
)
|
||||
backup_result = False
|
||||
storage_duration = time.monotonic() - storage_start
|
||||
self.storage_results.append(
|
||||
StorageRunResult(
|
||||
name=storage.name,
|
||||
success=backup_result,
|
||||
duration=storage_duration,
|
||||
)
|
||||
)
|
||||
logger.info(
|
||||
"Storage '%s' finished in %s (success=%s)",
|
||||
storage.name,
|
||||
format_duration(storage_duration),
|
||||
backup_result,
|
||||
)
|
||||
if not backup_result:
|
||||
self.errors.append(f"Storage '{storage.name}' backup failed")
|
||||
|
||||
# Determine overall success
|
||||
overall_success = overall_success and backup_result
|
||||
|
||||
# Send notification
|
||||
self._send_notification(overall_success)
|
||||
|
||||
logger.info("Backup process completed")
|
||||
|
||||
if self.errors:
|
||||
logger.error(f"Backup completed with {len(self.errors)} errors")
|
||||
return False
|
||||
elif self.warnings:
|
||||
logger.warning(f"Backup completed with {len(self.warnings)} warnings")
|
||||
return True
|
||||
else:
|
||||
logger.info("Backup completed successfully")
|
||||
return True
|
||||
|
||||
def _run_app_backup(self, script_path: str, app_dir: str, username: str) -> bool:
|
||||
"""Run backup script as the specified user"""
|
||||
try:
|
||||
logger.info(f"Running backup script {script_path} (user {username})")
|
||||
@@ -285,149 +432,51 @@ class BackupManager:
|
||||
self.errors.append(f"App {username}: {error_msg}")
|
||||
return False
|
||||
|
||||
def get_backup_directories(self) -> List[str]:
|
||||
"""Collect backup targets according to backup-targets rules"""
|
||||
backup_dirs: List[str] = []
|
||||
applications = self.find_applications()
|
||||
|
||||
def parse_targets_file(targets_file: Path) -> List[str]:
|
||||
"""Parse backup-targets file, skipping comments and empty lines."""
|
||||
targets: List[str] = []
|
||||
try:
|
||||
for raw_line in targets_file.read_text(encoding="utf-8").splitlines():
|
||||
line = raw_line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
targets.append(line)
|
||||
except OSError as e:
|
||||
warning_msg = f"Could not read backup targets file {targets_file}: {e}"
|
||||
logger.warning(warning_msg)
|
||||
self.warnings.append(warning_msg)
|
||||
return targets
|
||||
|
||||
for app in applications:
|
||||
app_dir = app.path
|
||||
targets_file = app_dir / BACKUP_TARGETS_FILE
|
||||
resolved_targets: List[Path] = []
|
||||
|
||||
if targets_file.exists():
|
||||
# Read custom targets defined by the application.
|
||||
for target_line in parse_targets_file(targets_file):
|
||||
target_path = Path(target_line)
|
||||
if not target_path.is_absolute():
|
||||
target_path = (app_dir / target_path).resolve()
|
||||
else:
|
||||
target_path = target_path.resolve()
|
||||
if target_path.exists():
|
||||
resolved_targets.append(target_path)
|
||||
else:
|
||||
warning_msg = (
|
||||
f"Backup target does not exist for {app_dir}: {target_path}"
|
||||
)
|
||||
logger.warning(warning_msg)
|
||||
self.warnings.append(warning_msg)
|
||||
else:
|
||||
# Fallback to default backups directory when no list is provided.
|
||||
default_target = (app_dir / BACKUP_DEFAULT_DIR).resolve()
|
||||
if default_target.exists():
|
||||
resolved_targets.append(default_target)
|
||||
else:
|
||||
warning_msg = f"Default backup path does not exist for {app_dir}: {default_target}"
|
||||
logger.warning(warning_msg)
|
||||
self.warnings.append(warning_msg)
|
||||
|
||||
for target in resolved_targets:
|
||||
target_str = str(target)
|
||||
if target_str not in backup_dirs:
|
||||
backup_dirs.append(target_str)
|
||||
|
||||
return backup_dirs
|
||||
|
||||
def send_notification(self, success: bool) -> None:
|
||||
def _send_notification(self, success: bool) -> None:
|
||||
"""Send notification to Notifiers"""
|
||||
|
||||
host = self.config.host_name
|
||||
|
||||
if success and not self.errors:
|
||||
message = f"<b>{self.config.host_name}</b>: бекап успешно завершен!"
|
||||
title = f"{host}: бекап успешно завершен"
|
||||
message = f"<p><b>{host}</b>: бекап успешно завершен!</p>"
|
||||
if self.successful_backups:
|
||||
message += f"\n\nУспешные бекапы: {', '.join(self.successful_backups)}"
|
||||
items = "".join(f"<li>{b}</li>" for b in self.successful_backups)
|
||||
message += f"<p>Успешные бекапы:</p><ul>{items}</ul>"
|
||||
else:
|
||||
message = f"<b>{self.config.host_name}</b>: бекап завершен с ошибками!"
|
||||
title = f"{host}: бекап завершен с ошибками ({len(self.errors)})"
|
||||
message = f"<p><b>{host}</b>: бекап завершен с ошибками!</p>"
|
||||
|
||||
if self.successful_backups:
|
||||
message += (
|
||||
f"\n\n✅ Успешные бекапы: {', '.join(self.successful_backups)}"
|
||||
)
|
||||
items = "".join(f"<li>{b}</li>" for b in self.successful_backups)
|
||||
message += f"<p>✅ Успешные бекапы:</p><ul>{items}</ul>"
|
||||
|
||||
if self.warnings:
|
||||
message += "\n\n⚠️ Предупреждения:\n" + "\n".join(self.warnings)
|
||||
items = "".join(f"<li>{w}</li>" for w in self.warnings)
|
||||
message += f"<p>⚠️ Предупреждения:</p><ul>{items}</ul>"
|
||||
|
||||
if self.errors:
|
||||
message += "\n\n❌ Ошибки:\n" + "\n".join(self.errors)
|
||||
items = "".join(f"<li>{e}</li>" for e in self.errors)
|
||||
message += f"<p>❌ Ошибки:</p><ul>{items}</ul>"
|
||||
|
||||
message += f"<p>⏱ Время архивации: {format_duration(self.archive_duration)}</p>"
|
||||
if self.storage_results:
|
||||
items = "".join(
|
||||
f"<li>{'✅' if r.success else '❌'} {r.name}: {format_duration(r.duration)}</li>"
|
||||
for r in self.storage_results
|
||||
)
|
||||
message += f"<p>⏱ Время записи в хранилища:</p><ul>{items}</ul>"
|
||||
|
||||
for notificator in self.notifiers:
|
||||
try:
|
||||
notificator.send(message)
|
||||
notificator.send(title, message)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send notification: {str(e)}")
|
||||
|
||||
def run_backup_process(self) -> bool:
|
||||
"""Main backup process"""
|
||||
logger.info("Starting backup process")
|
||||
|
||||
# Get all home directories
|
||||
applications = self.find_applications()
|
||||
logger.info(f"Found {len(applications)} application directories")
|
||||
|
||||
# Process each user's backup
|
||||
for app in applications:
|
||||
app_dir = str(app.path)
|
||||
username = app.owner
|
||||
logger.info(f"Processing backup for app: {app_dir} (user {username})")
|
||||
|
||||
# Find backup script
|
||||
backup_script = self.find_backup_script(app_dir)
|
||||
|
||||
if backup_script is None:
|
||||
warning_msg = (
|
||||
f"No backup script found for app: {app_dir} (user {username})"
|
||||
)
|
||||
logger.warning(warning_msg)
|
||||
self.warnings.append(warning_msg)
|
||||
continue
|
||||
|
||||
self.run_app_backup(backup_script, app_dir, username)
|
||||
|
||||
# Get backup directories
|
||||
backup_dirs = self.get_backup_directories()
|
||||
logger.info(f"Found backup directories: {backup_dirs}")
|
||||
|
||||
overall_success = True
|
||||
|
||||
for storage in self.storages:
|
||||
backup_result = storage.backup(backup_dirs)
|
||||
if not backup_result:
|
||||
self.errors.append("Restic backup failed")
|
||||
|
||||
# Determine overall success
|
||||
overall_success = overall_success and backup_result
|
||||
|
||||
# Send notification
|
||||
self.send_notification(overall_success)
|
||||
|
||||
logger.info("Backup process completed")
|
||||
|
||||
if self.errors:
|
||||
logger.error(f"Backup completed with {len(self.errors)} errors")
|
||||
return False
|
||||
elif self.warnings:
|
||||
logger.warning(f"Backup completed with {len(self.warnings)} warnings")
|
||||
return True
|
||||
else:
|
||||
logger.info("Backup completed successfully")
|
||||
return True
|
||||
|
||||
|
||||
def initialize(config_path: Path) -> BackupManager:
|
||||
def initialize(
|
||||
config_path: Path,
|
||||
) -> tuple[ApplicationFinder, BackupManager]:
|
||||
try:
|
||||
with config_path.open("rb") as config_file:
|
||||
raw_config = tomllib.load(config_file)
|
||||
@@ -459,22 +508,26 @@ def initialize(config_path: Path) -> BackupManager:
|
||||
if not isinstance(params, dict):
|
||||
raise ValueError(f"Notificator config for {name} must be a table")
|
||||
notifier_type = params.get("type", "")
|
||||
if notifier_type == TelegramNotifier.TYPE_NAME:
|
||||
notifiers.append(TelegramNotifier(name, params))
|
||||
if notifier_type == AppriseNotifier.TYPE_NAME:
|
||||
notifiers.append(AppriseNotifier(name, params))
|
||||
if not notifiers:
|
||||
raise ValueError("At least one notification backend must be configured")
|
||||
|
||||
config = Config(host_name=host_name, roots=roots)
|
||||
|
||||
return BackupManager(
|
||||
config=config, roots=roots, storages=storages, notifiers=notifiers
|
||||
config = Config(host_name=host_name)
|
||||
app_finder = ApplicationFinder(roots)
|
||||
backup_manager = BackupManager(
|
||||
config=config, storages=storages, notifiers=notifiers
|
||||
)
|
||||
|
||||
return app_finder, backup_manager
|
||||
|
||||
|
||||
def main() -> None:
|
||||
try:
|
||||
backup_manager = initialize(CONFIG_PATH)
|
||||
success = backup_manager.run_backup_process()
|
||||
app_finder, backup_manager = initialize(CONFIG_PATH)
|
||||
applications = app_finder.find_applications()
|
||||
backup_manager.warnings.extend(app_finder.warnings)
|
||||
success = backup_manager.run_backup_process(applications)
|
||||
if not success:
|
||||
sys.exit(1)
|
||||
except KeyboardInterrupt:
|
||||
|
||||
@@ -8,11 +8,21 @@ roots = [
|
||||
type = "restic"
|
||||
restic_repository = "{{ restic_repository }}"
|
||||
restic_password = "{{ restic_password }}"
|
||||
aws_access_key_id = "{{ restic_s3_access_key }}"
|
||||
aws_secret_access_key = "{{ restic_s3_access_secret }}"
|
||||
aws_default_region = "{{ restic_s3_region }}"
|
||||
|
||||
[notifier.server_notifications_channel]
|
||||
type = "telegram"
|
||||
telegram_bot_token = "{{ notifications_tg_bot_token }}"
|
||||
telegram_chat_id = "{{ notifications_tg_chat_id }}"
|
||||
[storage.yandex_cloud_s3.env]
|
||||
AWS_ACCESS_KEY_ID = "{{ restic_s3_access_key }}"
|
||||
AWS_SECRET_ACCESS_KEY = "{{ restic_s3_access_secret }}"
|
||||
AWS_DEFAULT_REGION = "{{ restic_s3_region }}"
|
||||
|
||||
[storage.pr86keedav]
|
||||
type = "restic"
|
||||
restic_repository = "{{ restic_pr86keedav_repository }}"
|
||||
restic_password = "{{ restic_pr86keedav_password }}"
|
||||
|
||||
[storage.pr86keedav.env]
|
||||
RCLONE_CONFIG = "{{ rclone_config_file }}"
|
||||
|
||||
[notifier.apprise]
|
||||
type = "apprise"
|
||||
api_url = "{{ apprise_external_url }}"
|
||||
tag = "server"
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
[pr86keedav]
|
||||
type = webdav
|
||||
url = {{ rclone_pr86keedav_url }}
|
||||
vendor = other
|
||||
user = {{ rclone_pr86keedav_user }}
|
||||
pass = {{ rclone_pr86keedav_password }}
|
||||
@@ -12,26 +12,74 @@
|
||||
metrics
|
||||
}
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Snippets
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
# Shared access log for all sites; consumed by GoAccess.
|
||||
# Mode 644 lets read-only consumers (goaccess and ad-hoc host-side tail)
|
||||
# read the file; lumberjack would otherwise default to 0600.
|
||||
(access_log) {
|
||||
log {
|
||||
output file /var/log/caddy/access.log {
|
||||
mode 644
|
||||
roll_size 100mib
|
||||
roll_keep 10
|
||||
roll_keep_for 720h
|
||||
}
|
||||
format json
|
||||
}
|
||||
}
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Applications
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
vakhrushev.me {
|
||||
tls anwinged@ya.ru
|
||||
import access_log
|
||||
|
||||
# Matrix federation delegation: tells other servers/clients that the
|
||||
# homeserver for vakhrushev.me lives at matrix.vakhrushev.me.
|
||||
# https://spec.matrix.org/latest/server-server-api/#server-discovery
|
||||
handle /.well-known/matrix/server {
|
||||
header Content-Type application/json
|
||||
header Access-Control-Allow-Origin *
|
||||
respond `{"m.server": "matrix.vakhrushev.me:443"}`
|
||||
}
|
||||
|
||||
handle /.well-known/matrix/client {
|
||||
header Content-Type application/json
|
||||
header Access-Control-Allow-Origin *
|
||||
respond `{"m.homeserver": {"base_url": "https://matrix.vakhrushev.me"}}`
|
||||
}
|
||||
|
||||
handle {
|
||||
reverse_proxy {
|
||||
to homepage_app:80
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
matrix.vakhrushev.me {
|
||||
tls anwinged@ya.ru
|
||||
import access_log
|
||||
|
||||
reverse_proxy {
|
||||
to homepage_app:80
|
||||
to tuwunel_app:6167
|
||||
}
|
||||
}
|
||||
|
||||
auth.vakhrushev.me {
|
||||
tls anwinged@ya.ru
|
||||
import access_log
|
||||
|
||||
reverse_proxy authelia_app:9091
|
||||
}
|
||||
|
||||
status.vakhrushev.me, :29999 {
|
||||
tls anwinged@ya.ru
|
||||
import access_log
|
||||
|
||||
forward_auth authelia_app:9091 {
|
||||
uri /api/authz/forward-auth
|
||||
@@ -43,6 +91,7 @@ status.vakhrushev.me, :29999 {
|
||||
|
||||
git.vakhrushev.me {
|
||||
tls anwinged@ya.ru
|
||||
import access_log
|
||||
|
||||
reverse_proxy {
|
||||
to gitea_app:3000
|
||||
@@ -51,6 +100,7 @@ git.vakhrushev.me {
|
||||
|
||||
outline.vakhrushev.me {
|
||||
tls anwinged@ya.ru
|
||||
import access_log
|
||||
|
||||
reverse_proxy {
|
||||
to outline_app:3000
|
||||
@@ -59,6 +109,7 @@ outline.vakhrushev.me {
|
||||
|
||||
gramps.vakhrushev.me {
|
||||
tls anwinged@ya.ru
|
||||
import access_log
|
||||
|
||||
reverse_proxy {
|
||||
to gramps_app:5000
|
||||
@@ -67,6 +118,7 @@ gramps.vakhrushev.me {
|
||||
|
||||
miniflux.vakhrushev.me {
|
||||
tls anwinged@ya.ru
|
||||
import access_log
|
||||
|
||||
reverse_proxy {
|
||||
to miniflux_app:8080
|
||||
@@ -75,6 +127,7 @@ miniflux.vakhrushev.me {
|
||||
|
||||
wakapi.vakhrushev.me {
|
||||
tls anwinged@ya.ru
|
||||
import access_log
|
||||
|
||||
reverse_proxy {
|
||||
to wakapi_app:3000
|
||||
@@ -83,6 +136,7 @@ wakapi.vakhrushev.me {
|
||||
|
||||
wanderer.vakhrushev.me {
|
||||
tls anwinged@ya.ru
|
||||
import access_log
|
||||
|
||||
reverse_proxy {
|
||||
to wanderer_web:3000
|
||||
@@ -91,6 +145,7 @@ wanderer.vakhrushev.me {
|
||||
|
||||
memos.vakhrushev.me {
|
||||
tls anwinged@ya.ru
|
||||
import access_log
|
||||
|
||||
reverse_proxy {
|
||||
to memos_app:5230
|
||||
@@ -99,6 +154,7 @@ memos.vakhrushev.me {
|
||||
|
||||
remembos.vakhrushev.me {
|
||||
tls anwinged@ya.ru
|
||||
import access_log
|
||||
|
||||
forward_auth authelia_app:9091 {
|
||||
uri /api/authz/forward-auth
|
||||
@@ -112,6 +168,7 @@ remembos.vakhrushev.me {
|
||||
|
||||
calibre.vakhrushev.me {
|
||||
tls anwinged@ya.ru
|
||||
import access_log
|
||||
|
||||
reverse_proxy {
|
||||
to calibre_web_app:8083
|
||||
@@ -120,6 +177,7 @@ calibre.vakhrushev.me {
|
||||
|
||||
wanderbase.vakhrushev.me {
|
||||
tls anwinged@ya.ru
|
||||
import access_log
|
||||
|
||||
forward_auth authelia_app:9091 {
|
||||
uri /api/authz/forward-auth
|
||||
@@ -133,6 +191,7 @@ wanderbase.vakhrushev.me {
|
||||
|
||||
rssbridge.vakhrushev.me {
|
||||
tls anwinged@ya.ru
|
||||
import access_log
|
||||
|
||||
forward_auth authelia_app:9091 {
|
||||
uri /api/authz/forward-auth
|
||||
@@ -146,6 +205,7 @@ rssbridge.vakhrushev.me {
|
||||
|
||||
dozzle.vakhrushev.me {
|
||||
tls anwinged@ya.ru
|
||||
import access_log
|
||||
|
||||
forward_auth authelia_app:9091 {
|
||||
uri /api/authz/forward-auth
|
||||
@@ -155,3 +215,21 @@ dozzle.vakhrushev.me {
|
||||
reverse_proxy dozzle_app:8080
|
||||
}
|
||||
|
||||
goaccess.vakhrushev.me {
|
||||
tls anwinged@ya.ru
|
||||
import access_log
|
||||
|
||||
forward_auth authelia_app:9091 {
|
||||
uri /api/authz/forward-auth
|
||||
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
|
||||
}
|
||||
|
||||
@websocket {
|
||||
header Connection *Upgrade*
|
||||
header Upgrade websocket
|
||||
}
|
||||
reverse_proxy @websocket goaccess_processor:7890
|
||||
|
||||
reverse_proxy goaccess_app:8080
|
||||
}
|
||||
|
||||
+7
-6
@@ -1,19 +1,20 @@
|
||||
services:
|
||||
|
||||
{{ service_name }}:
|
||||
caddyproxy:
|
||||
image: caddy:2.11.2
|
||||
restart: unless-stopped
|
||||
container_name: {{ service_name }}
|
||||
container_name: "caddyproxy"
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "443:443/udp"
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
- NET_ADMIN
|
||||
volumes:
|
||||
- {{ caddy_file_dir }}:/etc/caddy
|
||||
- {{ data_dir }}:/data
|
||||
- {{ config_dir }}:/config
|
||||
- "{{ caddy_file_dir }}:/etc/caddy"
|
||||
- "{{ data_dir }}:/data"
|
||||
- "{{ config_dir }}:/config"
|
||||
- "{{ caddy_logs_dir }}:/var/log/caddy"
|
||||
networks:
|
||||
- "web_proxy_network"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
|
||||
dozzle_app:
|
||||
image: amir20/dozzle:v10.2.1
|
||||
image: amir20/dozzle:v10.5.0
|
||||
container_name: dozzle_app
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
services:
|
||||
|
||||
gitea_app:
|
||||
image: gitea/gitea:1.25.5
|
||||
image: gitea/gitea:1.26.1
|
||||
restart: unless-stopped
|
||||
container_name: gitea_app
|
||||
ports:
|
||||
- "2222:22"
|
||||
volumes:
|
||||
- {{ data_dir }}:/data
|
||||
- {{ backups_dir }}:/backups
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- "{{ data_dir }}:/data"
|
||||
- "{{ backups_dir }}:/backups"
|
||||
- "/etc/timezone:/etc/timezone:ro"
|
||||
- "/etc/localtime:/etc/localtime:ro"
|
||||
networks:
|
||||
- "web_proxy_network"
|
||||
environment:
|
||||
@@ -0,0 +1,8 @@
|
||||
FROM allinurl/goaccess:1.10.2
|
||||
|
||||
RUN apk add --no-cache jq
|
||||
|
||||
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
RUN chmod 0755 /usr/local/bin/entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
||||
@@ -0,0 +1,41 @@
|
||||
services:
|
||||
|
||||
goaccess_processor:
|
||||
build: .
|
||||
image: local/goaccess-jq:1.10.2
|
||||
container_name: goaccess_processor
|
||||
restart: unless-stopped
|
||||
init: true
|
||||
user: "{{ app_owner_uid }}:{{ app_owner_gid }}"
|
||||
command:
|
||||
- --log-format=COMBINED
|
||||
- --enable-panel=VIRTUAL_HOSTS
|
||||
- --real-time-html
|
||||
- --port=7890
|
||||
- --ws-url=wss://goaccess.vakhrushev.me:443
|
||||
- --output=/srv/report/index.html
|
||||
- --persist
|
||||
- --restore
|
||||
- --db-path=/srv/db
|
||||
- --no-global-config
|
||||
volumes:
|
||||
- "{{ caddy_logs_dir }}:/srv/logs:ro"
|
||||
- "{{ db_dir }}:/srv/db"
|
||||
- "{{ report_dir }}:/srv/report"
|
||||
networks:
|
||||
- "web_proxy_network"
|
||||
|
||||
goaccess_app:
|
||||
image: caddy:2.11.2
|
||||
container_name: goaccess_app
|
||||
restart: unless-stopped
|
||||
user: "{{ app_owner_uid }}:{{ app_owner_gid }}"
|
||||
command: caddy file-server --listen :8080 --root /srv --browse
|
||||
volumes:
|
||||
- "{{ report_dir }}:/srv:ro"
|
||||
networks:
|
||||
- "web_proxy_network"
|
||||
|
||||
networks:
|
||||
web_proxy_network:
|
||||
external: true
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/bin/sh
|
||||
# Tail Caddy's JSON access log, transform each entry into Apache CLF
|
||||
# Combined with the virtual host glued to the request URI, and feed
|
||||
# the stream straight into goaccess via stdin. Result: every line in
|
||||
# the Requests panel renders as `host.example.com/path`.
|
||||
|
||||
set -eu
|
||||
|
||||
ACCESS_LOG="/srv/logs/access.log"
|
||||
|
||||
JQ_FILTER='
|
||||
"\(.request.remote_ip // "-") - - " +
|
||||
"[\((.ts // 0) | gmtime | strftime("%d/%b/%Y:%H:%M:%S +0000"))] " +
|
||||
"\"\(.request.method) \(.request.host)\(.request.uri) \(.request.proto)\" " +
|
||||
"\(.status) \(.size) " +
|
||||
"\"\(.request.headers.Referer[0]? // "-")\" " +
|
||||
"\"\(.request.headers["User-Agent"][0]? // "-")\""
|
||||
'
|
||||
|
||||
tail -F -n +1 "$ACCESS_LOG" \
|
||||
| jq --unbuffered -rc "$JQ_FILTER" \
|
||||
| exec goaccess - "$@"
|
||||
@@ -3,7 +3,7 @@
|
||||
services:
|
||||
|
||||
gramps_app: &gramps_app
|
||||
image: ghcr.io/gramps-project/grampsweb:26.4.0
|
||||
image: ghcr.io/gramps-project/grampsweb:26.4.1
|
||||
container_name: gramps_app
|
||||
depends_on:
|
||||
- gramps_redis
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
services:
|
||||
|
||||
memos_app:
|
||||
image: neosmemo/memos:0.26.2
|
||||
image: neosmemo/memos:0.28.0
|
||||
container_name: memos_app
|
||||
restart: unless-stopped
|
||||
user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
|
||||
netdata:
|
||||
image: netdata/netdata:v2.9.0
|
||||
image: netdata/netdata:v2.10.3
|
||||
container_name: netdata
|
||||
restart: unless-stopped
|
||||
cap_add:
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# Resource alerts for a low-spec home server.
|
||||
# Overrides stock alerts where thresholds differ; baseline RAM use is ~80%, so stock 80/90% would fire constantly.
|
||||
|
||||
# RAM: warn at >92%, crit at >95% — by then less than ~200 MB free.
|
||||
alarm: ram_in_use
|
||||
on: system.ram
|
||||
class: Utilization
|
||||
type: System
|
||||
component: Memory
|
||||
calc: $used * 100 / ($used + $cached + $free + $buffers)
|
||||
units: %
|
||||
every: 10s
|
||||
warn: $this > 92
|
||||
crit: $this > 95
|
||||
delay: down 5m multiplier 1.5 max 1h
|
||||
summary: System memory utilization
|
||||
info: System memory utilization (used / total, excluding reclaimable cache)
|
||||
to: sysadmin
|
||||
|
||||
# CPU: replace stock 10min_cpu_usage with two windowed alerts.
|
||||
alarm: 10min_cpu_usage
|
||||
on: system.cpu
|
||||
enabled: no
|
||||
|
||||
alarm: cpu_warn_30m
|
||||
on: system.cpu
|
||||
class: Utilization
|
||||
type: System
|
||||
component: CPU
|
||||
lookup: average -30m unaligned of user,system,softirq,irq,guest,guest_nice,nice
|
||||
units: %
|
||||
every: 1m
|
||||
warn: $this > 80
|
||||
delay: down 30m multiplier 1.5 max 2h
|
||||
summary: Sustained CPU load (30m avg)
|
||||
info: Average CPU utilization over the last 30 minutes
|
||||
to: sysadmin
|
||||
|
||||
alarm: cpu_crit_15m
|
||||
on: system.cpu
|
||||
class: Utilization
|
||||
type: System
|
||||
component: CPU
|
||||
lookup: average -15m unaligned of user,system,softirq,irq,guest,guest_nice,nice
|
||||
units: %
|
||||
every: 1m
|
||||
crit: $this > 95
|
||||
delay: down 30m multiplier 1.5 max 2h
|
||||
summary: High CPU load (15m avg)
|
||||
info: Average CPU utilization over the last 15 minutes
|
||||
to: sysadmin
|
||||
|
||||
# Disk: warn at >75%, crit at >90% on every mounted filesystem.
|
||||
template: disk_space_usage
|
||||
on: disk.space
|
||||
class: Utilization
|
||||
type: System
|
||||
component: Disk
|
||||
calc: $used * 100 / ($avail + $used)
|
||||
units: %
|
||||
every: 1m
|
||||
warn: $this > 75
|
||||
crit: $this > 90
|
||||
delay: down 15m multiplier 1.5 max 1h
|
||||
summary: Disk space utilization
|
||||
info: Disk space utilization on ${label:mount_point}
|
||||
to: sysadmin
|
||||
@@ -0,0 +1,45 @@
|
||||
# Override stock health_alarm_notify.conf — route every alert to apprise.
|
||||
# Stock conf is sourced first; this only sets what differs.
|
||||
|
||||
SEND_EMAIL="NO"
|
||||
SEND_CUSTOM="YES"
|
||||
DEFAULT_RECIPIENT_CUSTOM="server"
|
||||
|
||||
role_recipients_custom[sysadmin]="server"
|
||||
role_recipients_custom[domainadmin]="server"
|
||||
role_recipients_custom[dba]="server"
|
||||
role_recipients_custom[webmaster]="server"
|
||||
role_recipients_custom[proxyadmin]="server"
|
||||
role_recipients_custom[silent]=""
|
||||
|
||||
custom_sender() {
|
||||
local apprise_url="http://apprise:8000/notify/${1}/"
|
||||
|
||||
local notif_type="info"
|
||||
case "${status}" in
|
||||
CRITICAL) notif_type="failure" ;;
|
||||
WARNING) notif_type="warning" ;;
|
||||
CLEAR) notif_type="success" ;;
|
||||
esac
|
||||
|
||||
local title="[${status}] ${name} on ${host}"
|
||||
local body="${status_message}: ${alarm}
|
||||
Chart: ${chart}
|
||||
Value: ${value} ${units}
|
||||
Info: ${info}
|
||||
Raised for: ${raised_for}"
|
||||
|
||||
local httpcode
|
||||
httpcode=$(docurl -X POST \
|
||||
--data-urlencode "title=${title}" \
|
||||
--data-urlencode "body=${body}" \
|
||||
--data-urlencode "type=${notif_type}" \
|
||||
"${apprise_url}")
|
||||
|
||||
if [ "${httpcode}" = "200" ]; then
|
||||
info "sent custom notification for ${name} on ${host}"
|
||||
return 0
|
||||
fi
|
||||
error "failed to send notification for ${name} on ${host} (HTTP ${httpcode})"
|
||||
return 1
|
||||
}
|
||||
@@ -3,7 +3,7 @@ services:
|
||||
# See sample https://github.com/outline/outline/blob/main/.env.sample
|
||||
|
||||
outline_app:
|
||||
image: outlinewiki/outline:1.6.1
|
||||
image: outlinewiki/outline:1.7.1
|
||||
container_name: outline_app
|
||||
user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}"
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
|
||||
remembos_app:
|
||||
image: "{{ yc_container_registry_repository }}/remembos:v0.1.5"
|
||||
image: "{{ yc_container_registry_repository }}/remembos:v0.2.0"
|
||||
container_name: remembos_app
|
||||
restart: unless-stopped
|
||||
user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
services:
|
||||
|
||||
|
||||
rssbridge_app:
|
||||
image: rssbridge/rss-bridge:2025-08-05
|
||||
container_name: rssbridge_app
|
||||
@@ -0,0 +1,36 @@
|
||||
# See versions: https://github.com/matrix-construct/tuwunel/releases
|
||||
# Configuration reference: https://github.com/matrix-construct/tuwunel/blob/main/tuwunel-example.toml
|
||||
|
||||
services:
|
||||
|
||||
tuwunel_app:
|
||||
image: jevolk/tuwunel:v1.6.1
|
||||
container_name: tuwunel_app
|
||||
restart: unless-stopped
|
||||
user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}"
|
||||
networks:
|
||||
- "web_proxy_network"
|
||||
volumes:
|
||||
- "{{ data_dir }}:/var/lib/tuwunel"
|
||||
environment:
|
||||
TUWUNEL_SERVER_NAME: "{{ tuwunel_server_name }}"
|
||||
TUWUNEL_DATABASE_PATH: "/var/lib/tuwunel"
|
||||
TUWUNEL_ADDRESS: "0.0.0.0"
|
||||
TUWUNEL_PORT: "6167"
|
||||
TUWUNEL_MAX_REQUEST_SIZE: "20000000"
|
||||
|
||||
TUWUNEL_ALLOW_REGISTRATION: "false"
|
||||
TUWUNEL_ALLOW_FEDERATION: "true"
|
||||
TUWUNEL_ALLOW_CHECK_FOR_UPDATES: "false"
|
||||
TUWUNEL_TRUSTED_SERVERS: '["matrix.org"]'
|
||||
|
||||
# Well-known delegation values returned to clients/servers that query tuwunel directly.
|
||||
# The canonical delegation is served by Caddy on {{ tuwunel_server_name }} (see Caddyfile).
|
||||
TUWUNEL_WELL_KNOWN_SERVER: "{{ tuwunel_well_known_server }}"
|
||||
TUWUNEL_WELL_KNOWN_CLIENT: "{{ tuwunel_well_known_client }}"
|
||||
|
||||
TUWUNEL_LOG: "info"
|
||||
|
||||
networks:
|
||||
web_proxy_network:
|
||||
external: true
|
||||
@@ -7,6 +7,9 @@
|
||||
- name: 'Configure dozzle'
|
||||
ansible.builtin.import_playbook: playbook-dozzle.yml
|
||||
|
||||
- name: 'Configure goaccess'
|
||||
ansible.builtin.import_playbook: playbook-goaccess.yml
|
||||
|
||||
- name: 'Configure gitea'
|
||||
ansible.builtin.import_playbook: playbook-gitea.yml
|
||||
|
||||
@@ -34,6 +37,15 @@
|
||||
- name: 'Configure calibre'
|
||||
ansible.builtin.import_playbook: playbook-calibre.yml
|
||||
|
||||
- name: 'Configure remembos'
|
||||
ansible.builtin.import_playbook: playbook-remembos.yml
|
||||
|
||||
- name: 'Configure apprise'
|
||||
ansible.builtin.import_playbook: playbook-apprise.yml
|
||||
|
||||
- name: 'Configure tuwunel'
|
||||
ansible.builtin.import_playbook: playbook-tuwunel.yml
|
||||
|
||||
#
|
||||
|
||||
- name: 'Configure homepage'
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
- name: "Configure apprise application"
|
||||
hosts: all
|
||||
|
||||
vars_files:
|
||||
- vars/secrets.yml
|
||||
- vars/vars.yml
|
||||
|
||||
vars:
|
||||
app_name: "apprise"
|
||||
app_user: "{{ app_name }}"
|
||||
app_owner_uid: 1104
|
||||
app_owner_gid: 1104
|
||||
base_dir: "{{ (application_dir, app_name) | path_join }}"
|
||||
config_dir: "{{ (base_dir, 'config') | path_join }}"
|
||||
|
||||
tasks:
|
||||
- name: "Create user and environment"
|
||||
ansible.builtin.import_role:
|
||||
name: owner
|
||||
vars:
|
||||
owner_name: "{{ app_user }}"
|
||||
owner_uid: "{{ app_owner_uid }}"
|
||||
owner_gid: "{{ app_owner_gid }}"
|
||||
owner_extra_groups: ["docker"]
|
||||
|
||||
- name: "Create application internal directories"
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: "directory"
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_user }}"
|
||||
mode: "0750"
|
||||
loop:
|
||||
- "{{ base_dir }}"
|
||||
- "{{ config_dir }}"
|
||||
|
||||
- name: "Copy apprise config"
|
||||
ansible.builtin.template:
|
||||
src: "./files/{{ app_name }}/server.template.cfg"
|
||||
dest: "{{ config_dir }}/server.cfg"
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_user }}"
|
||||
mode: "0640"
|
||||
|
||||
- name: "Copy docker compose file"
|
||||
ansible.builtin.template:
|
||||
src: "./files/{{ app_name }}/docker-compose.template.yml"
|
||||
dest: "{{ base_dir }}/docker-compose.yml"
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_user }}"
|
||||
mode: "0640"
|
||||
|
||||
- name: "Run application with docker compose"
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ base_dir }}"
|
||||
state: "present"
|
||||
remove_orphans: true
|
||||
tags:
|
||||
- run-app
|
||||
+21
-1
@@ -4,11 +4,15 @@
|
||||
|
||||
vars_files:
|
||||
- vars/secrets.yml
|
||||
- vars/vars.yml
|
||||
|
||||
vars:
|
||||
backup_config_dir: "/etc/backup"
|
||||
backup_config_file: "{{ (backup_config_dir, 'config.toml') | path_join }}"
|
||||
|
||||
rclone_config_dir: "/etc/rclone"
|
||||
rclone_config_file: "{{ (rclone_config_dir, 'rclone.conf') | path_join }}"
|
||||
|
||||
restic_shell_script: "{{ (bin_prefix, 'restic-shell.sh') | path_join }}"
|
||||
backup_all_script: "{{ (bin_prefix, 'backup-all.py') | path_join }}"
|
||||
|
||||
@@ -21,6 +25,22 @@
|
||||
group: root
|
||||
mode: "0755"
|
||||
|
||||
- name: "Create rclone config directory"
|
||||
ansible.builtin.file:
|
||||
path: "{{ rclone_config_dir }}"
|
||||
state: "directory"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
|
||||
- name: "Create rclone config file"
|
||||
ansible.builtin.template:
|
||||
src: "files/backups/rclone.template.conf"
|
||||
dest: "{{ rclone_config_file }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0640"
|
||||
|
||||
- name: "Create backup config file"
|
||||
ansible.builtin.template:
|
||||
src: "files/backups/config.template.toml"
|
||||
@@ -39,7 +59,7 @@
|
||||
|
||||
- name: "Copy restic shell script"
|
||||
ansible.builtin.template:
|
||||
src: "files/backups/restic-shell.sh.j2"
|
||||
src: "files/backups/restic-shell.template.sh"
|
||||
dest: "{{ restic_shell_script }}"
|
||||
owner: root
|
||||
group: root
|
||||
|
||||
+32
-2
@@ -4,6 +4,7 @@
|
||||
|
||||
vars_files:
|
||||
- vars/secrets.yml
|
||||
- vars/vars.yml
|
||||
|
||||
vars:
|
||||
app_name: "caddyproxy"
|
||||
@@ -41,9 +42,38 @@
|
||||
- "{{ config_dir }}"
|
||||
- "{{ caddy_file_dir }}"
|
||||
|
||||
# Shared HTTP access log directory: caddy writes here, other
|
||||
# containers (goaccess, etc.) mount it read-only. Dir mode 0755
|
||||
# so anyone can list/read; the file mode itself comes from the
|
||||
# `mode 644` option in the Caddyfile log snippet.
|
||||
- name: "Create shared caddy logs directory"
|
||||
ansible.builtin.file:
|
||||
path: "{{ caddy_logs_dir }}"
|
||||
state: "directory"
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_user }}"
|
||||
mode: "0755"
|
||||
|
||||
- name: "Find pre-existing caddy log files"
|
||||
ansible.builtin.find:
|
||||
paths: "{{ caddy_logs_dir }}"
|
||||
file_type: "file"
|
||||
register: caddy_log_files
|
||||
|
||||
# Lumberjack created earlier files with 0600 before we set `mode`
|
||||
# in the Caddyfile; relax them so existing rotated archives stay
|
||||
# readable to consumers.
|
||||
- name: "Relax mode on pre-existing caddy log files"
|
||||
ansible.builtin.file:
|
||||
path: "{{ item.path }}"
|
||||
mode: "0644"
|
||||
loop: "{{ caddy_log_files.files }}"
|
||||
loop_control:
|
||||
label: "{{ item.path }}"
|
||||
|
||||
- name: "Copy caddy file"
|
||||
ansible.builtin.template:
|
||||
src: "./files/{{ app_name }}/Caddyfile.j2"
|
||||
src: "./files/{{ app_name }}/Caddyfile.template"
|
||||
dest: "{{ (caddy_file_dir, 'Caddyfile') | path_join }}"
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_user }}"
|
||||
@@ -51,7 +81,7 @@
|
||||
|
||||
- name: "Copy docker compose file"
|
||||
ansible.builtin.template:
|
||||
src: "./files/{{ app_name }}/docker-compose.yml.j2"
|
||||
src: "./files/{{ app_name }}/docker-compose.template.yml"
|
||||
dest: "{{ base_dir }}/docker-compose.yml"
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_user }}"
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@
|
||||
ansible.builtin.command:
|
||||
cmd: >
|
||||
{{ eget_bin_path }} rclone/rclone --quiet --upgrade-only --to {{ eget_install_dir }} --asset zip
|
||||
--tag v1.73.2
|
||||
--tag v1.73.4
|
||||
changed_when: false
|
||||
|
||||
- name: "Install restic"
|
||||
|
||||
+2
-2
@@ -38,7 +38,7 @@
|
||||
|
||||
- name: "Copy backup script"
|
||||
ansible.builtin.template:
|
||||
src: "files/{{ app_name }}/backup.sh.j2"
|
||||
src: "files/{{ app_name }}/backup.template.sh"
|
||||
dest: "{{ base_dir }}/backup.sh"
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_user }}"
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
- name: "Copy docker compose file"
|
||||
ansible.builtin.template:
|
||||
src: "./files/{{ app_name }}/docker-compose.yml.j2"
|
||||
src: "./files/{{ app_name }}/docker-compose.template.yml"
|
||||
dest: "{{ base_dir }}/docker-compose.yml"
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_user }}"
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
---
|
||||
- name: "Configure goaccess application"
|
||||
hosts: all
|
||||
|
||||
vars_files:
|
||||
- vars/secrets.yml
|
||||
- vars/vars.yml
|
||||
|
||||
vars:
|
||||
app_name: "goaccess"
|
||||
app_user: "{{ app_name }}"
|
||||
app_owner_uid: 1106
|
||||
app_owner_gid: 1106
|
||||
base_dir: "{{ (application_dir, app_name) | path_join }}"
|
||||
|
||||
db_dir: "{{ (base_dir, 'db') | path_join }}"
|
||||
report_dir: "{{ (base_dir, 'report') | path_join }}"
|
||||
|
||||
tasks:
|
||||
- name: "Create user and environment"
|
||||
ansible.builtin.import_role:
|
||||
name: owner
|
||||
vars:
|
||||
owner_name: "{{ app_user }}"
|
||||
owner_uid: "{{ app_owner_uid }}"
|
||||
owner_gid: "{{ app_owner_gid }}"
|
||||
owner_extra_groups: ["docker"]
|
||||
|
||||
- name: "Create internal application directories"
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: "directory"
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_user }}"
|
||||
mode: "0770"
|
||||
loop:
|
||||
- "{{ base_dir }}"
|
||||
- "{{ db_dir }}"
|
||||
- "{{ report_dir }}"
|
||||
|
||||
# Earlier runs left root-owned files inside db/report (the
|
||||
# containers used to start as root). Recurse-chown realigns them
|
||||
# so the now-non-root processor can rewrite/restore them.
|
||||
- name: "Realign ownership of generated artefacts"
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: "directory"
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_user }}"
|
||||
recurse: true
|
||||
loop:
|
||||
- "{{ db_dir }}"
|
||||
- "{{ report_dir }}"
|
||||
|
||||
- name: "Ensure caddy access log exists before goaccess starts"
|
||||
ansible.builtin.copy:
|
||||
content: ""
|
||||
dest: "{{ (caddy_logs_dir, 'access.log') | path_join }}"
|
||||
force: false
|
||||
owner: "root"
|
||||
group: "root"
|
||||
mode: "0644"
|
||||
|
||||
- name: "Copy docker compose file"
|
||||
ansible.builtin.template:
|
||||
src: "./files/{{ app_name }}/docker-compose.template.yml"
|
||||
dest: "{{ base_dir }}/docker-compose.yml"
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_user }}"
|
||||
mode: "0640"
|
||||
|
||||
- name: "Copy Dockerfile and entrypoint for the local jq-enabled goaccess image"
|
||||
ansible.builtin.copy:
|
||||
src: "./files/{{ app_name }}/{{ item.name }}"
|
||||
dest: "{{ (base_dir, item.name) | path_join }}"
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_user }}"
|
||||
mode: "{{ item.mode }}"
|
||||
loop:
|
||||
- {name: "Dockerfile", mode: "0640"}
|
||||
- {name: "entrypoint.sh", mode: "0750"}
|
||||
|
||||
- name: "Run application with docker compose"
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ base_dir }}"
|
||||
state: "present"
|
||||
build: "always"
|
||||
remove_orphans: true
|
||||
tags:
|
||||
- run-app
|
||||
@@ -6,7 +6,6 @@
|
||||
vars_files:
|
||||
- vars/secrets.yml
|
||||
- vars/homepage.yml
|
||||
- vars/homepage.images.yml
|
||||
|
||||
tasks:
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
vars_files:
|
||||
- vars/secrets.yml
|
||||
- vars/homepage.yml
|
||||
- vars/homepage.images.yml
|
||||
|
||||
tasks:
|
||||
- name: "Create user and environment"
|
||||
@@ -44,5 +43,6 @@
|
||||
project_src: "{{ base_dir }}"
|
||||
state: "present"
|
||||
remove_orphans: true
|
||||
pull: "always"
|
||||
tags:
|
||||
- run-app
|
||||
|
||||
+2
-2
@@ -39,7 +39,7 @@
|
||||
|
||||
- name: "Copy gobackup config"
|
||||
ansible.builtin.template:
|
||||
src: "./files/{{ app_name }}/gobackup.yml.j2"
|
||||
src: "./files/{{ app_name }}/gobackup.template.yml"
|
||||
dest: "{{ gobackup_config }}"
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_user }}"
|
||||
@@ -47,7 +47,7 @@
|
||||
|
||||
- name: "Copy backup script"
|
||||
ansible.builtin.template:
|
||||
src: "files/{{ app_name }}/backup.sh.j2"
|
||||
src: "files/{{ app_name }}/backup.template.sh"
|
||||
dest: "{{ base_dir }}/backup.sh"
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_user }}"
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
base_dir: "{{ (application_dir, app_name) | path_join }}"
|
||||
config_dir: "{{ (base_dir, 'config') | path_join }}"
|
||||
config_go_d_dir: "{{ (config_dir, 'go.d') | path_join }}"
|
||||
config_health_d_dir: "{{ (config_dir, 'health.d') | path_join }}"
|
||||
data_dir: "{{ (base_dir, 'data') | path_join }}"
|
||||
|
||||
tasks:
|
||||
@@ -37,6 +38,7 @@
|
||||
- "{{ data_dir }}"
|
||||
- "{{ config_dir }}"
|
||||
- "{{ config_go_d_dir }}"
|
||||
- "{{ config_health_d_dir }}"
|
||||
|
||||
- name: "Copy netdata config file"
|
||||
ansible.builtin.template:
|
||||
@@ -75,6 +77,43 @@
|
||||
loop: "{{ go_d_existing_files.files }}"
|
||||
when: (item.path | basename) not in (go_d_source_files.files | map(attribute='path') | map('basename') | list)
|
||||
|
||||
- name: "Find all health.d config files"
|
||||
ansible.builtin.find:
|
||||
paths: "files/{{ app_name }}/health.d"
|
||||
file_type: file
|
||||
delegate_to: localhost
|
||||
register: health_d_source_files
|
||||
|
||||
- name: "Template all health.d config files"
|
||||
ansible.builtin.template:
|
||||
src: "{{ item.path }}"
|
||||
dest: "{{ config_health_d_dir }}/{{ item.path | basename }}"
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_user }}"
|
||||
mode: "0640"
|
||||
loop: "{{ health_d_source_files.files }}"
|
||||
|
||||
- name: "Find existing health.d config files on server"
|
||||
ansible.builtin.find:
|
||||
paths: "{{ config_health_d_dir }}"
|
||||
file_type: file
|
||||
register: health_d_existing_files
|
||||
|
||||
- name: "Remove health.d config files that don't exist in source"
|
||||
ansible.builtin.file:
|
||||
path: "{{ item.path }}"
|
||||
state: absent
|
||||
loop: "{{ health_d_existing_files.files }}"
|
||||
when: (item.path | basename) not in (health_d_source_files.files | map(attribute='path') | map('basename') | list)
|
||||
|
||||
- name: "Copy health alarm notify config"
|
||||
ansible.builtin.template:
|
||||
src: "files/{{ app_name }}/health_alarm_notify.template.conf"
|
||||
dest: "{{ config_dir }}/health_alarm_notify.conf"
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_user }}"
|
||||
mode: "0640"
|
||||
|
||||
- name: "Grab docker group id."
|
||||
ansible.builtin.shell:
|
||||
cmd: |
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
- name: "Copy docker compose file"
|
||||
ansible.builtin.template:
|
||||
src: "./files/{{ app_name }}/docker-compose.yml.j2"
|
||||
src: "./files/{{ app_name }}/docker-compose.template.yml"
|
||||
dest: "{{ base_dir }}/docker-compose.yml"
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_user }}"
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
- name: "Configure tuwunel matrix server"
|
||||
hosts: all
|
||||
|
||||
vars_files:
|
||||
- vars/secrets.yml
|
||||
|
||||
vars:
|
||||
app_name: "tuwunel"
|
||||
app_user: "{{ app_name }}"
|
||||
app_owner_uid: 1105
|
||||
app_owner_gid: 1105
|
||||
base_dir: "{{ (application_dir, app_name) | path_join }}"
|
||||
data_dir: "{{ (base_dir, 'data') | path_join }}"
|
||||
backups_dir: "{{ (base_dir, 'backups') | path_join }}"
|
||||
|
||||
tuwunel_server_name: "vakhrushev.me"
|
||||
tuwunel_well_known_server: "matrix.vakhrushev.me:443"
|
||||
tuwunel_well_known_client: "https://matrix.vakhrushev.me"
|
||||
|
||||
tasks:
|
||||
- name: "Create user and environment"
|
||||
ansible.builtin.import_role:
|
||||
name: owner
|
||||
vars:
|
||||
owner_name: "{{ app_user }}"
|
||||
owner_uid: "{{ app_owner_uid }}"
|
||||
owner_gid: "{{ app_owner_gid }}"
|
||||
owner_extra_groups: ["docker"]
|
||||
|
||||
- name: "Create application internal directories"
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: "directory"
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_user }}"
|
||||
mode: "0750"
|
||||
loop:
|
||||
- "{{ base_dir }}"
|
||||
- "{{ data_dir }}"
|
||||
- "{{ backups_dir }}"
|
||||
|
||||
- name: "Disable backup script"
|
||||
ansible.builtin.file:
|
||||
dest: "{{ base_dir }}/backup.sh"
|
||||
state: absent
|
||||
|
||||
- name: "Create backup targets file"
|
||||
ansible.builtin.lineinfile:
|
||||
path: "{{ base_dir }}/backup-targets"
|
||||
line: "{{ item }}"
|
||||
create: true
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_user }}"
|
||||
mode: "0750"
|
||||
loop:
|
||||
- "{{ data_dir }}"
|
||||
|
||||
- name: "Copy docker compose file"
|
||||
ansible.builtin.template:
|
||||
src: "./files/{{ app_name }}/docker-compose.template.yml"
|
||||
dest: "{{ base_dir }}/docker-compose.yml"
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_user }}"
|
||||
mode: "0640"
|
||||
|
||||
- name: "Run application with docker compose"
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ base_dir }}"
|
||||
state: "present"
|
||||
remove_orphans: true
|
||||
tags:
|
||||
- run-app
|
||||
+3
-3
@@ -39,7 +39,7 @@
|
||||
|
||||
- name: "Copy gobackup config"
|
||||
ansible.builtin.template:
|
||||
src: "./files/{{ app_name }}/gobackup.yml.j2"
|
||||
src: "./files/{{ app_name }}/gobackup.template.yml"
|
||||
dest: "{{ gobackup_config }}"
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_user }}"
|
||||
@@ -47,7 +47,7 @@
|
||||
|
||||
- name: "Copy backup script"
|
||||
ansible.builtin.template:
|
||||
src: "files/{{ app_name }}/backup.sh.j2"
|
||||
src: "files/{{ app_name }}/backup.template.sh"
|
||||
dest: "{{ base_dir }}/backup.sh"
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_user }}"
|
||||
@@ -55,7 +55,7 @@
|
||||
|
||||
- name: "Copy docker compose file"
|
||||
ansible.builtin.template:
|
||||
src: "./files/{{ app_name }}/docker-compose.yml.j2"
|
||||
src: "./files/{{ app_name }}/docker-compose.template.yml"
|
||||
dest: "{{ base_dir }}/docker-compose.yml"
|
||||
owner: "{{ app_user }}"
|
||||
group: "{{ app_user }}"
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
|
||||
- name: 'Set up environment variables for user "{{ owner_name }}".'
|
||||
ansible.builtin.template:
|
||||
src: env.j2
|
||||
src: env.template
|
||||
dest: "/home/{{ owner_name }}/.env"
|
||||
owner: "{{ owner_name }}"
|
||||
group: "{{ owner_group }}"
|
||||
|
||||
@@ -80,6 +80,17 @@ def zj(ctx: Context) -> None:
|
||||
)
|
||||
|
||||
|
||||
@task(aliases=["login"])
|
||||
def login_as_app(ctx: Context, app: str) -> None:
|
||||
"""SSH и переключиться на пользователя приложения: inv login gitea"""
|
||||
# sudo -i: login shell, -u: от имени пользователя
|
||||
# bash -i: интерактивный режим (job control), -l: login (читает профиль)
|
||||
subprocess.run(
|
||||
f"""ssh {_remote_user()}@{_remote_host()} -t 'sudo -iu {app} bash -c "cd /mnt/applications/{app} && exec bash -il"'""",
|
||||
shell=True,
|
||||
)
|
||||
|
||||
|
||||
@task
|
||||
def btop(ctx: Context) -> None:
|
||||
"""Запустить btop на удалённом сервере"""
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
---
|
||||
homepage_nginx_image: "homepage-nginx:531c1cd-1772885069"
|
||||
@@ -6,5 +6,7 @@ app_owner_gid: 1009
|
||||
base_dir: "{{ (application_dir, app_name) | path_join }}"
|
||||
docker_registry_prefix: "cr.yandex/crplfk0168i4o8kd7ade"
|
||||
|
||||
homepage_nginx_image: "homepage-nginx:latest"
|
||||
|
||||
# Registry images
|
||||
registry_homepage_nginx_image: "{{ (docker_registry_prefix, homepage_nginx_image) | path_join }}"
|
||||
|
||||
+174
-156
@@ -1,157 +1,175 @@
|
||||
$ANSIBLE_VAULT;1.1;AES256
|
||||
30353464333261353263666333626634633162666239653631386263363431333734656631313930
|
||||
6539363363653239656166623839636131306462633834310a646438626666653163613762313637
|
||||
33316565343539376535303135366530386333313562306161633861306237313030386366656435
|
||||
3830353139613066350a666564333832393465316531353863373437646464316262396532383738
|
||||
36643034623263643038633162666265646661383461303335653935376330633033653730643235
|
||||
38663938333934353131393762363762366333306439396264346565336439653239353034386138
|
||||
65343832666363663731656263616263636662323038663835386437306564643265396434393832
|
||||
31373231636363656463366436303433616239646331353239363966393264623866636466323839
|
||||
61653037396130373763633265666663316666623961643131373564326361653038646530323964
|
||||
66383738303230313533653562356535343162633231636634646237663466313031346463393965
|
||||
65336164373666343932376234343133623531323163653961666432326134333865336262333336
|
||||
63643038636537323039616563613861386566393364306131326463366531363063613766383965
|
||||
66353237383734643564626239346662656435386166356161343031343633613331623433366465
|
||||
34613237383934343636613833353933613536386637626338326537373438646139623839366263
|
||||
31386137636362356339333530626565333561333634613964343133393038663033656433376165
|
||||
33323633383237343433636532613965303232306262326237383233656430363030366164643561
|
||||
37373930646233366439383635333133393631366265373439643635396231336263373266373338
|
||||
33613432616634666531623631356162363364643437623239643536336636396132313232656233
|
||||
64333032653930353430336364353335396130633634666131393166383765656362616139396265
|
||||
32653563366562383930623439363539333235326661383665336564623832313839306263386232
|
||||
61633734353733346266343936306364393835316635633061386661313262386630623933656436
|
||||
30633662613937326162666666353339363736373139626133316233383433373665623637326634
|
||||
62623165306431363931616335363339383836366363353565366136643661366638343361306438
|
||||
36346265383930326461653931626537306431633766653265373266623333643437323533353233
|
||||
30336437353739353861316134306361333063336233323437616630386532663934303032356238
|
||||
64626335653161313533636539353665656239336237633461346535636539393161643961613464
|
||||
30313366323937313265636563333261643732363161323934303463316663313033633831386536
|
||||
36313733326133643663353830643936613366653766636638643737323362303537636563336339
|
||||
66353930633761396666383138653339343632656232643866376337623732326635373735623362
|
||||
38613831356437383865653964656637396137386463316562663464383834323962333465356562
|
||||
62343864613364623639336334646462376366356138386162373763353930366663646463303736
|
||||
36323165373236643139303935333266323064633230353934343538616332653265373438633564
|
||||
62393332383533313564656434366435326362316666623763353563626138643732643531343238
|
||||
33313031313366656363343562653763613833653536613562343465376664376562356164613936
|
||||
66343763313262636532626363393562316661613130313866346165633862373136393661303936
|
||||
38633562663461623133653864306139393165386265313032653231306361393830653763663062
|
||||
63383430633734646134303239666532626264613332313136363238323139326237623035623964
|
||||
37663435353965383535646331343761383562653735363666666335613630373865643265643336
|
||||
32343664666239326337643366306531623834336463386637666564646338333433303134323564
|
||||
32323866323337303262363833313838623337323232616135363963363631336637623865373437
|
||||
31316539663237316162366338303763346139663435313037306338623339316637363133363965
|
||||
62303439383637353333333631363234343866623763616261666239316631353434323930373066
|
||||
32613334303566353830323064626537663339373130613533373739616236346337393033316638
|
||||
39303462633463306634373933396233666530303465323133386435333563373936623466376334
|
||||
39323131353866313263333436303831323166366165623263323730333663616665373261313232
|
||||
62333361623964316236303739363430636461396264333638363835646366633633326666326363
|
||||
30376339646361386337343561666230313066303263353430333436613464303965353634353831
|
||||
35343866396639366539653939313939656234663665666533326630306530333863383761613161
|
||||
61376433623635663636656564643135626239343663333532663366353165383463383038353232
|
||||
35346437383931653166323431353766393036626136643262623263363932396437303033356131
|
||||
31333066393166356665663963306366383837303036373930653462386232373563303630633636
|
||||
30363466353437306232383663363161396665393739306536643236663761323366613130656330
|
||||
30366661353939303030363833636238346134323738366138396336393463373563333632653262
|
||||
39633333353466313339386362656538336338373565323536646333616434626137323335353563
|
||||
31636564636537613033656236366362316134353733623961366333313165626637616536646361
|
||||
39623561656234356663313032326231313231363963643534666136343433663730333333633434
|
||||
35303363366133323330376163363662653534633431333035363837633831343861373633393930
|
||||
30626635353166303962313639326239623532396435623230353166323165656261643030356131
|
||||
65356632613834653063656138353261613231663465336366623564316165623932663530376631
|
||||
31616137386433336135323636613665356162353139306263633833336566346339666135626433
|
||||
30336361333736333334383162343336663631663333353739633036346633393763366636353161
|
||||
36373566613530366632666333646563336136663034663337313032643165643966303964613234
|
||||
38653332343037653464646635666634376138616636326531353865346663396534363338393465
|
||||
37393962343233633739313961663531616565363337346535303962663533643866363333353430
|
||||
35323864383338313837643266653937316432313834653038636561653238303164626362326432
|
||||
61626638636539303135646432643539306566316464383935333535343163393832316561323764
|
||||
38373239376363633237346664626137363562303134333563346434396335383235383032653637
|
||||
66356138396436623734316139373964373364633937373431386432333263326563326234623936
|
||||
62313830613436356337623865373432356165313930373362643037386239303937666637636332
|
||||
36666239343336316538623938376538383038386662396662626137633138663766396432353061
|
||||
66656665656535303535376161303562643035383961343832373137636335363534393861316332
|
||||
63353831616439376663616363363763323838366363396637313931616337633832643061616662
|
||||
31663738396162316538343763353330653739303963616530323064373331366433326465326532
|
||||
34303437346463366362346532376263356134633838373630323831643432663265313965633964
|
||||
63386361303666376162653938346236636434666564623638646161656661393462613237363935
|
||||
66346661353735646261333339313638653061636130623963346235376234346138613762643566
|
||||
30323831613461383766643836666633636364323139333261303632613730663730336637646233
|
||||
37373733373930333735306532663930396331343037383032636461653463353862613362303332
|
||||
66663361373030363031396663616638396465313438396230633835633335636664356565316664
|
||||
61356632303035366638613762306636333439373534356239626439626465613837336539636532
|
||||
61393564613962303938393435306238383761396339656533653265633461666435353133333164
|
||||
33316630663131323634343637643435326237666134326534666563623734633634366636623731
|
||||
33666536396138393137333761633139323735303039313837366635623037333037663231353338
|
||||
61633962643830663038343530643330356137663665663537336564353730323565626135313662
|
||||
38646430306638663431373037323061313438666433313264633732303664323634303236356364
|
||||
32326131656438656661616638393462333532313737396235643830663832643435373362373533
|
||||
34313638626264363561653232323263643135323666626539636435303131336533623137643735
|
||||
38333734636133336431393634316566626266356165663639333461306463363637613366643433
|
||||
61313031623933633731353533346264363336326534366231366239373137666430376266383836
|
||||
33396365343161616238396133326433343535333032643533616439643236323561353236356536
|
||||
39663534666533393638376639653161376562636333616437623937383035313032636632396232
|
||||
62373961616262313936623663613837316539616330336339363436373465363662313836656662
|
||||
31333231653836306362643234383631666431663431666333366563393830343837393339313533
|
||||
65343034373265343737326232316162386434326661336530373639633838373766323437393937
|
||||
38313962356364306165393730356339336639303731393931323130386462663139353639626338
|
||||
30373162356131313737326164393164373837366632643064326635323934396239363033303938
|
||||
62376539313164333365336331393735346138376362333962396364323063636539346663613631
|
||||
66353062643261666538633430653036636461626161356265303037353665326461333335373562
|
||||
62363237356166363139616532333530373230623865383862383362376534363162336163633133
|
||||
37636564353836643963613333373837343431346538303738346462663739623932363831353534
|
||||
64613934323136623761633830313335306666643765343037363665303733613639343466393963
|
||||
62386361326362386535376364303237313564316661326337336434616134313464313565333136
|
||||
64663436643939623438366437643034666362386233666238363434613862386333323134363566
|
||||
61646661386139393731666632343066366139333638333366336137353833383931383835363934
|
||||
35666563303333303932386264316134383035613035663466656465336439346137656336656434
|
||||
34363438663962393434633761633336623966636465623665356433343236383862636435303038
|
||||
33313661336364346562666234396462646231666162306566636461613462643034653861663439
|
||||
39633361643733643533346561363232336465383633396461376239623837366135336238346332
|
||||
33373762613230353537656566353466623864636161316361396338613566336132303762306538
|
||||
36633663643136303262373365363063396163613464333662653430396636623335313061626561
|
||||
38636165346532373732333463373436323239386432326634613762303665343636343366656162
|
||||
39613035626133306139663831313236353739313930666339333133663839313932336433353531
|
||||
63373966633635336632363137326533363864303466366664656532623362663265613936663636
|
||||
32336265393330616437646664333464623635353662616133623264393935303530356330633834
|
||||
64626161646366306134303634356138663634396231353064343661373662646132653563656162
|
||||
36343864396263343561333137383238393063333634376634656231346238383862313264303263
|
||||
66343766353133316337356265343361363931373362303462663935323735393330623632623237
|
||||
34636565646461666431313362613862303530343537306563396236663435623164326134326530
|
||||
61356566313733363035346530613934376236383136633363316263393761383532393133323939
|
||||
38653731643330666261356236623039633262343031393330356565326261336230303132656332
|
||||
37353464336365633633643063353237626535326537653131633535306361353165313232366664
|
||||
65613966356634633134633563343036323539633036363437626466363865626333376134613731
|
||||
31363434373534613239333031333564653733363732343835346435633934356236356166393538
|
||||
38613432303032333830633161353938383632313137633437313131653664323934613432356635
|
||||
32656661303163646335633137363734323363653330623137613438623831366135393261343833
|
||||
61383839623733346434653739626365376266353430363131636665636639333037646262346465
|
||||
65393934373564386335343865656161306635653833373430663064616264653135316530303338
|
||||
38366434323961396638663534396530636639613338643463326432646366646163316363383832
|
||||
31373331366565376439643131343631393663666434363838316435653132393034643536353332
|
||||
66343530363564346335623464653862643632356638656535353666353336323033356336636134
|
||||
39316165633562303239623863613633313238323862393636336464633934316266313235343736
|
||||
38353435663166643566653236623039353465303431383132306135323239346135323831336363
|
||||
65396136666433393334393731373437383631313139666139663239303330363031356330373035
|
||||
34366264396332643163626237636132623532326636396461356566363835363163643161396130
|
||||
30663637346464646333623833393534613464336432336239386238393630376435343535356464
|
||||
36393539653632393430663035613539626165396463323739353263636364346662653535633261
|
||||
63663461623435386231653232313539356131346534636137383562313733663031653730353533
|
||||
37393233363635643339373530376130326333643764356634643161333135663436663437373136
|
||||
38393163343166666237663062313236373938646263356530363136383666636536616436626139
|
||||
66633038303961336537613030323639383666333930623161633061643131313235333235303561
|
||||
64633631333964336164363963346163303730306334353834343061353661633436363536313839
|
||||
30366535376537313065303839323939396465623734656233396535316465333436323937323232
|
||||
36656662316232306330393435333836363234666161656434653033643236303864356137393834
|
||||
38643234663365663037353663616439383430616665643061363531343765363034386432626564
|
||||
62323630613337663562616664613138623262326134343430623661656166653135396562323364
|
||||
38616165316464376563636535643839346665613465316662363566313337333666383336303064
|
||||
37633130323464393831323962356633663430343761393932323266353864363062653132323639
|
||||
33656337666236303764653834326230613165636437306337393132626339663161623536376138
|
||||
32343063313864643164613739653038663430633466356331313638343766393166306138336239
|
||||
39303938303861343331626231303333343931306136373533613466616336366261313866656631
|
||||
37336566623933656433646465306237383334363264333532386537343336363933636133306263
|
||||
37333033633461656236623433356438643936343861363833306333386636633463356365643730
|
||||
61303931326162353638363035666333333661393262306234366165383530663231656334623133
|
||||
30363436616266313062636232313435376462633734393130656239396462613039623236396330
|
||||
62666336663661623239666530656664346461363436303930306338303031623461646137366432
|
||||
62643332353135663935
|
||||
30613937343031343632383733623435366535373231316163393436363636656462326262383565
|
||||
3032663665323131626263356531633934326639636231620a363635376263333438336331343366
|
||||
36386337323165333861633062656433313062343764636138663533333639316336306230653732
|
||||
3331336137616263630a306135333566646434663231383138363966386661643836626561376338
|
||||
33636362323937386664646630383062613535666431393634316337626564613733313861386238
|
||||
39316263666662633066633836366236346431313531656339613566303962656165396662326563
|
||||
65623036333932393739646162353836646562643866396263386232633933326538316637656365
|
||||
38656562383861613030306635613236646235613436316635386531656666363738396461313263
|
||||
30653934366537303133613962653137633131323431396266646339376339623034373963666438
|
||||
66383464303431353962323032316533613138383831343036383230303931326433396333623935
|
||||
37396432376637373135666236333332383262323931616432343665653836626265376632643765
|
||||
33303835393863333334653664613337343063313362363136383234666335636565383237656639
|
||||
34323839613765626231303230616661626530633530333165373535663139643339656438396237
|
||||
36656134643636643733363336343739616532666130393863666665393138383261353730626565
|
||||
38306133343463306462656534326431623238336562653433316233383861303032393437316336
|
||||
32353338613639653735393239333235633565636563313933333763323339656237326162316465
|
||||
66313034306263343462376632303539656533353265336366613338326439323732623438626162
|
||||
39393937613836656236383030343436303632363330313734333665643365666138633034323462
|
||||
66376333386237383666623434636662363338626538353933636632646236393630343739636666
|
||||
37666531633839363365633863646530396432613166313035353638313463373338313139616133
|
||||
62383234356665333132613664383931316238353863306538343831363233303862383737373939
|
||||
37303430303766343366633536643139363366663734326162366434333165613033653666383337
|
||||
62626538316463343466613065326666396266643661656164376336336532666134613663623163
|
||||
64316335633839356231393130343938613334393737666663363662356466326235666561653239
|
||||
39386635616165633063383032666366383861333038373636613663613461316433633562623664
|
||||
65373536663230356632663133356639323838653431333836376330316162633261333934363335
|
||||
34383937343063303835626435316534356239316230326566383036646237336238623036323161
|
||||
62326264636130323965313866616631663039623431363139363462663435323866393437373566
|
||||
37353463353731303434303435353061633531663464656336306439373238633038343237313133
|
||||
34333463626261333038363438343034373335346332316430376436656331626664376664323037
|
||||
32393631663434383265326231353035356333343739386132326435653438306136373237396539
|
||||
33613462623562343966343933363037326234323836363636313938666534333337646139326533
|
||||
61633666623936646366643336333339303633643230393465623031643963643635313264353236
|
||||
65313631663430336262326463663938386630363464386230383766376363373235366438393635
|
||||
65313232383334666263626662646264393565326164613364313138303638653333653963316561
|
||||
34346464653637376433356335663930396432386238366132393562393162353235393438633533
|
||||
62656533316431666463633530653832356263653030326366663932306662613465643638313633
|
||||
62396562616463313066343832316238386234343537346436623039643132393562303130613331
|
||||
38653261353132633036623138643338366534396237613333333765653436363032616235373035
|
||||
35313966623531373636363638383862333935353931653861663966643531383335653739356565
|
||||
31373937396234616135653765643131666530383030343064366531336135366265633232653433
|
||||
65396566626232633831343734353432633462343336616135373861303836613463393736306133
|
||||
64663531643630326432376235386433623365373163366663623632333531623863623663643434
|
||||
36646134623665633531643732663137613862343666613139336231646564363266343935653263
|
||||
66653366666635666535636637626134363633336233613732656166373063333237323465616434
|
||||
39623238636235333866666536346430373735323530633133663937636366663530326465386161
|
||||
39346466386133656633373438333133303566363233626238366133636333656462373065613863
|
||||
65363439383163323332383931663833303234326132343462333835323664363461656566393065
|
||||
38646261323336316239363465343238643132306235613031626438323838653066376561626661
|
||||
34356530666665323230646436633935343861323638656638323163306236393865366630636236
|
||||
62386161646131623738333664636361396239643666323837646332383538623734386531313664
|
||||
65313632343365393130643137353735666565663030383231616231313237323866386336316361
|
||||
66656165643261653464316639613635323531306362353164373531326461666437303434346233
|
||||
31383864346233313633353065343236633636386138323761666662373564623234613965323131
|
||||
36313861316563333262306434663265313237626631396561303236343330633738356666633663
|
||||
61313663336237653361383963333764336137396666613634313036373564353564643334623363
|
||||
62353531306532323664376363383938646536393339346666656339393230613362666337663861
|
||||
37633633653463343430666634643863383438633933343839663865616136363538643061343437
|
||||
34613037353835613866303230303162396531626663616164343263633261363335313936666339
|
||||
63383533616530356262363838636466333038656339316364626263383731313464313734613630
|
||||
62646266666136616632636161363631623362346230643134663664396565323932343462383661
|
||||
38303663653262333236613833396237663834333139316666343065396137306562613265343863
|
||||
65323065663862636230636664623132306231366462346432343030376236346465663831623537
|
||||
63633231333165613731626137656539366131633364623661616136616434306563656139346137
|
||||
63313032343161623235306230633361666163623061333738383135636664623438323238663631
|
||||
37613964643931323432353431306564393639386437666539376238643065343738313265373661
|
||||
61303764646463326632653335323432646436353765633862623838386337623464333839643833
|
||||
39383961666234363638323735636231623962666461373435633631323530643237656464396465
|
||||
66623431393461613634373237646636333965396435663563363161626666356638366462373261
|
||||
37633238323135666136623663653665303832656437663536383236313334313461353032663933
|
||||
63643164363664663939613635373362376162336262653332663936313737396130366330656532
|
||||
31653463383132643262613839613962663836376463343661393736633633396164643264653431
|
||||
30663732303236653165386537653432656266363239373030333630353661666636303730373937
|
||||
65363237366333376133306437376534636133356238326461333762326563386265363636323831
|
||||
39343665386262336265383865343563343832623766656534306661326462333561373835366631
|
||||
39636361623831623533353962633363393531313530363833613962616331653565633733303964
|
||||
32393433303938323566646264323761633035653231353761643261663839313665663434643834
|
||||
65356432393431336235306437643861653437643362363839623634333835376636623664616139
|
||||
66376562633232636431626436653161333137633466313433663433383230636337653535643430
|
||||
61613032656135323765613837626266313632353661346636643866613138303930346563623738
|
||||
35613831623565353432336338373465303437623234313736353661353430656661366365373230
|
||||
33646134356661616164303865623464306339653439613365626261323237623135346537393535
|
||||
62393465343134626333333462316331656134383362383031353863316632393061333933336362
|
||||
36326662363833303436663166383365346433323866346462663261333330656666663162383564
|
||||
35336438643064313833393638323864343237616163383033313966303262326135323335353931
|
||||
66333938393264323533353231303935346661653835386262306133393065356535643835663665
|
||||
38363930356530366135313734306464623739376438613430373634396339393864396264303135
|
||||
61356333636236326566386264353930626564636438616265353939383733663837313233356363
|
||||
63643835393437336366313030303864306536666638623430356263336234646462383666316431
|
||||
36313464346266646438383762313138376338323537386635636561656662306533316362396162
|
||||
38326165633532623933376165643861323735353831363264376162316561613038633961333337
|
||||
30646461636332623466643033633764333330353832616365376633643263336131313733653139
|
||||
39646239366261366465333962643565636430393464613866613038333636393362383636343534
|
||||
61323830616234633364346131336630393965373730343464366166376232346464636263323639
|
||||
63336464623733363139366665336131653163613833383261376138373032666663356637383832
|
||||
32313130633363346435383638616236633761616166663339316437353938636636613530383836
|
||||
64623661366130656439306266343435396334383564353466663339383862313733313931383463
|
||||
64323237656361383262343735366562623965356636343963363966616333313333646233373464
|
||||
33383939386262663730316333616663636161356463396362643237356532386162363131626461
|
||||
37323965313063623463356133626531393339336535303562343530316663613639646531323136
|
||||
30353732646237623264653963373863363965326338666264306562373932393333633639396131
|
||||
34303764396330326165636264313532393961303038623031336631653831323337306261333630
|
||||
37333964376533636132303335653935343932373330373632626235356437636165623436383036
|
||||
38313565373561393834316532333930356135623439373161643063643738353031353565396330
|
||||
37656162346433326638353439613666336534336562623633643230636134383931653538616665
|
||||
32393432383265613237323138386361353934373965306462393666616532653563626232643035
|
||||
61643732376434633537633663633130313437656166333239633533393334373163333566343430
|
||||
38633165353637306237316436663235633162353132646562353638333038663636323465633632
|
||||
34643037623634643534663366633133363030323966313065353333633636646636306565333238
|
||||
39336662626138306464613461343762316533656433626165323764616535623539336439396663
|
||||
63393365626235613063613934306132333162646237316364306637346136623061363236383765
|
||||
37353138363337346530626563366136333635663863313038643537366237633362343136396664
|
||||
61623237353433333238633163636565386134356565303763336238636366316330666339383365
|
||||
30323235356633656362353738393234616435663333613364316539636430623262643162313337
|
||||
36323466303832336530336566343731306362333862663537613339663562623739343636613162
|
||||
36343563373665376565366266343461643562636630623166626165636337613931653338633862
|
||||
65393138353661656265666335343263333063653430326532663839383433643966363639643636
|
||||
61376365363538636235666235623638376334363265626136313536353637386564303936636263
|
||||
32306239306339656238393864666135613663366332666135663461353366313833376430376263
|
||||
62613163303964333735396338373737653837666435656130376435376434356462383264636561
|
||||
34353563316132336663316166663832383939333634316562383634383838336531313731613666
|
||||
62643231636266353935343539366465376139643834306261623738313432306133653461383738
|
||||
39396332373364353833626661333634346131396337636235653431616336393666373231383030
|
||||
32306466613136346265653038636537646330643337663863383562323638616661333037323232
|
||||
35613138363330353533643064613366343339343032373737306364353135353334336666663732
|
||||
36343963613636376561666266623537316432666161326331383761323437383738373762643937
|
||||
30623737643239326261343939663065643265653363633661376265626637643336613635393335
|
||||
65373565333936333431656331633039323135336236656337343532643939386338663239393065
|
||||
65666536333732646235633762633032393463663334616165333834653938346230316236353839
|
||||
34396362386265646261373561636230363962663433303535373035346334353932643365383763
|
||||
32333239613961346466356562376663613062373162666264633636323833323263333765616563
|
||||
39646530343962353362363634336336323463623137646531373362353832343335366461646535
|
||||
38653735316536396438613866326438363036653833366636626130323437623366373833366165
|
||||
30303636666263323062343931306435363961643838636163366433376436303231316338613034
|
||||
37393631363632383461373566306365306631396335633432383939336332626237653462393136
|
||||
34316636643464363634366535333463326533333564633163363062666463343731396231656234
|
||||
39346333303465363037313063373366373439306333636465636366666437326362626264653033
|
||||
64613062343538303931646630373565663530336133633032366331626536353237336235633636
|
||||
63366639366439386530303966323563323862383865356630313636333333393464653762626634
|
||||
65366231613661313233626239303035323666346236636362393036353839333636343434646266
|
||||
65653039353966616361363335346565383863616161316134383365616636333732653233383261
|
||||
65616664343830353861616666616237313532363334653430313437313535666436383338396363
|
||||
33313436363061306431366332373936633034393733646137636338336431333033343532613531
|
||||
32613839393232646565663931303530376432376337613762346230646366613935383234313666
|
||||
61643339353933336434666466623133336637343534303737366162316561366632333335663233
|
||||
34643036326630306632353438643666623939393033646238353261386231626634303266303530
|
||||
64643436653234616332623835333165626135613465346162393335353133356233666536313632
|
||||
65616135666533343839666132623639343565303436623162383738353633613864356535646365
|
||||
32373337393936393830666365383462333437373539666633386361373135333163393334303235
|
||||
33663631386566356366666132616265373533373561616564343538303432346562356234336663
|
||||
32623866396434326264636539323132613239343938353739376539383139313833376563623434
|
||||
62636334326234313230666662396561393130396137306437393334323561356435343866386636
|
||||
32656337656439653830653365313031326562643437376538316561653963643232353434313538
|
||||
64373638616133666463393462643465306565646136643862363162643638343565316139626539
|
||||
64643939383936313035323936656438313039376635383733633032613165343130663930323166
|
||||
37343261333332663863366533386335373962323163616564376434636361356438393035656533
|
||||
30383139323931306232353664636662313036643431663536353035356139643761613235663837
|
||||
37613133363433356536316466343237613131386536356234343135323861396130663464323236
|
||||
63636563633031396465663563366263373938373531336239323138653531386535643332653736
|
||||
61396432356161643663623130656632633862333861656464613432623732656465376236313437
|
||||
65386630633036636663303633636134343739366562643062343030383138653466326636366266
|
||||
32633239343039633636313837643432333238366533393061646237626130303934356438633936
|
||||
38366265656365333338363431643432633463313438633361333764653637623964363732303737
|
||||
61343137653930353361653364656233343166633162313964306531383834356237343031396137
|
||||
34366135623530366164646532643636346233353563333031343931643037613463613639356238
|
||||
36306235336562333935643035313934366339623365616661616461653832336137336464393662
|
||||
38363433646139646633353162616661323433636531393339643562373538616430363061366330
|
||||
35333138613136323865346462653761666534343538313033663835653631363631623532663133
|
||||
64383135326333626438363066633366316364643332623030653230353861633837646362626333
|
||||
38363236616265313638626263316164323563616237653465353031353734333032323761393761
|
||||
31333331643161396338653330653537353634306139656536363665643437633433666236356334
|
||||
64386566623836306666653766626465646664303231613062663862613565393364303233333636
|
||||
61633631643437373235636133333832646463366633353939383834373362633539333766303661
|
||||
3132333365333061633665366432346636646564313437333061
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
apprise_external_port: 8000
|
||||
apprise_external_url: "http://127.0.0.1:{{ apprise_external_port }}"
|
||||
|
||||
# Shared HTTP access log written by caddyproxy and consumed by analytics
|
||||
# tools (goaccess and so on). Lives under the system log path so it is
|
||||
# decoupled from any individual application's data directory.
|
||||
caddy_logs_dir: "/var/log/caddy"
|
||||
Reference in New Issue
Block a user