Posts created by “skeirss”

L2/L3 Сегментация и изоляция домашней сети на роутере Mikrotik

Written by skeirss

Задача: изолировать мобильные и IoT-устройства от компьютера, сервера и других устройств, подключенных кабелем на LAN-порты на роутере Mikrotik с помощью VLAN.

Замечание: подключиться к роутеру лучше через IP, а не через MAC из-за некоторых технических особенностей. И не забыть включить safe-mode.

Зачем нужна L2 и L3-изоляция

IoT - недоверенный, плохо патчащийся и потенциально уже скомпрометированный сегмент, поэтому его лучше держать в отдельном L2-домене с default-deny L3-политикой в сторону LAN, оставляя только явно разрешённые направления (обычно только исходящий интернет + при необходимости точечные "дыры" к серверу автоматизации).

L2-изоляция через VLAN блокирует ARP-spoofing, ARP-poisoning и sniffing трафика внутри одного broadcast-домена - без неё скомпрометированная IoT-лампочка технически может перехватывать/подменять трафик соседних хостов.

Также VLAN ограничевает L2-discovery-протоколы (mDNS, SSDP/UPnP, NetBIOS, LLMNR) от расползания между сегментами - иначе любое IoT-устройство видит все узлы в локальной сети, которые мы не хотим, чтобы их видели.

Главная идея

Отделить wifi от основной сети сразу двумя слоями защиты:

  • VLAN разделяет сети на уровне L2 и не позволяет устройствам видеть друг друга как участников одного Ethernet-сегмента;
  • Firewall на L3 уровне контролирует, какой трафик маршрутизатор может передавать между этими разными сетями.

Краткий план действий

  • Выносим беспроводные порты wlan1 и wlan2 в изолированный VLAN
  • LAN порты ether2-5 оставляем на нативном VLAN'e (PVID 1), то есть отдельно второй VLAN не создаем
  • Создадим отдельную подсеть для wifi-устройств (192.168.99.1/24) и настроим DHCP-сервер
  • Настроим firewall, сделаем исключение для смартфона, чтобы можно было передавать файлы по wifi с помощью kde connect и других подобных приложений (не забыть сделать ip-адрес смартфона статическим на dhcp-сервере)

Приблизительная схема сети

Что делаем по шагам

Шаг 1: Создаем VLAN для Wi-Fi и назначаем IP

Создадим виртуальный интерфейс для VLAN 99 поверх бриджа и выдадим ему адрес:

/interface vlan
add interface=bridge name=vlan99-wifi vlan-id=99

/ip address
add address=192.168.99.1/24 interface=vlan99-wifi

Шаг 2: Настраиваем DHCP-сервер для Wi-Fi

Создадим пул, настроим сеть и поднимем DHCP-сервер на интерфейсе vlan99-wifi:

/ip pool
add name=pool-vlan99 ranges=192.168.99.10-192.168.99.254

/ip dhcp-server
add address-pool=pool-vlan99 interface=vlan99-wifi name=dhcp-vlan99 disabled=no

/ip dhcp-server network
add address=192.168.99.0/24 dns-server=192.168.99.1 gateway=192.168.99.1

Шаг 3: Настраиваем порты Bridge (VLAN Filtering)

Теперь мы назначаем портам wlan нужный PVID. Проводные порты (ether2-5) мы не трогаем, они останутся в PVID 1, что сохранит их работу и статические IP-адреса в сети 192.168.88.0/24.

/interface bridge port
# Мобильные устройства отправляют нетегированный трафик, поэтому admit-only-untagged
set [find interface=wlan1] pvid=99 frame-types=admit-only-untagged-and-priority-tagged
set [find interface=wlan2] pvid=99 frame-types=admit-only-untagged-and-priority-tagged

Шаг 4: Таблица Bridge VLAN и включение фильтрации

Указываем, что процессор роутера (сам bridge) должен иметь доступ к VLAN 99 (тегированный трафик), чтобы работал наш DHCP и шлюз.

/interface bridge vlan
add bridge=bridge tagged=bridge vlan-ids=99

# Включаем фильтрацию VLAN на бридже
/interface bridge set bridge vlan-filtering=yes

Шаг 5: Изоляция L3 (Firewall) и исключение для KDE Connect

Сейчас сети разделены на уровне L2, но маршрутизатор по умолчанию маршрутизирует трафик между всеми своими интерфейсами. Нужно запретить хождение пакетов между подсетями в файрволе.

