Compare commits

27 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
53 changed files with 1047 additions and 386 deletions
+5 -3
View File
@@ -11,7 +11,7 @@ Ansible-проект для автоматизации личного серве
- `vars/*.yml` — переменные приложений и образов, `vars/secrets.yml` — зашифрованные секреты (vault). - `vars/*.yml` — переменные приложений и образов, `vars/secrets.yml` — зашифрованные секреты (vault).
- `roles/` — кастомные роли (`eget`, `owner`, `secrets`), галактические роли в `galaxy.roles/`. - `roles/` — кастомные роли (`eget`, `owner`, `secrets`), галактические роли в `galaxy.roles/`.
- `files/<app>/` — docker-compose шаблоны, конфиги, скрипты бэкапов для каждого сервиса. - `files/<app>/` — docker-compose шаблоны, конфиги, скрипты бэкапов для каждого сервиса.
- `templates/` — общие шаблоны (например `env.j2`). - `templates/` — общие шаблоны (например `env.template`).
- `scripts/` — вспомогательные Python-скрипты (SMTP-утилиты для Yandex Cloud Postbox). - `scripts/` — вспомогательные Python-скрипты (SMTP-утилиты для Yandex Cloud Postbox).
- `.gitea/workflows/lint.yml` — CI: yamllint + ansible-lint. - `.gitea/workflows/lint.yml` — CI: yamllint + ansible-lint.
- `lefthook.yml` — pre-commit хуки (ruff, mypy, yamllint, ansible-lint, gitleaks, проверка vault). - `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-rssbridge.yml` — RSS-агрегатор.
- `playbook-netdata.yml` — мониторинг. - `playbook-netdata.yml` — мониторинг.
- `playbook-dozzle.yml` — просмотр Docker-логов. - `playbook-dozzle.yml` — просмотр Docker-логов.
- `playbook-goaccess.yml` — аналитика веб-логов Caddy в реальном времени.
- `playbook-gramps.yml` — генеалогия. - `playbook-gramps.yml` — генеалогия.
- `playbook-calibre.yml` — управление электронными книгами. - `playbook-calibre.yml` — управление электронными книгами.
- `playbook-transcriber.yml` — транскрибация (образ из Yandex Registry). - `playbook-transcriber.yml` — транскрибация (образ из Yandex Registry).
- `playbook-wanderer.yml` — пешие маршруты. - `playbook-wanderer.yml` — пешие маршруты.
- `playbook-remembos.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`. - Суффиксы шаблонов: `.template.yml`, `.template.sh`, `.template.cfg`, `.template.conf`, `.template.toml`, `.template` (для файлов без естественного расширения) — рендерятся Ansible модулем `template`. Расширение оригинального формата сохраняется после `.template.` ради подсветки синтаксиса в редакторе.
- Большинство приложений определяют переменные inline в плейбуке. Отдельные файлы переменных только у homepage и transcriber (`vars/homepage.yml`, `vars/transcriber.yml` + `*.images.yml`). - Большинство приложений определяют переменные inline в плейбуке. Отдельные файлы переменных только у homepage и transcriber (`vars/homepage.yml`, `vars/transcriber.yml` + `vars/transcriber.images.yml`).
- Общие переменные из `vars/secrets.yml`: `application_dir`, `bin_prefix`, `primary_user` и др. - Общие переменные из `vars/secrets.yml`: `application_dir`, `bin_prefix`, `primary_user` и др.
- Каждое приложение: `app_name`, `app_user`, `app_owner_uid`, `app_owner_gid`, `base_dir`, `data_dir`. - Каждое приложение: `app_name`, `app_user`, `app_owner_uid`, `app_owner_gid`, `base_dir`, `data_dir`.
+3 -1
View File
@@ -7,7 +7,9 @@ services:
ports: ports:
- "127.0.0.1:{{ apprise_external_port }}:8000" - "127.0.0.1:{{ apprise_external_port }}:8000"
networks: networks:
- "web_proxy_network" web_proxy_network:
aliases:
- "apprise"
volumes: volumes:
- "{{ config_dir }}:/config" - "{{ config_dir }}:/config"
environment: environment:
-1
View File
@@ -1 +0,0 @@
tgram://{{ notifications_tg_bot_token }}/{{ notifications_tg_chat_id }}
+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' subject: 'group:admins'
policy: 'two_factor' policy: 'two_factor'
- domain: 'goaccess.vakhrushev.me'
subject: 'group:admins'
policy: 'two_factor'
- domain: 'wanderbase.vakhrushev.me' - domain: 'wanderbase.vakhrushev.me'
subject: 'group:admins' subject: 'group:admins'
policy: 'two_factor' policy: 'two_factor'
+1 -1
View File
@@ -2,7 +2,7 @@ services:
authelia_app: authelia_app:
container_name: '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 }}' user: '{{ owner_create_result.uid }}:{{ owner_create_result.group }}'
restart: 'unless-stopped' restart: 'unless-stopped'
networks: networks:
+243 -184
View File
@@ -11,6 +11,7 @@ import sys
import subprocess import subprocess
import logging import logging
import pwd import pwd
import time
from abc import ABC from abc import ABC
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
@@ -43,16 +44,38 @@ logger = logging.getLogger(__name__)
@dataclass @dataclass
class Config: class Config:
host_name: str host_name: str
roots: List[Path]
@dataclass @dataclass
class Application: class Application:
path: Path path: Path
owner: str 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): class Storage(ABC):
name: str
def backup(self, backup_dirs: List[str]) -> bool: def backup(self, backup_dirs: List[str]) -> bool:
"""Backup directories""" """Backup directories"""
raise NotImplementedError() raise NotImplementedError()
@@ -65,19 +88,15 @@ class ResticStorage(Storage):
self.name = name self.name = name
self.restic_repository = str(params.get("restic_repository", "")) self.restic_repository = str(params.get("restic_repository", ""))
self.restic_password = str(params.get("restic_password", "")) 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( env_raw = params.get("env") or {}
[ if not isinstance(env_raw, dict):
self.restic_repository, raise ValueError(
self.restic_password, f"'env' must be a table for storage backend ResticStorage: '{self.name}'"
self.aws_access_key_id, )
self.aws_secret_access_key, self.env: Dict[str, str] = {str(k): str(v) for k, v in env_raw.items()}
self.aws_default_region,
] if not self.restic_repository or not self.restic_password:
):
raise ValueError( raise ValueError(
f"Missing storage configuration values for backend ResticStorage: '{self.name}'" f"Missing storage configuration values for backend ResticStorage: '{self.name}'"
) )
@@ -93,19 +112,13 @@ class ResticStorage(Storage):
return False return False
def __backup_internal(self, backup_dirs: List[str]) -> bool: 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) logger.info("Destination: %s", self.restic_repository)
env = os.environ.copy() env = os.environ.copy()
env.update( env["RESTIC_REPOSITORY"] = self.restic_repository
{ env["RESTIC_PASSWORD"] = self.restic_password
"RESTIC_REPOSITORY": self.restic_repository, env.update(self.env)
"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,
}
)
backup_cmd = ["restic", "backup", "--verbose"] + backup_dirs backup_cmd = ["restic", "backup", "--verbose"] + backup_dirs
result = subprocess.run(backup_cmd, env=env, capture_output=True, text=True) result = subprocess.run(backup_cmd, env=env, capture_output=True, text=True)
@@ -154,7 +167,7 @@ class ResticStorage(Storage):
class Notifier(ABC): class Notifier(ABC):
def send(self, html_message: str) -> None: def send(self, title: str, html_message: str) -> None:
raise NotImplementedError() raise NotImplementedError()
@@ -170,9 +183,10 @@ class AppriseNotifier(Notifier):
f"Missing notification configuration values for backend {name}" f"Missing notification configuration values for backend {name}"
) )
def send(self, html_message: str) -> None: def send(self, title: str, html_message: str) -> None:
url = f"{self.api_url}/notify/{self.tag}/" url = f"{self.api_url}/notify/{self.tag}/"
payload = { payload = {
"title": title,
"body": html_message, "body": html_message,
"format": "html", "format": "html",
} }
@@ -187,24 +201,13 @@ class AppriseNotifier(Notifier):
) )
class BackupManager: class ApplicationFinder:
def __init__( def __init__(self, roots: List[Path]):
self, self.roots = roots
config: Config,
roots: List[Path],
storages: List[Storage],
notifiers: List[Notifier],
):
self.errors: List[str] = []
self.warnings: List[str] = [] 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]: 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] = [] applications: List[Application] = []
source_dirs = itertools.chain(*(root.iterdir() for root in self.roots)) source_dirs = itertools.chain(*(root.iterdir() for root in self.roots))
@@ -215,32 +218,182 @@ class BackupManager:
try: try:
stat_info = app_dir.stat() stat_info = app_dir.stat()
owner = pwd.getpwuid(stat_info.st_uid).pw_name 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: except (KeyError, OSError) as e:
logger.warning(f"Could not get owner for {app_dir}: {e}") logger.warning(f"Could not get owner for {app_dir}: {e}")
applications.sort(key=lambda app: app.path.name)
return applications return applications
def find_backup_script(self, app_dir: str) -> Optional[str]: def _find_backup_script(self, app_dir: Path) -> Optional[Path]:
"""Find backup script in user's home directory""" """Find executable backup script in application directory."""
possible_scripts = [ for name in ("backup.sh", "backup"):
os.path.join(app_dir, "backup.sh"), script_path = app_dir / name
os.path.join(app_dir, "backup"), if script_path.exists():
]
for script_path in possible_scripts:
if os.path.exists(script_path):
# Check if file is executable
if os.access(script_path, os.X_OK): if os.access(script_path, os.X_OK):
return script_path return script_path
else: else:
logger.warning( logger.warning(
f"Backup script {script_path} exists but is not executable" f"Backup script {script_path} exists but is not executable"
) )
return None 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""" """Run backup script as the specified user"""
try: try:
logger.info(f"Running backup script {script_path} (user {username})") logger.info(f"Running backup script {script_path} (user {username})")
@@ -279,149 +432,51 @@ class BackupManager:
self.errors.append(f"App {username}: {error_msg}") self.errors.append(f"App {username}: {error_msg}")
return False return False
def get_backup_directories(self) -> List[str]: def _send_notification(self, success: bool) -> None:
"""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:
"""Send notification to Notifiers""" """Send notification to Notifiers"""
host = self.config.host_name
if success and not self.errors: 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: 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: 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: if self.successful_backups:
message += ( items = "".join(f"<li>{b}</li>" for b in self.successful_backups)
f"\n\n✅ Успешные бекапы: {', '.join(self.successful_backups)}" message += f"<p>✅ Успешные бекапы:</p><ul>{items}</ul>"
)
if self.warnings: 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: 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: for notificator in self.notifiers:
try: try:
notificator.send(message) notificator.send(title, message)
except Exception as e: except Exception as e:
logger.error(f"Failed to send notification: {str(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 def initialize(
applications = self.find_applications() config_path: Path,
logger.info(f"Found {len(applications)} application directories") ) -> tuple[ApplicationFinder, BackupManager]:
# 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:
try: try:
with config_path.open("rb") as config_file: with config_path.open("rb") as config_file:
raw_config = tomllib.load(config_file) raw_config = tomllib.load(config_file)
@@ -458,17 +513,21 @@ def initialize(config_path: Path) -> BackupManager:
if not notifiers: if not notifiers:
raise ValueError("At least one notification backend must be configured") raise ValueError("At least one notification backend must be configured")
config = Config(host_name=host_name, roots=roots) config = Config(host_name=host_name)
app_finder = ApplicationFinder(roots)
return BackupManager( backup_manager = BackupManager(
config=config, roots=roots, storages=storages, notifiers=notifiers config=config, storages=storages, notifiers=notifiers
) )
return app_finder, backup_manager
def main() -> None: def main() -> None:
try: try:
backup_manager = initialize(CONFIG_PATH) app_finder, backup_manager = initialize(CONFIG_PATH)
success = backup_manager.run_backup_process() applications = app_finder.find_applications()
backup_manager.warnings.extend(app_finder.warnings)
success = backup_manager.run_backup_process(applications)
if not success: if not success:
sys.exit(1) sys.exit(1)
except KeyboardInterrupt: except KeyboardInterrupt:
+13 -3
View File
@@ -8,9 +8,19 @@ roots = [
type = "restic" type = "restic"
restic_repository = "{{ restic_repository }}" restic_repository = "{{ restic_repository }}"
restic_password = "{{ restic_password }}" restic_password = "{{ restic_password }}"
aws_access_key_id = "{{ restic_s3_access_key }}"
aws_secret_access_key = "{{ restic_s3_access_secret }}" [storage.yandex_cloud_s3.env]
aws_default_region = "{{ restic_s3_region }}" 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] [notifier.apprise]
type = "apprise" type = "apprise"
+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 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 # Applications
# ------------------------------------------------------------------- # -------------------------------------------------------------------
vakhrushev.me { vakhrushev.me {
tls anwinged@ya.ru 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 { reverse_proxy {
to homepage_app:80 to tuwunel_app:6167
} }
} }
auth.vakhrushev.me { auth.vakhrushev.me {
tls anwinged@ya.ru tls anwinged@ya.ru
import access_log
reverse_proxy authelia_app:9091 reverse_proxy authelia_app:9091
} }
status.vakhrushev.me, :29999 { status.vakhrushev.me, :29999 {
tls anwinged@ya.ru tls anwinged@ya.ru
import access_log
forward_auth authelia_app:9091 { forward_auth authelia_app:9091 {
uri /api/authz/forward-auth uri /api/authz/forward-auth
@@ -43,6 +91,7 @@ status.vakhrushev.me, :29999 {
git.vakhrushev.me { git.vakhrushev.me {
tls anwinged@ya.ru tls anwinged@ya.ru
import access_log
reverse_proxy { reverse_proxy {
to gitea_app:3000 to gitea_app:3000
@@ -51,6 +100,7 @@ git.vakhrushev.me {
outline.vakhrushev.me { outline.vakhrushev.me {
tls anwinged@ya.ru tls anwinged@ya.ru
import access_log
reverse_proxy { reverse_proxy {
to outline_app:3000 to outline_app:3000
@@ -59,6 +109,7 @@ outline.vakhrushev.me {
gramps.vakhrushev.me { gramps.vakhrushev.me {
tls anwinged@ya.ru tls anwinged@ya.ru
import access_log
reverse_proxy { reverse_proxy {
to gramps_app:5000 to gramps_app:5000
@@ -67,6 +118,7 @@ gramps.vakhrushev.me {
miniflux.vakhrushev.me { miniflux.vakhrushev.me {
tls anwinged@ya.ru tls anwinged@ya.ru
import access_log
reverse_proxy { reverse_proxy {
to miniflux_app:8080 to miniflux_app:8080
@@ -75,6 +127,7 @@ miniflux.vakhrushev.me {
wakapi.vakhrushev.me { wakapi.vakhrushev.me {
tls anwinged@ya.ru tls anwinged@ya.ru
import access_log
reverse_proxy { reverse_proxy {
to wakapi_app:3000 to wakapi_app:3000
@@ -83,6 +136,7 @@ wakapi.vakhrushev.me {
wanderer.vakhrushev.me { wanderer.vakhrushev.me {
tls anwinged@ya.ru tls anwinged@ya.ru
import access_log
reverse_proxy { reverse_proxy {
to wanderer_web:3000 to wanderer_web:3000
@@ -91,6 +145,7 @@ wanderer.vakhrushev.me {
memos.vakhrushev.me { memos.vakhrushev.me {
tls anwinged@ya.ru tls anwinged@ya.ru
import access_log
reverse_proxy { reverse_proxy {
to memos_app:5230 to memos_app:5230
@@ -99,6 +154,7 @@ memos.vakhrushev.me {
remembos.vakhrushev.me { remembos.vakhrushev.me {
tls anwinged@ya.ru tls anwinged@ya.ru
import access_log
forward_auth authelia_app:9091 { forward_auth authelia_app:9091 {
uri /api/authz/forward-auth uri /api/authz/forward-auth
@@ -112,6 +168,7 @@ remembos.vakhrushev.me {
calibre.vakhrushev.me { calibre.vakhrushev.me {
tls anwinged@ya.ru tls anwinged@ya.ru
import access_log
reverse_proxy { reverse_proxy {
to calibre_web_app:8083 to calibre_web_app:8083
@@ -120,6 +177,7 @@ calibre.vakhrushev.me {
wanderbase.vakhrushev.me { wanderbase.vakhrushev.me {
tls anwinged@ya.ru tls anwinged@ya.ru
import access_log
forward_auth authelia_app:9091 { forward_auth authelia_app:9091 {
uri /api/authz/forward-auth uri /api/authz/forward-auth
@@ -133,6 +191,7 @@ wanderbase.vakhrushev.me {
rssbridge.vakhrushev.me { rssbridge.vakhrushev.me {
tls anwinged@ya.ru tls anwinged@ya.ru
import access_log
forward_auth authelia_app:9091 { forward_auth authelia_app:9091 {
uri /api/authz/forward-auth uri /api/authz/forward-auth
@@ -146,6 +205,7 @@ rssbridge.vakhrushev.me {
dozzle.vakhrushev.me { dozzle.vakhrushev.me {
tls anwinged@ya.ru tls anwinged@ya.ru
import access_log
forward_auth authelia_app:9091 { forward_auth authelia_app:9091 {
uri /api/authz/forward-auth uri /api/authz/forward-auth
@@ -155,3 +215,21 @@ dozzle.vakhrushev.me {
reverse_proxy dozzle_app:8080 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: services:
{{ service_name }}: caddyproxy:
image: caddy:2.11.2 image: caddy:2.11.2
restart: unless-stopped restart: unless-stopped
container_name: {{ service_name }} container_name: "caddyproxy"
ports: ports:
- "80:80" - "80:80"
- "443:443" - "443:443"
@@ -11,9 +11,10 @@ services:
cap_add: cap_add:
- NET_ADMIN - NET_ADMIN
volumes: volumes:
- {{ caddy_file_dir }}:/etc/caddy - "{{ caddy_file_dir }}:/etc/caddy"
- {{ data_dir }}:/data - "{{ data_dir }}:/data"
- {{ config_dir }}:/config - "{{ config_dir }}:/config"
- "{{ caddy_logs_dir }}:/var/log/caddy"
networks: networks:
- "web_proxy_network" - "web_proxy_network"
+1 -1
View File
@@ -1,7 +1,7 @@
services: services:
dozzle_app: dozzle_app:
image: amir20/dozzle:v10.2.1 image: amir20/dozzle:v10.5.0
container_name: dozzle_app container_name: dozzle_app
restart: unless-stopped restart: unless-stopped
volumes: volumes:
@@ -1,16 +1,16 @@
services: services:
gitea_app: gitea_app:
image: gitea/gitea:1.25.5 image: gitea/gitea:1.26.1
restart: unless-stopped restart: unless-stopped
container_name: gitea_app container_name: gitea_app
ports: ports:
- "2222:22" - "2222:22"
volumes: volumes:
- {{ data_dir }}:/data - "{{ data_dir }}:/data"
- {{ backups_dir }}:/backups - "{{ backups_dir }}:/backups"
- /etc/timezone:/etc/timezone:ro - "/etc/timezone:/etc/timezone:ro"
- /etc/localtime:/etc/localtime:ro - "/etc/localtime:/etc/localtime:ro"
networks: networks:
- "web_proxy_network" - "web_proxy_network"
environment: 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: services:
gramps_app: &gramps_app 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 container_name: gramps_app
depends_on: depends_on:
- gramps_redis - gramps_redis
+1 -1
View File
@@ -3,7 +3,7 @@
services: services:
memos_app: memos_app:
image: neosmemo/memos:0.26.2 image: neosmemo/memos:0.28.0
container_name: memos_app container_name: memos_app
restart: unless-stopped restart: unless-stopped
user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}" user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}"
+1 -1
View File
@@ -1,7 +1,7 @@
services: services:
netdata: netdata:
image: netdata/netdata:v2.9.0 image: netdata/netdata:v2.10.3
container_name: netdata container_name: netdata
restart: unless-stopped restart: unless-stopped
cap_add: 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 # See sample https://github.com/outline/outline/blob/main/.env.sample
outline_app: outline_app:
image: outlinewiki/outline:1.6.1 image: outlinewiki/outline:1.7.1
container_name: outline_app container_name: outline_app
user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}" user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}"
restart: unless-stopped restart: unless-stopped
+1 -1
View File
@@ -1,7 +1,7 @@
services: services:
remembos_app: remembos_app:
image: "{{ yc_container_registry_repository }}/remembos:v0.1.5" image: "{{ yc_container_registry_repository }}/remembos:v0.2.0"
container_name: remembos_app container_name: remembos_app
restart: unless-stopped restart: unless-stopped
user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}" 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
+6
View File
@@ -7,6 +7,9 @@
- name: 'Configure dozzle' - name: 'Configure dozzle'
ansible.builtin.import_playbook: playbook-dozzle.yml ansible.builtin.import_playbook: playbook-dozzle.yml
- name: 'Configure goaccess'
ansible.builtin.import_playbook: playbook-goaccess.yml
- name: 'Configure gitea' - name: 'Configure gitea'
ansible.builtin.import_playbook: playbook-gitea.yml ansible.builtin.import_playbook: playbook-gitea.yml
@@ -40,6 +43,9 @@
- name: 'Configure apprise' - name: 'Configure apprise'
ansible.builtin.import_playbook: playbook-apprise.yml ansible.builtin.import_playbook: playbook-apprise.yml
- name: 'Configure tuwunel'
ansible.builtin.import_playbook: playbook-tuwunel.yml
# #
- name: 'Configure homepage' - name: 'Configure homepage'
+1 -1
View File
@@ -37,7 +37,7 @@
- name: "Copy apprise config" - name: "Copy apprise config"
ansible.builtin.template: ansible.builtin.template:
src: "./files/{{ app_name }}/server.cfg.j2" src: "./files/{{ app_name }}/server.template.cfg"
dest: "{{ config_dir }}/server.cfg" dest: "{{ config_dir }}/server.cfg"
owner: "{{ app_user }}" owner: "{{ app_user }}"
group: "{{ app_user }}" group: "{{ app_user }}"
+20 -1
View File
@@ -10,6 +10,9 @@
backup_config_dir: "/etc/backup" backup_config_dir: "/etc/backup"
backup_config_file: "{{ (backup_config_dir, 'config.toml') | path_join }}" 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 }}" restic_shell_script: "{{ (bin_prefix, 'restic-shell.sh') | path_join }}"
backup_all_script: "{{ (bin_prefix, 'backup-all.py') | path_join }}" backup_all_script: "{{ (bin_prefix, 'backup-all.py') | path_join }}"
@@ -22,6 +25,22 @@
group: root group: root
mode: "0755" 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" - name: "Create backup config file"
ansible.builtin.template: ansible.builtin.template:
src: "files/backups/config.template.toml" src: "files/backups/config.template.toml"
@@ -40,7 +59,7 @@
- name: "Copy restic shell script" - name: "Copy restic shell script"
ansible.builtin.template: ansible.builtin.template:
src: "files/backups/restic-shell.sh.j2" src: "files/backups/restic-shell.template.sh"
dest: "{{ restic_shell_script }}" dest: "{{ restic_shell_script }}"
owner: root owner: root
group: root group: root
+32 -2
View File
@@ -4,6 +4,7 @@
vars_files: vars_files:
- vars/secrets.yml - vars/secrets.yml
- vars/vars.yml
vars: vars:
app_name: "caddyproxy" app_name: "caddyproxy"
@@ -41,9 +42,38 @@
- "{{ config_dir }}" - "{{ config_dir }}"
- "{{ caddy_file_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" - name: "Copy caddy file"
ansible.builtin.template: ansible.builtin.template:
src: "./files/{{ app_name }}/Caddyfile.j2" src: "./files/{{ app_name }}/Caddyfile.template"
dest: "{{ (caddy_file_dir, 'Caddyfile') | path_join }}" dest: "{{ (caddy_file_dir, 'Caddyfile') | path_join }}"
owner: "{{ app_user }}" owner: "{{ app_user }}"
group: "{{ app_user }}" group: "{{ app_user }}"
@@ -51,7 +81,7 @@
- name: "Copy docker compose file" - name: "Copy docker compose file"
ansible.builtin.template: 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" dest: "{{ base_dir }}/docker-compose.yml"
owner: "{{ app_user }}" owner: "{{ app_user }}"
group: "{{ app_user }}" group: "{{ app_user }}"
+1 -1
View File
@@ -23,7 +23,7 @@
ansible.builtin.command: ansible.builtin.command:
cmd: > cmd: >
{{ eget_bin_path }} rclone/rclone --quiet --upgrade-only --to {{ eget_install_dir }} --asset zip {{ 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 changed_when: false
- name: "Install restic" - name: "Install restic"
+2 -2
View File
@@ -38,7 +38,7 @@
- name: "Copy backup script" - name: "Copy backup script"
ansible.builtin.template: ansible.builtin.template:
src: "files/{{ app_name }}/backup.sh.j2" src: "files/{{ app_name }}/backup.template.sh"
dest: "{{ base_dir }}/backup.sh" dest: "{{ base_dir }}/backup.sh"
owner: "{{ app_user }}" owner: "{{ app_user }}"
group: "{{ app_user }}" group: "{{ app_user }}"
@@ -46,7 +46,7 @@
- name: "Copy docker compose file" - name: "Copy docker compose file"
ansible.builtin.template: 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" dest: "{{ base_dir }}/docker-compose.yml"
owner: "{{ app_user }}" owner: "{{ app_user }}"
group: "{{ 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_files:
- vars/secrets.yml - vars/secrets.yml
- vars/homepage.yml - vars/homepage.yml
- vars/homepage.images.yml
tasks: tasks:
+1 -1
View File
@@ -5,7 +5,6 @@
vars_files: vars_files:
- vars/secrets.yml - vars/secrets.yml
- vars/homepage.yml - vars/homepage.yml
- vars/homepage.images.yml
tasks: tasks:
- name: "Create user and environment" - name: "Create user and environment"
@@ -44,5 +43,6 @@
project_src: "{{ base_dir }}" project_src: "{{ base_dir }}"
state: "present" state: "present"
remove_orphans: true remove_orphans: true
pull: "always"
tags: tags:
- run-app - run-app
+2 -2
View File
@@ -39,7 +39,7 @@
- name: "Copy gobackup config" - name: "Copy gobackup config"
ansible.builtin.template: ansible.builtin.template:
src: "./files/{{ app_name }}/gobackup.yml.j2" src: "./files/{{ app_name }}/gobackup.template.yml"
dest: "{{ gobackup_config }}" dest: "{{ gobackup_config }}"
owner: "{{ app_user }}" owner: "{{ app_user }}"
group: "{{ app_user }}" group: "{{ app_user }}"
@@ -47,7 +47,7 @@
- name: "Copy backup script" - name: "Copy backup script"
ansible.builtin.template: ansible.builtin.template:
src: "files/{{ app_name }}/backup.sh.j2" src: "files/{{ app_name }}/backup.template.sh"
dest: "{{ base_dir }}/backup.sh" dest: "{{ base_dir }}/backup.sh"
owner: "{{ app_user }}" owner: "{{ app_user }}"
group: "{{ app_user }}" group: "{{ app_user }}"
+39
View File
@@ -13,6 +13,7 @@
base_dir: "{{ (application_dir, app_name) | path_join }}" base_dir: "{{ (application_dir, app_name) | path_join }}"
config_dir: "{{ (base_dir, 'config') | path_join }}" config_dir: "{{ (base_dir, 'config') | path_join }}"
config_go_d_dir: "{{ (config_dir, 'go.d') | 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 }}" data_dir: "{{ (base_dir, 'data') | path_join }}"
tasks: tasks:
@@ -37,6 +38,7 @@
- "{{ data_dir }}" - "{{ data_dir }}"
- "{{ config_dir }}" - "{{ config_dir }}"
- "{{ config_go_d_dir }}" - "{{ config_go_d_dir }}"
- "{{ config_health_d_dir }}"
- name: "Copy netdata config file" - name: "Copy netdata config file"
ansible.builtin.template: ansible.builtin.template:
@@ -75,6 +77,43 @@
loop: "{{ go_d_existing_files.files }}" loop: "{{ go_d_existing_files.files }}"
when: (item.path | basename) not in (go_d_source_files.files | map(attribute='path') | map('basename') | list) 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." - name: "Grab docker group id."
ansible.builtin.shell: ansible.builtin.shell:
cmd: | cmd: |
+1 -1
View File
@@ -34,7 +34,7 @@
- name: "Copy docker compose file" - name: "Copy docker compose file"
ansible.builtin.template: 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" dest: "{{ base_dir }}/docker-compose.yml"
owner: "{{ app_user }}" owner: "{{ app_user }}"
group: "{{ 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" - name: "Copy gobackup config"
ansible.builtin.template: ansible.builtin.template:
src: "./files/{{ app_name }}/gobackup.yml.j2" src: "./files/{{ app_name }}/gobackup.template.yml"
dest: "{{ gobackup_config }}" dest: "{{ gobackup_config }}"
owner: "{{ app_user }}" owner: "{{ app_user }}"
group: "{{ app_user }}" group: "{{ app_user }}"
@@ -47,7 +47,7 @@
- name: "Copy backup script" - name: "Copy backup script"
ansible.builtin.template: ansible.builtin.template:
src: "files/{{ app_name }}/backup.sh.j2" src: "files/{{ app_name }}/backup.template.sh"
dest: "{{ base_dir }}/backup.sh" dest: "{{ base_dir }}/backup.sh"
owner: "{{ app_user }}" owner: "{{ app_user }}"
group: "{{ app_user }}" group: "{{ app_user }}"
@@ -55,7 +55,7 @@
- name: "Copy docker compose file" - name: "Copy docker compose file"
ansible.builtin.template: 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" dest: "{{ base_dir }}/docker-compose.yml"
owner: "{{ app_user }}" owner: "{{ app_user }}"
group: "{{ app_user }}" group: "{{ app_user }}"
+1 -1
View File
@@ -39,7 +39,7 @@
- name: 'Set up environment variables for user "{{ owner_name }}".' - name: 'Set up environment variables for user "{{ owner_name }}".'
ansible.builtin.template: ansible.builtin.template:
src: env.j2 src: env.template
dest: "/home/{{ owner_name }}/.env" dest: "/home/{{ owner_name }}/.env"
owner: "{{ owner_name }}" owner: "{{ owner_name }}"
group: "{{ owner_group }}" 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 }}" base_dir: "{{ (application_dir, app_name) | path_join }}"
docker_registry_prefix: "cr.yandex/crplfk0168i4o8kd7ade" docker_registry_prefix: "cr.yandex/crplfk0168i4o8kd7ade"
homepage_nginx_image: "homepage-nginx:latest"
# Registry images # Registry images
registry_homepage_nginx_image: "{{ (docker_registry_prefix, homepage_nginx_image) | path_join }}" 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 $ANSIBLE_VAULT;1.1;AES256
30353464333261353263666333626634633162666239653631386263363431333734656631313930 30613937343031343632383733623435366535373231316163393436363636656462326262383565
6539363363653239656166623839636131306462633834310a646438626666653163613762313637 3032663665323131626263356531633934326639636231620a363635376263333438336331343366
33316565343539376535303135366530386333313562306161633861306237313030386366656435 36386337323165333861633062656433313062343764636138663533333639316336306230653732
3830353139613066350a666564333832393465316531353863373437646464316262396532383738 3331336137616263630a306135333566646434663231383138363966386661643836626561376338
36643034623263643038633162666265646661383461303335653935376330633033653730643235 33636362323937386664646630383062613535666431393634316337626564613733313861386238
38663938333934353131393762363762366333306439396264346565336439653239353034386138 39316263666662633066633836366236346431313531656339613566303962656165396662326563
65343832666363663731656263616263636662323038663835386437306564643265396434393832 65623036333932393739646162353836646562643866396263386232633933326538316637656365
31373231636363656463366436303433616239646331353239363966393264623866636466323839 38656562383861613030306635613236646235613436316635386531656666363738396461313263
61653037396130373763633265666663316666623961643131373564326361653038646530323964 30653934366537303133613962653137633131323431396266646339376339623034373963666438
66383738303230313533653562356535343162633231636634646237663466313031346463393965 66383464303431353962323032316533613138383831343036383230303931326433396333623935
65336164373666343932376234343133623531323163653961666432326134333865336262333336 37396432376637373135666236333332383262323931616432343665653836626265376632643765
63643038636537323039616563613861386566393364306131326463366531363063613766383965 33303835393863333334653664613337343063313362363136383234666335636565383237656639
66353237383734643564626239346662656435386166356161343031343633613331623433366465 34323839613765626231303230616661626530633530333165373535663139643339656438396237
34613237383934343636613833353933613536386637626338326537373438646139623839366263 36656134643636643733363336343739616532666130393863666665393138383261353730626565
31386137636362356339333530626565333561333634613964343133393038663033656433376165 38306133343463306462656534326431623238336562653433316233383861303032393437316336
33323633383237343433636532613965303232306262326237383233656430363030366164643561 32353338613639653735393239333235633565636563313933333763323339656237326162316465
37373930646233366439383635333133393631366265373439643635396231336263373266373338 66313034306263343462376632303539656533353265336366613338326439323732623438626162
33613432616634666531623631356162363364643437623239643536336636396132313232656233 39393937613836656236383030343436303632363330313734333665643365666138633034323462
64333032653930353430336364353335396130633634666131393166383765656362616139396265 66376333386237383666623434636662363338626538353933636632646236393630343739636666
32653563366562383930623439363539333235326661383665336564623832313839306263386232 37666531633839363365633863646530396432613166313035353638313463373338313139616133
61633734353733346266343936306364393835316635633061386661313262386630623933656436 62383234356665333132613664383931316238353863306538343831363233303862383737373939
30633662613937326162666666353339363736373139626133316233383433373665623637326634 37303430303766343366633536643139363366663734326162366434333165613033653666383337
62623165306431363931616335363339383836366363353565366136643661366638343361306438 62626538316463343466613065326666396266643661656164376336336532666134613663623163
36346265383930326461653931626537306431633766653265373266623333643437323533353233 64316335633839356231393130343938613334393737666663363662356466326235666561653239
30336437353739353861316134306361333063336233323437616630386532663934303032356238 39386635616165633063383032666366383861333038373636613663613461316433633562623664
64626335653161313533636539353665656239336237633461346535636539393161643961613464 65373536663230356632663133356639323838653431333836376330316162633261333934363335
30313366323937313265636563333261643732363161323934303463316663313033633831386536 34383937343063303835626435316534356239316230326566383036646237336238623036323161
36313733326133643663353830643936613366653766636638643737323362303537636563336339 62326264636130323965313866616631663039623431363139363462663435323866393437373566
66353930633761396666383138653339343632656232643866376337623732326635373735623362 37353463353731303434303435353061633531663464656336306439373238633038343237313133
38613831356437383865653964656637396137386463316562663464383834323962333465356562 34333463626261333038363438343034373335346332316430376436656331626664376664323037
62343864613364623639336334646462376366356138386162373763353930366663646463303736 32393631663434383265326231353035356333343739386132326435653438306136373237396539
36323165373236643139303935333266323064633230353934343538616332653265373438633564 33613462623562343966343933363037326234323836363636313938666534333337646139326533
62393332383533313564656434366435326362316666623763353563626138643732643531343238 61633666623936646366643336333339303633643230393465623031643963643635313264353236
33313031313366656363343562653763613833653536613562343465376664376562356164613936 65313631663430336262326463663938386630363464386230383766376363373235366438393635
66343763313262636532626363393562316661613130313866346165633862373136393661303936 65313232383334666263626662646264393565326164613364313138303638653333653963316561
38633562663461623133653864306139393165386265313032653231306361393830653763663062 34346464653637376433356335663930396432386238366132393562393162353235393438633533
63383430633734646134303239666532626264613332313136363238323139326237623035623964 62656533316431666463633530653832356263653030326366663932306662613465643638313633
37663435353965383535646331343761383562653735363666666335613630373865643265643336 62396562616463313066343832316238386234343537346436623039643132393562303130613331
32343664666239326337643366306531623834336463386637666564646338333433303134323564 38653261353132633036623138643338366534396237613333333765653436363032616235373035
32323866323337303262363833313838623337323232616135363963363631336637623865373437 35313966623531373636363638383862333935353931653861663966643531383335653739356565
31316539663237316162366338303763346139663435313037306338623339316637363133363965 31373937396234616135653765643131666530383030343064366531336135366265633232653433
62303439383637353333333631363234343866623763616261666239316631353434323930373066 65396566626232633831343734353432633462343336616135373861303836613463393736306133
32613334303566353830323064626537663339373130613533373739616236346337393033316638 64663531643630326432376235386433623365373163366663623632333531623863623663643434
39303462633463306634373933396233666530303465323133386435333563373936623466376334 36646134623665633531643732663137613862343666613139336231646564363266343935653263
39323131353866313263333436303831323166366165623263323730333663616665373261313232 66653366666635666535636637626134363633336233613732656166373063333237323465616434
62333361623964316236303739363430636461396264333638363835646366633633326666326363 39623238636235333866666536346430373735323530633133663937636366663530326465386161
30376339646361386337343561666230313066303263353430333436613464303965353634353831 39346466386133656633373438333133303566363233626238366133636333656462373065613863
35343866396639366539653939313939656234663665666533326630306530333863383761613161 65363439383163323332383931663833303234326132343462333835323664363461656566393065
61376433623635663636656564643135626239343663333532663366353165383463383038353232 38646261323336316239363465343238643132306235613031626438323838653066376561626661
35346437383931653166323431353766393036626136643262623263363932396437303033356131 34356530666665323230646436633935343861323638656638323163306236393865366630636236
31333066393166356665663963306366383837303036373930653462386232373563303630633636 62386161646131623738333664636361396239643666323837646332383538623734386531313664
30363466353437306232383663363161396665393739306536643236663761323366613130656330 65313632343365393130643137353735666565663030383231616231313237323866386336316361
30366661353939303030363833636238346134323738366138396336393463373563333632653262 66656165643261653464316639613635323531306362353164373531326461666437303434346233
39633333353466313339386362656538336338373565323536646333616434626137323335353563 31383864346233313633353065343236633636386138323761666662373564623234613965323131
31636564636537613033656236366362316134353733623961366333313165626637616536646361 36313861316563333262306434663265313237626631396561303236343330633738356666633663
39623561656234356663313032326231313231363963643534666136343433663730333333633434 61313663336237653361383963333764336137396666613634313036373564353564643334623363
35303363366133323330376163363662653534633431333035363837633831343861373633393930 62353531306532323664376363383938646536393339346666656339393230613362666337663861
30626635353166303962313639326239623532396435623230353166323165656261643030356131 37633633653463343430666634643863383438633933343839663865616136363538643061343437
65356632613834653063656138353261613231663465336366623564316165623932663530376631 34613037353835613866303230303162396531626663616164343263633261363335313936666339
31616137386433336135323636613665356162353139306263633833336566346339666135626433 63383533616530356262363838636466333038656339316364626263383731313464313734613630
30336361333736333334383162343336663631663333353739633036346633393763366636353161 62646266666136616632636161363631623362346230643134663664396565323932343462383661
36373566613530366632666333646563336136663034663337313032643165643966303964613234 38303663653262333236613833396237663834333139316666343065396137306562613265343863
38653332343037653464646635666634376138616636326531353865346663396534363338393465 65323065663862636230636664623132306231366462346432343030376236346465663831623537
37393962343233633739313961663531616565363337346535303962663533643866363333353430 63633231333165613731626137656539366131633364623661616136616434306563656139346137
35323864383338313837643266653937316432313834653038636561653238303164626362326432 63313032343161623235306230633361666163623061333738383135636664623438323238663631
61626638636539303135646432643539306566316464383935333535343163393832316561323764 37613964643931323432353431306564393639386437666539376238643065343738313265373661
38373239376363633237346664626137363562303134333563346434396335383235383032653637 61303764646463326632653335323432646436353765633862623838386337623464333839643833
66356138396436623734316139373964373364633937373431386432333263326563326234623936 39383961666234363638323735636231623962666461373435633631323530643237656464396465
62313830613436356337623865373432356165313930373362643037386239303937666637636332 66623431393461613634373237646636333965396435663563363161626666356638366462373261
36666239343336316538623938376538383038386662396662626137633138663766396432353061 37633238323135666136623663653665303832656437663536383236313334313461353032663933
66656665656535303535376161303562643035383961343832373137636335363534393861316332 63643164363664663939613635373362376162336262653332663936313737396130366330656532
63353831616439376663616363363763323838366363396637313931616337633832643061616662 31653463383132643262613839613962663836376463343661393736633633396164643264653431
31663738396162316538343763353330653739303963616530323064373331366433326465326532 30663732303236653165386537653432656266363239373030333630353661666636303730373937
34303437346463366362346532376263356134633838373630323831643432663265313965633964 65363237366333376133306437376534636133356238326461333762326563386265363636323831
63386361303666376162653938346236636434666564623638646161656661393462613237363935 39343665386262336265383865343563343832623766656534306661326462333561373835366631
66346661353735646261333339313638653061636130623963346235376234346138613762643566 39636361623831623533353962633363393531313530363833613962616331653565633733303964
30323831613461383766643836666633636364323139333261303632613730663730336637646233 32393433303938323566646264323761633035653231353761643261663839313665663434643834
37373733373930333735306532663930396331343037383032636461653463353862613362303332 65356432393431336235306437643861653437643362363839623634333835376636623664616139
66663361373030363031396663616638396465313438396230633835633335636664356565316664 66376562633232636431626436653161333137633466313433663433383230636337653535643430
61356632303035366638613762306636333439373534356239626439626465613837336539636532 61613032656135323765613837626266313632353661346636643866613138303930346563623738
61393564613962303938393435306238383761396339656533653265633461666435353133333164 35613831623565353432336338373465303437623234313736353661353430656661366365373230
33316630663131323634343637643435326237666134326534666563623734633634366636623731 33646134356661616164303865623464306339653439613365626261323237623135346537393535
33666536396138393137333761633139323735303039313837366635623037333037663231353338 62393465343134626333333462316331656134383362383031353863316632393061333933336362
61633962643830663038343530643330356137663665663537336564353730323565626135313662 36326662363833303436663166383365346433323866346462663261333330656666663162383564
38646430306638663431373037323061313438666433313264633732303664323634303236356364 35336438643064313833393638323864343237616163383033313966303262326135323335353931
32326131656438656661616638393462333532313737396235643830663832643435373362373533 66333938393264323533353231303935346661653835386262306133393065356535643835663665
34313638626264363561653232323263643135323666626539636435303131336533623137643735 38363930356530366135313734306464623739376438613430373634396339393864396264303135
38333734636133336431393634316566626266356165663639333461306463363637613366643433 61356333636236326566386264353930626564636438616265353939383733663837313233356363
61313031623933633731353533346264363336326534366231366239373137666430376266383836 63643835393437336366313030303864306536666638623430356263336234646462383666316431
33396365343161616238396133326433343535333032643533616439643236323561353236356536 36313464346266646438383762313138376338323537386635636561656662306533316362396162
39663534666533393638376639653161376562636333616437623937383035313032636632396232 38326165633532623933376165643861323735353831363264376162316561613038633961333337
62373961616262313936623663613837316539616330336339363436373465363662313836656662 30646461636332623466643033633764333330353832616365376633643263336131313733653139
31333231653836306362643234383631666431663431666333366563393830343837393339313533 39646239366261366465333962643565636430393464613866613038333636393362383636343534
65343034373265343737326232316162386434326661336530373639633838373766323437393937 61323830616234633364346131336630393965373730343464366166376232346464636263323639
38313962356364306165393730356339336639303731393931323130386462663139353639626338 63336464623733363139366665336131653163613833383261376138373032666663356637383832
30373162356131313737326164393164373837366632643064326635323934396239363033303938 32313130633363346435383638616236633761616166663339316437353938636636613530383836
62376539313164333365336331393735346138376362333962396364323063636539346663613631 64623661366130656439306266343435396334383564353466663339383862313733313931383463
66353062643261666538633430653036636461626161356265303037353665326461333335373562 64323237656361383262343735366562623965356636343963363966616333313333646233373464
62363237356166363139616532333530373230623865383862383362376534363162336163633133 33383939386262663730316333616663636161356463396362643237356532386162363131626461
37636564353836643963613333373837343431346538303738346462663739623932363831353534 37323965313063623463356133626531393339336535303562343530316663613639646531323136
64613934323136623761633830313335306666643765343037363665303733613639343466393963 30353732646237623264653963373863363965326338666264306562373932393333633639396131
62386361326362386535376364303237313564316661326337336434616134313464313565333136 34303764396330326165636264313532393961303038623031336631653831323337306261333630
64663436643939623438366437643034666362386233666238363434613862386333323134363566 37333964376533636132303335653935343932373330373632626235356437636165623436383036
61646661386139393731666632343066366139333638333366336137353833383931383835363934 38313565373561393834316532333930356135623439373161643063643738353031353565396330
35666563303333303932386264316134383035613035663466656465336439346137656336656434 37656162346433326638353439613666336534336562623633643230636134383931653538616665
34363438663962393434633761633336623966636465623665356433343236383862636435303038 32393432383265613237323138386361353934373965306462393666616532653563626232643035
33313661336364346562666234396462646231666162306566636461613462643034653861663439 61643732376434633537633663633130313437656166333239633533393334373163333566343430
39633361643733643533346561363232336465383633396461376239623837366135336238346332 38633165353637306237316436663235633162353132646562353638333038663636323465633632
33373762613230353537656566353466623864636161316361396338613566336132303762306538 34643037623634643534663366633133363030323966313065353333633636646636306565333238
36633663643136303262373365363063396163613464333662653430396636623335313061626561 39336662626138306464613461343762316533656433626165323764616535623539336439396663
38636165346532373732333463373436323239386432326634613762303665343636343366656162 63393365626235613063613934306132333162646237316364306637346136623061363236383765
39613035626133306139663831313236353739313930666339333133663839313932336433353531 37353138363337346530626563366136333635663863313038643537366237633362343136396664
63373966633635336632363137326533363864303466366664656532623362663265613936663636 61623237353433333238633163636565386134356565303763336238636366316330666339383365
32336265393330616437646664333464623635353662616133623264393935303530356330633834 30323235356633656362353738393234616435663333613364316539636430623262643162313337
64626161646366306134303634356138663634396231353064343661373662646132653563656162 36323466303832336530336566343731306362333862663537613339663562623739343636613162
36343864396263343561333137383238393063333634376634656231346238383862313264303263 36343563373665376565366266343461643562636630623166626165636337613931653338633862
66343766353133316337356265343361363931373362303462663935323735393330623632623237 65393138353661656265666335343263333063653430326532663839383433643966363639643636
34636565646461666431313362613862303530343537306563396236663435623164326134326530 61376365363538636235666235623638376334363265626136313536353637386564303936636263
61356566313733363035346530613934376236383136633363316263393761383532393133323939 32306239306339656238393864666135613663366332666135663461353366313833376430376263
38653731643330666261356236623039633262343031393330356565326261336230303132656332 62613163303964333735396338373737653837666435656130376435376434356462383264636561
37353464336365633633643063353237626535326537653131633535306361353165313232366664 34353563316132336663316166663832383939333634316562383634383838336531313731613666
65613966356634633134633563343036323539633036363437626466363865626333376134613731 62643231636266353935343539366465376139643834306261623738313432306133653461383738
31363434373534613239333031333564653733363732343835346435633934356236356166393538 39396332373364353833626661333634346131396337636235653431616336393666373231383030
38613432303032333830633161353938383632313137633437313131653664323934613432356635 32306466613136346265653038636537646330643337663863383562323638616661333037323232
32656661303163646335633137363734323363653330623137613438623831366135393261343833 35613138363330353533643064613366343339343032373737306364353135353334336666663732
61383839623733346434653739626365376266353430363131636665636639333037646262346465 36343963613636376561666266623537316432666161326331383761323437383738373762643937
65393934373564386335343865656161306635653833373430663064616264653135316530303338 30623737643239326261343939663065643265653363633661376265626637643336613635393335
38366434323961396638663534396530636639613338643463326432646366646163316363383832 65373565333936333431656331633039323135336236656337343532643939386338663239393065
31373331366565376439643131343631393663666434363838316435653132393034643536353332 65666536333732646235633762633032393463663334616165333834653938346230316236353839
66343530363564346335623464653862643632356638656535353666353336323033356336636134 34396362386265646261373561636230363962663433303535373035346334353932643365383763
39316165633562303239623863613633313238323862393636336464633934316266313235343736 32333239613961346466356562376663613062373162666264633636323833323263333765616563
38353435663166643566653236623039353465303431383132306135323239346135323831336363 39646530343962353362363634336336323463623137646531373362353832343335366461646535
65396136666433393334393731373437383631313139666139663239303330363031356330373035 38653735316536396438613866326438363036653833366636626130323437623366373833366165
34366264396332643163626237636132623532326636396461356566363835363163643161396130 30303636666263323062343931306435363961643838636163366433376436303231316338613034
30663637346464646333623833393534613464336432336239386238393630376435343535356464 37393631363632383461373566306365306631396335633432383939336332626237653462393136
36393539653632393430663035613539626165396463323739353263636364346662653535633261 34316636643464363634366535333463326533333564633163363062666463343731396231656234
63663461623435386231653232313539356131346534636137383562313733663031653730353533 39346333303465363037313063373366373439306333636465636366666437326362626264653033
37393233363635643339373530376130326333643764356634643161333135663436663437373136 64613062343538303931646630373565663530336133633032366331626536353237336235633636
38393163343166666237663062313236373938646263356530363136383666636536616436626139 63366639366439386530303966323563323862383865356630313636333333393464653762626634
66633038303961336537613030323639383666333930623161633061643131313235333235303561 65366231613661313233626239303035323666346236636362393036353839333636343434646266
64633631333964336164363963346163303730306334353834343061353661633436363536313839 65653039353966616361363335346565383863616161316134383365616636333732653233383261
30366535376537313065303839323939396465623734656233396535316465333436323937323232 65616664343830353861616666616237313532363334653430313437313535666436383338396363
36656662316232306330393435333836363234666161656434653033643236303864356137393834 33313436363061306431366332373936633034393733646137636338336431333033343532613531
38643234663365663037353663616439383430616665643061363531343765363034386432626564 32613839393232646565663931303530376432376337613762346230646366613935383234313666
62323630613337663562616664613138623262326134343430623661656166653135396562323364 61643339353933336434666466623133336637343534303737366162316561366632333335663233
38616165316464376563636535643839346665613465316662363566313337333666383336303064 34643036326630306632353438643666623939393033646238353261386231626634303266303530
37633130323464393831323962356633663430343761393932323266353864363062653132323639 64643436653234616332623835333165626135613465346162393335353133356233666536313632
33656337666236303764653834326230613165636437306337393132626339663161623536376138 65616135666533343839666132623639343565303436623162383738353633613864356535646365
32343063313864643164613739653038663430633466356331313638343766393166306138336239 32373337393936393830666365383462333437373539666633386361373135333163393334303235
39303938303861343331626231303333343931306136373533613466616336366261313866656631 33663631386566356366666132616265373533373561616564343538303432346562356234336663
37336566623933656433646465306237383334363264333532386537343336363933636133306263 32623866396434326264636539323132613239343938353739376539383139313833376563623434
37333033633461656236623433356438643936343861363833306333386636633463356365643730 62636334326234313230666662396561393130396137306437393334323561356435343866386636
61303931326162353638363035666333333661393262306234366165383530663231656334623133 32656337656439653830653365313031326562643437376538316561653963643232353434313538
30363436616266313062636232313435376462633734393130656239396462613039623236396330 64373638616133666463393462643465306565646136643862363162643638343565316139626539
62666336663661623239666530656664346461363436303930306338303031623461646137366432 64643939383936313035323936656438313039376635383733633032613165343130663930323166
62643332353135663935 37343261333332663863366533386335373962323163616564376434636361356438393035656533
30383139323931306232353664636662313036643431663536353035356139643761613235663837
37613133363433356536316466343237613131386536356234343135323861396130663464323236
63636563633031396465663563366263373938373531336239323138653531386535643332653736
61396432356161643663623130656632633862333861656464613432623732656465376236313437
65386630633036636663303633636134343739366562643062343030383138653466326636366266
32633239343039633636313837643432333238366533393061646237626130303934356438633936
38366265656365333338363431643432633463313438633361333764653637623964363732303737
61343137653930353361653364656233343166633162313964306531383834356237343031396137
34366135623530366164646532643636346233353563333031343931643037613463613639356238
36306235336562333935643035313934366339623365616661616461653832336137336464393662
38363433646139646633353162616661323433636531393339643562373538616430363061366330
35333138613136323865346462653761666534343538313033663835653631363631623532663133
64383135326333626438363066633366316364643332623030653230353861633837646362626333
38363236616265313638626263316164323563616237653465353031353734333032323761393761
31333331643161396338653330653537353634306139656536363665643437633433666236356334
64386566623836306666653766626465646664303231613062663862613565393364303233333636
61633631643437373235636133333832646463366633353939383834373362633539333766303661
3132333365333061633665366432346636646564313437333061
+5
View File
@@ -1,3 +1,8 @@
--- ---
apprise_external_port: 8000 apprise_external_port: 8000
apprise_external_url: "http://127.0.0.1:{{ apprise_external_port }}" 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"