Compare commits

32 Commits

Author SHA1 Message Date
av bc6fff68bb Outline: update to 1.7.1
Linting / YAML Lint (push) Successful in 9s
Linting / Ansible Lint (push) Failing after 33s
2026-05-04 12:24:02 +03:00
av 512f31b350 Tuwunel: update to 1.6.1 2026-05-04 12:23:44 +03:00
av d25a28c611 Netdata: add alerts for cpu, ram, disks
Linting / YAML Lint (push) Successful in 9s
Linting / Ansible Lint (push) Failing after 31s
2026-05-01 13:58:51 +03:00
av 472c7a984f Homepage: simplify deploy
Linting / YAML Lint (push) Successful in 9s
Linting / Ansible Lint (push) Failing after 30s
2026-05-01 11:15:39 +03:00
av df3a37e610 Backups: backup to home server storage
Linting / YAML Lint (push) Successful in 41s
Linting / Ansible Lint (push) Failing after 1m4s
2026-05-01 10:49:27 +03:00
av 8efab2002f Rename all j2 files to templates
Not according to convention, but it reads better.
2026-05-01 10:01:26 +03:00
av 6edb72077a Homepage: release homepage-nginx:89c2a66-1777484248 2026-04-29 20:37:38 +03:00
av 07eacad003 Netdata: update to 2.10.3
Linting / YAML Lint (push) Successful in 9s
Linting / Ansible Lint (push) Failing after 29s
2026-04-29 20:26:23 +03:00
av 3b1736534d GoAccess: combine host and path in reports 2026-04-29 20:26:05 +03:00
av 4d92b3bd3e GoAccess: add for caddy logs monitoring
Linting / YAML Lint (push) Successful in 10s
Linting / Ansible Lint (push) Failing after 33s
2026-04-29 20:10:08 +03:00
av 27834c6711 Dozzle: update to 10.5.0
Linting / YAML Lint (push) Successful in 9s
Linting / Ansible Lint (push) Failing after 29s
2026-04-27 09:39:24 +03:00
av 89f46566c8 Memos: update to 0.28.0 2026-04-27 09:39:01 +03:00
av 7f1809b4ca Netdata: update to 2.10.2
Linting / YAML Lint (push) Successful in 10s
Linting / Ansible Lint (push) Failing after 34s
2026-04-26 10:41:54 +03:00
av 6784381833 Outline: update to 1.7.0 2026-04-26 10:41:40 +03:00
av 7c42acf893 Gitea: update to 1.26.1 2026-04-26 10:41:24 +03:00
av 452f7973a9 Tuwunel: install matrix server
Linting / YAML Lint (push) Successful in 13s
Linting / Ansible Lint (push) Failing after 34s
2026-04-20 21:39:49 +03:00
av 303aefb75f Gitea: update to 1.26.0
Linting / YAML Lint (push) Successful in 12s
Linting / Ansible Lint (push) Failing after 35s
2026-04-19 13:54:55 +03:00
av 22307d81c9 Memos: update to 0.27.1 2026-04-19 13:54:41 +03:00
av cc811f954d Dozzle: update to 10.4.1 2026-04-19 13:54:25 +03:00
av f17c4ac227 backup: error count in title 2026-04-12 18:05:49 +03:00
av 25d20df5a9 backup: notifications as html 2026-04-12 18:03:25 +03:00
av 7e1a8e2e99 backup: method ordering 2026-04-12 18:00:32 +03:00
av b90b87caa1 backup: sort apps 2026-04-12 17:58:03 +03:00
av 75ce60d8a0 backup: extend application with scripts and backup paths 2026-04-12 17:53:17 +03:00
av 0aa34efd00 backup: add application finder class 2026-04-12 17:42:43 +03:00
av b7a18f1296 apps: updates 2026-04-12 17:33:59 +03:00
av 6bfb362b20 backups: notifications to email
Linting / YAML Lint (push) Successful in 9s
Linting / Ansible Lint (push) Failing after 31s
2026-04-04 13:16:54 +03:00
av 5f619eaccc backups: use apprise for notifications
Linting / YAML Lint (push) Successful in 9s
Linting / Ansible Lint (push) Failing after 29s
2026-04-04 11:30:53 +03:00
av 362d6d8710 apprise: changed to simple stateful setup 2026-04-04 11:25:27 +03:00
av 5e6df110c8 apprise: only one worker for small memory consumption 2026-04-04 10:54:27 +03:00
av a0543e13f4 Add apprise application for notifications 2026-04-04 10:50:10 +03:00
av 41fe116dd7 Remove old public key 2026-04-04 10:49:53 +03:00
53 changed files with 1152 additions and 433 deletions
+5 -3
View File
@@ -11,7 +11,7 @@ Ansible-проект для автоматизации личного серве
- `vars/*.yml` — переменные приложений и образов, `vars/secrets.yml` — зашифрованные секреты (vault).
- `roles/` — кастомные роли (`eget`, `owner`, `secrets`), галактические роли в `galaxy.roles/`.
- `files/<app>/` — docker-compose шаблоны, конфиги, скрипты бэкапов для каждого сервиса.
- `templates/` — общие шаблоны (например `env.j2`).
- `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).
@@ -68,11 +68,13 @@ uv run ansible-galaxy install --role-file requirements.yml
- `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-домен.
### Агрегатные и служебные
@@ -91,8 +93,8 @@ uv run ansible-galaxy install --role-file requirements.yml
## Шаблоны и переменные
- Суффиксы шаблонов: `.template.yml`, `.yml.j2`, `.template.sh` — рендерятся Ansible модулем `template`.
- Большинство приложений определяют переменные inline в плейбуке. Отдельные файлы переменных только у homepage и transcriber (`vars/homepage.yml`, `vars/transcriber.yml` + `*.images.yml`).
- Суффиксы шаблонов: `.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`.
+23
View File
@@ -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
+2
View File
@@ -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'
+1 -1
View File
@@ -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:
-25
View File
@@ -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
View File
@@ -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:
+17 -7
View File
@@ -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"
+6
View File
@@ -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 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
}
@@ -1,9 +1,9 @@
services:
{{ service_name }}:
caddyproxy:
image: caddy:2.11.2
restart: unless-stopped
container_name: {{ service_name }}
container_name: "caddyproxy"
ports:
- "80:80"
- "443:443"
@@ -11,9 +11,10 @@ services:
cap_add:
- 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 -1
View File
@@ -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:
+8
View File
@@ -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
+22
View File
@@ -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 - "$@"
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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 -1
View File
@@ -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
}
+1 -1
View File
@@ -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 -1
View File
@@ -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 }}"
+36
View File
@@ -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
+9
View File
@@ -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
@@ -37,6 +40,12 @@
- 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'
+60
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 }}"
+90
View File
@@ -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
-1
View File
@@ -6,7 +6,6 @@
vars_files:
- vars/secrets.yml
- vars/homepage.yml
- vars/homepage.images.yml
tasks:
+1 -1
View File
@@ -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
View File
@@ -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 }}"
+39
View File
@@ -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: |
+1 -1
View File
@@ -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 }}"
+73
View File
@@ -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
View File
@@ -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 }}"
+1 -1
View File
@@ -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 }}"
-2
View File
@@ -1,2 +0,0 @@
---
homepage_nginx_image: "homepage-nginx:531c1cd-1772885069"
+2
View File
@@ -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
View File
@@ -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
+8
View File
@@ -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"