# Изоляция на уровне подсети: 
/ip firewall filter
add action=drop chain=forward comment="Isolate WiFi from LAN 88" dst-address=192.168.88.0/24 \
    src-address=192.168.99.0/24
add action=drop chain=forward comment="Isolate WiFi from LAN 77" dst-address=192.168.77.0/24 \
    src-address=192.168.99.0/24
add action=drop chain=forward comment="Isolate LAN 88 from WiFi" dst-address=192.168.99.0/24 \
    src-address=192.168.88.0/24

# Исключения для смартфона с адресом 192.168.99.254
add action=accept chain=forward comment="Allow Phone to PC (KDE Connect)" dst-address=192.168.88.34 \
    src-address=192.168.99.254
add action=accept chain=forward comment="Allow PC to Phone (KDE Connect)" dst-address=192.168.99.254 \
    src-address=192.168.88.34

Важный нюанс про KDE Connect. KDE Connect ищет устройства в сети с помощью широковещательных UDP-пакетов (UDP Broadcast). Широковещательные пакеты никогда не пересекают границы VLAN из-за этого телефон и компьютер не увидят друг друга автоматически. Чтобы это исправить нужно добавить устройство по IP вручную в приложении KDE Connect.

Базовая настройка завершена. Проверяем, что всё работает. Скорее всего интернет через wifi-сегмент работать не будет из-за дефолтного firewall-правила на микротике:

/ip firewall filter add action=drop chain=input comment="defconf: drop all not coming from LAN" in-interface-list=!LAN

Проблему можно решить просто добавив интерфейс VLAN для wifi в LAN-лист, но это не очень безопасно, так как wifi-сети таким образом дается то же доверие, что и проводной LAN - то есть телефон (и вообще любой, кто подключится к wifi) сможет ломиться в Winbox/SSH/API роутера. Это прямо противоречит цели изоляции.

Правильнее - не добавлять vlan99-wifi в LAN, а точечно разрешить только то, что реально нужно (DHCP + DNS), до финального drop-правила:

/ip firewall filter
add chain=input action=accept in-interface=vlan99-wifi protocol=udp dst-port=67-68 comment="WiFi: allow DHCP to router"
add chain=input action=accept in-interface=vlan99-wifi protocol=udp dst-port=53 comment="WiFi: allow DNS to router"
add chain=input action=accept in-interface=vlan99-wifi protocol=tcp dst-port=53 comment="WiFi: allow DNS to router"

На этом настрйка завершена. Если наблюдаются какие-то проблемы, то велика вероятность в неправильной расстановке firewall-правил.

CI/CD Pipeline: anime-backlog-web

Written by skeirss

CI/CD Pipeline: anime-backlog-web

About the Project

anime-backlog-web is a custom-built application for tracking anime, consisting of separate Docker containers:

  • Backend — Python
  • Frontend — Node.js / Next.js
  • Database — PostgreSQL

1. Infrastructure Preparation

Terraform

Terraform is used to provision a virtual machine in Yandex Cloud. The main configuration file requests the following resources:

  • 1 boot disk
  • 1 virtual network
  • 1 subnet
  • 1 virtual machine with minimal specifications
  • attachment of the disk and network to the VM

cloud-terraform/main.tf

Cloud-init

Initial VM configuration (bootstrap), executed once on first boot:

  • user creation
  • SSH key addition

cloud-terraform/cloud-init.yml

Ansible

More detailed configuration of the cloud VM instance:

  • installation and configuration of Docker
  • firewall setup
  • installation of fail2ban
  • hardening of sshd security

After the Terraform → Cloud-init → Ansible sequence, the virtual machine is fully prepared for application deployment.

ansible/harden.yml


2. Pipeline Trigger

A git push to the remote repository on GitHub causes GitHub to send a webhook to: https://jenkins.bernd32.xyz/github-webhook/

Access security:

Resource Access
https://jenkins.bernd32.xyz (Jenkins itself) restricted to IP addresses allowed in the nginx configuration
.../github-webhook/ (webhook endpoint) open to all IP addresses, protected by HMAC signature of the payload

Jenkins clones the repository and reads the declarative pipeline from jenkins/Jenkinsfile.


3. Quality Gates

Frontend and backend tests are executed in parallel, each within its own Docker container.

Backend

  • Container: python:3.14-slim-bookworm
  • Creation of a virtual environment (venv)
  • Installation of dependencies
  • Execution of pytest with JUnit report generation
  • post.always → publication of test results

Frontend

  • Container: node:22-bookworm-slim
  • npm ci
  • typecheck
  • npm test (Vitest, JUnit report)
  • npm run build
  • post.always → publication of test results

4. Image Build and Publication

Docker images are built and pushed to the registry (Docker Hub) tagged with the git-sha.

This approach provides:

  • reproducibility (ability to roll back to a specific tag)
  • complete image history

5. Security Scanning

Scans are performed in non-blocking mode — they do not cause the build to fail but generate reports in the Jenkins dashboard.

Tool Scope
pip-audit Python dependencies of the backend
npm audit JS/TS dependencies of the frontend
Trivy The complete built Docker image (immediately after the build)

6. Deployment

On the remote virtual machine, with a secret .env file mounted via withCredentials (the file is not committed to the repository), the following command is executed:

docker compose up -d --remove-orphans --wait

using the newly built images.

Unused images are removed after successful pipeline completion:

docker image prune -f

The application is accessible and ready to use at:

http://cloud-ip:20773

7. Monitoring

A Grafana dashboard is configured on the local server to monitor the local server hosting the "production" version of the application.

The Prometheus + cAdvisor/node-exporter stack tracks:

  • overall server status (CPU / RAM / disk I/O)
  • Docker containers statuses (filesystem usage, CPU, network, and memory per container)

8. Rollback Strategy

A dedicated pipeline is implemented for rollback: jenkins/rollback/Jenkinsfile.

Procedure:

  1. Locate the last successful build of the required version in Jenkins
  2. In the console output, find the line Building commit XXXXXXXX (the commit hash corresponds to the image tag on Docker Hub)
  3. Navigate to the anime-backlog-rollback job in Jenkins
  4. Select Build with Parameters
  5. Paste the required commit hash into the ROLLBACK_SHA field
  6. Start the build — the production version will be restored on the deployment server

8. Flowchart

flowchart

CI/CD Pipeline: anime-backlog-web (in Russian)

Written by skeirss

CI/CD Pipeline: anime-backlog-web

О проекте

anime-backlog-web — самописное приложение для отслеживания аниме-тайтлов, состоящее из отдельных Docker-контейнеров:

  • Backend — Python
  • Frontend — Node.js / Next.js
  • База данных — PostgreSQL

1. Подготовка инфраструктуры

Terraform

С помощью Terraform создаётся виртуальная машина в Yandex Cloud. В главном конфигурационном файле запрашиваются:

  • 1 загрузочный диск
  • 1 виртуальная сеть
  • 1 подсеть
  • 1 виртуальная машина с минимальными характеристиками
  • подключение диска и сети к ВМ

cloud-terraform/main.tf

Cloud-init

Первоначальная настройка ВМ (bootstrap), выполняется один раз при первом запуске:

  • добавление пользователя
  • добавление SSH-ключа

cloud-terraform/cloud-init.yml

Ansible

Более детальная конфигурация облачной машины:

  • установка и настройка Docker
  • настройка firewall
  • установка fail2ban
  • усиление безопасности sshd

После связки Terraform → Cloud-init → Ansible виртуальная машина полностью готова к деплою приложения.

ansible/harden.yml


2. Запуск pipeline

git push в удалённый репозиторий на GitHub → GitHub отправляет webhook на:

https://jenkins.bernd32.xyz/github-webhook/

Безопасность доступа:

Ресурс Доступ
https://jenkins.bernd32.xyz (сам Jenkins) только для IP, разрешённых в конфигурации nginx
.../github-webhook/ (endpoint для вебхуков) открыт для всех IP, защищён HMAC-подписью payload

Jenkins клонирует репозиторий и считывает декларативный pipeline из jenkins/Jenkinsfile.


3. Quality Gates

Тесты фронтенда и бэкенда запускаются параллельно, каждый в своём Docker-контейнере.

Backend

  • Контейнер: python:3.14-slim-bookworm
  • Создание venv
  • Установка зависимостей
  • Запуск pytest с JUnit-отчётом
  • post.always → публикация результатов тестов

Frontend

  • Контейнер: node:22-bookworm-slim
  • npm ci
  • typecheck
  • npm test (Vitest, JUnit-отчёт)
  • npm run build
  • post.always → публикация результатов тестов

4. Сборка и публикация образов

Docker-образы собираются и выгружаются в registry (Docker Hub) с тегом git-sha.

Это даёт:

  • воспроизводимость (откат на конкретный тег)
  • полную историю образов

5. Проверка безопасности

Проверки выполняются в non-blocking режиме — не приводят к падению сборки, а формируют отчёты в панели Jenkins.

Инструмент Что сканирует
pip-audit Python-зависимости бэкенда
npm audit JS/TS-зависимости фронтенда
Trivy Собранный Docker-образ целиком (сразу после сборки)

6. Деплой

На удалённом хосте ВМ, с подключённым через withCredentials секретным .env-файлом (не коммитится в репозиторий), выполняется:

docker compose up -d --remove-orphans --wait

с только что собранными образами.

После успешного завершения pipeline:

docker image prune -f

Приложение доступно по адресу:

http://cloud-ip:20773

7. Мониторинг

В Grafana на локальном сервере настроен дашборд для мониторинга локального сервера с "production"-версией приложения.

Стек Prometheus + cAdvisor/node-exporter отслеживает:

  • общее состояние сервера (CPU / RAM / disk I/O)
  • состояние Docker-контейнеров (использование FS, CPU, сети, памяти по каждому контейнеру)

8. Rollback-стратегия

Для отката реализован отдельный pipeline: jenkins/rollback/Jenkinsfile.

Порядок действий:

  1. Найти последний успешный билд нужной версии
  2. В console output найти строку Building commit XXXXXXXX (хеш коммита = тег образа на Docker Hub)
  3. Перейти в job anime-backlog-rollback в Jenkins
  4. Выбрать Build with Parameters
  5. В поле ROLLBACK_SHA вставить нужный хеш коммита
  6. Запустить билд — версия на проде будет восстановлена

9. Flowchart

flowchart

Anime backlog app

Written by skeirss

There's a little oddity about me: I like to make lists and do things systematically and in order. I also like to watch anime, and there are a lot of anime that I’m planning to watch later. And so I had a need to create an app that would solve my problem of making lists of anime that I would like to watch in the future.

Before creating this app, I was making do with Google Docs spreadsheets, but I wanted to create something of my own, with features that are difficult or impossible to implement in spreadsheets (for example, requesting information about anime from an external database using the API). And besides, it happened that I got a subscription to chatgpt plus, and so I decided to try my hand at so-called “vibe coding”.

I have created applications, simple programs and scripts for my personal use before, but this time I decided to take the project seriously, having read quite a lot of material about vibe-coding.

I used a tactic of the multi-staged prompting with set a set of high-quality build prompts. Here’s the sets of prompts I used to create my “anime-backlog” app: https://github.com/bernd32/anime-tracker-web/blob/main/prompts.txt I have put a lot of effort into creating these sets of LLM prompts, but it was worth it because the app turned out quite well. I'm not a professional programmer, but I think my app would be perfectly ready for production, if it weren't for the fact that it's for my personal use and might only be of interest to me.

Here’s the link to the application I’m talking about: https://backlog.bernd32.xyz

By the way, there are a lot (most of them) of the features that are not available to non-logged users. Basically, all actions that can modify the database are available only to logged users (only to me, basically).

And here’s the github repo: https://github.com/bernd32/anime-tracker-web/

The full list of features in this app:

Core backlog management

  • Track anime entries with name, year, season, status, type, comment, URL, and downloaded flag.
  • Create new anime entries.
  • Edit existing anime entries.
  • Delete individual anime entries.
  • Prevent duplicate entries with the same normalized name, year, and season.
  • Validate years in the supported range.
  • Normalize pre-2010 entries to the "Other" season bucket.

Library organization and browsing

  • Dashboard view for tracked years from 2010 onward.
  • Separate "Pre-2010" library view for titles released before 2010.
  • Per-year library pages grouped by season.
  • Pre-2010 list sorted alphabetically.
  • Global search by anime name within the current view.
  • Sidebar navigation with quick links to dashboard, Pre-2010, statistics, and all tracked years.

Progress and status tracking

  • Track anime status as unwatched, watching, or completed.
  • Track whether an anime has been downloaded.
  • Year progress card with completion percentage and progress bar.
  • Pre-2010 progress card with completion percentage and progress bar.
  • Year list completion percentages in the sidebar.
  • Color-coded list rows/cards based on status and downloaded state.

Random selection

  • Random pick button for the current scope.
  • Random pick limited to unwatched titles in the selected scope.

Statistics

  • Overall backlog totals and completion percentage.
  • Counts by status.
  • Counts by type.
  • Pre-2010 totals and completion stats.
  • Per-year totals and completion stats.

Import and export

  • CSV export of the backlog.
  • CSV import with dry-run preview mode.
  • CSV import validation with row-level errors and warnings.
  • Duplicate detection during import.
  • Automatic coercion of invalid seasons and statuses during import when possible.

Shikimori metadata

  • Fetch cached metadata from Shikimori for an anime entry.
  • Display Russian title, Japanese title, score, episode count, air date, studios, genres, and description.
  • Strip character-link markup from returned descriptions.
  • Cache Shikimori metadata in memory and in the database.
  • Owner-only manual refresh endpoint for Shikimori metadata.
  • Owner-only cache reset for Shikimori metadata.
  • Fallback to stale cached Shikimori data when the upstream request fails.

Authentication and access control

  • Read-only access for anonymous visitors.
  • Single owner sign-in flow for write access.
  • Owner-only create, edit, delete, import, year-delete, preferences-update, Shikimori refresh, and Shikimori cache reset actions.
  • CSRF protection for write requests.
  • Signed session cookies for authenticated access.
  • Login throttling with configurable failure limit, rolling window, and lockout duration.

Preferences and settings

  • Preferences page protected by owner access.
  • Remember last visited scope kind.
  • Remember last visited scope year.
  • Remember last used season.
  • Theme preference setting.
  • Density preference setting.

Year management

  • List tracked years from 2010 onward.
  • Delete an entire year and all anime entries in that year.

UI and UX

  • Responsive layout for desktop and mobile.
  • Mobile navigation drawer.
  • Add anime button shown only to users with write access.
  • Import / Export menu shown only to users with write access.
  • External links for anime URLs.
  • Login redirect back to the intended page after successful sign-in.

Backend and operational features

  • FastAPI backend with structured API routes.

  • PostgreSQL persistence.

  • Alembic database migrations.

  • Docker Compose setup for production-style deployment.

  • Development Docker overlay for local bind-mounted development.

  • Health and readiness endpoints.

  • Configurable CORS settings.

  • Configurable Shikimori proxy support.

And if you think that a set of high-quality prompts allowed me to create the app perfectly on my first try, you're very much mistaken. The first attempt was a flop, and I had to ask LLM to fix quite a few syntax errors and bugs, as well as add and fix various features. However, this helped me to create a fairly solid framework (architecture) for a high-quality application, so any rough edges that appeared during “development” were very easy to fix, as they hardly affected the core architecture of the application that was created the first time.

Borgmatic backup issue

Written by skeirss

Problem

When running borgmatic create prune via the systemd service (borgmatic.service), the backup process hung indefinitely during the create phase after the log line Processing files … and the warning /var/lib/docker/containers/...-json.log: file changed while we backed it up.

The hang occurred only when the database hooks were active (mariadb_databases and sqlite_databases). These hooks automatically enable --read-special and cause borgmatic to include temporary dump files, which in turn triggered extensive special-file exclusions under /var/lib/docker. A broad exclusion - /var/lib/docker/** prevented the hang but also omitted all Docker volume data (persistent application files located under /var/lib/docker/volumes/).

Manual execution of borgmatic create from an interactive root shell completed successfully, confirming the issue was specific to the non-interactive systemd execution environment combined with Docker’s overlay2 layers, container logs, and runtime files.

Root Cause

  • Database hooks force --read-special and interact with rapidly changing or special files inside /var/lib/docker.

  • Borg’s file processing becomes blocked when encountering Docker overlay files, container log files that change during backup, and certain pseudo-files in the overlay2 directory.

  • The broad Docker exclusion resolved the hang but removed the desired volume backups.

Solution

Retain the database hooks (they provide consistent, point-in-time dumps) and replace the broad Docker exclusion with granular exclusions that target only the ephemeral/problematic paths. Update the exclude_patterns section in /etc/borgmatic/config.yaml as follows (add/replace the Docker-related lines):

exclude_patterns:
...
  - /var/lib/docker/overlay2/**
  - /var/lib/docker/containers/**
  - /var/lib/docker/image/**
  - /var/lib/docker/tmp/**
  - /var/lib/docker/volumes/*/_data/*.log

More info about this issue in the docs: https://torsion.org/borgmatic/how-to/backup-your-databases/

Organazing home backup system

Written by skeirss

I've organized my first home automated backup system and tried my best to describe it (the system seems a bit complicated to me, so in case something goes wrong I'll definitely need a reference). Since this is my first attempt at setting up a backup system, it may have some flaws.

Backup System Documentation

TL;DR

  • All backups are stored on NAS (192.168.88.40)
  • Storage path: /srv/backups, /mnt/media/backups (for media)
  • Tools: borg + borgmatic + bash scripts + ansible (optional, for bootstrapping)
  • Current Clients: arch-pc, vps-nl, NAS itself
  • Check status: systemctl status borgmatic.timer
  • Logs: journalctl -u borgmatic.service
  • Restore: see "Restore" section

Architecture

[arch-pc] ──(borgmatic.timer -> daily at 22:00)──borgmatic {/etc/borgmatic.d/arch-pc.yaml} ──▶ [NAS]──/mnt/backups/arch-pc (External HDD)

[vps-nl] ──(borgmatic.timer -> daily at 16:00 Server time (21:00 EKT))──borgmatic {/etc/borgmatic/config.yaml} ──▶ [NAS]──/mnt/backups/vps-nl (External HDD)

[arch-pc-media] ──media-backup.sh (running manually)──borgmatic {/etc/borgmatic.d/arch-pc-media.yaml} ──▶ [NAS]──/mnt/media/backups/arch-pc-media (External HDD)

[nas] ──(borgmatic.timer)──borgmatic {/etc/borgmatic/config.yaml}──▶ /mnt/backups/nas (External HDD)

Networking

arch-pc → NAS
  • Direct SSH connection
  • SSH Alias: home-borg (described in /root/.ssh/config)
  • Path: ssh://home-borg/mnt/backups/arch-pc
arch-pc-media → NAS
VPS → NAS
  • FRP tunnel (/etc/frp/frpc.toml) from VPS to NAS
  • Mapping: VPS:[redacted] → NAS:3315
  • SSH Alias: home-borg (described in /root/.ssh/config)
  • Path: ssh://home-borg/mnt/backups/vps-nl
NAS → NAS
  • No network, direct data transfer

Storage Layout

/mnt/backups/

arch-pc/

vps-nl/

nas/

/run/media/bernd/backups/

arch-pc-media/

Archive format: {hostname}-{timestamp}

Monitoring

Notifications:

  • Telegram bot

Triggers:

  • failure

Script:

  • /usr/local/bin/send_message_tg.sh

Restore

Ansible bootstrapping for NAS: https://github.com/bernd32/Ansible-NAS-Bootstrap

List archives: borg list /path/to/repo or outside the NAS: borgmatic repo-list --config /etc/borgmatic.d/arch-pc.yaml

Mount the archive to /mnt/borg (*): sudo borgmatic mount --config /etc/borgmatic.d/arch-pc.yaml --archive archpc-2025-10-03T21:25:12 --mount-point /mnt/borg

Unmount: sudo borgmatic umount --mount-point /mnt/borg

Extract a file from the archive: sudo borgmatic extract --config /etc/borgmatic.d/arch-pc.yaml --archive archpc-2025-10-03T21:25:12 --path /path/to/file/to/extact --destination /tmp

Restore all files from the archive: borgmatic extract --config /etc/borgmatic.d/arch-pc.yaml --archive archpc-2025-10-03T21:25:12

(*) fuse2 and python-llfuse packages should be installed

DB restore

The borgmatic configuration file used for the original backup must be available on the host.

Case 1. Restore all configured databases from the latest archive: /root/.local/bin/borgmatic restore --config /etc/borgmatic/config.yaml --archive latest

Case 2. Restore a specific database (e.g., only Bookstack’s MariaDB): 1) Stop all containers that use the mariadb container, e.g.: docker stop bookstack; 2) borgmatic restore --config /etc/borgmatic/config.yaml --archive latest --database bookstack

Case 3. Restore only the Chyrp-lite SQLite database: 1) Stop the container; 2) /root/.local/bin/borgmatic restore --config /etc/borgmatic/config.yaml --archive latest --database chyrp

Troubleshooting

Case 1. borg.crypto.key.ArchiveTAMInvalid: Data integrity error: Archive authentication did not verify error when trying to open an archive

Solution: This problem is related to Trusted Archive Metadata (TAM). The solution is to repair the archives:

su - 

BORG_WORKAROUNDS=ignore_invalid_archive_tam borg upgrade --archives-tam /path/to/repo

Check: borg check /path/to/repo

Automatic consistency checks and file restore testing

Borgmatic performs integrity checks automatically on a periodic basis stated in the borgmatic configuration: https://torsion.org/borgmatic/reference/configuration/consistency-checks/

(updated 2026-09-24) File restore test. A weekly automated test that confirms my Borg backups can actually be restored. Every Sunday at 18:00, a systemd timer runs a custom shell script that does the following: for each repository listed in /etc/borg-extraction-test.conf, the script takes the latest archive and picks a random non-empty file from it. It gets that file's SHA-256 from Borg, computed directly from the repository data, then extracts the file to a temporary directory and compares its checksum with the reference. The extracted file is deleted afterwards. A single summary report (✅/❌ per repo, with archive, file and any errors) is sent to my Telegram through /usr/local/bin/send_message_tg.sh. The script exits with an error status if any repo fails, so systemd also flags the run as failed.

Components:

/usr/local/bin/borg-verify.sh: the check script

/etc/borg-verify.conf: repo list with a password source per repo (mode 600)

borg-verify.service and borg-verify.timer: the schedule

To-Do

  • Docker containers configs + DB backup and restore (done)
  • There are two weak points in the current setup: the NAS backing up to itself (so this is not really a backup, just versioning), and the lack of an offsite backup. Both issues could be addressed by periodically copying backup archives to an encrypted external drive stored elsewhere. (done)

Flowchart

flowchart

Docker container traffic routing through proxy

Written by skeirss

So today last.fm stopped working for Russia:

❯ curl -I https://www.last.fm/ | head -n1
HTTP/2 403

So here's the problem: now my dockerized Navidrome server won't scrobble tracks to last.fm, and since Navidrome doesn't support proxy configuration, I have to route all network traffic from the container through a proxy server.

Sometimes I feel like I'm between a rock and a hard place, because the restrictions are coming from both sides... But something good always comes out of every bad situation, and from this one I'll learn a bit more about proxying docker containers. Anyways.

What's my goal: make Navidrome scrobble tracks to last.fm again by routing its traffic though my host machine's Xray proxy. What's my steps:

  • Install and configure xray on my server
  • Update docker compose, add proxy variables

So first I need to update my Xray configuration. Basically I need to tell Xray to listen on all network interfaces (0.0.0.0) so the Docker container can reach it:

  "inbounds": [
    {
      "listen": "0.0.0.0",
      "port": 10808,
      "protocol": "socks"
    },
    {
      "listen": "0.0.0.0",
      "port": 10809,
      "protocol": "http"
    }
  ],

And since my server is local and working behind a router and cg-nat, it is a safe approach.

Now I need to update my docker compose file, so Navidrome can find the host and use the proxy vars:

services:
  navidrome:
    extra_hosts:
      - "host.docker.internal:host-gateway"
    environment:
      - HTTP_PROXY=http://host.docker.internal:10809
      - HTTPS_PROXY=http://host.docker.internal:10809
      - ALL_PROXY=socks5://host.docker.internal:10808
      - NO_PROXY=localhost,127.0.0.1

Why extra_hosts? It creates a "map" so the container knows host.docker.internal = your host machine's IP. Checking that variables described in the docker file are read within the container:

 root@thinkcentre:~# docker exec navidrome-navidrome-1 env | grep -i proxy
no_proxy=localhost,127.0.0.1
http_proxy=http://host.docker.internal:10809
https_proxy=http://host.docker.internal:10809
all_proxy=socks5://host.docker.internal:10809

Looks like it's fine. Too bad we can't test connection inside the container using curl, because there's no such utility in the container. Let's just play a song and see if it is scrobbling or not: Yes, it is working as intended, yay!

Only what he finds conducive is to his taste; his pleasure, his enjoyment stops when the mark of what is conducive is overstepped. He guesses correctly what will heal harm, he exploits strokes of bad luck to his advantage; what does not kill him makes him stronger.