AWS: сетап базової інфраструктури для WordPress
0 (0)

11 Березня 2026

Прийшов час для мажорного апгрейду серверу RTFM, який зазвичай роблю переїздом на новий сервер, бо заодно роблю різні інші апгрейди, як-от версію PHP або навіть міграцію в інший клауд.

Цього разу планую переїжджати з DigitalOcean, де RTFM хоститься з 2020 року. До самого DigitalOcean претензій нуль – всі ці роки системи працювали бєз єдіного разриву (с) Антон Уральский, але, раптом, з’ясував, що в мене на AWS накопичилась купа AWS Credits, які мені як AWS Hero видають кожного року. При цьому за хостинг і бекапи в DigitalOcean я плачу живими гривнями і, в принципі, немало – близько 40 баксів в місяць.

Ну і RTFM в AWS вже колись хостився – з 2015, здається, до 2020 року.

Отже, що треба захостити – це невеликий блог WordPress, тому сетап буде без fault tolerance і high availability.

Робити будемо методом “clickops” – без Terraform, просто руками.

Чому без Terraform – бо, по-перше, мені цікаво глянути, що ж нового з’явилось в AWS Console, бо насправді не так часто туди заглядаю і тим більш щось роблю руками. По-друге – просто нема сенсу робити якусь автоматизацію, бо скоріш за все щось буду перероблювати-міняти і потім більше часу витрачу на зміни в коді, ніж на сам сетап. Ну і сама інфраструктура відносно маленька.

А оскільки це чисто особистий проект для хостінгу одного сайту і без всяких Dev/Staging/Prod оточень – то і сенсу тягнути сюди Terraform мало.

Та і насправді коли робив все описане нижче – ловив дуже приємні флешбеки в роки 2015-2016, коли тільки ще знайомився з AWS і мало користувався Terraform.

І навіть є якийсь особливий вайб в тому, щоб самому все створити ручками, а не просто описати в коді Terraforrm resources.

Втім, новий пост напишу, і буде він більше для тих, хто тільки знайомиться з AWS і хоче побачити як можна побудувати базову інфрастуктуру для хостінг веб-сайта – або для тих, хто хочеться трохи ностальгії за тими часами, коли ми не все крутили в Кубернітісах 🙂

Намагався описати максимально стисло – але вийшло багато матеріалу.

Планування архітектури

Чисто базовий етап для майже будь-якого веб-сервісу – базова мережа, один EC2, один RDS, все в одній Availabilty Zone.

На EC2 буде Amazon Linux з NGINX та PHP-FPM, база даних блогу – AWS RDS MariaDB.

Спочатку планував Debian, бо система “поставив і забув”, але її використання в AWS потребує трохи геморою – а Amazon Linux працює просто з коробки.

По ходу діла знайшов непогане порівняння – Amazon Linux vs Debian: What are the differences?

EC2 буде в приватній мережі, без прямого доступу з інтернету, і спочатку для доступу до інстансу думав використати AWS SSM, який насправді ніколи толком не юзав, бо по роботі все в єтіх ваших Кубернетісах, але він прям overkill, потребує достатньо багато додаткових налаштувань – IAM Role та VPC Endpoints, що коштує додаткових грошей, тому все ж вирішив робити через AWS EC2 Instance Connect.

Для доступу до WordPress на EC2 в систему додамо AWS Load Balancer, до якого потім ще можна буде підключити AWS WAF.

І не буде робитись EC2 AutoScaling – бо це теж трошки overkill для маленького блогу. Правда, RDS, у якого мінімум 20 гіг диск при базі RTFM в 1.2 гіга теж таке собі, але нехай буде – подивимось на “традиційний” сетап подібної інфраструктури.

Отже, план такий

  • AWS Availability Zones:
    • всі ресурси (EC2 та RDS) будуть в одній AZ, але мережа має бути мінімум в двох
  • Network – VPC та Subnets:
    • створимо одну AWS VPC з чотирма Subnets у двох Availability Zones:
      • Public Subnets: тут будуть жити сервіси, яким треба мати Public IP – Load Balancer, NAT Gateway
      • Private Subnets: тут буде жити EC2 з WordPress та RDS з MariaDB
    • налаштуємо AWS Load Balancer для доступу до WordPress
  • EC2:
    • один сервер з Amazon Linux та NGINX та PHP-FPM
  • RDS:
    • мінімальний інстанс RDS з MariaDB – буде жити в приватному сабнеті в власною Secuity Group та автоматичними бекапами
  • Route 53:
    • для доступу до бази даних створимо окрему локальну DNS zone, яка буде доступна тільки в межах VPC
  • Security:
    • перший рівень захисту – це мережа: всі робочі ресурси будуть в приватних сабнетах
    • до них додамо Security Groups – для самого EC2, для AWS RDS і для Load Balancer,
    • пізніше можна буде глянути на AWS VPC NACL і потрогати AWS WAF – дуже давно з ним не працював
    • SSH та VPN і доступ до EC2:
      • EC2 Instance Connect для SSH
      • пізніше встановлю WireGuard і підключу до мого домашнього MikroTik та зможу підключатись по SSH вже напряму

І кілька слів по самому AWS.

Перше – вибір регіону: тут в першу чергу звертаємо увагу на локацію і клієнтів – якщо у нас основні клієнти в USA, то, логічно, вибираємо регіони там.

Другий момент, на який звертаємо увагу – ціна, бо в кожному регіоні ціни трохи відрізняються, хоч і не в рази.

Тому для RTFM візьму Ірландію (eu-west-1) – там і тихо (не літають шахеди, як в ОАЕ у 2026 році), і з європейських AWS Regions вона сама дешева.

Поїхали.

AWS Costs

Питання костів, коли працюєш з AWS, актуальне завжди.

Описаний нижче сетап вийшов в 5 USD/day, тобто 150 баксів на місяць – і це ще без урахування трафіку і додаткових сервісів типу бекапів, SSM та WAF.

В кінці буде детальний розбір по костам.

Ну і хоча пост називається “сетап базової інфраструктури” – але за великим рахунком для якогось персонального блогу він дуже overengineered: спокійно можна обійтися без приватних сабнетів, без AWS Apliaction Load Balancer, і навіть без AWS RDS. І якби я робив для RTFM і не мав вільних кредитів – то робив би набагато простіше.

Втім, якщо будувати щось більш “production grade”, то описана нижче інфраструктура як раз і є базовою, або, радше, “традиційною” – з розділенням мережевого доступу, з винесенням баз даних на окремий інстанс, з Load Balancer.

Створення VPC

Починаємо з основи всього – VPC.

VPC дасть нам ізоляцію, дасть можливість отримати доступ до ресурсів в приватних сабнетах, дасть можливість зекономити на трафіку – бо зможемо ходити до ресурсів AWS S3 через внутрішні ендпоінти, а не через інтернет.

Нам треба зробити:

  • Private Subnets: для EC2 та RDS
    • ще можна і бази даних винести в окремі subnets – але це вже точно поза “сетап базової інфраструктури для WordPress”, тому не робимо
  • Public Subnets: для Load Balancer та NAT Gateway

AWS ALB потребує мінімум 2 subnets, тому робити будемо у двох Availability Zones, хоча всі ресурси будуть жити тільки в одній.

Основні налаштування

В панелі створення VPC багато чого змінилось з того часу, як я тут щось робив руками – додалась можливість через “VPC and more” створити відразу все – спробуємо, як це працює.

Єдиний, як на мене, недолік в цій можливості “все і одразу” – не так добре розумієш що і для чого створюється, а створення якихось ресурсів взагалі проходить повз уваги: я, наприклад, тільки через декілька днів згадав, що в AWS VPC для Public Subnets створюється ще і Internet gateway.

Тому якщо вперше знайомишся з AWS і VPC, то в старому підході “робити все руками” все ж є сенс.

Якщо хочеться зробити “по-старому” – то описував цей процес ще у 2016 році, якихось кардинальних змін в побудові нетворку не було:

Автогенерація імен ресурсів – теж прикольна штука, і генерує достатньо адекватні імена як раз в тому стилі, як я це завжди робив – з іменем subnet type та Availability Zone:

Вибір CIDR важливий, особливо, якщо планується мати кілька VPC і між ними будувати “мости” у вигляді VPC Peering – треба розрахувати так, щоб адреси не перетинались.

Крім того, конкретно в моєму кейсі, треба враховувати майбутній VPN, у якого власна мережа для клієнтів – 10.100.0.0/24.

AWS по дефолту пропонує 10.0.0.0/16 – можна так і залишити, хоча, звісно, для такого проекту адрес буде забагато.

Але, головне, щоб ця мережа не перетинається з 10.100.0.0/24, бо в 10.0.0.0/16 входять адреси від 10.0.0.0 до 10.0.255.255.

тут я вже почав писати про розрахунок адрес, але вийшло таке полотенце тексту, що вирішив його винести окремим матеріалом

Отже, залишаємо дефолтний блок 10.0.0.0/16:

IPv6 нам не треба, пропускаємо.

Tenancy – щось на дуже дорогому: можливість запускати всі свої EC2 на виділеному для AWS Account hardware серверах, зараз це точно не треба, див. Amazon EC2 Dedicated Instances.

VPC encryption control – щось нове, дозволяє включити контроль використання plaintext трафіку в мережі, нам не треба, пропускаємо.

Number of Availability Zones задаємо у 2, це мінімум для ALB:

Створення VPC Subnets

Далі треба налаштувати сабнети двох типів і створити по одному сабнету кожного типу в кожній Availability Zone.

Перший блок /24 я залишаю “резервним”, да і виглядає так красивіше:

  • дві публічні:
    • 10.0.1.0/24
    • 10.0.2.0/24
  • дві приватні:
    • 10.0.3.0/24
    • 10.0.4.0/24

Створення NAT Gateway

Тут буде описано створення звичайного, AWS Managed NAT Gateway, але пізніше я його замінив на “NAT Gateway для бідних” – просто окремий EC2, див. AWS: власний EC2 в ролі NAT Gateway замість AWS Managed NAT Gateway.

Regional NAT Gateways – нова плюшка від AWS, не так давно  з’явилась – дозволяє повністю автоматизувати створення NAT Gateways в нових Availablity Zones, не потребує Public Subnets, автоматично апдейтить Route Tables.

Але коштує, звісно, дорожче, та і взагалі в цьому сетапі не потрібна.

Створюємо класичний Zonal NAT Gateway і тільки в одній Availability Zone:

Завершення налаштувань VPC

VPC Endponts – залишаємо дефолтний S3, бо в мене, наприклад, до S3 пишуться бекапи блогу. Пізніше ще додамо новий, для EC2 Instance Connect.

Про VPC Endpoints трохи детальніше писав в пості Terraform: створення EKS, частина 1 – VPC, Subnets та Endpoints.

DNS Options залишаємо включеними – штука корисна і грошей не просить:

  • DNS hostnames: чи створювати “локальні” імена, наприклад – ip-10-0-3-226.eu-west-1.compute.internal – потрібно, аби коректно працювали RDS, EFS та інші мережеві ресурси
  • DNS resolution: чи зможуть сервіси всередині VPC використовувати його внутрішній DNS – теж штука корисна і зручна, хоча має свої обмеження (див, наприклад, Kubernetes: нагрузочное тестирование и high-load тюнинг – проблемы и решения)

І в результаті маємо таку картину (раніше теж не було, дуже зручно, і, здається, навіть route tables і маршрути створювались руками):

Все – поїхали створювати, займе трохи часу – міжна поки зробити чай.

За кілька хвилин – все готово:

Створення Security Groups

Зробимо три окремі Security Groups – для EC2, для RDS, та для Load Balancer:

В Security Group для EC2 дозволяємо SSH в межах VPC, дозволяємо HTTP від Public Subnets – там будуть інстанси Load Balancer (які, по суті, під капотом являють собою звичайні AWS EC2 – як і для AWS RDS):

Для SSH можна зробити більш суворі правила – дозволити тільки з VPN CIDR та VPC Private Subnet в eu-west-1a, де пізніше буде створюватись EC2 Instnace Connect Ednpoint – але це вже можна буде підтюнити пізніше, коли все буде працювати.

Аналогічно створюємо Security Group для Load Balancer – тут дозволяємо весь In на порти 80 та 443:

І для RDS – відкриваємо порт 3306 з Private Subnets, бо окрім EC2 сюди ніхто ходити не має:

Створення EC2 Instance Connect Endpoint

Коли планував робити з сервер на Debian, то думав доступ робити через AWS SSM – але для SSM потребує аж трьох VPC Endpoints, і за кожен треба платити гроші.

Тому зробив простіше – через EC2 Instance Connect Endpoint.

Переходимо в Endpoints, створюємо новий:

Задаємо ім’я і тип:

Вибираємо створену вище VPC. Опція “Preserve Client IP” – штука прикольна, передає клієнтський IP замість адреси самого Endpoint – можна буде спробувати пізніше, поки залишаємо в дефолтному “off”:

Створюється довго, хвилин 5 – як раз встигнемо зробити ще чаю та запустити EC2.

Створення EC2

Вибираємо Amazon Linux, задаємо ім’я інстансу:

Вибір типу інстансу та підрахунок потрібної пам’яті

Дуже коротко про типи, бо матеріал і так виходить великий, детальніше див. Amazon EC2 instance types.

Всі AWS EC2 діляться на кілька основних типів інстансів:

  • general purpose: збалансовані по CPU/RAM та вартості типи
    • сюди входять і burstable type, такі як t3/t4 – доступ до CPU більш обмежений, але на короткий час може, власне, burst – видаватись 100% процесорного часу, див. CPU Credsts, див. Key concepts for burstable performance instances
    • приклад general purpose:
      • t3.medium – 2 vCPU, 4 GiB RAM, ~ 30 USD/month
      • t3.large: 2 vCPU, 8 GiB RAM, ~ 60 USD/month
      • m5.large: 2 vCPU, 8 GiB RAM, ~ 69 USD/month
  • compute optimized: “заточені” під CPU – більше CPU, менше RAM:
    • приклад: c5.large – 2 vCPU, 4 GiB RAM, ~61 USD/month
  • memory optimized: і навпаки – більше RAM і менше CPU
    • приклад: r5.large – 2 vCPU, 16 GiB RAM, ~90 USD/month
  • storage optimized: мають NVMe диски з високим IOPS
    • приклад: i3.large – 2 vCPU, 15.25 GiB RAM, ~112 USD/month

Цифри 3/4/5/6 etc – покоління інстансів, чи вище цифра – тим новіше залізо “під капотом”, плюс можливості самого AWS (наприклад, в старих t2 нема підтримки підключення з serial console).

Плюс кожен тип має “підтипи”:

  • g: процесори Graviton – процесори від самого AWS на архітектурі – можуть бути не з усім сумісні, але використання ~20-30% дешевше, ніж у звичайних типів інстансів ARM при вищій швидкості виконання задач
  • i: процесори Intel – Intel Xeon, Intel Ice Lake
  • a: процесори AMD – AMD EPYC
  • n: окремий модифікатор, “network” – вищий network bandwidth, наприклад, інстанси R6in – Intel Network

Для вибору користуємось сервісами типу https://instances.vantage.sh або https://calculator.holori.com/aws.

Зараз сервер для RTFM в DigitalOcean має 2 vCPU і 4 GB RAM:

При цьому навантаження на процесор в середньому в районі 5%, а пам’ять зайнята на 60%:

Але зараз на цьому ж сервері живе MariaDB Server:

Тобто, якщо винести базу даних в AWS RDS, то на новому інстансі основним “споживачем” пам’яті буде PHP-FPM.

Можна подивитись скільки пам’яті процеси php-fpm використовують зараз:

root@setevoy-do-2023-09-02:~# ps aux --sort=rss | grep php-fpm | awk '{print $6}' | awk '{sum+=$1} END {print sum/1024 " MB total"}'
410.477 MB total

І кількість процесів:

root@setevoy-do-2023-09-02:~# ps aux --sort=rss | grep php-fpm | grep 'master\|rtfm.co.ua' | grep -v grep
root     1157320  0.0  0.3 264156 13092 ?        Ss   Mar03   0:55 php-fpm: master process (/etc/php/8.2/fpm/php-fpm.conf)
rtfm     1238997  1.3  3.1 362608 126980 ?       S    15:23   0:07 php-fpm: pool rtfm.co.ua
rtfm     1237462  1.7  3.2 360912 129788 ?       S    12:16   3:26 php-fpm: pool rtfm.co.ua
rtfm     1237598  1.7  3.3 364440 132484 ?       S    12:34   3:04 php-fpm: pool rtfm.co.ua

Прикинемо скільки йому треба.

Параметри PHP-FPM pool для RTFM зараз:

...
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
...

Див. PHP-FPM: Process Manager – dynamic vs ondemand vs static (2018 рік) та NGINX: настройка сервера и PHP-FPM (2014 рік).

Тобто максимум може бути 5 FPM workers – і з цією кількістю воркерів блог спокійно переживав TCP/IP: SYN flood атака на сервер RTFM, та “Hacker News hug of death”, бо більшість запитів оброблюються на CloudFlare:

Аби прикинути споживання пам’яті кожним можемо глянути в RSS (Resident Set Size), реальна фізична пам’ять процесу – але сюди включається пам’ять на shared бібліотеки, тобто якщо кілька PHP-FPM workers використовують одну і ту ж libc – RSS кожного включає її повністю і сумарний RSS буде завищений.

Втім – нехай буде завищена, бо ми прикидуємо “найгірший” варіант.

Дивимось скільки пам’яті на кожен воркер зараз:

root@setevoy-do-2023-09-02:~# ps aux | grep php-fpm | grep 'pool rtfm.co.ua' | awk '{print $6/1024 " MB - " $13}'
126.773 MB - rtfm.co.ua
133.355 MB - rtfm.co.ua
126.168 MB - rtfm.co.ua

І якщо маємо pm.max_children = 5 – то максимум пам’яті буде ~150 MB * 5 == 750 MB.

Можна для початку взяти t3.medium – хоча це прям запас з головою:

Решта налаштувань

Створюємо ключ для доступу по SSH:

Зберігаємо собі на робочу машину, відразу задаємо права:

$ chmod 600 ~/.ssh/rtfm-al-2026-03.pem

Вибираємо VPC, сабнет eu-west-1a та Securty Group, яку створили вище:

Сторейдж – одного диску на 50 гігабайт вистачить з запасом:

В Advanced details включаємо Termination protection – дуже корисна для production ресурсів опція.

І додатково можна поки що додати Detailed CloudWatch monitoring – але за нього доведеться платити додаткові гроші, тому потім краще відключити:

Запускаємо інстанс.

Поки робили це – EC2 Instance Connect Endpoint вже готовий:

Сам EC2 інстанс стартує дуже швидко – перевіряємо підключення:

Вибираємо EC2 Instance Connect, вибираємо “Connect using Private IP”:

І ми в системі:

Аби підключитись з ноутбука – використовуємо AWS CLI:

$ aws --region eu-west-1 --profile setevoy ec2-instance-connect ssh --instance-id i-026523e8f29147e3e --connection-type eice

А пізніше вже буде пряме підключення через VPN.

Поки робимо апгрейд і встановлюємо для тесту NGINX:

# dnf update -y
# dnf install -y nginx
# systemctl enable nginx
# systemctl start nginx

Перевіряємо:

# curl localhost:80
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...

Тут все готове – можна переходити до SSL/TLS та Load Balancer, а потім встановити PHP і вже перевірити роботу WordPress.

Отримання SSL/TLS сертифікату з AWS Certificate Manager

Дуже зручна штука – бо один раз отримати, підключити до Load Balancer, і забути – далі AWS сам менеджить всі renew.

Переходимо в ACM, клікаємо Request a certificate:

Задаємо імена – змінити пізніше їх не можна, тільки робити новий сертифікат, тому відразу вказуємо всі домени і для кожного додаємо wildcard.

Залишаємо дефолту опцію DNS validation:

Клікаємо Create records in Route 53 – це, звісно, тільки для тих доменів, які обслуговуються на Route 53, див. далі приклад з Cloudflare Name Servers:

Перевіряємо чи додався новий запис для домену в Route 53:

AWS ACM Certificate та DNS validation для домену на Cloudflare Nameservers

Домен rtfm.co.ua обслуговується серверами Cloudflare:

$ whois rtfm.co.ua | grep "Name Server"
Name Server:ETHAN.NS.CLOUDFLARE.COM
Name Server:NOVALEE.NS.CLOUDFLARE.COM

Тому в панелі керування DNS Cloudflare додаємо новий запис з типом CNAME:

Але…

Фікс помилки AWS ACM Certificate DNS validation failed

Але валідація сфейлилась:

Читаємо документацію Certification Authority Authorization (CAA) problems, перевіряємо записи з типом CAA (Certification Authority Authorization) для домену – хто може видавати сертифікати для цього домену:

$ dig rtfm.co.ua CAA +short
0 issuewild "digicert.com; cansignhttpexchanges=yes"
0 issuewild "letsencrypt.org"
0 issuewild "pki.goog; cansignhttpexchanges=yes"
0 issuewild "ssl.com"
0 issue "comodoca.com"
0 issue "digicert.com; cansignhttpexchanges=yes"
0 issue "letsencrypt.org"
0 issue "pki.goog; cansignhttpexchanges=yes"
0 issue "ssl.com"
0 issuewild "comodoca.com"

І дійсно – AWS тут нема.

Додаємо два CAA записи – один для сертифікатів для корневого домену:

І один для wildcard сертифікатів:

Ще раз перевіряємо:

$ dig @ethan.ns.cloudflare.com rtfm.co.ua CAA +short
0 issue "amazon.com"
...
0 issuewild "amazon.com"
...

Видаляємо перший сертифікат, повторюємо процес створення та валідації – і тепер все готово:

Створення AWS Load Balancer

Використання Load Balancer дасть змогу, власне, балансувати навантаження – якщо планується мати кілька інстансів, то зручно мати один і той статичний URL, який можна використати як CNAME для домену, буде можливість додавати або замінювати інстанси без необхідності внесення змін в DNS домену, і дасть можливість використання AWS Web Application Firewall.

Плюс, ALB спрощує менеджмент SSL/TLS – один раз створюємо сертифікат, підключаємо його до ALB, і SSL termination буде на Load Balancer: клієнти до Load Balancer ходять з HTTPS, а Load Balancer до EC2 по HTTP – простіший конфіг NGINX, нема потреби в налаштуванні Let’s Encrypt.

Хоча за великим рахунком, якщо планується дійсно невеликий персональний сайт – то ALB теж overkill. Втім, якщо є зайві кредити, то його використання дійсно спрощує життя.

Створення Target Group

Load Balancer працює через Target Groups (TG), де кожна TG включає в себе один або декілька EC2 на які ALB буде слати трафік.

Створюємо нову Target Group:

В Type вказуємо Instance, задаємо ім’я групи та протокол, за яким буде відбуватись комунікація ALB з сервісами на EC2 в цій групі.

На EC2 у нас NGINX який приймає підключення на порт 80 – тому в Protocol та Port залишаємо дефолтні параметри:

Якщо на ALB плануємо використовувати і HTTP і HTTPS – то вказуємо HTTP/1, якщо тільки HTTPS – то можна HTTP/2.

Хоча зазвичай для HTTP просто налаштовується redirect на HTTPS і можна було б тут відразу вказати HTTP2 – але деякі клієнти все ще можуть використовувати HTTP v1 – тому залишимо їм право вибору і залишаємо дефолтну опцію HTTP1:

В Health checks можна залишити все як є – Health check path на NGINX буде “/”, Traffic port буде 80:

Вибираємо інстанс(и) для цієї Target Group:

Підтверджуємо створення:

Типи Load Balancers: ALB vs NLB vs GLB vs CLB

Amazon дозволяє створити кілька типів Load Balancer:

  • Application Load Balancer:
    • модно-молодьожно
    • працює на L7 (HTTP/HTTPS), може читати зміст HTTP-запиту і мати окремі налаштування по, наприклад, URI (/api/ – слати на одну Target Group, /users/ – слати на Auth0 тощо)
    • підтримує роботу з WebSocket, gRPC
  • Network Load Balancer:
    • працює на L4 (TCP/UDP) – дуже швидкий, чудовий вибір для high load applications, має можливість використання Static IP
  • Gateway Load Balancer:
    • доволі специфічна штука, для роутингу трафіку через сторонні network appliances (firewall, IDS/IPS), я ніколи не користувався

Окремо загадаємо про Classic Load Balancer – легасі, deprecated. Підтримує і L4 і L7 але гірше ніж ALB/NLB окремо.

Детальніше див. What’s the Difference Between Application, Network, and Gateway Load Balancing?

Налаштування AWS Load Balancer

Переходимо до створення Load Balancer:

 

Вибираємо тип Application Load Balancer:

 

Задаємо ім’я, тип Internet-facing (тип Internal – корисна опція, коли треба мати ALB, який доступний тільки всередині VPC):

Вибираємо VPC, Subnets та Secuiruty Group, яку робили на початку:

Для HTTP Listener налаштовуємо редірект на HTTPS:

А в HTTPS Listener підключаємо створену вище Target Group:

Підключаємо SSL сертифікат із ACM:

Перевіряємо, що все ОК і створюємо:

Створення займе кілька хвилин – робимо ще один чай.

Налаштування DNS для ALB

Поки створюється ALB – додамо новий запис в Route 53, який буде прив’язаний до створеного Load Balancer.

Тут приклад на іншому домені, але він в AWS ACM був доданий, тому буде працювати без помилок TLS.

Створюємо новий DNS Record, вибираємо тип Alias, знаходимо наш ALB, Routing policy залишаємо Simple (див. Choosing a routing policy):

Перевіряємо, чи все працює (може зайняти 5-10 хвилин на апдейт DNS):

Створення AWS Relational Database Service

Останній крок перед запуском WordPress – створити сервер даних.

З AWS RDS працюю дуже давно, сервіс класний, хоча, звісно, не безкоштовний. Але “перекласти відповідальність” за стабільність і бекапи на плечі AWS – чудове рішення для якогось production.

Плюс інтеграція з AWS IAM, CloudWatch Logs та Metrics, автоматичні бекапи, автоскейлінг – можливостей багато.

Створюємо новий сервер (хоча меню називається “Create database” – але створюється саме окремий інстанс):

В Credentails management можна залишити дефолтний AWS Secrets Manager – він вміє автоматично ротейтити пароль root для сервера, див. Set up automatic rotation for Amazon RDS:

Залишаємо опцію Password and IAM database authentication – хоча інтеграція IAM обмежує доступ тільки до самого серверу, а не баз даних, і все одно треба буде створювати юзера з власними правами доступу і паролем, див. AWS: RDS з IAM database authentication, EKS Pod Identities та Terraform.

Тип інстансу вибираємо мінімально доступний, db.t3.micro – хоча для RTFM і цього вистачить з великим запасом:

Розмір диску – мінімум 20 гігабайт, що при розмірі БД у RTFM в 1.2 гігабайти теж з головою.

Корисна штука для production – storage autoscaling: працює повністю непомітно для сервера і клієнтів:

В Connectivity можна автоматично налаштувати підключення до EC2 – створить всі необхідні параметри в VPC та Subnets, але давайте хоч тут зробимо вручну.

DB Subnet Group – створюємо нову, RDS сам вибере потрібні private subnets, бо далі, в Public access, ми задаємо “No” – сервер баз даних має жити тільки в приватних мережах, без доступу у світ.

Згадав про цікаву історію – MySQL/MariaDB: like Petya ransomware для баз данных и ‘root’@’%’: клієнт створив штук 10 серверів БД з публічним доступом, доступом root з інтернету, і… Без пароля.

Результат прєдсказуємий 🙂

У VPC Security Group вибираємо групу, яку створювали на початку:

В Additional monitoring settigns – інтересу раді можна включити Enhanced monitoring, це коштує додаткових грошей – але в production може дуже знадобитись, бо додає метрики по роботі операційної системи (CPU per process, RAM, disk I/O, network, file system), див. довгий пост по PostgreSQL: AWS RDS Performance and monitoring – був цікавий випадок, коли Enhanced monitoring знадобився:

Additional configuration – тут відразу можемо створити базу даних і налаштувати автоматичні бекапи.

Автоматичні бекапи (Periodic snapshots) – дуже рекомендована штука, рятувала не один раз: створює повний snapshot інстансу, і потім з цього снапшоту можна в будь-який момент створити новий інстанс з усіма даними.

До того для RDS є можливість налаштувати Continuous backups – для відновлення стану баз(и) на якийсь конкретний момент часу, див. Amazon Relational Database Service backups.

Базу створимо пізніше вручну, залишаємо бекапи:

І в кінці відразу бачимо приблизну вартість – раніше не було цього, зручно зробили:

DNS та Private hosted zone

Корисна з точки зору безпеки штука – приватні доменні зони, які доступні тільки всередині VPC, див. Working with private hosted zones.

Тому створимо окрему зону з DNS Records, які потрібні тільки в VPC, в нашому випадку – Database URL як раз чудовий приклад:

Знаходимо URL в самому RDS:

Додаємо його як value в CNAME нового запису:

Підключення до RDS

Підключаємось по SSH до EC2, шукаємо пакет mariadb:

[ec2-user@ip-10-0-3-146 ~]$ dnf search mariadb
...
mariadb114.x86_64 : A very fast and robust SQL database server
...

Аби встановити тільки клієнт – вибираємо mariadb без -server:

[ec2-user@ip-10-0-3-146 ~]$ sudo dnf install -y mariadb114

В AWS Secrets Manager знаходимо пароль RDS root:

І підключаємось, використовуючи local DNS record, який створили вище:

[ec2-user@ip-10-0-3-146 ~]$ mysql -h db.rtfm.local -P 3306 -u rtfm_root -p
Enter password:
...
MariaDB [(none)]>

Або можна це трохи автоматизувати з AWS CLI – в RDS є приклад команди:

Запуск WordPress

Ну і нарешті – у нас все готово для запуску WordPress.

Що нам залишилось – це створити базу даних, юзера, і на EC2 встановити PHP.

Створення бази даних в RDS

Створюємо базу даних і юзера – для WordPress рекомендовано utf8mb4_unicode_ci (підтримка всяких емодзі):

MariaDB [(none)]> CREATE DATABASE test_wp_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
Query OK, 1 row affected (0.064 sec)

MariaDB [(none)]> CREATE USER 'test_wp_user'@'%' IDENTIFIED BY 'test_wp_pass';
Query OK, 0 rows affected (0.058 sec)

MariaDB [(none)]> GRANT ALL PRIVILEGES ON test_wp_db.* TO 'test_wp_user'@'%';
Query OK, 0 rows affected (0.034 sec)

MariaDB [(none)]> FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.027 sec)

Установка PHP та модулів

Встановлюємо PHP і модулі – хоча тут не всі модулі, не пам’ятаю, що треба ще для роботи RTFM, але це базовий набір для WordPress та, в принципі, будь-якого вебсайту:

[root@ip-10-0-3-146 ~]# dnf install -y php-fpm php-mysqlnd php-json php-mbstring php-xml php-gd

Додаємо в автостарт та запускаємо сервіс PHP-FPM:

[root@ip-10-0-3-146 ~]# systemctl enable php-fpm
Created symlink /etc/systemd/system/multi-user.target.wants/php-fpm.service → /usr/lib/systemd/system/php-fpm.service.
[root@ip-10-0-3-146 ~]# systemctl start php-fpm

Дефолтний конфіг – /etc/php-fpm.d/www.conf, для RTFM буде окремий, але це не зараз.

Перевіряємо файл сокету, щоб впевнитись, що FPM готовий приймати підключення:

[root@ip-10-0-3-146 ~]# ll /run/php-fpm/www.sock
srw-rw----+ 1 root root 0 Mar  8 11:34 /run/php-fpm/www.sock

Створення NGINX virtualhost

Додаємо файл налаштувань тестового сайту – /etc/nginx/conf.d/test.conf.

Каталог /etc/nginx/conf.d/ включається в конфіг через основний файл налаштувань /etc/nginx/nginx.conf:

...
 include /etc/nginx/conf.d/*.conf;
...

У файлі /etc/nginx/conf.d/test.conf описуємо HTTP сервер на порту 80 з fastcgi_pass на PHP-FPM socket:

server {
    listen 80;
    server_name test-alb.setevoy.org.ua;

    root /var/www/html;
    index index.php;

    location / {
        try_files $uri $uri/ =404;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/run/php-fpm/www.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }
}

Для перевірки PHP створюємо тестовий файл /var/www/html/index.php:

<?php phpinfo(); ?>

Виконуємо nginx check config && reload:

[root@ip-10-0-3-146 ~]# nginx -t && systemctl reload nginx
nginx: [warn] conflicting server name "_" on 0.0.0.0:80, ignored
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

І перевіряємо файл index.php в браузері:

Установка WordPress

Завантажуємо архів, розпаковуємо, міняємо власника та групу на nginx:nginx:

[root@ip-10-0-3-146 ~]# cd /var/www/html
[root@ip-10-0-3-146 html]# wget https://wordpress.org/latest.tar.gz
...
[root@ip-10-0-3-146 html]# tar -xzf latest.tar.gz
[root@ip-10-0-3-146 html]# mv wordpress/* .
mv: overwrite './index.php'? y
[root@ip-10-0-3-146 html]# rm -rf wordpress latest.tar.gz
[root@ip-10-0-3-146 html]# chown -R nginx:nginx /var/www/html

Відкриваємо в браузері – не завантажуються CSS та картинки.

Але це ОК, далі поправимо, не критично.

Критично буде далі з RDS – тому спочатку дочитайте цю частину:

Клікаємо Let’s go, задаємо параметри підключення до RDS:

І ловимо помилку “Error establishing a database connection“:

Ну… 🙂

WordPress users know that feeling 🙂

AWS RDS та WordPress “Error establishing a database connection”

Перше, що можна спробувати – це створити файл wp-config.php вручну і задати параметри явно:

...
// ** Database settings - You can get this info from your web host ** //
/** The name of the database for WordPress */
define( 'DB_NAME', 'test_wp_db' );

/** Database username */
define( 'DB_USER', 'test_wp_user' );

/** Database password */
define( 'DB_PASSWORD', 'test_wp_pass' );

/** Database hostname */
define( 'DB_HOST', 'db.rtfm.local' );

/** Database charset to use in creating database tables. */
define( 'DB_CHARSET', 'utf8mb4' );
...

Але в цьому конкретному випадку це не допоможе.

Тому встановлюємо php-cli:

[root@ip-10-0-3-146 html]# dnf install -y php-cli

І спершу перевіряємо, чи працює DNS Resolver на нашу приватну DNS zone:

[root@ip-10-0-3-146 html]# php -r "echo gethostbyname('db.rtfm.local');"
10.0.3.53

Да, все чудово.

Тепер пробуємо MySQL connect – і от тут вже ловимо  саму помилку – “Connections using insecure transport are prohibited“:

[root@ip-10-0-3-146 html]# php -r "mysqli_connect('db.rtfm.local', 'test_wp_user', 'test_wp_pass', 'test_wp_db') or die(mysqli_connect_error());"
PHP Fatal error:  Uncaught mysqli_sql_exception: Connections using insecure transport are prohibited while --require_secure_transport=ON. in Command line code:1
Stack trace:
#0 Command line code(1): mysqli_connect()
#1 {main}
  thrown in Command line code on line 1

Тут два варіанти вирішення – або в wp-config.php примусово включити SSL для підключення (рекомендується):

define('MYSQL_CLIENT_FLAGS', MYSQLI_CLIENT_SSL);

Або змінити параметр require_secure_transport в RDS (не рекомендується):

Після додавання MYSQLI_CLIENT_SSL установка вже пішла нормально – картинки зараз поправимо:

Фікс CSS та картинок

Проблема виникає через змішаний трафік – до ALB ми ходимо по HTTPS, а між ALB та EC2 маємо простий HTTP.

Для фіксу в wp-config.php додаємо:

$_SERVER['HTTPS'] = 'on';
define('WP_HOME', 'https://test-alb.setevoy.org.ua');
define('WP_SITEURL', 'https://test-alb.setevoy.org.ua');

І блог працює без проблем:

AWS Costs Breakdown: а шо по грошам?

Болюча тема для будь-якого IaaS/PaaS провайдеру – будь-то Google Cloud Engine, Microslop Microsoft Azure чи AWS.

Коротко пройдемось по описаному вище сетапу – що і скільки в результаті коштує по грошам.

В Cost Explorer бачимо таку картину:

5 доларів на день, за 30 днів буде 150 доларів.

Ну – дуже не слабо, як для приватного блогу. Але і описана вище інфраструктура трохи завелика для такого проекту.

Подивимось що саме нам так дорого обходиться.

EC2-Other costs

Часте питання – “що за EC2 Other в Cost Explorer“, бо не дуже зрозуміла назва.

Фактично, сюди входять Public IP адреси, трафік, EBS volumes.

Подивитись що конкретно нам обходиться в $1.29 можна в тому ж Cost Explorer – у Filters > Service вибираємо EC2 Other, а в Group by > Dimension вибираємо Usage type або API Operation:

Власне, бачимо вартість NAT Gateway.

Деталі заходимо в документації Amazon VPC pricing, і рахуємо: $0.045 на годину, множимо на 24 і маємо 1.080 долари на добу, або 30+ доларів на місяць.

І це ще без трафіку через NAT Gateway, який рахується окремо – $0.048 за кожен гігабайт в cross-region або cross AvailabilityZone трафіку.

Вартість трафіку в AWS це взагалі окрема тема, тут вже розбирати не буду, але колись писав пост AWS: Cost optimization – обзор расходов на сервисы и стоимость трафика в AWS.

Власне, саме з цієї причини варто мати VPC Endpoint для S3: тип gateway безкоштовний, зато трафік буде йти не через NAT Gateway – а всередині VPC.

EBS Volumes costs

На скріншоті вище бачимо API Operation CreateVolume-Gp3 – це вартість EBS, який підключений до EC2, див. Amazon EBS pricing: 50 гігабайт диск дає нам $4.4 на місяць, або $0.147/день.

Диск для RDS рахується окремо:

EC2-Instances costs

Тут все просто – маємо один t3.medium, який коштує $0.0416 – маємо $0.99 в день.

Але до того ж і тут рахується трафік – він $0.04 до $0.09 за гігабайт outgoing в залежності від об’єму.

Вхідний трафік не оплачується.

Втім, є свої нюанси з трафіком:

  • ALB: трафік, який віддаємо клієнтам через Load Balancer – оплачується
  • NAT Gateway: тут взагалі платимо двічі:
    • NAT GW processing fee: це з costs самого NAT Gateway, аза кожен переданий через нього назовні гігабайт
    • EC2 Data Transfer Out: і додатково платимо за кожен гігабайт “у світ” з самого EC2
  • RDS: дані в межах одної Availability Zone не оплачуються, але якщо є cross-AZ або cross-region сетап – то платимо $0.01/GB за вхідний і вихідний трафік

А, і це ще не згадував On-Demand, Reserved, Spot інстанси. Але це теж окрема тема, див. Amazon EC2 billing and purchasing options.

А ще окремо оплачуються CPU Credits, $0.05 per vCPU-Hour 🙂 (для RDS теж)

Elastic Load Balancing costs

Див. Elastic Load Balancing pricing.

  • платимо за кону годину роботи ALB
  • платимо за LCU (Load Balancer Capacity Units) – навантаження на ALB, загальна вартість буде залежати від того, скільки ALB опрацював запитів від клієнтів (або під час DDoS :trollface: )
  • платимо за outgoing трафік – але тільки за трафік з ALB, бо трафік між EC2 та ALB в межах одної Availability Zone безкоштовний

Relational Database Service costs

Див. Amazon RDS for MariaDB pricing.

Вже бачили на скріншоті вище – EU-InstanceUsage:db.t3.micro, EU-RDS:GP3-Storage, EU-DataTransfer-In-Bytes, EU-DataTransfer-Out-Bytes.

  • за db.t3.micro в одній Availability Zone платимо $0.018, або $0.43 на добу, або ~13 в місяць
  • CPU Credits для t3 – $0.075 per vCPU-hour
  • Storage: $0.115/GB-month
  • Backup snapshots: безкоштовний в розміні 100% від розміру диска для RDS instance

Route 53 costs

Див. Amazon Route 53 pricing.

Тут платимо за:

  • $0.50 за кожну домену зону
  • і окремо за запити до DNS, але там дуже багато безкоштовних запитів, ще і різні типи – кожну вже окремо описувати не буду

VPC costs

Див. Amazon VPC pricing.

Тут багато свої особливостей – VPC Peering, IPAM, Encryption.

Конкретно в нашому випадку платиться тільки за Public IP (бо у Load Balancer та NAT Gateway власні Elastic IP addresses):

При чому за IP платимо два рази:

  • AllocateAddressVPC: просто за те, що нам дозволили користуватись адресою IPv4
  • AssociateAddressVPC: за прив’язку адреси до інстансу

Перевірити які адреси до чого підключені можна в VPC.

Тут дві адреси для ALB:

І одна для NAT Gateway:

І одна знайшлась unused – а я за неї плачу.

Ну і, власне, це основне по вартості AWS.

Платимо за “кожний чіх”. Втім, як в інших подібних провайдерах.

Loading

FreeBSD: Home NAS, part 15: автоматизація бекапів – скрипти, rsync, rclone
0 (0)

7 Березня 2026

Фактично, це вже остання велика задача – налаштувати автоматичне створення бекапів.

В пості FreeBSD: Home NAS, part 13: планування зберігання даних та бекапів описав загальну ідею детальніше – як і що бекапиться, де, що, як зберігається, а сьогодні – вже суто технічна частина про саму реалізацію.

Про що буде йти мова в цьому пості – як робилась автоматизація збору даних з Linux-хостів на NAS, трохи про підводні камені rsync, і як всі бекапи з самого NAS синхронізуються в Rclone remotes.

Всі описані тут скрипти і приклади конфігураційних файлів є в GitHub setevoy2/nas-backup.

Всі частини серії по налаштуванню домашнього NAS на FreeBSD:

Короткий опис ідеї та реалізації

Взагалі, спочатку ідея була все робити з restic і NFS: мати на NAS окрему NFS share, яка б підключалась до хостів, потім на хостах в цю шару з restic робити бекапи, і після цього з rclone копіювати дані в Google Drive та/або AWS S3.

Але чим більше думав – тим більше розумів, що це не найкраще рішення:

  • по-перше – зав’язуватись на NFS, яка має бути постійно підключена – треба перевіряти, чи вона є, чи активна, ну і взагалі – це прив’язка до постійного network connection
  • по-друге – сам restic, як система бекапу для домашнього використання – трошки overkill:
    • snapshots – да, круто, але ті самі снапшоти робляться із ZFS
    • другий момент – це те, що restic працює виключно з crypted data у своїх репозиторіях – а я хотів мати можливість просто зайти в каталог бекапів, і подивитись що там є

Тому врешті-решт замість restic вирішив робити зі звичайним rsync, а замість restic remotes – взяти rclone, і заливати в клауди дані з ним.

Але і тут виникли свої нюанси і зміни в планах.

Спершу ідея була мати shell-скрипти з викликом rsync на Linux-хостах, ці скрипти запускати по cron, робити бекапи і заливати їх на NAS – і навіть написав такі скрипти на робочому ноутбуці.

Втім, коли вже почав збирати всю систему, то постало питання – а коли саме з NAS запускати rclone, щоб вже оновлені з rsync бекапи залити в клауд?

Власне, тоді і прийшло розуміння, що потрібен якийсь “control loop”, який буде і запускати копіювання даних з інших хостів, і на самому NAS, і після завершення копіювання даних – буде заливати апдейти в Google Drive та Backblaze, ще і виконувати якісь додаткові дії.

Тобто загальна схема тепер така: запускати rsync прямо з NAS, з цього “control loop бекап-скрипта”, з rsync по SSH підключатись до віддалених хостів, збирати дані, і в кінці, точно знаючи, що всі дані зібрані – вже можна спокійно запускати rclone.

Коротко про мою мережу і хости взагалі – детальніше було в пості FreeBSD: Home NAS, part 13:

Архітектура реалізації

Є три системи, які керують даними:

  • Syncthing: синхронізує частину даних з /home/setevoy між ноутбуками, NAS і телефоном (див. FreeBSD: Home NAS, part 12: синхронізація даних з Syncthing)
  • Rsync: основна “робоча лошадка” для копіювання даних між хостами – збирає з Linux, Raspberry PI, DigitalOcean, та з самого NAS/FreeBSD
  • Rclone: займається синхронізацією даних в клауди

rclone робить sync в Google Drive та Backblaze з опцією --backup-dir – тому навіть якщо Syncthing щось наламає і видалить, а потім ці зміни синхронізуються в клауд – то все одно залишаться копії видалених даних.

І плюс в самому Syncthing для всіх shared директорій включена “Trash Can Versioning”.

Загальна схема виглядає так:

Як писав в попередньому пості – є кілька різних “класів даних” які зберігаються в окремих датасетах, і кожен датасет мапиться на власний rclone remote з власними налаштуваннями шифрування.

Якщо спростити схему і відобразити тільки потік даних – то це виглядає так:

Структура каталогів і файлів

Взагалі, в чорнетці був розписаний весь процес створення “утиліти”, але вирішив вже просто описати фінальне рішення (і то вийшло нічого собі тексту).

Всі операції виконуються кількома shell-скриптами, всі потрібні налаштування – описані в конфіг-файлах.

Структура файлів та каталогів:

root@setevoy-nas:~ # tree -L 3 /opt/nas_backup/
/opt/nas_backup/
├── backup.sh
├── config
│   ├── hosts.conf
│   └── rclone-remotes.conf
├── excludes
│   └── global.exclude
├── includes
│   ├── setevoy-nas
│   │   ├── etc.include
│   │   ├── opt.include
│   │   ├── root.include
│   │   ├── user-home-Data.include
│   │   ├── usr-local-bin.include
│   │   ├── usr-local-etc.include
│   │   └── var.include
│   ├── setevoy-pi
│   │   ├── opt.include
│   │   └── system.include
│   ├── setevoy-work
│   │   ├── user-home-Films.include
│   │   ├── user-home-Media.include
│   │   └── user-home-Vault.include
│   └── template.include
├── validate-config.sh
├── vmbackup-backup.sh
└── web-backup.sh

Скрипти тут:

  • backup.sh: основний скрипт, який виконує перевірки, запускає rsync, запускає інші скрипти
  • validate-config.sh: перевіряє синтаксис файлів конфігурації з каталога config/
  • vmbackup-backup.sh: виконує бекап бази VictoriaMetrics з vmbackup
  • web-backup.sh: виконує бекап локального WordPress – файлів та бази даних MariaDB

Про каталог config/ – трохи далі, а каталоги excludes/ та includes/ містять файли для rsync, і для кожного хоста власний каталог з власними налаштуваннями include/exclude.

Тепер трохи про файли і організацію, а потім вже до скриптів.

rsync, include та exclude

Наприклад, файл includes/setevoy-work/user-home-Media.include описує дані, які треба скопіювати з хоста work.setevoy (робочий ноутбук) і каталогу /home/setevoy:

root@setevoy-nas:~ # cat /opt/nas_backup/includes/setevoy-work/user-home-Media.include

# Syncthing:
# - Books/     => /nas/media
# - Documents/ => /nas/media
# - Music/     => /nas/media
# - Photos/    => /nas/media
# - Pictures   => /nas/media
# - Videos     =>/nas/media
# Rsync:
# - Vault/     => /nas/vault/ !
# - Films/     => /nas/private/ !
# - Drobox/    => /nas/media
# - Ops/       => /nas/media
# - Projects/  => /nas/media
# - To-Sort    => /nas/media
# - VMs        => /nas/media
# - Work       => /nas/media
# - Backups/   => /nas/media

############
### ROOT ###
############

/home/
/home/setevoy/

### Backups ###

/home/setevoy/Backups/
/home/setevoy/Backups/**

...

### Work ###

/home/setevoy/Work/

# <COMPANY_NAME>
/home/setevoy/Work/<COMPANY_NAME>/
/home/setevoy/Work/<COMPANY_NAME>/**

..

Файл exclude – один на всіх із загальними шаблонами того, що треба виключити з даних, які включені в include:

root@setevoy-nas:~ # cat /opt/nas_backup/excludes/global.exclude
######################
### Exclude Global ###
######################

# Syncthing
**/.stversions/

**/.git/
**/logs/
**/log/

# Vim temp files
**/*.swp
**/*.swo
**/*.swx
**/.*.sw?

...

Сам rsync запускається з exclude=all, але про це детальніше буде далі, бо там є свої нюанси.

Каталог config та файли з налаштуваннями

Тут два файли: один для rsynchosts.conf, другий, для rclonerclone-remotes.conf.

Файли перевіряються валідатором – validate-config.sh, а потім парсяться основним скриптом backup.sh.

hosts.conf – параметри для rsync

Файл hosts.conf виглядає так:

root@setevoy-nas:~ # cat /opt/nas_backup/config/hosts.conf
##############
### Syntax ###
##############

# hostname|user|include_file|exclude_file|destination|delete=yes/no

# Notes:
# - include/exclude files can be in subdirectories (e.g., 'setevoy-work/user-home-Vault.include')
# - multiple lines for the same host are allowed (different sources to different destinations)
# - destination directories will be created automatically if they don't exist
# - delete field format: delete=yes or delete=no (explicit format required!)
#   - delete=yes: rsync will use --delete-delay --delete-excluded (removes files on destination that don't exist on source)
#   - delete=no: rsync will only copy/update files (no deletion)

# IMPORTANT! For system backups and multiple hosts with same username:
# - Always include hostname/machine identifier in the destination path
# - Example: /nas/systems/work.setevoy/thinkpad-t14-g5/ (not just /nas/systems/)
# - This prevents mixing configs from different machines

#############################
### work.setevoy - laptop ###
#############################

### HOME ###

# Syncthing:
# - Books/
# - Documents/
# - Music/
# - Photos/
# - Pictures
# - Videos
# Rsync:
# - Vault/    => /nas/vault/
# - Films/    => /nas/private/
# - Drobox/   => /nas/media
# - Ops/      => /nas/media
# - Projects/ => /nas/media
# - To-Sort   => /nas/media
# - VMs       => /nas/media
# - Work      => /nas/media

# '/home/setevoy/ALL' => '/nas/media/home/setevoy/ALL/'
work.setevoy|setevoy|setevoy-work/user-home-Media.include|global.exclude|/nas/media/|delete=yes
...

#################################
### pi.setevoy - Raspberry PI ###
#################################

# '/opt/' => '/nas/systems/setevoy-pi/raspberry-pi-cm4-rev11/opt/'
pi.setevoy|root|setevoy-pi/opt.include|global.exclude|/nas/systems/setevoy-pi/raspberry-pi-cm4-rev11/|delete=yes
...

#############################
### nas.setevoy - FreeBSD ###
#############################

# '/opt/' => '/nas/systems/setevoy-nas/thinkcentre-10SUSCF000/opt/'
nas.setevoy|root|setevoy-nas/opt.include|global.exclude|/nas/systems/setevoy-nas/thinkcentre-10SUSCF000/|delete=yes
...

Власне, в ньому параметри для запуску rsync:

  • ім’я хоста, з якого буде виконуватись бекап
  • ім’я юзера для підключення – бо не всюди один, і до деяких взагалі треба root – коли бекапляться якісь системні файли
  • третім – відносний шлях до файлу include
  • четвертий параметр – exclude, якщо треба задати окремий
  • п’ятий – локальний каталог на самому NAS, в який будуть копіюватись дані (і який буде використовуватись для створення ZFS snapshots)
  • останній параметр – чи включати опцію rsync --delete, якщо треба в бекапах на NAS видаляти дані, які були видалені на source

rclone-remotes.conf – параметри для rclone

Синтаксис rclone-remotes.conf аналогічний:

root@setevoy-nas:~ # cat /opt/nas_backup/config/rclone-remotes.conf
# used for rclone sync only

##############
### Syntax ###
##############

# set as:

# dataset|rclone_remote

# - no leading and closing slashes on the 'dataset'
# - no closing ":" on the rclone_remote

# use commands:
# - rclone listremotes
# - rclone listremotes nas-google-drive-crypted-test
# - rclone config show nas-google-drive-crypted-test:

#############
### Media ###
#############

# Google
nas/media|nas-google-drive-media

# Backblaze
nas/media|nas-backblaze-crypted-media
...

Тут:

  • першим заданий ZFS dataset, з якого будуть копіюватись дані
  • другим – rclone remote config name, в який дані заливаються

Скрипти

Скрипти 4, поділені по функціональності:

  • backup.sh: основний скрипт, головний “control loop” – запускає всі інші скрипти та rsync && rclone
  • validate-config.sh: перевіряє синтаксис файлів конфігурації, про які писав вище
  • vmbackup-backup.sh: запускає vmbackup для VictoriaMetrics
  • web-backup.sh: створює архів файлів мого щоденнику на WordPress та mysqldump його бази

Скрипт backup.sh розглянемо останнім, аби спочатку подивитись на те, що він запускає, а вже потім – як він це запускає.

Скрипт validate-config.sh: перевірка синтаксису config-файлів

Запускається з backup.sh самим першим і виконує такий собі “preflight check”.

В глобальних змінних має два конфіг-файли, які йому треба перевірити.

Перевірки для hosts.conf та rclone-remotes.conf трохи відрізняються, бо у них вочевидь різний зміст:

  • для hosts.conf:
    • перевіряє чи в ньому вказані всі необхідні поля
    • виконує перевірку, що include/exclude файли, задані для хостів, реально існують
    • хост пінгується (якщо ні – просто видає WARNING, а не ERROR)
    • важливо – виконує перевірку синтаксису поля delete=yes/no, бо це найбільш “болюча” опція (хоча ще є zfs destroy :trollface: )
  • для rclone-remotes.conf:
    • перевіряє наявність ZFS dataset
    • перевіряє наявність rclone remote в його конфігу

Цей скрипт ніяких алертів не шле – це виконується в самому backup.sh, якщо валідатор повернув помилку.

Валідація файлу hosts.conf

Перевірка наявності всіх необхідних параметрів доволі проста – маємо файл, читаємо кожен рядок, маємо список полів.

Поля в файлі конфігурації розділені символом “|” – використовуємо його в while IFS='|'.

IFS – це вбудована змінна shell, Internal Field Separator, якій можна перевизначити символ, за яким буде розбиватись зміст строки чи файлу.

Якщо поле пусте – повертаємо помилку:

...
while IFS='|' read -r hostname user include_file exclude_file destination delete_field; do
  LINE_NUM=$((LINE_NUM + 1))

  # Skip comments and empty lines
  case "$hostname" in
    \#*|'') continue ;;
  esac

  echo "Validating line $LINE_NUM: $hostname"

  # Check if all fields are present
  if [ -z "$hostname" ] || [ -z "$user" ] || [ -z "$include_file" ] || [ -z "$exclude_file" ] || [ -z "$destination" ] || [ -z "$delete_field" ]; then
    echo "  ERROR: Missing field(s) in line $LINE_NUM"
    ERRORS=$((ERRORS + 1))
    continue
  fi
...

Перевірка опції delete=yes/no розбита на дві окремі перевірки:

  1. спершу перевіряємо, що опція задана саме як delete=, а не просто “yes” чи просто “delete
  2. потім перевіряємо значення після “=“, має бути або саме “yes“, або “no

Виглядає це так:

...
  # Validate delete field format
  if ! echo "$delete_field" | grep -q '^delete='; then
    echo "  ERROR: Invalid delete field format. Expected 'delete=yes' or 'delete=no', got: $delete_field"
    ERRORS=$((ERRORS + 1))
  else
    delete_value=$(echo "$delete_field" | cut -d'=' -f2)
    if [ "$delete_value" != "yes" ] && [ "$delete_value" != "no" ]; then
      echo "  ERROR: Invalid delete value. Expected 'yes' or 'no', got: $delete_value"
      ERRORS=$((ERRORS + 1))
    fi
  fi
...

Валідація файлу rclone-remotes.conf

Тут аналогічно: читаємо файл, перевіряємо, що отримали саме дві опції, які розділені символом “|“.

Потім перевіряємо ZFS dataset із zfs list "$dataset", і перевіряємо rclone remote з rclone listremotes:

...
  while IFS='|' read -r dataset remote; do
    ...

    # Check if all fields are present
    if [ -z "$dataset" ] || [ -z "$remote" ]; then
      echo "  ERROR: Missing field(s) in rclone config line $RCLONE_LINE_NUM"
      ERRORS=$((ERRORS + 1))
      continue
    fi

    # Check if dataset exists
    if ! zfs list "$dataset" > /dev/null 2>&1; then
      echo "  ERROR: Dataset $dataset does not exist"
      ERRORS=$((ERRORS + 1))
    fi

    # Check if rclone remote exists
    if ! rclone listremotes | grep -q "^${remote}:$"; then
      echo "  ERROR: Rclone remote $remote not found"
      ERRORS=$((ERRORS + 1))
    fi
...

В кінці скрипта рахуємо помилки і виходимо з помилкою, якщо $ERRORS більше нуля:

...

if [ $ERRORS -gt 0 ]; then
  echo "=== Validation FAILED with $ERRORS error(s) ==="
  exit 1
else
  echo "=== Validation PASSED ==="
  exit 0
fi

Скрипт vmbackup-backup.sh

“Під капотом” використовує власну утиліту VictoriaMetrcis vmbackup. Єдиний мінус утиліти – поки що не підтримує VictoriaLogs, але PR є, скоро, мабуть, додадуть.

Хоча особисто мені бекап логів і непотрібний, а от бекап бази – треба, бо в мене там дані мого “Self Monitoring Project”, де я записую дані по тому, як спав, який настрій – і ці дані записую з 2023 року, втратити їх не хочеться.

Скрипт виконує два типи бекапів – інкрементальний по буднях, і повний – в неділю, плюс видаляє старі бекапи.

На відміну від валідатора – тут вже свій обробник алертів, який шле нотифікації на ntfy.sh.

ntfy.sh – дуже класний сервіс для таких випадків, дуже простий, і, сподіваюсь, я таки запущу self hosted версію і напишу про нього окремо.

Для алертів в скрипті описана окрема функція, яка просто з curl шле POST-запит до сервісу:

...
# Alerts configuration (same as backup.sh)
NTFY_TOPIC="my-alerts"
NTFY_URL="https://ntfy.sh/$NTFY_TOPIC"
NTFY_TOKEN_FILE="/root/ntfy.token"
HOSTNAME=$(hostname)

...

NTFY_TOKEN=$(cat "$NTFY_TOKEN_FILE" | tr -d '\n')

send_alert() {
  TITLE="$1"
  MESSAGE="$2"
  TAGS="${3:-warning,backup}"

  curl -s \
    -H "Authorization: Bearer $NTFY_TOKEN" \
    -H "Title: $TITLE" \
    -H "Tags: $TAGS" \
    -d "$MESSAGE" \
    "$NTFY_URL" >/dev/null
}
...

В параметрах vmbackup задаються дві опції:

...
# VictoriaMetrics settings
VM_DATA_PATH="/var/db/victoria-metrics"
VM_SNAPSHOT_URL="http://localhost:8428/snapshot/create"
...

VM_DATA_PATH використовується для того, щоб, власне, скопіювати дані, а через ендпоінт VM_SNAPSHOT_URLvmbackup передає команду до VictoriaMetrics на “заморозку” операцій, аби створити консистентний snapshot.

Запуск самих бекапів і відправка алертів виглядають так:

...
vmbackup \
  -storageDataPath="$VM_DATA_PATH" \
  -snapshot.createURL="$VM_SNAPSHOT_URL" \
  -dst="fs://$BACKUP_BASE/latest" >> "$LOGFILE" 2>&1
INCREMENTAL_EXIT=$?

if [ $INCREMENTAL_EXIT -ne 0 ]; then
  echo "ERROR: Daily incremental backup failed with exit code $INCREMENTAL_EXIT" | tee -a "$LOGFILE"
  send_alert "VMBackup: Incremental backup failed" "❌ VictoriaMetrics incremental backup failed on $HOSTNAME
Exit code: $INCREMENTAL_EXIT
Log: $LOGFILE"
  FAILED=$((FAILED + 1))
else
  echo "Daily incremental backup completed successfully" | tee -a "$LOGFILE"
fi
...

В результаті є кілька директорій – latest для інкрементальних бекапів, та <DATE> для weekly:

root@setevoy-nas:~ # tree -d -L 2 /nas/services/victoriametrics/
/nas/services/victoriametrics/
├── 20260222
│   ├── data
│   ├── indexdb
│   └── metadata
├── 20260301
│   ├── data
│   ├── indexdb
│   └── metadata
└── latest
    ├── data
    ├── indexdb
    └── metadata

Видалення старих бекапів виконується з find, як і в інших скриптах.

В цьому прикладі спеціально залишаю першу, тестову версію – без реального rm -rf:

...

# Calculate cutoff date (RETENTION_WEEKS weeks ago, in YYYYMMDD format)
CUTOFF=$(date -v-${RETENTION_WEEKS}w +%Y%m%d 2>/dev/null || date -d "${RETENTION_WEEKS} weeks ago" +%Y%m%d)

find "$BACKUP_BASE" -maxdepth 1 -type d -name '[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]' | while read dir; do
  DIR_DATE=$(basename "$dir")
  if [ "$DIR_DATE" -lt "$CUTOFF" ]; then
    echo "Deleting old weekly backup: $dir" | tee -a "$LOGFILE"
    # TODO: uncomment when tested
    #rm -rf "$dir"
    echo "[DRY-RUN] would delete: $dir"
  fi
done

...

Скрипт web-backup.sh

Тут задача – створити архів файлів та зробити дамп бази даних.

Дуже простий, бекапить тільки один сайт, але мені поки більше і не треба.

Також має власний алертинг.

Бекап файлів створюється з tar:

...

SITE_DIR="/usr/local/www/blog.setevoy"
DB_NAME="nas_blog_setevoy_production_db"
DB_CREDENTIALS="/root/.my.cnf.blog-setevoy"
FILES_DEST="$BACKUP_BASE/setevoy/files/${DATE}-blog-setevoy.tar.gz"
DB_DEST="$BACKUP_BASE/setevoy/databases/${DATE}-blog-setevoy.sql"

# Backup files
echo "Archiving files: $SITE_DIR -> $FILES_DEST" | tee -a "$LOGFILE"
tar -czf "$FILES_DEST" --exclude="$SITE_DIR/wp-content/updraft" "$SITE_DIR" >> "$LOGFILE" 2>&1
TAR_EXIT=$?
if [ $TAR_EXIT -ne 0 ]; then
  echo "ERROR: Failed to archive blog.setevoy files" | tee -a "$LOGFILE"
  send_alert "Web Backup: Failed" "❌ Failed to archive blog.setevoy files on $HOSTNAME
Log: $LOGFILE"
  FAILED=$((FAILED + 1))
else
  echo "Files archived successfully" | tee -a "$LOGFILE"
fi

...

А база MariaDB – з mysqldump:

...

DB_CREDENTIALS="/root/.my.cnf.blog-setevoy"

...
# Backup database
echo "Dumping database: mysqldump --defaults-file="$DB_CREDENTIALS" "$DB_NAME" > "$DB_DEST" 2>> "$LOGFILE""
mysqldump --defaults-file="$DB_CREDENTIALS" "$DB_NAME" > "$DB_DEST" 2>> "$LOGFILE"
DB_EXIT=$?

if [ $DB_EXIT -ne 0 ]; then
  echo "ERROR: Failed to dump database $DB_NAME" | tee -a "$LOGFILE"
  send_alert "Web Backup: Failed" "❌ Failed to dump database $DB_NAME on $HOSTNAME
Log: $LOGFILE"
  FAILED=$((FAILED + 1))
else
  echo "Database dumped successfully" | tee -a "$LOGFILE"
fi

...

Тут mysqldump без додаткових опцій, бо це чисто мій власний щоденник, де окрім мене нікого не буває.

Але взагалі варто мати на увазі такі параметри:

  • --single-transaction: тільки для InnoDB – виконати дамп одною транзакцією без блокування таблиць, бо це може заафектити юзерів
  • --routines та --triggers: бекапити процедури і тригери – це не WordPress case, але можуть бути корисним
  • --add-drop-table: дефолтне значення true, додає в sql-дампі DROP TABLE IF EXISTS перед кожним CREATE TABLE – спрощує відновлення в існуючу базу даних

В скрипті використовується опція --defaults-file, через яку передається шлях до файлу з юзером та паролем:

root@setevoy-nas:~ # cat /root/.my.cnf.blog-setevoy
[mysqldump]
user=mysql-username
password="mysql-password"
host=localhost

Видалення старих бекапів – аналогічно до попереднього скрипту, просто з find:

...

find "$BACKUP_BASE" -type f \( -name "*.tar.gz" -o -name "*.sql" \) -mtime "+$RETENTION_DAYS" | while read f; do
  echo "Deleting old backup: $f" | tee -a "$LOGFILE"
  # TODO: uncomment when tested
  #rm -f "$f"
  ls -l "$f"
done

...

І самі бекапи виглядають так:

root@setevoy-nas:/opt/nas_backup # tree -L 3 /nas/services/web/
/nas/services/web/
└── setevoy
    ├── databases
        ...
    │   ├── 2026-03-05-03-00-blog-setevoy.sql
    │   ├── 2026-03-06-03-00-blog-setevoy.sql
    │   └── 2026-03-07-03-00-blog-setevoy.sql
    └── files
        ...
        ├── 2026-03-05-03-00-blog-setevoy.tar.gz
        ├── 2026-03-06-03-00-blog-setevoy.tar.gz
        └── 2026-03-07-03-00-blog-setevoy.tar.gz

Скрипт backup.sh

Ну і, нарешті, основний скрипт backup.sh, який, власне, і займається “оркестрацією” всього процесу.

Тут по черзі виконуються всі необхідні дії і запускаються скрипти, про які говорили вище.

Логіка виконання

  1. створюємо lock-файл: корисно, якщо попередній запуск скрипта завис – щоб не запустити одночасно два процеси виконання
  2. скриптом validate-config.sh виконується перевірка файлів hosts.conf та rclone-remotes.conf
  3. по черзі запускаємо скрипти бекапів:
    1. з web-backup.sh – бекапиться WordPress
    2. з vmbackup-backup.sh – бекапиться VictoriaMetrics
  4. далі читаємо конфіг hosts.conf для rsync, для кожного хоста визначаємо потрібні параметри, і в циклі для кожного хоста:
    1. виконуємо rsync – спершу з --dry-run, потім вже реальний запуск
    2. якщо rsync виконався без помилок – то створюємо ZFS снапшот
  5. вже не в циклах – видаляємо старі ZFS снапшоти
  6. читаємо конфіг rclone-remotes.conf для rclone
    1. в циклі запускаємо rclone sync для кожного заданого в конфігу ZFS dataset та відповідного rclone remote
  7. і в кінці з ntfy.sh відправляємо результат виконання

Step 1: створення lock file

...
LOCKFILE="/var/run/nas-backup.lock"
...

# Check if another instance is running
if [ -f "$LOCKFILE" ]; then
  echo "ERROR: Another backup is already running (lock file exists: $LOCKFILE)" | tee -a "$LOGFILE"
  send_alert "NAS Backup: Already running" "⚠️ Another backup instance is already running on $HOSTNAME
Lock file: $LOCKFILE"
  exit 1
fi

# Create lock file
echo $$ > "$LOCKFILE"

# Remove lock file on exit
trap 'echo ""; echo "Caught interrupt, cleaning up..."; kill $(jobs -p) 2>/dev/null; rm -f $LOCKFILE; exit 130' INT TERM
trap 'rm -f $LOCKFILE' EXIT

..

Тут:

  • перевіряємо, що файлу зараз нема – тобто попередній запуск скрипта вже завершено
  • створюємо файл /var/run/nas-backup.lock, з $$ в файл записуємо PID процесу
  • запускаємо trap, який перехопить Ctrl+C (Interrupt) або SIGTERM і видалить lock file

Step 2: запуск валідатора validate-config.sh

Тут все просто – після створення lock file запускаємо validate-config.sh, з if перевіряємо його код виконання:

...

# Run validator first
echo "Running configuration validator..." | tee -a "$LOGFILE"
if ! /opt/nas_backup/validate-config.sh >> "$LOGFILE" 2>&1; then
  echo "ERROR: Configuration validation failed" | tee -a "$LOGFILE"
  send_alert "NAS Backup: Config validation failed" "❌ Config validation failed on $HOSTNAME
Script: backup.sh
Log: $LOGFILE"
  exit 1
fi

echo "" | tee -a "$LOGFILE"

...

Steps 3 та 4: запуск бекапів Web та VictoriaMetrics

Аналогічно – запускаються з if:

...

echo "=== Starting web backups ===" | tee -a "$LOGFILE"
if ! /opt/nas_backup/web-backup.sh >> "$LOGFILE" 2>&1; then
  echo "WARNING: web_backup.sh failed, continuing..." | tee -a "$LOGFILE"
  send_alert "NAS Backup: Web backup failed" "⚠️ web_backup.sh failed on $HOSTNAME, continuing with rsync\nLog: $LOGFILE"
  exit 1
fi

echo "" | tee -a "$LOGFILE"

# Step 2: VictoriaMetrics backup
echo "=== Starting VictoriaMetrics backup ===" | tee -a "$LOGFILE"
if ! /opt/nas_backup/vmbackup-backup.sh >> "$LOGFILE" 2>&1; then
  echo "WARNING: vmbackup-backup.sh failed, continuing..." | tee -a "$LOGFILE"
  send_alert "NAS Backup: VMBackup failed" "⚠️ vmbackup-backup.sh failed on $HOSTNAME, continuing with rsync\nLog: $LOGFILE"
fi

echo "" | tee -a "$LOGFILE"

...

Step 5: запуск циклу з hosts.conf

Сама, мабуть, важлива і цікава частина – тут починається процес збору даних з усіх хостів, які задані в hosts.conf.

Спершу в циклі читається файл конфігу, заповнюються всі “локальні” змінні:

...
while IFS='|' read -r hostname user include_file exclude_file destination delete_field; do
  # Skip comments and empty lines
  case "$hostname" in
    \#*|'') continue ;;
  esac

  # Parse delete option
  delete_value=$(echo "$delete_field" | cut -d'=' -f2)
...

Далі хост пінгується, і, якщо ping не пройшов – то цикл while переходить до наступного рядка з файлу конфігурації.

Реалізовано це за допомогою оператора continue, який є в тому числі в “\#*|'') continue ;;“: якщо в hosts.conf строка – це коментар, то скіпаємо її і переходимо до наступної.

Аналогічно continue використовується для ping:

...

  # Check if host is reachable
  if ! ping -c 3 "$hostname" > /dev/null 2>&1; then
    echo "WARNING: Host $hostname is not reachable, skipping" | tee -a "$LOGFILE"
    send_alert "NAS Backup: Host unreachable" "⚠️ Host $hostname is not reachable on $HOSTNAME
Skipping backup
Log: $LOGFILE"
    echo "" | tee -a "$LOGFILE"
    continue
  fi

...

Тут – якщо ping повернув не success – то шлемо алерт і через continue переходимо до наступного хоста.

Аналогічно в наступній дії – перевіряється локальна директорія, в яку будуть копіюватись дані, якщо її нема – вона створюється, якщо створення завершилось з помилкою – то переходимо до наступного хоста:

...

  # Create destination directory if it doesn't exist
  if [ ! -d "$destination" ]; then
    echo "Creating destination directory: $destination" | tee -a "$LOGFILE"
    mkdir -p "$destination" >> "$LOGFILE" 2>&1
    if [ $? -ne 0 ]; then
      echo "ERROR: Failed to create destination directory" | tee -a "$LOGFILE"
      send_alert "NAS Backup: Failed to create destination" "❌ Failed to create destination directory on $HOSTNAME
Host: $hostname
Destination: $destination
Log: $LOGFILE"
      echo "" | tee -a "$LOGFILE"
      continue
    fi
  fi

...

Step 6: запуск rsync

Після цього, власне, починається процес копіювання даних з кожного хоста – і тут є кілька підводних каменів.

rsync та опції –delete

Дуже важлива – бо небезпечна – опція: чи видаляти на NAS дані, які були видалені на source.

В hosts.conf вона задається в кінці строки:

...
work.setevoy|setevoy|setevoy-work/user-home-Media.include|global.exclude|/nas/media/|delete=yes
...

В самому backup.sh перевіряється її значення і, якщо delete=yes – то в змінну $RSYNC_DELETE_OPTS задається значення --delete-delay:

...

  # Build rsync options based on delete setting
  # default is empty, i.e. no delete
  # IMPORTANT: DON NOT SET '--delete-excluded' if using multiply .includes: `rsync` is running with the `--exclude='*'` and will wipe all other data
  RSYNC_DELETE_OPTS=""
  if [ "$delete_value" = "yes" ]; then
    RSYNC_DELETE_OPTS="--delete-delay"
  fi

...

Тут в коментарі до перевірки записав, і ще раз підкреслю окремо – бо я з цим трохи мав проблему:

  • rsync запускається  опцією --exclude='*' (про це трохи далі)
  • якщо в $RSYNC_DELETE_OPTS вказати --delete-excluded – то, відповідно, rsync на NAS почне видаляти всі дані, які явно не задані в include-файлі

А так як файли include можуть бути різними для різних даних на source, але при цьому на destination – тобто самому NAS, каталог може бути єдиним – то rsync при кожній ітерації видалить дані іншої строки з конфігу.

Ось приклад з Raspberry PI:

...
# '/opt/' => '/nas/systems/setevoy-pi/raspberry-pi-cm4-rev11/opt/'
pi.setevoy|root|setevoy-pi/opt.include|global.exclude|/nas/systems/setevoy-pi/raspberry-pi-cm4-rev11/|delete=yes

# '/etc/systemd/' => '/nas/systems/setevoy-pi/raspberry-pi-cm4-rev11/etc/'
pi.setevoy|root|setevoy-pi/system.include|global.exclude|/nas/systems/setevoy-pi/raspberry-pi-cm4-rev11/|delete=yes
...

І тут rsync:

  • візьме все, що дозволено в opt.include
  • скопіює в /nas/systems/setevoy-pi/raspberry-pi-cm4-rev11/
  • перейде до наступної строки, візьме все з system.include
  • почне копіювати в /nas/systems/setevoy-pi/raspberry-pi-cm4-rev11/ – і видалить звідти те, що скопіював при запуску з opt.include

rsync та –exclude=’*’

Чому rsync запускається з --exclude='*'?

Бо, по-перше, особисто я віддаю перевагу підходу “заборонити все і копіювати тільки те, що дозволено явно“.

По-друге – простіший конфіг hosts.conf та сам скрипт backup.sh – достатньо передати тільки ім’я хоста, а далі rsync рекурсивно від / файлової системи проходиться по каталогам які явно дозволені в include-файлі, і копіює дані тільки з них.

Без exclude='*' довелось би або додавати виключення в файл global.exclude, або в include через “-“.

Виконання та опції rsync

Власне запуск самого rsync виглядає так – спочатку --dry-run, потім “running real backup” – те саме, тільки без --dry-run:

...

  RSYNC_DELETE_OPTS=""
  if [ "$delete_value" = "yes" ]; then
    RSYNC_DELETE_OPTS="--delete-delay"
  fi

  echo "Rsync command: rsync -avh $RSYNC_DELETE_OPTS --prune-empty-dirs --itemize-changes --progress --exclude-from=$EXCLUDES_DIR/$exclude_file --include-from=$INCLUDES_DIR/$include_file --exclude='*' $user@$hostname:/ $destination" | tee -a "$LOGFILE"
  echo "" | tee -a "$LOGFILE"

  # Run rsync with dry-run first
  echo "Running dry-run..." | tee -a "$LOGFILE"
  rsync -avh \
    --dry-run \
    $RSYNC_DELETE_OPTS \
    --prune-empty-dirs \
    --itemize-changes \
    --progress \
    --exclude-from="$EXCLUDES_DIR/$exclude_file" \
    --include-from="$INCLUDES_DIR/$include_file" \
    --exclude='*' \
    "$user@$hostname:/" "$destination" >> "$LOGFILE" 2>&1

  EXIT_CODE=$?

  if [ $EXIT_CODE -ne 0 ]; then
    echo "=== Dry-run FAILED with exit code $EXIT_CODE, skipping real backup ===" | tee -a "$LOGFILE"
    send_alert "NAS Backup: Dry-run failed" "❌ Rsync dry-run failed on $HOSTNAME
Host: $hostname
Exit code: $EXIT_CODE
Log: $LOGFILE"
    BACKUP_FAILED=$((BACKUP_FAILED + 1))
    echo "" | tee -a "$LOGFILE"
    continue
  fi

  echo "Dry-run successful, running real backup..." | tee -a "$LOGFILE"

  # Run REAL rsync
  rsync -avh \
    $RSYNC_DELETE_OPTS \
    --prune-empty-dirs \
    --itemize-changes \
    --progress \
    --exclude-from="$EXCLUDES_DIR/$exclude_file" \
    --include-from="$INCLUDES_DIR/$include_file" \
    --exclude='*' \
    "$user@$hostname:/" "$destination" >> "$LOGFILE" 2>&1

  EXIT_CODE=$?

...

З корисних опцій тут:

  • -a (archive): зберігає права, власника, сімлінки, timestamps
  • -v (verbose): виводить в лог інформацію що саме виконується
  • -h (human): відображати розмір як 1G замість байт
  • --delete-delay: видаляти дані по завершенню передачі даних, а не в процесі
  • --prune-empty-dirs: якщо на source каталог пустий – не копіювати його
  • --itemize-changes: детальна інформація в лог що саме змінилось в файлі, які перезаписуються/видаляються
  • --progress: показує прогрес передачі кожного файлу

Порядок передачі опцій –include-from та –exclude-from

І окремо про exclude та include.

Має значення в якому порядку параметри передаються до rsync:

  • першим йде --exclude-from – аби rsync перед запуском копіювання вже “знав” що треба пропускати
  • далі через --include-from передаємо список каталогів та файлів, які дозволено прочитати та скопіювати
  • і останнім з --exclude='*' виключаємо  бекапу все, що явно не задано в --include-from

Формат файлів –include-from та –exclude-from

Exclude-файл поки що один глобальний:

######################
### Exclude Global ###
######################

# Syncthing
**/.stversions/

**/.git/
**/logs/
**/log/

# Vim temp files
**/*.swp
**/*.swo
**/*.swx
**/.*.sw?

**/node_modules/

# Python
**/.venv/
**/venv/
**/__pycache__/

...

Тут через “**” вказуємо “без різниці, де саме цей файл чи каталог буде знайдено“, тобто виключаємо і /root/some-dir/.git/ – і /home/setevoy/some-dir/.git/.

Приклад одного з include-файлів – тут трохи цікавіше:

############
### ROOT ###
############

/home/
/home/setevoy/

### Books ###

/home/setevoy/Books/
/home/setevoy/Books/**

### Backups ###

/home/setevoy/Backups/
/home/setevoy/Backups/**

### Downloads ###

/home/setevoy/Downloads/
/home/setevoy/Downloads/Books/
/home/setevoy/Downloads/Books/**

...

Так як rsync запускається з --exclude='*' – то в include йому треба явно дозволити “зайти” в корневий каталог.

Тобто, при виконані rsync -avh [email protected]:/rsync зайде в корінь, “/“, потім – маючи /home/ в include-from – зможе “заглянути” в /home/, а далі вже завітати до /home/setevoy/.

І далі аналогічно дозволяємо доступ в /home/setevoy/Books/, де з “**” вказуємо “взяти тут все, що знайдеш” – окрім того, що було задано в exclude-file.

При цьому дані з, наприклад, каталогу /home/setevoy/Bob/rsync пропустить, бо не має явного дозволу їх читати і копіювати.

Step 7: створення ZFS snapshots

Після того як rsync для хоста завершився без помилок – запускається наступний if/else:

...

  EXIT_CODE=$?

  if [ $EXIT_CODE -eq 0 ]; then
    echo ""
    echo "=== Backup from $hostname completed successfully ===" | tee -a "$LOGFILE"
    BACKUP_SUCCESS=$((BACKUP_SUCCESS + 1))

    # Create ZFS snapshot
    SNAPSHOT_NAME="nas-backup-$(date +%Y-%m-%d-%H-%M-%S)"

    # Get dataset name from destination path
    DATASET=$(zfs list -H -o name "$destination" 2>/dev/null | head -1)

    if [ -z "$DATASET" ]; then
      echo "ERROR: Could not determine ZFS dataset for $destination" | tee -a "$LOGFILE"
      send_alert "NAS Backup: Snapshot failed" "❌ Could not determine ZFS dataset on $HOSTNAME
      ...
    else
      echo ""
      echo "Creating ZFS snapshot: $DATASET@$SNAPSHOT_NAME" | tee -a "$LOGFILE"

      zfs snapshot "$DATASET@$SNAPSHOT_NAME" >> "$LOGFILE" 2>&1

      if [ $? -eq 0 ]; then
        echo "ZFS snapshot created successfully" | tee -a "$LOGFILE"
      else
        echo "ERROR: Failed to create ZFS snapshot" | tee -a "$LOGFILE"
        send_alert "NAS Backup: Snapshot failed" "❌ Failed to create ZFS snapshot on $HOSTNAME
        ...
      fi
    fi

..

В BACKUP_SUCCESS=$((BACKUP_SUCCESS + 1)) просто інкрементиться значення, яке використовується виключно для фінального повідомлення через ntfy.sh.

Далі формуємо ім’я снапшоту, і в змінну $DATASET записуємо ім’я датасету.

Для цього беремо параметр $destination, який в hosts.conf заданий як повний шлях – /nas/systems/setevoy-pi/raspberry-pi-cm4-rev11/, а потім із zfs list отримуємо mountpoint:

root@setevoy-nas:/opt/nas_backup # zfs list -H -o name /nas/systems/setevoy-pi/raspberry-pi-cm4-rev11/
nas/systems

А потім викликаємо zfs snapshot для, власне, створення снапшоту.

Step 8: видалення старих ZFS snapshots

Тут теж потенційно небезпечна операція, бо викликається zfs destroy – яка може дропнути повний ZFS dataset:

...

CUTOFF_DATE=$(date -v-${SNAPSHOT_RETENTION_DAYS}d +%Y-%m-%d 2>/dev/null || date -d "${SNAPSHOT_RETENTION_DAYS} days ago" +%Y-%m-%d)

zfs list -H -t snapshot -o name | grep '@nas-backup-' | while read snapshot; do
  SNAP_DATE=$(echo "$snapshot" | sed 's/.*@nas-backup-\([0-9-]*\)-.*/\1/')

  if [ "$SNAP_DATE" \< "$CUTOFF_DATE" ]; then
    echo "Deleting old snapshot: $snapshot" | tee -a "$LOGFILE"
    zfs destroy "$snapshot" >> "$LOGFILE" 2>&1
  fi
done

...

Як працює:

  • в змінну $CUTOFF_DATE вносимо дату “сьогодні мінус 30 днів” – бо $SNAPSHOT_RETENTION_DAYS заданий в 30.
  • із zfs list -H -t snapshot -o name відображаємо список всіх наявних снапшотів і вибираємо тільки ті, які робились цим скриптом – grep '@nas-backup-'
  • потім в циклі для кожного снапшоту із zfs list -t snapshot отримуємо дату, коли цей снапшот був створений, записуємо в змінну $SNAP_DATE
  • порівнюємо $SNAP_DATE та $CUTOFF_DATE
  • і, якщо $SNAP_DATE старша за $CUTOFF_DATE – то виконуємо zfs destroy

Step 9: запуск rclone

Тут в цілому підхід аналогічний – читаємо кожну строку з конфігу:

...

while IFS='|' read -r dataset remote; do

...

В $dataset записуємо ім’я ZFS dataset, в $remote – ім’я rclone remote.

Ще раз приклад конфігу:

# used for rclone sync only

##############
### Syntax ###
##############

# set as:

# dataset|rclone_remote

# - no leading and closing slashes on the 'dataset'
# - no closing ":" on the rclone_remote

# use commands:
# - rclone listremotes
# - rclone listremotes nas-google-drive-crypted-test
# - rclone config show nas-google-drive-crypted-test:

#############
### Media ###
#############

# Google
nas/media|nas-google-drive-media

# Backblaze
nas/media|nas-backblaze-crypted-media

...

Тобто – беремо dataset nas/media – і копіюємо його зміст до nas-google-drive-media, а потім його ж – але до nas-backblaze-crypted-media.

Приклад rclone remote для Backblaze:

root@setevoy-nas:/opt/nas_backup # rclone config show nas-backblaze-crypted-media
[nas-backblaze-crypted-media]
type = crypt
remote = nas-backblaze-root-media:setevoy-nas-media
filename_encryption = off
directory_name_encryption = false
password = *** ENCRYPTED ***

Весь цикл виглядає так:

...

RCLONE_CONF="/opt/nas_backup/config/rclone-remotes.conf"

if [ ! -f "$RCLONE_CONF" ]; then
  echo "WARNING: rclone config not found at $RCLONE_CONF, skipping cloud sync" | tee -a "$LOGFILE"
else
  TS=$(date +%F-%H-%M)

  while IFS='|' read -r dataset remote; do
    # Skip comments and empty lines
    case "$dataset" in
      \#*|'') continue ;;
    esac

    echo "Syncing dataset $dataset to $remote" | tee -a "$LOGFILE"

    # Get mount point for dataset
    MOUNT_POINT=$(zfs get -H -o value mountpoint "$dataset" 2>/dev/null)

    if [ -z "$MOUNT_POINT" ] || [ "$MOUNT_POINT" = "-" ]; then
      echo "ERROR: Could not get mount point for dataset $dataset" | tee -a "$LOGFILE"
      RCLONE_FAILED=$((RCLONE_FAILED + 1))
      continue
    fi

    ...

    rclone sync "$MOUNT_POINT/" "${remote}:data" \
      --backup-dir "${remote}:_archive/$TS" \
      --progress \
      --stats=30s \
      --log-level INFO >> "$LOGFILE" 2>&1

    EXIT_CODE=$?

    if [ $EXIT_CODE -eq 0 ]; then
      echo "Rclone sync for $dataset completed successfully" | tee -a "$LOGFILE"
      RCLONE_SUCCESS=$((RCLONE_SUCCESS + 1))
    else
      echo "ERROR: Rclone sync for $dataset failed with exit code $EXIT_CODE" | tee -a "$LOGFILE"
      send_alert "NAS Backup: Rclone sync failed" "❌ Rclone sync failed on $HOSTNAME
      ...
      RCLONE_FAILED=$((RCLONE_FAILED + 1))
    fi

    echo "" | tee -a "$LOGFILE"

  done < "$RCLONE_CONF"

...

Сам rclone sync виконує саме синхронізацію: якщо на NAS файл або каталог був видалений – то він видалиться і на rclone remote.

Тому, для більш спокійного сну, rclone запускається з --backup-dir, куди копіює дані, які під час виконання sync видаляються або змінюються.

Як це виглядає на remote:

root@setevoy-nas:/home/setevoy # rclone tree --dirs-only --level 4 nas-backblaze-crypted-media:
/
├── _archive
...
│   ├── 2026-03-05-03-07
│   │   └── home
│   │       └── setevoy
│   └── 2026-03-07-03-07
│       └── home
│           └── setevoy
└── data
    └── home
        └── setevoy
            ├── Backups
            ├── Books
            ...
            ├── Videos
            └── Work

Ну і, власне, на цьому все. Останнім виконується відправка повідомлення про те, як пройшов бекап:

...

# Send summary
if [ $BACKUP_FAILED -eq 0 ] && [ $RCLONE_FAILED -eq 0 ]; then
  send_alert "NAS Backup: Completed successfully" "✅ All backups completed successfully on $HOSTNAME
Rsync successful: $BACKUP_SUCCESS
Rsync failed: $BACKUP_FAILED
Rclone successful: $RCLONE_SUCCESS
Rclone failed: $RCLONE_FAILED
Log: $LOGFILE" "white_check_mark,backup"
else
  send_alert "NAS Backup: Completed with errors" "⚠️ Backups completed with errors on $HOSTNAME
Rsync successful: $BACKUP_SUCCESS
Rsync failed: $BACKUP_FAILED
Rclone successful: $RCLONE_SUCCESS
Rclone failed: $RCLONE_FAILED
Log: $LOGFILE"
fi

Запускається скрипт з crontab:

root@setevoy-nas:~ # crontab -l
...
0 3 * * * /opt/nas_backup/backup.sh

Приклад результату виконання

Як це все щастя виглядає в лог-файлі та повідомлення ntfy.sh.

Початок – робота валідатора:

Завершення – виконання rclone sync:

Повідомлення в ntfy.sh:

І на телефоні:

Що можна покращити

Скрипт(и), звісно, не ідеальні, і можна було б зробити ще:

  • запуск rsync та rclone для загальної картини можна винести окремим скриптами, як це зроблено для validate-config.sh та vmbackup-backup.sh
  • зараз весь цикл виконання виконується без можливості вказати “зроби мені тільки web” або “зроби мені тільки rclone” – можна було б додати getopt чи getopts, парсити аргументи, з якими запускається скрипт та вибирати, що саме виконувати
  • додати в аргументи можливість окремого запуску rsync чисто з --dry-run
  • для rclone зараз не використовується --ignore-from – можна було б додати
  • ну і “вішенка на торті” – писати метрики в VictoriaMetrics про те, скільки байт передано, скільки місця на диску було витрачено або звільнено – щось таке

Все.

Поки працює, як є – вже кілька тижнів, поки що без проблем.

Loading

FreeBSD: Home NAS, part 14 – логи з VictoriaLogs і алерти з VMAlert
0 (0)

28 Лютого 2026

Продовження серії по налаштуванню домашнього NAS.

Моніторинг в цілому вже налаштований в попередніх частинах, але залишилось налаштувати роботу з логами – бо робити це в консолі з tail -f /var/log/messages, звісно, можна – але є і більш зручні інструменти.

Використаємо VictoriaLogs – тим більш для метрик на моїй FreeBSD вже є стек VictoriaMetrics + VMAlert + Alertmanager.

Всі частини серії по налаштуванню домашнього NAS на FreeBSD:

Установка VictoriaLogs

Є в репозиторії, просто встановлюємо з pkg:

root@setevoy-nas:~ # pkg install -y victoria-logs

Глянемо, які файлу додасть в систему:

root@setevoy-nas:~ # pkg info -l victoria-logs
victoria-logs-1.43.1_2:
  /usr/local/bin/victoria-logs
  /usr/local/bin/vlogscli
  /usr/local/etc/rc.d/victoria-logs
...

Глянемо, що в скрипті rc.d:

root@setevoy-nas:~ # cat /usr/local/etc/rc.d/victoria-logs
...
rcvar="victoria_logs_enable"
...
victoria_logs_user="victoria-logs"
...

Додаємо до /etc/rc.conf:

root@setevoy-nas:~ # sysrc victoria_logs_enable="YES"
victoria_logs_enable:  -> YES

Запускаємо сервіс:

root@setevoy-nas:~ # service victoria-logs start

Перевіряємо порт:

root@setevoy-nas:~ # sockstat -4 -l | grep logs
victoria-logs victoria-l 33088 5  tcp4 *:9428              *:*

Відкриваємо в браузері на порту 9428:

Поки тут пусто – додаємо збір логів.

Установка Fluent Bit

Хотів взяти Vector.dev – але його нема в репозиторії і портах FreeBSD, і нема навіть в списку підтримуваних систем.

Є відкрита GitHub issue – ще у 2020 році.

Що є з інших рішень:

  • Promtail: від Grafana – не хочу, і вони його наче вже депрікейтять
  • Filebeat: від Elastic, на Go – але пам’ятаю, що трохи важкуватий по ресурсам
  • Fluent Bit: на C, швидкий, легкий, хоча конфіг може показатись незручним
  • Logstash: Java – nuff said
  • Rsyslog: ну от де конфіг справді незручний, тому ні (див. rsyslog: добавление наблюдения за файлом в конфигурацию – 2014 рік)

Отже, візьмемо Fluent Bit.

Глянемо, чи є в репозиторії:

root@setevoy-nas:~ # pkg search fluent
fluent-bit-4.2.2_2             Fast and lightweight data forwarder

Встановлюємо:

root@setevoy-nas:~ # pkg install -y fluent-bit

Перевіряємо, які файли додає в систему:

root@setevoy-nas:~ # pkg info -l fluent-bit | grep etc
  /usr/local/etc/fluent-bit/fluent-bit.conf.sample
  /usr/local/etc/fluent-bit/parsers.conf.sample
  /usr/local/etc/fluent-bit/plugins.conf
  /usr/local/etc/rc.d/fluent-bit

Дефолтний конфіг /usr/local/etc/fluent-bit/fluent-bit.conf.

Перевіряємо, як запускається:

root@setevoy-nas:~ # cat /usr/local/etc/rc.d/fluent-bit
...
# fluent_bit_enable (bool):	Set to YES to enable fluent-bit
# 				Default: NO
# fluent_bit_config (str):	config files to use
#				Default: /usr/local/etc/fluent-bit/fluent-bit.conf
# fluent_bit_flags (str):	Extra flags passed to fluent-bit
# fluent_bit_user (str):	Default run as user nobody
# fluent_bit_group (str):	Default run as group nogroup
...

: ${fluent_bit_enable:="NO"}
: ${fluent_bit_user:="nobody"}
: ${fluent_bit_group:="nogroup"}
: ${fluent_bit_config:="/usr/local/etc/fluent-bit/fluent-bit.conf"}

pidfile=/var/run/${name}.pid
procname="/usr/local/bin/fluent-bit"
command="/usr/sbin/daemon"
command_args="-H -p ${pidfile} -o /var/log/${name}/${name}.log -t ${name} ${procname} --quiet --config ${fluent_bit_config} ${fluent_bit_flags}"
...

Що нам треба буде – додати fluent_bit_enable в /etc/rc.conf. І звертаємо увагу на fluent_bit_user та fluent_bit_group.

Створюємо каталог для його бази – fluent-bit буде сюди записувати позиції в файлах логів:

root@setevoy-nas:~ # mkdir -p /var/db/fluent-bit
root@setevoy-nas:~ # chown nobody:nogroup /var/db/fluent-bit/

Видаляємо (переносимо) дефолтний конфіг:

root@setevoy-nas:~ # mv /usr/local/etc/fluent-bit/fluent-bit.conf /usr/local/etc/fluent-bit/fluent-bit.conf-default

Пишемо свій файл /usr/local/etc/fluent-bit/fluent-bit.conf, поки додаємо збір тільки /var/log/messages:

[SERVICE]
    flush        5
    daemon       Off
    log_level    info
    parsers_file parsers.conf
    plugins_file plugins.conf

[INPUT]
    name        tail
    path        /var/log/messages
    tag         freebsd.messages
    db          /var/db/fluent-bit/messages.db

[OUTPUT]
    name        loki
    match       *
    host        localhost
    port        9428
    uri         /insert/loki/api/v1/push?_msg_field=log&_time_field=date
    labels      job=fluent-bit, host=setevoy-nas, logfile=messages

В полі uri вказуємо адресу VictoriaLogs, задаємо поле для _msg, в labels вказуємо набір тегів, які будуть додаватись до логів.

Запускаємо для тесту:

root@setevoy-nas:~ # vim /usr/local/etc/fluent-bit/fluent-bit.conf ^C
root@setevoy-nas:~ # fluent-bit -c /usr/local/etc/fluent-bit/fluent-bit.conf
Fluent Bit v4.2.2
* Copyright (C) 2015-2025 The Fluent Bit Authors
* Fluent Bit is a CNCF graduated project under the Fluent organization
* https://fluentbit.io

______ _                  _    ______ _ _             ___   _____
|  ___| |                | |   | ___ (_) |           /   | / __  \
| |_  | |_   _  ___ _ __ | |_  | |_/ /_| |_  __   __/ /| | `' / /'
|  _| | | | | |/ _ \ '_ \| __| | ___ \ | __| \ \ / / /_| |   / /
| |   | | |_| |  __/ | | | |_  | |_/ / | |_   \ V /\___  |_./ /___
\_|   |_|\__,_|\___|_| |_|\__| \____/|_|\__|   \_/     |_(_)_____/

             Fluent Bit v4.2 – Direct Routes Ahead
         Celebrating 10 Years of Open, Fluent Innovation!
...
[2026/02/28 16:38:18.797199771] [ info] [output:loki:loki.0] configured, hostname=localhost:9428
...

Записуємо повідомлення в /var/log/messages:

root@setevoy-nas:~ # logger "test message from fluent-bit"

Перевіряємо ім’я:

root@setevoy-nas:~ # cat /usr/local/etc/rc.d/fluent-bit | grep name
name="fluent_bit"
rcvar=${name}_enable
...

Додаємо в автостарт:

root@setevoy-nas:~ # sysrc fluent_bit_enable="YES"

Запускаємо:

root@setevoy-nas:~ # service fluent-bit start
Starting fluent_bit.

VictoriaLogs та робота з логами з консолі

VictoriaLogs прям дуже зручна в плані роботи з даними з консолі, і, думаю, юзери FreeBSD це оцінять.

Для роботи у нас два варіанти – або робити запити з curl, а потім їх парсити – або використати vlogscli.

Запити з curl

Приклад з curl:

root@setevoy-nas:~ # curl -s 'http://localhost:9428/select/logsql/query?query=*'
{"_time":"2026-02-28T14:42:00.20468201Z","_stream_id":"0000000000000000782dd9afdaaf4d53bfb843de46a3d91b","_stream":"{host=\"setevoy-nas\",job=\"fluent-bit\",logfile=\"messages\"}","_msg":"Feb 28 16:42:00 setevoy-nas setevoy[36200]: test message from fluent-bit 2","host":"setevoy-nas","job":"fluent-bit","logfile":"messages"}

Результат отримуємо в JSON, тому можна передати в jq:

І робити всякі пайпи:

root@setevoy-nas:~ # curl -s http://localhost:9428/select/logsql/query -d 'query=test' | jq -r '._time + " " + ._msg'
2026-02-28T14:42:00.20468201Z Feb 28 16:42:00 setevoy-nas setevoy[36200]: test message from fluent-bit 2
2026-02-28T14:53:49.481172045Z Feb 28 16:48:41 setevoy-nas setevoy[36663]: test message from fluent-bit 3
2026-02-28T14:54:05.981200313Z Feb 28 16:54:05 setevoy-nas setevoy[37055]: test message from fluent-bit 3
2026-02-28T15:06:21.481220267Z Feb 28 17:06:21 setevoy-nas setevoy[37991]: test message from fluent-bit
2026-02-28T15:12:46.231202127Z Feb 28 17:12:46 setevoy-nas setevoy[38385]: test message from fluent-bit
2026-02-28T15:14:42.731213928Z Feb 28 17:14:42 setevoy-nas setevoy[38502]: test message for vlogscli
2026-02-28T15:15:48.981198786Z Feb 28 17:15:48 setevoy-nas setevoy[38569]: test message for vlogscli
2026-02-28T15:17:59.731198268Z Feb 28 17:17:59 setevoy-nas setevoy[38684]: test message for vlogscli

Запити з vlogscli

Запускаємо vlogscli:

root@setevoy-nas:~ # vlogscli
sending queries to -datasource.url=http://localhost:9428/select/logsql/query
type ? and press enter to see available commands
;>

І, наприклад, запустити \tail:

;> \tail *;
executing [*]...; duration: client 9.003s
{
  "_msg": "Feb 28 17:17:59 setevoy-nas setevoy[38684]: test message for vlogscli",
  "_stream": "{host=\"setevoy-nas\",job=\"fluent-bit\",logfile=\"messages\"}",
  "_stream_id": "0000000000000000782dd9afdaaf4d53bfb843de46a3d91b",
  "_time": "2026-02-28T15:17:59.731198268Z",
  "host": "setevoy-nas",
  "job": "fluent-bit",
  "logfile": "messages"
}

Або використовувати різні LogsQL filters та pipes, наприклад – Time filter:

;> _time:5m;
executing [_time:5m]...; duration: server 0.000s
{
  "_msg": "Feb 28 17:17:59 setevoy-nas setevoy[38684]: test message for vlogscli",
  "_stream": "{host=\"setevoy-nas\",job=\"fluent-bit\",logfile=\"messages\"}",
  "_stream_id": "0000000000000000782dd9afdaaf4d53bfb843de46a3d91b",
  "_time": "2026-02-28T15:17:59.731198268Z",
  "host": "setevoy-nas",
  "job": "fluent-bit",
  "logfile": "messages"
}

Або включити compact mode:

;> \c
compact output mode

І тоді результат буде таким:

vmalert та алерти з логів

Для vmalert можна створити Recodring Rules – читати логи, генерувати метрики, а потім з цих метрик або можемо малювати графіки в Grafana – або створювати алерти.

Див. VictoriaLogs: створення Recording Rules з VMAlert.

Але для цього vmalert треба робити запити до двох datasources:

  • до VictoriaLogs на порт 9428 і URI /select/logsql/ – аби прочитати логи
  • до VictoriaMetrics на порт 8428 – аби записати метрики і виконати запити для створення алерту

Але два --datasource.url для vmalert задати не можна – але можна зробити базовий роутинг через vmauth, як я робив на робочому проекті, де в мене це все працює в Kubernetes – а потім для vmalert в --datasource.url вказати адресу vmauth.

Див. VictoriaMetrics: VMAuth – проксі, аутентифиікація та авторизація.

Налаштування vmauth

vmauth в мене вже встановлена з пакету vmutils, зараз треба просто додати конфіг з роутами та rc.d скрипт, бо в комплекті vmutils його нема:

root@setevoy-nas:~ # pkg info -l vmutils | grep vmauth
  /usr/local/bin/vmauth
  /usr/local/share/doc/vmutils/vmauth.md
  /usr/local/share/doc/vmutils/vmauth_flags.md

Створюємо конфіг /usr/local/etc/vmauth.yml з двома роутами – для VictoriaLogs та VictoriaMetrics:

unauthorized_user:
  url_map:
    - src_paths:
        - "/select/logsql/.*"
      url_prefix: "http://127.0.0.1:9428"
    - src_paths:
        - "/.*"
      url_prefix: "http://127.0.0.1:8428"

Пишемо rc.d скрипт – /usr/local/etc/rc.d/vmauth:

#!/bin/sh

# PROVIDE: vmauth
# REQUIRE: LOGIN
# KEYWORD: shutdown

. /etc/rc.subr

name="vmauth"
rcvar="vmauth_enable"

load_rc_config $name

: ${vmauth_enable:="NO"}
: ${vmauth_user:="victoria-metrics"}
: ${vmauth_logfile:="/var/log/vmauth.log"}
: ${vmauth_args:="-auth.config=/usr/local/etc/vmauth.yml -httpListenAddr=:8427"}

pidfile="/var/run/${name}.pid"
command="/usr/sbin/daemon"
procname="/usr/local/bin/vmauth"

command_args="-f -o ${vmauth_logfile} -p ${pidfile} ${procname} ${vmauth_args}"

start_cmd="vmauth_start"
stop_cmd="vmauth_stop"

vmauth_start()
{
  echo "Starting vmauth"
  touch ${vmauth_logfile}
  chown ${vmauth_user} ${vmauth_logfile}
  ${command} ${command_args}
}

vmauth_stop()
{
  echo "Stopping vmauth"
  kill `cat ${pidfile}`
}

run_rc_command "$1"

Задаємо execution права:

root@setevoy-nas:~ # chmod +x /usr/local/etc/rc.d/vmauth

Додаємо запуск в /etc/rc.conf:

root@setevoy-nas:~ # sysrc vmauth_enable="YES"
vmauth_enable:  -> YES

Запускаємо:

root@setevoy-nas:~ # service vmauth start
Starting vmauth

Перевіряємо порт:

root@setevoy-nas:~ # sockstat -4 -l | grep vmauth
root     vmauth     42277 4   tcp4   *:8427                *:*

Додаємо vmalert_args:

root@setevoy-nas:~ # sysrc vmalert_args="--datasource.url=http://127.0.0.1:8427 --notifier.url=http://127.0.0.1:9093 --rule=/usr/local/etc/vmalert/*.yml --remoteWrite.url=http://127.0.0.1:8428"

Тут:

  • --datasource.url=http://127.0.0.1:8427: адреса vmauth, який буде роутити запити по URI до VictoriaLogs або VictoriaMetrics
  • --notifier.url=http://127.0.0.1:9093: адреса Alertmanager
  • --remoteWrite.url=http://127.0.0.1:8428: адреса VictoriaMetrics, в яку будемо писати згенеровані метрики

Створення vmalert Recording Rule та алерту

І приклад метрики та алерту – файл /usr/local/etc/vmalert/freebsd-system-alerts.yml:

groups:

  - name: freebsd-logs-records
    type: vlogs
    interval: 1m
    rules:
      - record: freebsd:messages:errors_per_minute
        expr: 'error | stats count() as errors_count'

  - name: freebsd-logs-alerts
    rules:
      - alert: FreeBSDTooManyErrors
        expr: freebsd:messages:errors_per_minute > 1
        for: 1m
        labels:
          severity: warning
        annotations:
          summary: "Too many errors in logs"

Перезапускаємо vmalert:

root@setevoy-nas:~ # service vmalert restart
Stopping vmalert
Starting vmalert

Запускаємо для тесту запис “error” до /var/log/messages:

root@setevoy-nas:~ # while true; do logger "error test message"; sleep 1; done

Перевіряємо в VictoriaMetircs:

Перевіряємо в VMAlert:

Отримуємо алерт в Alertmanager:

І алерт в Telegram (бот робився для алертів EcoFlow, тому ім’я таке):

Готово.

Loading

FreeBSD: Home NAS, part 13: планування зберігання даних та бекапів
0 (0)

28 Лютого 2026

Коли тільки починав робити свій NAS і думав про бекапи, то все здавалось доволі простим: є робочий ноутбук з даними, є сервер з FreeBSD під NAS – треба просто взяти, і скопіювати дані.

Тому перша задумка була мати backup-скрипт/и на Linux-хостах, які б з rsync заливали дані на NAS, а потім з NAS іншим скриптом заливати дані до Rclone remotes.

Але…

Але коли почав вже робити, то виникло питання:

  • rsync з хоста setevoy-work заливає дані на NAS
  • в цей жеж час rsync з хоста setevoy-home заливає свої дані

А коли запускати rclone => Google Drive? Як знати, що всі дані з хостів вже готові для копіювання?

І це була тільки верхівка айсбергу задачі по організації процесу.

Всі частини серії по налаштуванню домашнього NAS на FreeBSD:

“Технічне завдання”: складність організації даних та бекапів

Отже, виявилось, що тут є купа нюансів:

  • хостів, з яких треба робити бекапи – не один, і не два, і навіть не три:
    • є робочий ноутбук з Arch Linux
    • є домашній ноутбук з Arch Linux
    • є ігровий ПК, на якому Windows для ігор і Arch Linux для роботи – бо раніше (до “сезону блекаутів”) це був мій робочий комп
    • є власне сервер з FreeBSD під NAS
    • нещодавно з’явився Raspberry Pi (див. Raspberry Pi: перший досвід і установка Raspberry Pi OS Lite)
    • а ще і телефон з власними даними типу фоток
  • на кожному хості є різні типи даних:
    • відносно статичні та однакові дані для ноутбуків/ПК – фото, відео, документи
    • часто змінюються, але в цілому однакові на ноутбуках/ПК, хоча можуть дещо відрізнятись – робочі дані по проектам, якісь власні скрипти
    • конфіденційні дані, але однакові на всіх хостах – SSH-ключі, бекапи KeePass/1Password, Recovery Codes тощо
    • колекція home video 😉
    • системні бекапи – різні конфіги з /etc, /usr/local/etc, ~/.ssh/config, всякі dotfiles налаштувань OpenBox або KDE Plasma
  • всі ці дані треба синхронізувати на декілька зовнішніх ресурсів (далі – просто “клауди”):
    • ZFS datasets на самому NAS
    • зберігати копії в клаудах – Google та/або Proton Drive, AWS S3 чи Backblaze
  • і до всього цього – мати резервні копії резервних копій на випадок випадкового видалення, для чого є:
    • ZFS snapshots – резервні копії на самому NAS
    • Syncthing Trash – резервні копії Syncthing
    • rclone --backup-dir – при копіюванні змін з NAS в клауди використовується окремий каталог для зберігання видалених або змінених даних
  • і все діло треба ще і автоматизувати, аби копіювання з NAS до Google Drive не виконувалось одночасно з копіюванням з хостів на NAS
    • тобто, просто запустити rsync з робочого ноута можна – але як знати, коли запускати rclone з NAS в Google Drive?

Відчуваєте, як в голові починається каша? 😉

Далі в цьому пості я всі remotes буду писати просто як “Google Drive” або “клауди” – але сюди ж входять і Proton Drive, і AWS S3, і, можливо, якісь інші, які почну використовувати пізніше – бо Rclone дає просто безліч варіантів (UPD: замість AWS S3 взяв собі Backblaze, див. Backblaze: знайомство з B2 Cloud Storage – перші враження).

Просто поки що Google Drive основний, хоча, можливо, я буду взагалі зав’язувати роботу з Google, бо їхній “AI”, який вони пхають в кожну дірку вже починає зайо*вати.

Схема моїх хостів та мереж

Для загальної картини схематично всі хости та мережі можна відобразити так:

В офісі знаходиться MikroTik RB4011, який грає таку собі роль “VPN-хаба” – на ньому налаштований WireGuard, який об’єднує офісну та домашню мережі і до якого підключений сервер в Digital Ocean, на якому зараз працює rtfm.co.ua.

Про MikroTik писав в пості MikroTik: перше знайомство та Getting Started, про WireGuard на ньому – MikroTik: налаштування WireGuard та підключення Linux peers, і там жеж є більш детальна схема мережі.

Отже, питання мережі вирішено – тепер треба з усіх цих хостів почати збирати дані і якось продумати організацію їхнього збереження на NAS та процес бекапу з хостів на NAS – і з NAS в клауди.

Типи та класи даних для зберігання і бекапів

Коли я почав планувати організацію збереження даних на NAS, то в мене сформувався такий підхід:

  • всі дані ділимо на класи
  • для кожного класу маємо окремий ZFS dataset  – маємо можливість налаштовувати власні квоти, окремі політики снапшотів та їхніх retention
  • кожен датасет має власний remote backend в Rclone – маємо можливість налаштувати власні параметри шифрування

Головний “source of truth” даних – це робочий ноутбук: тут 1 терабайт диск, тому на ньому зберігаються всі дані, які потім вже бекапляться на NAS з rsync, а частина даних синхронізується з домашнім ноутбуком та NAS через Syncthing.

Всі свої дані виділив в такі класи:

  • User Data: дані з /home/setevoy на ноутбуках на ПК:
    • Shared Static Unencrypted Data:
      • ці дані однакові на всіх хостах
      • сюди входять всякі ~/Books, ~/Music, ~/Photos
      • в Google зберігаються просто plaintext, аби можна було через Web щось подивитись або скачати без Rclone
      • синхронізуються між NAS, setevoy-work та setevoy-home з Syncthing (див. FreeBSD: Home NAS, part 12: синхронізація даних з Syncthing)
    • Shared Dynamic Unencrypted Data:
      • дані на всіх хоста – однакові або з невеликими відмінностями
      • часто оновлюються та/або можуть мати багато “мусору” типу каталогів .git
      • в Google Drive зберігаються просто plaintext
      • сюди входять всякі ~/Work, ~/Projects (власні скрипти), ~/Opt (MikroTik WinBox, якісь локальні Prometheus Exporters, etc)
      • для таких даних source of truth – робочий ноут, а на NAS – mirror даних з робочої машини з rsync під час виконання бекапів
      • на домашній комп, при потребі, просто синхронізувати вручну з NAS
    • Shared Static Crypted Data:
      • дані, однакові на всіх хостах
      • але конфіденційні, тому в Google Drive будуть шифруватись
      • сюди входять ~/Vault (Recovery Codes, KeePass/1Pass, etc)
      • синхронізуються/бекапляться на NAS з rsync з робочого ноута
    • Non-Shared Static Crypted Data:
      • колекція приватних відео – зберігається тільки на робочому ( 🙂 ) ноуті та на самому NAS
      • в Google Drive будуть шифруватись і імена каталогів, і імена файлів, і самі дані
      • сюди входить ~/Films/Private/
      • синхронізуються/бекапляться на NAS з rsync з робочого ноута
  • System Data: різні dotfiles, конфіги з /etc, бекапи сервісів та блогів:
    • System Backups:
      • /boot, /etc, /usr/local/etc, ~/.ssh/config, /root, etc – з усіх хостів на NAS
      • різні конфіги і скрипти з самого FreeBSD
    • Services Backups:
      • тут в основному локальні дані самого NAS, на якому в мене WordPress з моїм приватним щоденником, VictoriaMetrics з метриками всіх хостів і моїм Self-Monitoring (див. InfluxDB: запуск на Debian з NGINX і підключення Grafana, але дані з Influx мігрував до VictoriaMetrics)
      • /usr/local/www – файли блогів
      • mysqldump – бекапи баз даних
      • vmbackup – дані VictoriaMetrics
      • бекапи блогу rtfm.co.ua – але в нього власна (дуже стара) система бекапів, яку поки не міняю – вона заливає дані в AWS S3, а вже звідти архіви нової системою зберігаються на NAS

Data Layers: діаграми збереження та передачі даних

Для кращого уявлення про те, як дані зберігаються та передаються – сформував такий собі концепт “Data Layers“:

  • Storage Layer: ZFS pool, datasets – тут визначаємо що і як зберігається
    • Snaphost Policy Layer: можна ввести додатковий рівень – тут визначаємо локальні ZFS snaphotting policy
  • Transport Layer: Rclone, Rsync – тут визначаємо що, чим, як і куди копіюється
  • Cloud Policy Layer: тут визначаємо які у нас є Rclone Remotes та encryption policy директорій і даних в них

Схема Transport Layer: передача та синхронізація даних

Тепер можна продумати та відобразити весь flow даних.

Про саму автоматизацію бекапів буду писати окремо, інакше пост буде кілометровий – там кілька shell-скриптів, які запускаються на NAS.

Але поки що можна схематично відобразити всі дані та процес збереження і передачі такою от діаграмою:

  • Syncthing: контролює дані, які змінюються не надто часто, мають мало “мусору”, та загальні для всіх хостів – постійно запущений процес на ноутбуках та телефоні
  • Rsync: для решти даних, має описані правила з include та exclude файлах – запускається з NAS, підключається до хостів, збирає з них дані
  • Rclone: займається копіювання даних в remotes, виконує шифрування – запускається з NAS після того, як rsync зібрав всі дані та сам backup-скрипт створив локальні (на NAS) бекапи WordPress та VictoriaMetrics

Схема Cloud Policy Layer: шифрування даних в Google Drive та Backblaze

Тут – для кращого розуміння самим собою – зібрав такий собі “mapping” даних із ZFS-датасетів на NAS до Rclone Remotes:

В Google Drive є окремий каталог Backups/Rclone, в якому створені окремі каталоги для кожного типу зберігання, і для кожного з них на NAS для Rclone налаштовані власні remotes:

  • /nas/services та /nas/systems: шифруємо самі дані, але імена каталогів та файлів plaintext, аби простіше було шукати
  • /nas/media: тут просто все відкритим текстом, бо нічого sensetive не маємо
  • /nas/vault та /nas/private: максимальний рівень конфіденційності – шифруються і імена каталогів та файлів, і їхній зміст

А для Backblaze – просто окремі корзини. Про Rclone remotes буде трохи далі.

NAS ZFS datasets: організація збереження даних

Далі для кожного класу даних визначив датасети:

  • Shared Static Unencrypted Data (~/Pictures, ~/Photos):
    • ZFS dataset: nas/media
  • Shared Dynamic Unencrypted Data (~/Work, ~/Projects, ~/Opt):
    • ZFS dataset: nas/media
  • Non-Shared Static Crypted Data (~/Films/Private/):
    • ZFS dataset: nas/private
  • Shared Static Crypted Data (~/Vault):
    • ZFS dataset: nas/vault
  • System Backups (/boot, /etc, /usr/local/etc):
    • ZFS dataset: nas/systems
  • Services Backups (WordPress databases та файли, VictoriaMetrics):
    • ZFS dataset nas/services

Всі ZFS datasets на NAS зараз виглядають так:

root@setevoy-nas:~ # zfs list
NAME                 USED  AVAIL  REFER  MOUNTPOINT
nas                 2.24T  1.27T   128K  /nas
nas/backups-manual   593G  1.27T   593G  /nas/backups-manual
nas/jellyfin        9.20G  1.27T  9.20G  /nas/jellyfin
nas/media            369G  1.27T   341G  /nas/media
nas/mobile          52.3G  1.27T  52.3G  /nas/mobile
nas/private          208G  1.27T   208G  /nas/private
nas/services         133G  1.27T   133G  /nas/services
nas/systems         4.82G  1.27T  3.78G  /nas/systems
nas/to-sort          560G  1.27T   560G  /nas/to-sort
nas/vault           56.2G  1.27T  56.2G  /nas/vault

Тут є кілька додаткових датасетів, які не пов’язані бекапами:

  • /nas/backups-manual: просто окремі ручні бекапи з хостів або з самого NAS, коли роблю якісь потенційно небезпечні операції з даними
  • /nas/jellyfin: фільми для Jellyfin (про нього є чорнетка, може якось допишу) – це чисто фільми-серіали, тому в бекапи не включено і зберігається окремо
  • /nas/mobile: датасет для даних з мобільного телефона, які копіюються сюди з Syncthing на телефоні
  • /nas/to-sort: копії даних зі старих компів, які треба перебрати та включити в загальні бекапи

Приклад організації даних системних бекапів – все по окремим каталогам:

root@setevoy-nas:~ # tree -d -L 3 /nas/systems/
/nas/systems/
├── setevoy-nas
│   └── thinkcentre-10SUSCF000
│       ├── boot
│       ├── etc
│       ├── home
│       ├── opt
│       ├── root
│       ├── usr
│       └── var
├── setevoy-pi
│   └── raspberry-pi-cm4-rev11
│       ├── etc
│       └── opt
└── setevoy-work
    └── thinkpad-t14-g5-21ML003URA
        ├── etc
        ├── home
        ├── root
        ├── usr
        └── var

І дані сервісів:

root@setevoy-nas:~ # tree -d -L 3 /nas/services/
/nas/services/
├── victoriametrics
│   ├── 20260222
│   │   ├── data
│   │   ├── indexdb
│   │   └── metadata
│   └── latest
│       ├── data
│       ├── indexdb
│       └── metadata
└── web
    └── setevoy
        ├── databases
        └── files

База VictoriaMetrics бекапиться з vmbackup, дані в web – скриптом, який створює tar.gz з файлами та запускає mysqldump для бекапів баз даних.

Дані на диску виглядають так:

root@setevoy-nas:~ # tree -L 4 /nas/services/
/nas/services/
├── victoriametrics
│   ├── 20260222
│   │   ├── backup_complete.ignore
│   │   ├── backup_metadata.ignore
│   │   ├── data
│   │   │   ├── indexdb
│   │   │   └── small
│   │   ├── indexdb
│   │   │   └── 1882B20388AAC712
│   │   └── metadata
│   │       └── minTimestampForCompositeIndex
│   └── latest
│       ├── backup_complete.ignore
│       ├── backup_metadata.ignore
│       ├── data
│       │   ├── indexdb
│       │   └── small
│       ├── indexdb
│       │   └── 1882B20388AAC712
│       └── metadata
│           └── minTimestampForCompositeIndex
└── web
    └── setevoy
        ├── databases
        │   ├── 2026-02-19-18-05-blog-setevoy.sql
        │   ├── 2026-02-21-18-47-blog-setevoy.sql
        │   ├── 2026-02-22-00-00-blog-setevoy.sql
        ...
        └── files
            ├── 2026-02-19-18-05-blog-setevoy.tar.gz
            ├── 2026-02-21-18-47-blog-setevoy.tar.gz
            ├── 2026-02-22-00-00-blog-setevoy.tar.gz
        ...
...

А дані в /nas/media – просто всі в одному каталозі:

root@setevoy-nas:~ # tree -L 3 /nas/media/
/nas/media/
└── home
    └── setevoy
        ├── Backups
        ├── Books
        ├── Documents
        ├── Downloads
        ├── Dropbox
        ├── Music
        ├── Opt
        ├── Photos
        ├── Pictures
        ├── Projects
        ├── VMs
        ├── Videos
        └── Work

Rclone remotes та пов’язані ZFS datasets

Для Rclone створені remotes під кожен датасет, з якого треба бекапити дані в клауди:

root@setevoy-nas:~ # rclone listremotes
nas-aws-s3-setevoy-backups-root:
nas-google-drive-total-root:
nas-google-drive-root:
nas-google-drive-media:
nas-google-drive-mobile:
nas-google-drive-crypted-systems:
nas-google-drive-crypted-services:
nas-google-drive-crypted-vault:
nas-google-drive-crypted-private:
nas-backblaze-root-media:
nas-backblaze-crypted-media:
nas-backblaze-root-mobile:
nas-backblaze-crypted-mobile:
nas-backblaze-root-systems:
nas-backblaze-crypted-systems:
nas-backblaze-root-services:
nas-backblaze-crypted-services:
nas-backblaze-root-vault:
nas-backblaze-crypted-vault:
nas-backblaze-root-private:
nas-backblaze-crypted-private:

Кожен remote “мапиться” на ZFS datasets:

  • nas-google-drive-media та nas-backblaze-crypted-media: сюди заливаються дані з /nas/media
  • nas-google-drive-crypted-systems та nas-backblaze-crypted-systems: сюди – з /nas/systems

І так далі.

Кожен Rclone Remote має власну директорію в Google Drive або Backblaze бакет.

В Google Drive це виглядає так:

root@setevoy-nas:~ # rclone tree -d --max-depth 2  nas-google-drive-total-root:Backups/Rclone
/
└── nas
    ├── media
    ├── mobile
    ├── private
    ├── services
    ├── systems
    └── vault

І приклад одного з бакетів в Backblaze:

root@setevoy-nas:~ # rclone tree -d --max-depth 4 nas-backblaze-crypted-media:
/
├── _archive
│   ├── 2026-02-22-14-53
│   ├── 2026-02-24-03-06
│   │   └── home
│   │       └── setevoy
│   ├── 2026-02-25-03-07
│   │   └── home
│   │       └── setevoy
│   └── 2026-02-27-12-18
│       └── home
│           └── setevoy
└── data
    └── home
        └── setevoy
            ├── Backups
            ├── Books
            ├── Documents
            ├── Downloads
            ├── Dropbox
            ├── Music
            ├── Opt
            ├── Photos
            ├── Pictures
            ├── Projects
            ├── VMs
            ├── Videos
            └── Work

Ну, наче все описав.

Наступна частина – про сам shell-скрипт, який запускає rsync, створює бекапи WordPress та VictoriaMetrics, потім створює ZFS snapshots, а потім всі дані синхронізує в Rclone remotes.

І потім вже остання частина всієї цієї серії постів по Home NAS – з повним описом того, як це все будувалось, які сервіси, як все це моніториться, та як виглядає:

Loading

ilert: альтернатива Opsgenie – перше знайомство, Alertmanager, Slack
5 (1)

24 Лютого 2026

Думаю, всі юзери Opsgenie в курсі, що Atlassian вбиває закриває проект.

Я користувався Opsgenie з 2018 року, до нього звик, і він, в цілому, мав все те, що мені треба було від системи алертинга – хоч місцями і кривувато, але потрібні інтеграції працювали та налаштовувались достатньо легко.

Коли почав шукати альтернативи – натрапив на пост на Reddit – Anyone using Opsgenie? What’s your replacement plan, де дуже багато писали про incident.io – але це саме той випадок, коли мільйони мух таки можуть помилятись, бо більш ущєрбної системи я не бачив.

До речі, зрозумів одну річ: якщо знайомство з системою приводить тебе до думки піти на Youtube, аби глянути як люди цю систему налаштовують – то у цієї у системи явні проблеми або з UI/UX, або з документацією – і це 100% випадок incident.io, при чому по обом пунктам.

Прої… Провозившись з нею кілька днів в спробах таки змусити її відправляти текст в Slack так, як я того хочу – знов почав шукати альтернативи, і в тому ж треді на Reddit натрапив на ilert, і… Боже – це любов з першого погляду.

Все завелось просто за 15 хвилин і без всякого додаткового геморою з налаштуванням того, як повідомлення будуть виглядати в Slack.

Якісь косяки/незручності 100% зустрінуться, але поки що система виглядає саме так, як це має бути – без зайвих свістопєрдєлок, з простим, зручним і інтуїтивно зрозумілим (інтуїтивно зрозумілим, блт – чуєш, incident.io?!) інтерфейсом.

Отже сьогодні подивимось на основні можливості і налаштуємо ilert на відправку алертів.

Власне – що особисто мені треба від системи алертів? Ну… алерти. Відправка алертів. Зручний UI для перегляду алертів, і можливість налаштування шаблонів для повідомлень в Slack, бо це у нас основний канал доставки.

З інтеграцій треба вміти приймати алерти від стандартного Alertmanager та від AWS SNS.

Ну і наявність адекватної (адекватної, incident.io!) документації.

Все!

Поїхали.

Посилань на документацію ilert буде багато, почати можна з Opsgenie to ilert Migration Guide або VictoriaMetrics Integration.

ilert overview та основні можливості

Після реєстрації попадаємо в дашборд – і ви просто зацініть, як тут все класно виглядає – просто, функціонально, зрозуміло:

Основні можливості:

  • є Terraform provider
    • є експорт конфігурації відразу в Terraform
  • алертинг – SMS, Voice calls, Slack, Telegram, web-hooks і упасібоже MS Teams
  • стандартний On-call management  – ротації, розклад, ескалації
  • ChatOps – управління алертами зі Slack
  • AI SRE – ще не трогав, але спробую, хоча це наче ще в Beta
  • postmortems, incidents
  • REST API
  • десь бачив можливість збирати метрики з Prometheus/VictoriaMetrics, але це ще тестив
  • MCP для LLM
  • мобільна апка (теж ще не дивився)
  • Status Pages

Ну і овер 100 інтеграцій – All Integrations.

Pricing

Див. Pricing.

Є Free Plan, в який до того ж входить Heaerbeat, є Status Page, On-call/voice/SMS – з обмеженнями, але включено.

Є в дашборді:

Початок роботи

Основне – це, звісно, алерти, тому глянемо що тут є.

Головні концепти ilert по алертам – це Alert sources та Alert actions:

  • Alert sources: власне, джерела алертів – Alertmanager, AWS SNS, etc
  • Alert actions: правила того, що з алертами робити – відправити повідомлення, оновити статус Status Pages, пушнути webhook

З чого почнемо, і що мені треба з основного:

  • почати приймати алерти від Alertmanager
  • налаштувати відправку в Slack
    • подивитись на роутинг алертів – аби в Slack відправляти по різним каналам
    • подивитись на шаблони повідомлень

Підключення Alertmanager

Переходимо в Alert sources, додаємо новий:

Вибираємо Prometheus – це, власне, і буде Alertmanager:

Документація – Prometheus Integration, і взагалі посилання на документацію є майже всюди, та і сама документація чудова.

Задаємо ім’я:

Задаємо Esclation Policy – далі трохи про них ще поговоримо:

Grouping – none, в мене цим займається Alertmanager:

Тут жеж можна налаштувати Templaing, але поки не робимо – далі буде трохи детальніше:

Тут жеж можна задати фільтри – при яких умовах алерт буде відправлений сюди, але поки теж не включаємо – теж буде трохи далі:

Клікаємо внизу Finish setup, отримуємо ключ і повний URL:

Налаштування Alertmanager

Переходимо до конфіга Alertmanager, додаємо новий роут:

...
      routes:

        - matchers:
            - component="devops"
          receiver: ilert-notifications
          continue: true
...

Та reciever:

...
    receivers:

      - name: 'ilert-notifications'
        webhook_configs:
        - url: 'https://api.ilert.com/api/v1/events/prometheus/il1prom***c94'
...

Можна відправити тестовий алерт з curl:

$ curl -X POST https://api.ilert.com/api/v1/events/prometheus/*** -H "Content-Type: application/json" -d '{"receiver":"ilert-default","status":"firing","alerts":[{"status":"firing","labels":{"alertname":"Test","severity":"warning"},"annotations":{"summary":"Test alert"},"startsAt":"2026-02-24T00:00:00Z","endsAt":"0001-01-01T00:00:00Z","fingerprint":"test123"}],"groupLabels":{"alertname":"Test"},"commonLabels":{"alertname":"Test","severity":"warning"},"commonAnnotations":{"summary":"Test alert"},"externalURL":"http://localhost:9093","version":"4","groupKey":"test"}'

І дуже корисні логи:

В яких буде видно весь payload, і як його розпарсив ilert:

Ну і простий і дуже зручний UI з алертами:

Налаштування алертів в Slack

Переходимо в Alert Actions, додаємо новий:

Вибираємо Slack, не включаємо галочку “Use webhook“:

Підключаємо Alert source, який буде сюди слати алерти, вибираємо на які події слати повідомлення:

Ну і фільтри – але поки пропускаємо:

Задаємо ім’я Action, вибираємо канал, в який будуть йти повідомлення:

Можна тицнути Test:

І посміятись 🙂

Чекаємо на алерт з Alertmanager – і ви просто подивіться на цю красу з коробки!

Всі Connectors трохи сховали – шукаємо в Settings:

Роутинг повідомлень в Slack channels

Тепер, як загальний алертинг працює – починаємо тюнити.

Перше, що треба – це роутинг алертів:

  • є кілька оточень – dev, staging, prod, ops
  • є різні команди – devops, backend, web, data

Для кожної команди у нас є окремі Slack channels – #alerts-backend-prod, #alerts-devops-ops і так далі.

Що треба зробити – щоб алерти Backend Prod йшли в канал #alerts-backend-prod, а алерти для девопсів, відповідно, в канал #alerts-devops-ops.

Для Opsgenie це в мене було реалізовано через роутинг в самому Opsgenie:

А labels задаються в алертах:

...
      - alert: Kubernetes Pod UnHealthy
        expr: k8s:pod:unhealthy{namespace="prod-backend-api-ns"} > 0
        for: 15m
        labels:
          severity: warning
          component: backend
          environment: prod
...

І вже по ним Opsgenie  вибирає через яку Slack-інтеграцію слати алерт.

Власне, в ilert можна зробити таким самим чином, бо аналогічно до Opsgenie – кожен Slack Connector прив’язується до конкретного каналу.

А тому:

  • маємо Alert Source: Alertmanager
  • для нього створюємо кілька Alert actions:
    • slack-alerts-backend-prod: використовує Slack connector, налаштований на #alerts-backend-prod
    • slack-alerts-devops-ops: використовує Slack connector, налаштований на #alerts-devops-ops

Ще є дуже прикольна штука з динамічними роутами через Escalation Policy – далі подивимось.

Static alert rounting

Спершу дивимось, в якому вигляді ilert отримує алерт – переходимо в Alert source > Alert logs, відкриваємо якийсь алерт:

Далі переходимо в Alert actions і додаємо новий (чи редагуємо старий) Action.

Тут все аналогічно до того, як підключали Slack вище – але тепер включаємо Conditional execution і задаємо умову:

Або пишемо кодом – див. ICL – ilert condition language:

(alert.labels.component in ["devops"])

Задаємо канал:

Фільтр готовий:

Тепер алерти з label component="devops" підуть в канал #alerts-devops-ops.

Dynamic alert routing

Інший прикольний варіант – через Dynamic escalation policy routing.

Суть його в тому, що створюється кілька Escalation policices, яким задається Routing key – просто якесь string значення.

Далі в Alert source вмикається Dynamic routing та вказується поле алерта, з якого отримується значення – і, використовуючи це значення, до нового алерта автоматично підключається Escalation policy.

А в Alert Action використовується значення не (alert.labels.component in ["devops"]), як робили вище – а з roting_id, через який підключається відповідна політика.

Цей підхід краще в плані того, що відразу налаштовується не тільки куди слати алерт – а і як його escalate.

Пробуємо.

Переходимо в On-Call > Escalation policies, створюємо нову політику:

Задаємо Routing key:

Аналогічно для Backend:

Тепер переходимо в Alert sources > Alert actions і задаємо фільтр по alert.escalationPolicy.id:

І для бекенду:

Дал редагуємо сам Alert source, включаємо Dynamic routing вказуємо label з алерта, яку треба читати – в моєму прикладі це {{ alerts[0].labels.routing }}:

Тепер:

  • Alert source розпарсить {{ alerts[0].labels.routing }}
  • отримає значення “devops
  • по цьому значенню динамічно підключить Escalation policy
  • передать алерт в Alert Actions
  • а кожен екшен з Alert Actions перевірить свій фільтр alert.escalationPolicy.id – і спрацює той Action і його Connector, який “підключений” (чи “замаплений”) саме до slack-alerts-backend-prod або slack-alerts-devops-ops

В алертах додаємо нову лейблу – routing: alerts-devops-ops та routing: alerts-backend-prod:

...

      - alert: Route Test => alerts-devops-ops (alertname) 5
        expr: sum(kube_pod_info{namespace="ops-monitoring-ns", pod=~".*grafana.*"}) by (cluster, namespace, pod) >= 0
        for: 1s
        labels:
          severity: warning
          component: devops
          environment: ops
          routing: alerts-devops-ops
        annotations:
          summary: "Route Test (summary) 5"
          description:

      - alert: Route Test => alerts-backend-prod (alertname) 5
        expr: sum(kube_pod_info{namespace="ops-monitoring-ns", pod=~".*grafana.*"}) by (cluster, namespace, pod) >= 0
        for: 1s
        labels:
          severity: warning
          component: devops
          environment: ops
          routing: alerts-backend-prod
        annotations:
          summary: "Route Test (summary) 5"
          description:

...

І отримуємо алерти в різні канали.

Девопси:

Бекенди:

Блін – я в захваті від системи 🙂

Slack messages templating

Документація – Alert template.

Тут, в принципі, все доволі просто – задаємо поля з alert payload, можемо використовувати різні Functions – наприклад, [{{ commonLabels.severity.upperCase() }}].

В полях маленька кнопочка “play” справа внизу дозволяє відразу перевірити, як шаблон буде працювати:

Додамо новий алерт:

...
      - alert: Template Test (alertname) 1
        expr: sum(kube_pod_info{namespace="ops-monitoring-ns", pod=~".*grafana.*"}) by (cluster, namespace, pod) >= 0
        for: 1s
        labels:
          severity: warning
          component: devops
          environment: ops
          routing: alerts-devops-ops
        annotations:
          summary: "Template Test (summary) 1"
          description: "Template Test (description)"
          grafana_pod_overview_url: 'https://{{ .Values.monitoring.root_url }}/d/kubernetes-pod-overview/kubernetes-pod-overview?orgId=1&var-namespace={{ "{{" }} $labels.involvedObject_namespace }}&var-pod={{ "{{" }} $labels.pod }}'
...

Результат:

Heartbeat monitors

Heartbeat monitors – теж прикольна штука, але тут, в принципі, все просто – створюємо Alert source, отримуємо URL, і періодично до нього шлемо запит.

Як тільки пропустили відправку сигналу – спрацьовує алерт.

Пробуємо з curl:

$ curl -v https://beat.ilert.com/api/pings/ih2***
* Host beat.ilert.com:443 was resolved.
...
> GET /api/pings/ih2:25*** HTTP/1.1
...

І через заданий таймаут він стане Expired, і спрацює Alert action.

Deployment events – не тестив, але виглядає цікаво.

Summary або загальні враження

Просто система алертів, якою вона має бути.

Чим мені сподобався Backblaze – це простим і інтуїтивно зрозумілим інтерфейсом (див. Backblaze: знайомство з B2 Cloud Storage – перші враження).

Чому я просто закохався в ilert – простий інтерфейс, інтуїтивно зрозумілий інтерфейс і чудова документація.

Можливо, ще випливуть якісь проблеми вже під час реального використання, але після першого дня налаштувань – враження виключно позивні.

Loading

Backblaze: знайомство з B2 Cloud Storage – перші враження
5 (1)

23 Лютого 2026

По факту, цей пост – частина серій по сетапу домашнього NAS на FreeBSD (див. початок тут – FreeBSD: Home NAS, part 1 – налаштування ZFS mirror), але винесу його окремо.

В мене вже налаштована автоматизація бекапів (про неї теж будуть пости), і зараз дані з NAS раз на добу заливаються до Google Drive.

Але, по-перше – з сервісів Google я потроху позбавляюся, по-друге – хочеться мати друге місце для бекапів в клауді, а не тримати всі яйки в одній корзині.

Взагалі планував другим сторейджем взяти AWS S3, але потім подивився на альтернативи та відкрив для себе Backblaze, який мені настільки сподобався, що я прямо в той жеж день оформив підписку і налаштував Rclone на копіювання Backblaze теж.

Про Rclone детальніше писав у FreeBSD: Home NAS, part 9 – backup даних з rclone до AWS S3 та Google Drive, сьогодні тут теж про нього згадаємо, ну і познайомимось з Backblaze в цілому – подивимось на можливості, прайсінг, налаштуємо Rclone remotes та глянемо на швидкість.

Backblaze overview

Загальна документація – Backblaze Documentation, та по Cloud Storage – Get Started with the Web Console.

У Backblaze є два основні продукти – Computer Backup та B2 Cloud Storage.

І якщо до Computer Backup у багатьох є питання (див., наприклад Reddit) – то Cloud Storage прям дуже класна штука.

Головна перевага Backblaze – ціна за зберігання і простота UI (при цьому з усіма необхідними можливостями), а до мінусів можна віднести хіба що доволі невеликий вибір регіонів – але USA та Europe є.

Ну і ще з мінусів, мабуть, відсутність можливості перегляду файлів – але це і не Google Drive, а просто сторейдж, тому ОК.

За вартість детальніше поговоримо трохи нижче, але головне – ціна зберігання: у Backblaze це 6 USD за терабайт, тоді як в AWS S3 Standart Storage це було б 23.55 бакси, плюс ще і вартість за скачування даних з корзин.

Коротко про можливості та плюшки Backblaze:

  • дуже простий в використанні – просто той базовий набір утиліт, які потрібні Cloud Storage – без зайвого ускладнення
  • є можливість налаштувати реплікацію бакетів між регіонами
  • для аутентифікації є Application keys, яким можна здавати окремі scope
  • алерти – базові, по костам і використанню ресурсів
  • дашборда зі статистикою API-запитів і використанню storage
  • можна включити server-side encryption
  • можливість створення снапшотів даних (але тільки якщо не включений SSE)
  • трафік upload в бакет – безкоштовний, download – дуже великий безкоштовний ліміт
  • підтримка базових lifecycle політик
  • є офіційний мобільний клієнт – дуже простенький

Створення тестової корзини

Реєстрація проста, через пошту, після логіну бачимо чудову мінімалістичну дашборду:

Створимо тестовий бакет – звертаємо увагу на попередження про шифрування:

Тобто, як писав вище – не буде можливості створювати снапшоти.

Хоча трохи незрозуміло чому, бо в тому ж попередженні пишуть, що “Backblaze creates, manages key“.

Але для мене це в будь-якому випадку не дуже актуально, бо шифруванням буде займатись Rclone.

Документація по шифруванню – Enable Encryption on a Bucket.

Завершуємо створення бакету – буквально пара кліків:

Створення Application key

В моєму кейсі працювати з Backblaze буде Rclone, тому для нього створимо окремий ключик, див. Create and Manage App Keys:

Задаємо ліміт на конкретну корзину:

Відразу зберігаємо, бо більше його не побачимо:

Налаштування rclone B2 remote

Документація Rclone – Backblaze B2.

Запускаємо rclone config, створюємо новий remote:

$ rclone config
...
e) Edit existing remote
n) New remote
d) Delete remote
...
Enter name for new remote.
name> setevoy-backblaze-testing
...
Option Storage.
Type of storage to configure.
Choose a number from below, or type in your own value.
...
 5 / Backblaze B2
   \ (b2)
...
Account ID or Application Key ID.
Enter a value.
account> 003***001
...
Application Key.
Enter a value.
key> K00***MXQ
...
Option hard_delete.
Permanently delete files on remote removal, otherwise hide files.
Enter a boolean value (true or false). Press Enter for the default (false).
hard_delete>

Edit advanced config?
y) Yes
n) No (default)
y/n>

Configuration complete.
Options:
- type: b2
- account: 003f07593a16f1d0000000001
- key: K003+sr5NBQhJvsTdlCnfdt9UYHqMXQ
Keep this "setevoy-backblaze-testing" remote?
y) Yes this is OK (default)
...

Тут hard_delete залишив в дефолтному значенні, відключеним, але в prodution включив – бо інакше файли будуть не видалятись, а переміщатись в таку собі trash, ховатись.

Мені це не потрібно, бо цим всім займається rclone через --backup-dir.

Перевіряємо як новий ремоут спрацює – створюємо файл, копіюємо в бакет:

[setevoy@setevoy-work ~]  $ echo "test" > /tmp/test-b2.txt
[setevoy@setevoy-work ~]  $ rclone copy  /tmp/test-b2.txt setevoy-backblaze-testing:setevoy-test-1

[setevoy@setevoy-work ~]  $ rclone ls setevoy-backblaze-testing:setevoy-test-1
        5 test-b2.txt

І він в UI – для цього бакету я включав SSE, відразу відображає, що файл зашифрований:

І створення снапшоту для цього файлу недоступне:

Налаштування rclone crypt remote

Тут аналогічно до AWS S3 або Google Drive – створюємо новий remote з типом crypt, підключаємо його до основного remote.

Ще раз запускаємо rclone config, тут приклад з шифруванням тільки даних – імена файлів і директорій будуть plaintext:

$ rclone config
...

name> setevoy-backblaze-testing-crypted
...

Option Storage.
Type of storage to configure.
...
Storage> crypt

Option remote.
Remote to encrypt/decrypt.
...
Enter a value.
remote> setevoy-backblaze-testing:setevoy-test-1

Option filename_encryption.
How to encrypt the filenames.
...
Press Enter for the default (standard).
   / Encrypt the filenames.
 1 | See the docs for the details.
   \ (standard)
 2 / Very simple filename obfuscation.
   \ (obfuscate)
   / Don't encrypt the file names.
 3 | Adds a ".bin", or "suffix" extension only.
   \ (off)
filename_encryption> 3

Option directory_name_encryption.
...
Press Enter for the default (true).
 1 / Encrypt directory names.
   \ (true)
 2 / Don't encrypt directory names, leave them intact.
   \ (false)
directory_name_encryption> 2

Option password.
...
y) Yes, type in my own password
g) Generate random password
y/g> y
Enter the password:
password:
Confirm the password:
password:

Option password2.
Password or pass phrase for salt.
...
n) No, leave this optional password blank (default)
y/g/n>

...
Configuration complete.
Options:
- type: crypt
- remote: setevoy-backblaze-testing:setevoy-test-1
- filename_encryption: off
- directory_name_encryption: false
- password: *** ENCRYPTED ***

...

Копіюємо тестовий файл в цей ремоут:

$ rclone copy  /tmp/test-b2.txt setevoy-backblaze-testing-crypted

І маємо цей жеж файл, але вже як .bin:

Реальні дані та помилка “Cannot upload files, storage cap exceeded”

Взагалі думав на цьому і закінчувати, але не втримався – налаштував вже всю систему повністю, і тут ще є трохи шо показати.

Перше – коли почав заливати вже великі обсяги даних, то спіймав помилку “storage cap exceeded“:

2026/02/22 12:39:37 NOTICE: Failed to sync with 4 errors: last error was: Cannot upload files, storage cap exceeded. See the Caps & Alerts page to increase your cap. (403 storage_cap_exceeded)
ERROR: Rclone sync for nas/vault failed with exit code 7

Бо на безкоштовному акаунті маємо ліміт на 10 ГБ upload:

Додаємо картку – отримуємо повний доступ.

Backblaze pricing

Ну і раз вже дійшло до платежів – то трохи про вартість Backblaze.

Платимо тільки за сам storage – $6 за кожен терабайт, і за завантаження даних з бакетів – але тільки за той обсяг, який в 3 рази перевищує розмір даних, який зберігається в бакетах.

Тобто, зберігаємо 1 терабайт – можемо на місяць безкоштовно скачати 3 терабайти. Вище цього обсягу буде рахуватись по $0.01/GB.

Дательніше див. Backblaze Product and Pricing Updates.

Крім того, оплачується частина API-операцій, при цьому операції поділені на три окремі класи:

  • Class A: безкоштовні – створення бакетів, завантаження файлів (upload), видалення
  • Class B: скачування файлів (download), сюди входить b2_download_file_*, b2_get_file_info, наприклад – rclone sync з бакета до себе на машину
    • $0.004 за 10,000 операцій.
  • Class C: listing файлів, перевірка метаданих – сюди входять b2_list_file_names, b2_list_file_versions, наприклад – коли ми робимо rclone sync від себе в бакет
    • $0.004 за 1,000 операцій

Але при цьому маємо 2500 бескшотовних API-викликів на день.

Див. Backblaze B2 Cloud Storage Frequent Questions.

Для моєї схеми найбільше буде Class C транзакцій, бо rclone при кожному sync перевіряє всі файли і їх modification time, тобто читає метадані (хоча це наче тюниться).

Ну і можна використати опцію rclone --fast-list – менше операції, але більше RAM.

Подивимось на суму, коли прийде перший рахунок 🙂

В Reports є повна інформація по кількості викликів кожного типу:

Власне, додаємо картку, через пару хвилин запускаємо копіювання даних – і все пройшло:

Але через помилку залишилось кілька incomplete uploads (“started large file” на скріншоті) – можна почистити з rclone cleanup або просто видалити вручну:

Upload speed і порівняння з Google Drive та AWS S3

Коли запустив завантаження свої бекапів швидкість виглядала так – але тут частково були невеликі файли:

Пізніше зробив окремий “бенчмарк” – завантаження файлу в 50 гігабайт з rclone copy.

Перший результат – Google Drive, максимум було 322 Mb/s, Backblaze розкачався до 417 Mb/s, а AWS видав цілих 516 Mb/s:

Ну а результат завантаження взагалі всього мого бекапу виглядає так:

В цілому враження – принаймні поки що – чудові.

Подивимось, як це буде працювати.

Loading

MikroTik: налаштування WireGuard та підключення Linux peers
5 (1)

17 Лютого 2026

Ще одна з (багатьох) приємних можливостей MikroTik – вбудована підтримка WireGuard (хоча вона є навіть на дешевому TP-Link Archer).

В моєму сетапі MikroTik RB4011 грає роль такого собі “VPN Hub” – всі клієнти підключаються до нього і об’єднуються в єдину мережу, і роль VPN трохи перебільшена тут дійсно важлива – бо це такий собі gateway, через які всі хости комунікують один з одним, і саме через VPN-тунелі з NAS мій скрипт для створення бекапів (про автоматизацію бекапів буде окремим великим постом, вже є в чернетках) підключається до всіх хостів, аби запустити rsync і стягнути до себе дані.

Крім того, Syncthing теж працює виключно в межах внутрішніх мереж і синхронізує дані між FreeBSD/NAS, ноутбуках з Arch Linux та мобільним телефоном – див. FreeBSD: Home NAS, part 12: синхронізація даних з Syncthing.

Власне сьогодні – налаштування WireGuard на самому MikroTik та на Debian (сервер rtfm.co.ua).

На Arch Linux (домашній ноутбук) все аналогічно до Debian, а для телефона є офіційний клієнт WireGuard, і там все в принципі схоже.

Див. також перший пост по MikroTik – MikroTik: перше знайомство та Getting Started, і далі буде ще, мабуть, ціла серія – вже пачка чернеток є.

Що будемо робити:

  • на MikroTik створимо інтерфейс для WireGuard, назначимо йому IP
  • налаштуємо MikroTik Firewall для трафіку між всіма хостами
  • налаштуємо роути – додамо в домашню мережу
  • створимо WireGuard ключі, додамо WireGuard Peer на MikroTik

Хоча пост вийшов довгий – але насправді все доволі просто.

Головне не плутати приватні і публічні ключі в конфігах – в мене на початку з цим було трохи складностей.

Архітектура мереж і хостів

Схематично вся мережа виглядає так:

Тут:

  • 192.168.0.0/24: мережа офісу, основні хости тут:
    • 192.168.0.1: MikroTik RB4011 – основний роутер
    • 192.168.0.2: FreeBSD з NAS
    • 192.168.0.3: робочий ноутбук з Arch Linux
  • 192.168.100.0/24: домашня мережа
    • 192.168.100.100: домашній ноутбук
  • 10.100.0.0/24: WireGuard VPN
    • 10.100.0.1: MikroTik RB4011
    • 10.100.0.3: домашній ноутбук з Arch Linux
    • 10.100.0.10: сервер rtfm.co.ua в DigitalOcean з Debian Linux

Адресацію буду перероблювати, але поки що так.

Конфігурація WireGuard MikroTik

Офіційна документація – WireGuard.

Отже, мережа VPN 10.100.0.0/24, адреса самого MikroTik в ній – 10.100.0.1.

Додаємо інтерфейс, на якому буде працювати WireGuard – задаємо ім’я, порт, задаємо MTU:

/interface wireguard add name=wg0 listen-port=51820 mtu=1420

(місцями вже без скріншотів, бо робив давненько)

MTU задаємо 1420 – бо дефолтний MTU в Ethernet 1500 байт, а WireGuard додає свої заголовки:

  • IP header: 20 байтів (IPv4) – задається операційною системою роутера
  • UDP header: 8 байтів – аналогічно, роутер
  • WireGuard header: 32 байти – задається WireGuard, де вказується тип повідомлення, індекс піра, аутентифікація

Разом під headers 60 байт: 20 (IP) + 8 (UDP) + 32 (WireGuard), що залишає нам 1440 байт для корисних даних (payload), і ще -20 “запас”.

Див. Header / MTU sizes for Wireguard та мій пост TCP/IP: моделі OSI та TCP/IP, TCP-пакети, Linux sockets і порти.

Перевіряємо:

/interface wireguard print detail where name=wg0

Задаємо адресу інтерфейсу:

/ip address add address=10.100.0.1/24 interface=wg0 comment=wg-hub

Перевіряємо:

/ip address print where interface=wg0

Конфігурація MikroTik Firewall

Нам треба додати дозволи на доступ з інтернету до самого WireGuard на MikroTik, а потім налаштувати доступи між мережами.

Детальніше про фаєрвол на MikroTik теж напишу окремо, теж є в чернетках.

Доступ з інтернету до WireGuard

Включаємо доступ з будь-якого хосту, правило в chain=input:

/ip firewall filter add chain=input protocol=udp dst-port=51820 action=accept comment="allow wireguard"

Або, краще, створюємо список дозволених адрес:

/ip firewall address-list add list=wg-allowed address=46.101.201.123 comment=setevoy-rtfm
/ip firewall address-list add list=wg-allowed address=178.***.236 comment=setevoy-home
/ip firewall address-list add list=wg-allowed address=64.***.***.83 comment=kyiv-work-office

Момент з адресами для дроплетів в DigitalOcean: в мене підключений Reserved IP 67.207.75.157, який використовується на DNS – але для WireGuard використовуємо саме “дефолтний” Public IP від DigitalOcean – 46.101.201.123:

Перевіряємо наш новий address-list:

/ip firewall address-list print where list=wg-allowed

Тепер створюємо правило з цим списком в chain=input:

/ip firewall filter add chain=input protocol=udp dst-port=51820 src-address-list=wg-allowed action=accept comment="allow wireguard from whitelist" place-before=0

Перевіряємо:

/ip firewall filter print where src-address-list~"wg-allowed"

Доступ між внутрішніми мережами

Налаштовуємо доступ між мережами – додаємо правила в chain=forward.

Дозволяємо доступ з мережі VPN 10.100.0.0/24 в локальну мережу офісу 192.168.0.0/24:

/ip firewall filter add chain=forward src-address=10.100.0.0/24 dst-address=192.168.0.0/24 action=accept comment="wg to lan"

І обратно – з офісної мережі в мережу VPN:

/ip firewall filter add chain=forward src-address=192.168.0.0/24 dst-address=10.100.0.0/24 action=accept comment="lan to wg"

Далі – доступ з VPN до домашньої мережі 192.168.100.0/24:

/ip firewall filter add chain=forward src-address=10.100.0.0/24 dst-address=192.168.100.0/24 action=accept comment="wg to home"

І обратно, з дому в мережу VPN:

/ip firewall filter add chain=forward src-address=192.168.100.0/24 dst-address=10.100.0.0/24 action=accept comment="home to wg"

Аналогічно – доступ вже між офісною 192.168.0.0/24  домашньою 192.168.100.0/24.

З офісу в домашню:

/ip firewall filter add chain=forward src-address=192.168.0.0/24 dst-address=192.168.100.0/24 action=accept comment="office to home"

І обратно:

/ip firewall filter add chain=forward src-address=192.168.100.0/24 dst-address=192.168.0.0/24 action=accept comment="home to office"

Перевіряємо:

/ip firewall filter print where comment~"wg|office|home"

MikroTik Routes

MikroTik автоматично створює dynamic connected route для мережі WireGuard (10.100.0.0/24) після створення інтерфейсу wg0, і вже має routes для своїх локальних мереж, які безпосередньо підключені до його інтерфейсів (bridge, ether):

/ip route print

Або виберемо тільки ті, що нам зараз цікаві:

/ip route print where dst-address~"192.168.|10.100"

Тут 10.100.0.0/24 – мережа WireGuard, 192.168.0.0/24 – активна мережа DHCP-серверу на MikroTik, 192.168.88.0/24 – дефолтна мережа DHCP-серверу, зараз не використовується.

Але мережа дома – 192.168.100.0/24, і аби ходити з дому в офіс і навпаки – треба додати ще один роут:

/ip route add dst-address=192.168.100.0/24 gateway=wg0 comment="route to home via wg"

І роути тепер:

Можна переходити до підключення клієнтів.

WireGuard Peer на Linux

Сервер rtfm.co.ua хоститься в DigitalOcean, працює на Debian 12.

Сетап на Arch Linux такий самий, тільки пакет встановлюємо wireguard-toolspacman -S wireguard-tools.

На Debian встановлюємо пакет wireguard з apt:

root@setevoy-do-2023-09-02:~# apt update && apt install -y wireguard

Переходимо в /etc/wireguard/, створюємо ключі:

root@setevoy-do-2023-09-02:/etc/wireguard# cd /etc/wireguard/

root@setevoy-do-2023-09-02:/etc/wireguard# wg genkey | tee /etc/wireguard/privatekey | wg pubkey > /etc/wireguard/publickey

Тут privatekey будемо використовувати для локального інтерфейсу, а publickey потім додамо на MikroTik WireGuard.

На Debian отримуємо значення приватного ключа:

root@setevoy-do-2023-09-02:/etc/wireguard# cat privatekey 
ML+***dmk=

На MikroTik отримуємо public-key:

/interface wireguard print

На Debian створюємо конфіг /etc/wireguard/wg0.conf:

[Interface]
PrivateKey = ML+0***mk=
Address = 10.100.0.10/32

[Peer]
PublicKey = hxz***0o=
Endpoint = 178.***.***.184:51820

AllowedIPs = 10.100.0.0/24,192.168.0.0/24,192.168.100.0/24
PersistentKeepalive = 25

Тут:

  • [Interface]
    • PrivateKey: приватний ключ на самому Debian
    • Address: IP-адреса цього Peer, буде використана для локального інтерфейсу wg0
  • [Peer]
    • PublicKey: публічний ключ з MikroTik
    • Endpoint: зовнішня адреса за якою доступний MikroTik, та порт, на якому WireGuard приймає підключення
    • AllowedIPs: в які мережі може ходити цей peer і для яких будуть створені локальні роути

На Debian отримуємо публічний ключ:

root@setevoy-do-2023-09-02:/etc/wireguard# cat publickey 
x+Pr***0TE=

На MikroTik додаємо новий peer:

/interface wireguard peers add interface=wg0 public-key="x+Pr***0TE=" allowed-address=10.100.0.10/32,192.168.0.0/24,192.168.100.0/24 comment=setevoy-rtfm

В allowed-address на MikroTik задаємо дозвіл на доступ в мережі – перевіряється і для src-addr, і для dst-addr.

Власне – на цьому все.

На Linux запускаємо підключення:

root@setevoy-do-2023-09-02:/etc/wireguard# wg-quick up wg0 
[#] ip link add wg0 type wireguard
[#] wg setconf wg0 /dev/fd/63
[#] ip -4 address add 10.100.0.10/32 dev wg0
[#] ip link set mtu 1420 up dev wg0
[#] ip -4 route add 192.168.100.0/24 dev wg0
[#] ip -4 route add 192.168.0.0/24 dev wg0
[#] ip -4 route add 10.100.0.0/24 dev wg0

Перевіряємо статус:

root@setevoy-do-2023-09-02:/etc/wireguard#wg show
interface: wg0
  public key: x+Pr/***TE=
  private key: (hidden)
  listening port: 59014

peer: hxz***50o=
  endpoint: 178.***.184:51820
  allowed ips: 10.100.0.0/24, 192.168.0.0/24, 192.168.100.0/24
  latest handshake: 1 minute, 51 seconds ago
  transfer: 16.21 KiB received, 21.75 KiB sent
  persistent keepalive: every 25 seconds

На що звертаємо увагу – це latest handshake, який відображає, що підключення активне, і піри один з одним змогли зв’язатись.

Перевіряємо peers на MikroTik:

/interface wireguard peers print where comment="setevoy-rtfm"

Перевіряємо підключення з Linux на офісний ноутбук:

root@setevoy-do-2023-09-02:~# ssh [email protected] -i .ssh/setevoy-work 
[setevoy@setevoy-work ~]$ 

І на домашній ноут – який теж є VPN peer, і у нього дві адреси – 10.100.0.3 в мережі VPN, і 192.168.100.100 в мережі домашнього роутера.

Пробуємо його адресу в VPN:

root@setevoy-do-2023-09-02:~# ping -c 1 10.100.0.3
PING 10.100.0.3 (10.100.0.3) 56(84) bytes of data.
64 bytes from 10.100.0.3: icmp_seq=1 ttl=63 time=116 ms

--- 10.100.0.3 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 115.925/115.925/115.925/0.000 ms

І на його домашню адресу:

root@setevoy-do-2023-09-02:~# ping -c 1 192.168.100.100
PING 192.168.100.100 (192.168.100.100) 56(84) bytes of data.
64 bytes from 192.168.100.100: icmp_seq=1 ttl=63 time=36.2 ms

--- 192.168.100.100 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 36.181/36.181/36.181/0.000 ms

І статус всіх клієнтів на MikriTik зараз:

WireGuard Connection Troubleshooting

Для дебагу може бути корисним додати логування проходження пакетів на фаєрволі MikroTik:

/ip firewall filter add chain=forward src-address=192.168.0.0/24 dst-address=192.168.100.0/24 action=log log-prefix="office->home" place-before=0

/ip firewall filter add chain=forward src-address=192.168.100.0/24 dst-address=192.168.0.0/24 action=log log-prefix="home->office" place-before=0

Потім пінгуємо з офісу додому – тут ще була проблема:

$ ping 192.168.100.100
PING 192.168.100.100 (192.168.100.100) 56(84) bytes of data.
^C
--- 192.168.100.100 ping statistics ---
4 packets transmitted, 0 received, 100% packet loss, time 3090ms

І дивимось логи на MikroTik – тут клієнт вдома на Arch Linux вже підключений, пофіксив:

/log print where message~"office->home|home->office"
 ...
 2026-02-17 14:53:32 firewall,info home->office forward: in:wg0 out:bridge, connection-state:established proto ICMP (type 0, code 0), 192.168.100.100->192.168.0.3, len 84
 2026-02-17 14:53:33 firewall,info office->home forward: in:bridge out:wg0, connection-state:established src-mac c8:a3:62:2e:fd:cb, proto ICMP (type 8, code 0), 192.168.0.3->192.168.100.100, len 84
 2026-02-17 14:53:33 firewall,info home->office forward: in:wg0 out:bridge, connection-state:established proto ICMP (type 0, code 0), 192.168.100.100->192.168.0.3, len 84
 2026-02-17 14:53:34 firewall,info office->home forward: in:bridge out:wg0, connection-state:established src-mac c8:a3:62:2e:fd:cb, proto ICMP (type 8, code 0), 192.168.0.3->192.168.100.100, len 84
 2026-02-17 14:53:34 firewall,info home->office forward: in:wg0 out:bridge, connection-state:established proto ICMP (type 0, code 0), 192.168.100.100->192.168.0.3, len 84

Потім видаляємо, бо буде багато писати:

/ip firewall filter remove [find log-prefix="office->home"]

/ip firewall filter remove [find log-prefix="home->office"]

Або використовуємо tcpdump на хостах.

Наприклад, з офісного ноута пінгуємо домашній:

[setevoy@setevoy-work ~]  $ ping -c 1 192.168.100.100
PING 192.168.100.100 (192.168.100.100) 56(84) bytes of data.
64 bytes from 192.168.100.100: icmp_seq=1 ttl=63 time=107 ms

--- 192.168.100.100 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 106.714/106.714/106.714/0.000 ms

А на домашньому ноуті слухаємо весь ICMP:

root@setevoy-home:~ # tcpdump -i any icmp and host 192.168.100.100
tcpdump: WARNING: any: That device doesn't support promiscuous mode
(Promiscuous mode not supported on the "any" device)
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
14:59:17.433334 wg0   In  IP work.setevoy > setevoy-home: ICMP echo request, id 25635, seq 1, length 64
14:59:17.433381 wg0   Out IP setevoy-home > work.setevoy: ICMP echo reply, id 25635, seq 1, length 64

Якщо треба оновити параметри peer – виконуємо через interface wireguard peers set.

Для домашнього ноута при створені не задав 192.168.100.0/24 в allowed-address, треба було оновити параметр:

/interface wireguard peers set [find comment="setevoy-home"] allowed-address=10.100.0.3/32,192.168.100.0/24,192.168.0.0/24

А через те, що не було 192.168.100.0/24 в allowed-address – не було прямого підключення із 192.168.0.0/24 – бо пакет йшов через WireGuard тунель, приходив на домашній ноутбук на інтерфейс wg0, потім відправлявся на інтерфейс WiFi з адресою 192.168.100.100, але так як цього не було в allowed-address – то пакет дропався.

Готово.

Loading

FreeBSD: Home NAS, part 12: синхронізація даних з Syncthing
0 (0)

14 Лютого 2026

Вже потроху наближаюсь до завершення історії з налаштування домашнього NAS на FreeBSD.

Вже є ZFS pool, є датасети, є моніторинг – можна починати налаштування автоматизації бекапів.

Але якщо на початку все здавалось доволі просто – “просто скопіювати потрібні каталоги з робочого ноутбука”, то чим далі – тим цікавішою виявлялась задача.

Більш детальний опис планування та автоматизації бекапів опишу окремо, а сьогодні познайомимось з ще одною класною утилітою – Syncthing.

Всі частини серії по налаштуванню домашнього NAS на FreeBSD:

Syncthing overview

Отже, для чого вона мені: є кілька хостів (робочий та домашній ноутбуки, ігровий ПК), між якими треба синхронізувати загальні дані.

Загальні дані – це каталоги з фотками, музикою, картинками – все те, що змінюється не дуже часто, і де нема “мусора” типу каталогів .git, logs або tmp.

Такі каталоги повинні бути однаковими між ноутами та ПК і самим NAS, і коли я почав думати як жеж це все синхронізувати – то вперся в проблему того, що дані на будь-якому хості можуть і додатись і видалитись – і треба це все діло відстежувати і копіювати всі зміни.

Rsync чи Rclone тут не дуже підходять, бо у них принцип роботи “master-slave” – є один source of truth, і його зміст контролюється з Rsync/Rclone.

А в данному випадку, коли хостів декілька і кожен може робити власні зміни, які треба “відзеркалити” на інші – треба і інший інструмент, який зможе сам моніторити і копіювати всі зміни.

До того ж є і мобільний телефон з фотками, які хочеться бекапити напряму до NAS, а не в Goolge чи Proton Drive.

Власне, тут на сцену і виходить Syncthing:

  • підключається до кількох хостів
  • для кожного хоста налаштовується які саме локальні каталоги синхронізувати з іншими хостами та які каталоги з інших хостів синхронізувати локально
  • передає дані з шифруванням трафіку

До того ж має зручний Web UI, конфіг зберігає в файлі, який легко бекапити та має клієнтів під Android та iOS, і має чудову документацію.

Трохи забігаючи наперед (бо схема с наступного поста FreeBSD: Hone NAS, part 12: планування бекапів) – роль Syncthing в моєму сетапі виглядає так:

Отже, сьогодні установимо Syncthing на NAS з FreeBSD та на ноутбук з Arch Linux, і подивимось як це все працює.

Установка Syncthing на FreeBSD

Syncthing є в репозиторії, встановлюємо його:

root@setevoy-nas:~ # pkg install syncthing

Додаємо до /etc/rc.conf:

root@setevoy-nas:~ # sysrc syncthing_enable="YES"
syncthing_enable:  -> YES
root@setevoy-nas:~ # sysrc syncthing_user="setevoy"
syncthing_user:  -> setevoy

Файл налаштувань – /usr/local/etc/syncthing/config.xml.

Більшість налаштувань виконуються через Web (хоча є і CLI), але по дефолту Syncthing запускається на localhost.

А так як це FreeBSD без X-серверу – то і браузеру там нема.

Тому редагуємо файл і задаємо IP зовнішнього інтерфейсу, в мене це 192.168.0.2 (хоча адресацію буду перероблювати, коли доберусь до MikroTik та його DHCP):

...
    <gui enabled="true" tls="false" sendBasicAuthPrompt="false">
        <address>192.168.0.2:8384</address>
        <metricsWithoutAuth>false</metricsWithoutAuth>
        <apikey>L2P***eAk</apikey>
        <theme>default</theme>
    </gui>
...

Запускаємо сервіс:

root@setevoy-nas:~ # service syncthing start
Starting syncthing.

Перевіряємо порт:

root@setevoy-nas:~ # sockstat -4 -l | grep 8384
setevoy  syncthing  34083 18  tcp4   192.168.0.2:8384   *:*

Відкриваємо дашборду:

Створення ZFS dataset

Поки знайомлюсь з системою – зробив окремий датасет:

root@setevoy-nas:~ # zfs create nas/syncthing-test
root@setevoy-nas:~ # zfs list nas/syncthing-test
NAME                 USED  AVAIL  REFER  MOUNTPOINT
nas/syncthing-test    96K  2.24T    96K  /nas/syncthing-test

Задаємо власника – бо Syncthing запускається від юзера setevoy (який заданий через syncthing_user="setevoy" в /etc/rc.conf):

root@setevoy-nas:~ # chown setevoy:setevoy /nas/syncthing-test
root@setevoy-nas:~ # ls -ld /nas/syncthing-test
drwxr-xr-x  2 setevoy setevoy 2 Feb 13 19:05 /nas/syncthing-test

Додавання каталогу

Тепер додамо локальний каталог, який можна буде зробити доступним для синхронізації на інших хостах:

Вказуємо ім’я та локальний шлях.

Folder ID залишаємо – це просто унікальний ідентифікатор для використання між хостами:

Тут жеж є налаштування Versioning – зберігання копій, далі подивимось детальніше:

І цікаві опції в Advanced – але це вже іншим разом:

Після додавання нового каталогу він буде збережений в .../syncthing/config.xml:

root@setevoy-nas:~ # cat /usr/local/etc/syncthing/config.xml | grep jmw5s-hotah
    <folder id="jmw5s-hotah" label="syncthing-test" path="/nas/syncthing-test" type="sendreceive" rescanIntervalS="3600" fsWatcherEnabled="true" fsWatcherDelayS="10" fsWatcherTimeoutS="0" ignorePerms="false" autoNormalize="true">

Тепер додаємо першого “клієнта” – хоча Syncthing все ж peer-to-peer архітектура, але конкретно в моєму випадку є окремий сервер чи хаб, а інші хости – це клієнти.

Установка Syncthing на Arch Linux

Теж є в репозиторії, встановлюємо:

[setevoy@setevoy-work ~] $ sudo pacman -S syncthing

Можна поки запустити руками – подивитись на його output:

[setevoy@setevoy-work ~]  $ syncthing
2026-02-13 19:14:21 INF syncthing v2.0.14 "Hafnium Hornet" (go1.25.6 X:nodwarf5 linux-amd64) syncthing@archlinux 2026-02-03 09:05:00 UTC [noupgrade] (log.pkg=main)
2026-02-13 19:14:21 INF Generating key and certificate (cn=syncthing log.pkg=syncthing)
2026-02-13 19:14:21 INF Default config saved; edit to taste (with Syncthing stopped) or use the GUI (path=/home/setevoy/.local/state/syncthing/config.xml log.pkg=syncthing)
2026-02-13 19:14:21 INF Archiving a copy of old config file format (path=/home/setevoy/.local/state/syncthing/config.xml.v0 log.pkg=syncthing)
2026-02-13 19:14:21 INF Calculated our device ID (device=2W2JHRW-T***-2TRDAAF log.pkg=syncthing)
2026-02-13 19:14:21 INF Overall rate limit in use (send="is unlimited" recv="is unlimited" log.pkg=connections)
2026-02-13 19:14:21 INF Using discovery mechanism (identity="global discovery server https://discovery-lookup.syncthing.net/v2/?noannounce" log.pkg=discover)
2026-02-13 19:14:21 INF Using discovery mechanism (identity="global discovery server https://discovery-announce-v4.syncthing.net/v2/?nolookup" log.pkg=discover)
2026-02-13 19:14:21 INF Using discovery mechanism (identity="global discovery server https://discovery-announce-v6.syncthing.net/v2/?nolookup" log.pkg=discover)
2026-02-13 19:14:21 INF Using discovery mechanism (identity="IPv4 local broadcast discovery on port 21027" log.pkg=discover)
2026-02-13 19:14:21 INF Using discovery mechanism (identity="IPv6 local multicast discovery on address [ff12::8384]:21027" log.pkg=discover)
2026-02-13 19:14:21 INF Relay listener starting (id=dynamic+https://relays.syncthing.net/endpoint log.pkg=connections)
2026-02-13 19:14:21 INF QUIC listener starting (address="[::]:22000" log.pkg=connections)
2026-02-13 19:14:21 INF Creating new HTTPS certificate (log.pkg=api)
2026-02-13 19:14:21 INF TCP listener starting (address="[::]:22000" log.pkg=connections)
2026-02-13 19:14:21 INF GUI and API listening (address=127.0.0.1:8384 log.pkg=api)
2026-02-13 19:14:21 INF Access the GUI via the following URL: http://127.0.0.1:8384/ (log.pkg=api)
2026-02-13 19:14:21 INF Loaded configuration (name=setevoy-work log.pkg=syncthing)
2026-02-13 19:14:21 INF Measured hashing performance (perf="1978.89 MB/s" log.pkg=syncthing)

Додавання Remote Devices

Тепер треба Syncthing на Linux додати в пул до Syncthing на FreeBSD.

На Linux йдемо в Actions > Show ID:

(QR дуже ручний для підключення мобільних клієнтів – теж вже робив, працює чудово)

Далі на FreeBSD клікаємо Add Remote Device:

Він відразу в мережі побачив клієнта на Linux-хості (див. Syncthing Discovery Server та Security Principles):

Клікаємо Save, але Linux-клієнт поки що в статусі Disconnected:

Повертаємось до Syncthing на Linux – туди приходить запит на підключення:

І тепер маємо два девайси, об’єднані в мережу.

На FreeBSD:

І на ноутбуці з Linux:

Налаштування Folder Sharing

Тепер подивимось, як працює синхронізація.

На FreeBSD у створеному раніше Folder клікаємо Edit:

Переходимо на вкладку Sharing, вибираємо девайси, і якими хочемо зашарити папку:

Аналогічно до процесу додавання Devices – спочатку нам на Linux-клієнт прийде запит на підтвердження:

Клікаємо Add, задаємо локальний шлях на ноутбуці з Linux:

Перевіряємо, як це все діло працює.

Створимо файл на FreeBSD:

root@setevoy-nas:~ # echo "hello from nas" > /nas/syncthing-test/test1.txt

Дивимось Syncthing output на ноуті – пише що і коли змінилось:

...
2026-02-13 19:23:54 INF Synced file (folder.label=syncthing-test folder.id=jmw5s-hotah folder.type=sendreceive file.name=test1.txt file.modified="2026-02-13 19:23:43.432316 +0200 EET" file.permissions=0644 file.size=15 file.blocksize=131072 blocks.local=0 blocks.download=1 log.pkg=model)
...

І файл тепер є на Linux-клієнті:

[setevoy@setevoy-work ~]  $ ll nas/syncthing-test/
total 4
-rw-r--r-- 1 setevoy setevoy 15 Feb 13 19:23 test1.txt

Перевіримо зворотню синхронізацію – додамо файл на Linux:

[setevoy@setevoy-work ~]  $ echo "hello from laptop" > /home/setevoy/nas/syncthing-test/test2.txt

І через декілька секунд – він є і на FreeBSD:

root@setevoy-nas:~ # cat /nas/syncthing-test/test2.txt 
hello from laptop

Тестуємо видалення:

[setevoy@setevoy-work ~]  $ rm /home/setevoy/nas/syncthing-test/test2.txt

І на FreeBSD він теж зникає:

root@setevoy-nas:~ # ll /nas/syncthing-test/
total 3
drwxr-xr-x  2 setevoy setevoy    3B Feb 13 19:09 .stfolder
-rw-r--r--  1 root    setevoy   15B Feb 13 19:23 test1.txt
-rw-r--r--  1 setevoy setevoy   18B Feb 13 19:25 test2.txt
root@setevoy-nas:~ # ll /nas/syncthing-test/
total 2
drwxr-xr-x  2 setevoy setevoy    3B Feb 13 19:09 .stfolder
-rw-r--r--  1 root    setevoy   15B Feb 13 19:23 test1.txt

Налаштування Versioning для бекапів

Тепер про те, як можна бекапити дані – захист від випадкового видалення.

Документація – File Versioning.

Переходимо в Folder > Edit, вкладка File Versioning:

Тут опції:

  • Trash Can: при видаленні файл переноситься в .stversions
  • Simple: зберігає N останніх версій
  • Staggered: зберігає версії з часом (1h, 1d, 1w і т.д.)
  • External: викликати зовнішній скрипт

Спробуємо з Trash Versioning:

На Linux-клієнті створимо новий файл:

[setevoy@setevoy-work ~]  $ echo "hello from laptop" > /home/setevoy/nas/syncthing-test/test-trash.txt

Чекаємо на його появу на FreeBSD-хості:

root@setevoy-nas:/home/setevoy # ll /nas/syncthing-test/
total 3
drwxr-xr-x  2 setevoy setevoy    3B Feb 13 19:09 .stfolder
-rw-r--r--  1 setevoy setevoy   18B Feb 13 19:33 test-trash.txt
-rw-r--r--  1 root    setevoy   15B Feb 13 19:23 test1.txt

Видаляємо на ноутбуці:

[setevoy@setevoy-work ~]  $ rm /home/setevoy/nas/syncthing-test/test-trash.txt

І через кілька секунд він зникає на FreeBSD:

root@setevoy-nas:/home/setevoy # ll /nas/syncthing-test/
total 3
drwxr-xr-x  2 setevoy setevoy    3B Feb 13 19:09 .stfolder
-rw-r--r--  1 setevoy setevoy   18B Feb 13 19:33 test-trash.txt
-rw-r--r--  1 root    setevoy   15B Feb 13 19:23 test1.txt
root@setevoy-nas:/home/setevoy # ll /nas/syncthing-test/
total 3
drwxr-xr-x  2 setevoy setevoy    3B Feb 13 19:09 .stfolder
drwxr-xr-x  2 setevoy setevoy    3B Feb 13 19:34 .stversions
-rw-r--r--  1 root    setevoy   15B Feb 13 19:23 test1.txt

Але збережений в .stversions/:

root@setevoy-nas:/home/setevoy # ll /nas/syncthing-test/.stversions/
total 1
-rw-r--r--  1 setevoy setevoy   18B Feb 13 19:34 test-trash.txt

Крім того, у Web тепер є кнопочка Versions:

Де показані видалені файли, і які звідси можна відновити:

Для NAS, скоріш за все, зроблю Trash на 30 днів, а довготривалі бекапи будуть через ZFS snaphosts + копіювання на Google/Roton Drive та AWS S3.

Наступні кроки

Ну і тепер можна на Linux-клієнт додати Syncthing в автостарт:

[setevoy@setevoy-work ~]  $ systemctl --user enable syncthing.service
Created symlink '/home/setevoy/.config/systemd/user/default.target.wants/syncthing.service' → '/usr/lib/systemd/user/syncthing.service'.
[setevoy@setevoy-work ~]  $ systemctl --user start syncthing.service
[setevoy@setevoy-work ~]  $ systemctl --user status syncthing.service
● syncthing.service - Syncthing - Open Source Continuous File Synchronization
     Loaded: loaded (/usr/lib/systemd/user/syncthing.service; enabled; preset: enabled)
     Active: active (running) since Fri 2026-02-13 19:41:17 EET; 3s ago
...

Можна додати запуск сервісу без user login – корисно для ребутів, див. loginctl:

[setevoy@setevoy-work ~]  $ loginctl enable-linger setevoy

І додати базову перевірку в Uptime Kuma (про неї теж скоріш за все буду писати ще окремо, в мене Kuma крутиться на окремому хості для “міні-моніторинга” на Raspberry PI):

Є у Syncthing і Prometheus метрики, див. Prometheus-Style Metrics – можна буде додати до VictoriaMetrics і створити Grafana dashboard та алерти.

І варто налаштувати бекапи для файлів Syncthing:

[setevoy@setevoy-work ~]  $ ll ~/.local/state/syncthing
total 40
-rw-r--r-- 1 setevoy setevoy   623 Feb 13 19:14 cert.pem
-rw------- 1 setevoy setevoy 11236 Feb 13 20:15 config.xml
...
-rw------- 1 setevoy setevoy   119 Feb 13 19:14 key.pem
...

Далі почитати і поробити Configuration Tuning, налаштувати Firewall Setup, і уважно перечитати Security Principles.

Наостанок – як Syncthing виглядає на телефоні з Syncthing-Fork:

І клієнт телефона в дашборді на FreeBSD:

Готово.

Loading

Raspberry Pi: перший досвід і установка Raspberry Pi OS Lite
0 (0)

12 Лютого 2026

Для тих, хто не слідкує за апдейтами в Telegram-каналі rtfmcoua або просто перший раз зайшов на мій блог – нагадаю, що останні пару місяці збираю такий собі “self-hosted home stack”, в якому вже є пара MikroTik і ThinkCentre з FreeBSD.

На ThinkCentre / FreeBSD в мене NAS на ZFS mirror pool (див. FreeBSD: Home NAS, part 1), і “центральний моніторинг” з VictoriaMetrics + Grafana (див. FreeBSD: Home NAS, part 10 – моніторинг з VictoriaMetrics).

До цього всього щастя вирішив ще додати окрему машинку під mini-monitoring, плюс там захостити системи типу Glance (див. Glance: налаштування self-hosted home page для браузера), бо ThnikCentre під час довгих блекаутів виключаю (хоча там споживання всього близько 20 Вт-год).

Ну і… Колись пробував Arduino – класно штука, але далі “Hello, World” діло не пішло (принаймні поки що), да і якісь системи типу Uptime Kuma на Arduino не захостиш.

Давно хотів спробувати погратись з Raspberry Pi, тільки раніше не міг придумати “а нахуа?” – і ось, нарешті, з’явилась відповідь на це велике питання.

Вибір Raspberry Pi

Чесно – я особо не вибирав 🙂

Точніше, вибирав, бо “вау, кросівєнько!” – десь випадково побачив Raspberry Pi Compute Module 4 PoE Mini-Computer, уявив, як він класно став би в мою серверну шафу – і вирішив взяти його.

Виглядає він ось так:

Є більш нові Compute Module версії 5 – але для моїх цілей, до того ж для першого досвіду, 4 версії вистачить з головою.

Отже, маємо:

Купував в магазині https://minicomp.com.ua – не реклама, але магазин наче нормальний, відправили швидко, підтримка по телефону/Telegram працює, нарікань нуль.

Єдине, що ще окремо довелось купувати кріплення.

Установка операційної системи

Трохи сексу 🙂

Бо перший раз, і це не “вставити USB-флешку з готовим образом”.

Спочатку хотів встановлювати Debian, і, в принципі, вдалося, але…

Я не зміг залогінитись в систему :facepalm:

Тому просто поставив Raspberry Pi OS Lite, і там все пройшло (точніше – увійшло 🙂 ) без проблем.

Втім, так як перший досвід – то збережу тут процес і для Debian теж.

Установка Raspberry Pi Debian

Качаємо на raspi.debian.net:

[setevoy@setevoy-work ~] $ ls ~/Downloads/Rasp/
debian-13-raspi-arm64-daily.tar.xz

Розпаковуваємо (хоча, як виявилось, можна і без цього – див. простіший варіант далі в Установка Raspberry Pi OS Lite):

[setevoy@setevoy-work ~] $ cd ~/Downloads/Rasp/
[setevoy@setevoy-work ~] $ tar xfp debian-13-raspi-arm64-daily.tar.xz 

В архіві лежить disk.raw – це повний образ диска з вже готовим GPT/MBR та boot partition:

[setevoy@setevoy-work ~] $ fdisk -l ~/Downloads/Rasp/disk.raw 
Disk /home/setevoy/Downloads/Rasp/disk.raw: 3 GiB, 3221225472 bytes, 6291456 sectors
...
Disklabel type: gpt
Disk identifier: 580A523C-E6C1-4021-8A56-D664D3C75FA2

Device                                    Start     End Sectors  Size Type
/home/setevoy/Downloads/Rasp/disk.raw1  1048576 6289407 5240832  2.5G Linux root (ARM-64)
/home/setevoy/Downloads/Rasp/disk.raw15    2048 1048575 1046528  511M EFI System

Partition table entries are not in disk order.

Підключення USB до ноутбука

Отуто теж трохи витратив часу, бо дуже незвична і ніфіга не очевидна схема перемикання на завантаження по USB.

Навіть якась ностальгія по часам, коли на HDD перемикав джампери Primary/Slave.

Картинка для тих, хто не бачив це наживо

На моємо CM4 піни для включення завантаження з USB знайшлись отут:

Зайвого джамперу не було, але можна взяти з FAN/VDD:

Перемикаємо джампер, підключаємо звичайним USB-кабелем до ноутбука, перевіряємо девайси – має з’явитись Broadcom:

[setevoy@setevoy-work ~] $ lsusb | grep Broa
Bus 003 Device 024: ID 0a5c:2711 Broadcom Corp. BCM2711 Boot

Встановлюємо rpiusbboot – утиліта підключиться до Raspberry Pi Compute Module та змонтує її eMMC (embedded MultiMediaCard) диск до ноутбука як звичайну флешку:

[setevoy@setevoy-work ~] $ yay -S rpiusbboot

Запускаємо:

[setevoy@setevoy-work ~] $ sudo rpiusbboot 
RPIBOOT: build-date Feb 12 2026 version 20221215~105525 b41ab04a
Waiting for BCM2835/6/7/2711...
Loading embedded: bootcode4.bin
Sending bootcode.bin
Successful read 4 bytes 
Waiting for BCM2835/6/7/2711...
Loading embedded: bootcode4.bin
Second stage boot server
Cannot open file config.txt
Cannot open file pieeprom.sig
Loading embedded: start4.elf
File read: start4.elf
Cannot open file fixup4.dat
Second stage boot server done

Тепер маємо новий диск в системі:

[setevoy@setevoy-work ~] $ lsblk 
NAME          MAJ:MIN RM   SIZE RO TYPE  MOUNTPOINTS
sda             8:0    1  29.1G  0 disk  

Копіюємо образ, який скачали вище:

[setevoy@setevoy-work ~] $ sudo dd if=~/Downloads/Rasp/disk.raw of=/dev/sda bs=4M status=progress conv=fsync

По завершенню – відключаємо живлення Малинки, повертаємо джампер назад до FAN/VDD, і завантажуємось у звичайному режимі.

Але… Як писав вище – я не не зміг залогінитись.

Є такий gist з дефолтними логінами:паролями – жоден не підійшов.

В документації Debian говориться, що root просто без пароля – але не пускало.

Тому я забив на “чистий” Debian, і просто взяв Raspberry Pi OS Lite, тим більш він все одно Debian-based.

І, мабуть, це для Малинки навіть краще.

До того ж – вперше подивився, як робити переустановку системи на eMMC.

Установка Raspberry Pi OS Lite

Знов переключаємо джампер, підключаємо USB до ноутбука, ще раз запускаємо rpiusbboot.

Видаляємо все з диска на Raspberry (УВАЖНО перевіряємо девайс!):

[setevoy@setevoy-work ~] $ sudo wipefs -a /dev/sda
/dev/sda: 8 bytes were erased at offset 0x00000200 (gpt): 45 46 49 20 50 41 52 54
/dev/sda: 2 bytes were erased at offset 0x000001fe (PMBR): 55 aa
/dev/sda: calling ioctl to re-read partition table: Success

Качаємо образ з сайту Raspberry, отримуємо архів 2025-12-04-raspios-trixie-arm64-lite.img.xz.

Тепер робимо той самий dd, але вже просто передаємо до нього образ через xzcat і pipe:

[setevoy@setevoy-work ~] $ xzcat ~/Downloads/ISO/2025-12-04-raspios-trixie-arm64-lite.img.xz | sudo dd of=/dev/sda bs=4M status=progress conv=fsync

Повертаємо джампер, завантажуємось, і – ура!

Створюємо юзера, логінимось – все працює.

Включення SSH

Suprize – але systemctl start sshd тут не варіант 🙂

Хоча systemd в системі є.

Запускаємо raspi-config:

setevoy@raspberrypi:~ $ sudo raspi-config

Переходимо в Interface Options:

Вибираємо і вмикаємо SSH:

Підключаємось:

[setevoy@setevoy-work ~] $ ssh 192.168.0.61
...
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
setevoy@raspberrypi:~ $ 

Запускаємо апгрейд системи:

setevoy@raspberrypi:~ $ sudo apt update && sudo apt full-upgrade -y

Ну і вже налаштовуємо всякі hostname, timezone і решту потрібних параметрів системи.

Static IP на MikroTik

Трохи про мережу, хоча тут все доволі стандартно – NetworkManager та nmcli.

В мене MikroTik, і зараз Raspberry Pi має динамічний IP з пулу DHCP сервера:

Додаємо статичний lease на MAC-адресу Малинки:

/ip dhcp-server lease add address=192.168.0.5 mac-address=2C:CF:67:59:14:9D comment=setevoy-pi

Видаляємо старий:

/ip dhcp-server lease remove 5

Перевіряємо підключення на Raspberry:

setevoy@raspberrypi:~ $ nmcli device status
DEVICE  TYPE      STATE                   CONNECTION         
eth0    ethernet  connected               Wired connection 1 
lo      loopback  connected (externally)  lo

І виконуємо або sudo nmcli device reapply eth0, або sudo nmcli device disconnect eth0 && sudo nmcli device connect eth0, або просто reboot – і тепер можемо підключатись за новою адресою:

[setevoy@setevoy-work ~] $ ssh 192.168.0.5
...
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Thu Feb 12 11:54:41 2026 from 192.168.0.3
setevoy@raspberrypi:~ $ 

Можна відразу на MikroTik додати новий DNS record:

/ip dns static add name=pi.setevoy address=192.168.0.5 ttl=1d

Перевіряємо:

/ip dns static print where name=pi.setevoy

І перевіряємо з робочого ноутбука:

[setevoy@setevoy-work ~]  $ dig pi.setevoy +short
192.168.0.5

В принципі, на цьому все.

Встановлюємо Docker, Docker Compose, запускаємо всякі Glance і Online Kuma.

Далі Online Kuma можна налаштувати на відправку альортів до, наприклад, ntfy.sh – і мати моніторинг свого моніторингу.

А стоїть моя Малинка ось тут:

Про збірку шафи буду писати окремо в заключній частині по налаштуванню Home NAS.

Loading

Glance: налаштування self-hosted home page для браузера
0 (0)

11 Лютого 2026

Є така прикольна штука, як self-hosted home pages.

Колись побачив їх десь на Reddit, зберіг в закладки, і ось тепер, як в мене є всяка self-hosted тема з NAS (див. FreeBSD: Home NAS, part 1), Grafana і іншими корисними в роботі і побуті речами – то подумав, що було б непогано зробити і собі таку дашборду.

З тих, що в мене збережені, і вони, здається, найбільш популярні – це gethomepage/homepage та glanceapp/glance.

Але Homepage якась більш важка, з купою компонентів – фронтенд, бекенд, якісь рендери, а Glance – простіша, і при цьому, в принципі, має все, що я хотів побачити – хоча місцями це робиться через костилі 🙂

Власне – налаштування Glance.

Робити поки буду локально на ноутбуці з Docker Compose, пізніше перенесу конфіг або на FreeBSD/NAS чи на Raspberry PI з Debian.

Налаштування Glance

Див. документацію Pages & Columns.

Основні компоненти – це Pages, Columns та Widgets:

  • pages: власне, сторінки – можна мати кілька вкладок
  • columns: кожна page розбивається на кілька колонок
  • widgets: і в кожній колонці є набір віджетів

Віджети теж можна групувати і робити вкладки – далі побачимо на прикладі Reddit.

Для погратись – легко запускається з Docker, див. Installation.

Окремих тем нема – все налаштовується через стилі, наприклад:

...
theme:
  background-color: 225 14 15
  primary-color: 157 47 65
  contrast-multiplier: 1.1
...

Але є готові стилі – див. Themes.

Окрім дефолтних віджетів є community widgets, наприклад я там собі взяв NextDNS Stats.

Мої Pages та Widgets

Коротко – приклад того, як це все можна налаштувати.

Так як в мене це все хоститься локально і кудись в GitHub я конфіг зберігати не буду – то всякі токени записав прямо в конфіг, але взагалі для них можна використати змінні оточення – див. Other ways of providing tokens/passwords/secrets.

Перша сторінка – Home з трьома колонками:

Clock, Weather, Calendar

Час, погода і календар:

...
pages:
  - name: Home
    columns:
      # ---------- LEFT ----------
      - size: small
        widgets:
          - type: clock
            hour-format: 24h
            timezones:
              - timezone: Asia/Bangkok
                label: Chiang Mai
              - timezone: America/New_York
                label: New York

          - type: weather
            location: Kyiv
            units: metric

          - type: calendar
...

В clock віджеті додав ще дві зони – бо в США у нас частина розробників, а в Chiang Mai – розробник один, але часто з ним спілкуюсь і постійно згадую яка в нього зараз година.

Центрально колонка – з типом full, і для кожної page треба мати як мінімум одну колонку з full.

Search

Тут віджет search:

...
      # ---------- CENTER ----------
      - size: full
        widgets:
          - type: search
            search-engine: duckduckgo
            new-tab: true
            autofocus: true
            bangs:
              - title: YouTube
                shortcut: "!yt"
                url: https://www.youtube.com/results?search_query={QUERY}
...

Bookmarks

Bookmarks – основні для швидкого доступу, розбиті по категоріям:

...
          - type: bookmarks
            groups:
              - title: Local
                color: 120 70 50
                target: _self
                links:
                  - title: MikroTik Gateway
                    url: http://192.168.0.1/webfig/#IP:DHCP_Server.Leases
...
              - title: AI
                color: 260 50 70
                target: _self
                links:
                  - title: ChatGPT
                    url: https://chat.openai.com/chat
...
              - title: RTFM
                color: 200 50 50
                target: _self
                links:
                  - title: RTFM Main
                    url: https://rtfm.co.ua/
...

Для вибору color можна скористатись colorpicker.dev: перша цифра – сам колір, друга – saturation (насиченість), третя – lightness (яскравість).

Group для Reddit

Далі приклад групування з Group – зробив собі окремі вкладки для різних сабредітів, але двома окремими групами – умовний “Reddit Ukraine” і “Reddit IT”:

...
          - type: group
            widgets:
              - type: reddit
                subreddit: finance_ukr
                show-thumbnails: true
              - type: reddit
                subreddit: durka_ukr
                show-thumbnails: true

          - type: group
            widgets:
              - type: reddit
                subreddit: aws
                show-thumbnails: false
              - type: reddit
                subreddit: archlinux
                show-thumbnails: false
...

Split Column для новин

Наступний віджет – Split Column, де більше стисло новини і якісь цікаві матеріали:

...
          - type: split-column
            max-columns: 2
            widgets:
              - type: lobsters
                sort-by: hot
                limit: 15
                collapse-after: 5
              - type: rss
                title: News
                style: vertical-list
                feeds:
                  - url: https://aws.amazon.com/blogs/aws/feed/
                    title: AWS Blogs
                  - url: https://skeletor.org.ua/?feed=rss2
                    title: Skeletor
...

Ну і більш цікава частина – справа, інформація по статусу сервісів.

GitHub Releases

Віджет releases – останні релізи в GitHub:

...
      - size: small
        widgets:
          - type: releases
            token: github_pat_11A***1jF
            repositories:
              - VictoriaMetrics/VictoriaMetrics
              - pdf/zfs_exporter
              - tess1o/go-ecoflow-exporter
...

Мені за zfs_exporter і go-ecoflow-exporter є сенс слідкувати, бо вони в мене деплояться вручну, див. FreeBSD: Home NAS, part 11 – extended моніторинг з додатковими експортерами.

Хоча, звісно, ніхто не відміняє можливість просто підписатись на апдейти в самому GitHub 🙂

Custom API для NextDNS

Приклад кастомного віджета custom-api – інформація по моєму NextDNS:

...
          - type: custom-api
            title: NextDNS Analytics
            title-url: https://api.nextdns.io/profiles/***/analytics/status
            cache: 1h
            url: https://api.nextdns.io/profiles/***/analytics/status
            headers:
              X-Api-Key: 3f8***457
            template: |
              {{ if eq .Response.StatusCode 200 }}
                <div style="display: flex; justify-content: space-between;">
                  {{ $total := 0.0 }}
                  {{ $blocked := 0.0 }}
                  {{ range .JSON.Array "data" }}
                    {{ $total = add $total (.Int "queries" | toFloat) }}
                    {{ if eq (.String "status") "blocked" }}
                      {{ $blocked = add $blocked (.Int "queries" | toFloat) }}
              ...
                <div style="text-align: center; color: var(--color-negative);">
                  Error: {{ .Response.StatusCode }} - {{ .Response.Status }}
                </div>
              {{ end }}
...

І далі ще буде приклад з власним міні-API сервісом.

Monitor – статуси HTTP-сервісів

Віджет monitor – прикольна штука для відображення статусу сервісів, робить простий GET-запит на вказаний URL, ну і працює (принаймні поки що) тільки з HTTP/S:

...
          - type: monitor
            cache: 1m
            title: Services
            sites:
              - title: RTFM
                url: https://rtfm.co.ua
                icon: /assets/rtfm-icon-2.png
              - title: MikroTik RB4011
                url: http://192.168.0.1
                icon: sh:mikrotik-light
              - title: Grafana
                url: http://nas.setevoy:3000
                icon: sh:grafana
              - title: Jellyfin
                url: http://nas.setevoy:8096
                icon: sh:jellyfin
...

На MikroTik RB4011 (див. MikroTik: перше знайомство та Getting Started), наприклад, з локальної мережі доступний web-інтерфейс, тому через нього можна перевіряти статус.

Аби підключити іконки – шукаємо дефолтні на, наприклад, selfh.st або dashboardicons.com.

Або можна задати кастомні іконки – додаємо файли в каталог /assets і включаємо його в конфігу Glance:

...
server:
  host: 0.0.0.0
  port: 8080
  assets-path: /app/assets
...

Server Stats – CPU, RAM, Disk

Цікава штука server-stats, хоча вона ще в beta:

...
          - type: server-stats
            servers:
              - type: local
                name: setevoy-work
...

Потребує додаткового сервісу – glanceapp/agent, я його поки що просто додав до docker-compose.yaml:

services:
  glance:
    container_name: glance
    image: glanceapp/glance
    restart: unless-stopped
    volumes:
      - ./config:/app/config
      - ./assets:/app/assets
      - /etc/localtime:/etc/localtime:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
    ports:
      - 8080:8080
    env_file: .env

  glance-agent:
    container_name: glance-agent
    image: glanceapp/agent
    ports:
      - 27973:27973

Docker containers

І останній тут – запущені Docker-контейнери, бо іноді забуваю, що щось запущено:

...
          - type: docker-containers
            hide-by-default: false
            running-only: true
...

(про Uptime Kuma теж напишу)

Вся сторінка Home вийшла поки що такою:

Custom API для FreeBSD/NAS

Ну і з цікавих рішень: хочеться з FreeBSD виводити якусь цікаву інформацію.

Active SSH connections

Спершу приклад “активні SSH-юзери” – перевіряємо хто підключений і звідки, виводимо тільки унікальні адреси:

root@setevoy-nas:~ # who | awk '$6 ~ /^\(/ {print $1, $6}' | sort -u
setevoy (10.100.0.3)
setevoy (192.168.0.3)

10.100.0.3 – це я підключений з домашнього ноута через VPN, а 192.168.0.3 – ноутбук в локальній мережі в офісі.

Тепер робимо простий python-скрипт, який буде нашим API-ендпоінтом.

Пишемо файл /usr/local/bin/glance_api.py:

#!/usr/bin/env python3.11
# minimal API server for Glance NAS page
# exposes active SSH sessions as JSON

from http.server import BaseHTTPRequestHandler, HTTPServer
import subprocess
import json

HOST = "192.168.0.2"
PORT = 9001


class Handler(BaseHTTPRequestHandler):

    def do_GET(self):

        if self.path == "/ssh":

            # run who and extract unique SSH sessions
            output = subprocess.check_output(
                "who | awk '$6 ~ /^\\(/ {print $1, $6}' | sort -u",
                shell=True
            ).decode().strip().splitlines()

            sessions = []

            for line in output:
                if line:
                    parts = line.split()
                    sessions.append({
                        "user": parts[0],
                        "ip": parts[1].strip("()")
                    })

            response = {
                "count": len(sessions),
                "sessions": sessions
            }

            self.send_response(200)
            self.send_header("Content-Type", "application/json")
            self.end_headers()
            self.wfile.write(json.dumps(response).encode())

        else:
            self.send_response(404)
            self.end_headers()


if __name__ == "__main__":
    server = HTTPServer((HOST, PORT), Handler)
    server.serve_forever()

Запускаємо його:

root@setevoy-nas:~ # chmod +x /usr/local/bin/glance_api.py

root@setevoy-nas:~ # /usr/local/bin/glance_api.py

Перевіряємо локально:

root@setevoy-nas:~ # curl 192.168.0.2:9001/ssh
{"count": 2, "sessions": [{"user": "setevoy", "ip": "10.100.0.3"}, {"user": "setevoy", "ip": "192.168.0.3"}]}

Тепер додаємо в Glance новий віджет з типом custom-api:

...
      # ---------- RIGHT ----------
      - size: small
        widgets:
          - type: custom-api
            title: Active SSH
            url: http://nas.setevoy:9001/ssh
            template: |
              {{ $count := .JSON.Int "count" }}

              {{ if eq $count 0 }}
                No active SSH sessions
              {{ else }}
                <ul class="list list-gap-10">
                {{ range .JSON.Array "sessions" }}
                  <li>
                    <span class="color-highlight">{{ .String "user" }}</span>
                    <span class="color-muted">({{ .String "ip" }})</span>
                  </li>
                {{ end }}
                </ul>
              {{ end }}
...

І результат:

Uptime, CPU, ZFS pool status

Додатково можна вивести ще інформацію – uptime, CPU load, etc.

Додаємо в скрипт ще один ендпоінт /status, тепер весь скрипт такий:

#!/usr/bin/env python3.11
# minimal API server for Glance NAS page
# exposes active SSH sessions as JSON

from http.server import BaseHTTPRequestHandler, HTTPServer
import subprocess
import json

HOST = "192.168.0.2"
PORT = 9001


class Handler(BaseHTTPRequestHandler):

    def do_GET(self):

        if self.path == "/ssh":

            # run who and extract unique SSH sessions
            output = subprocess.check_output(
                "who | awk '$6 ~ /^\\(/ {print $1, $6}' | sort -u",
                shell=True
            ).decode().strip().splitlines()

            sessions = []

            for line in output:
                if line:
                    parts = line.split()
                    sessions.append({
                        "user": parts[0],
                        "ip": parts[1].strip("()")
                    })

            response = {
                "count": len(sessions),
                "sessions": sessions
            }

            self.send_response(200)
            self.send_header("Content-Type", "application/json")
            self.end_headers()
            self.wfile.write(json.dumps(response).encode())

        elif self.path == "/status":

            # get uptime and load averages
            uptime_raw = subprocess.check_output(
                "uptime",
                shell=True
            ).decode().strip()

            # extract load averages
            load_part = uptime_raw.split("load averages:")[1].strip()
            load_values = [x.strip() for x in load_part.split(",")]

            # extract uptime text
            uptime_text = uptime_raw.split(" up ", 1)[1].split(", load averages:", 1)[0].rsplit(",", 1)[0].strip()

            # get zpool info
            zpool_raw = subprocess.check_output(
                "zpool list -H -o name,health,size,alloc,free,capacity",
                shell=True
            ).decode().strip().split()

            zpool = {
                "name": zpool_raw[0],
                "health": zpool_raw[1],
                "size": zpool_raw[2],
                "alloc": zpool_raw[3],
                "free": zpool_raw[4],
                "capacity": zpool_raw[5],
            }

            # get child datasets only
            datasets_raw = subprocess.check_output(
                "zfs list -H -o name,used,avail -r nas | tail -n +2",
                shell=True
            ).decode().strip().splitlines()

            datasets = []

            for line in datasets_raw:
                parts = line.split()
                datasets.append({
                    "name": parts[0],
                    "used": parts[1],
                    "avail": parts[2],
                })

            response = {
                "load": {
                    "1m": load_values[0],
                    "5m": load_values[1],
                    "15m": load_values[2],
                },
                "uptime": uptime_text,
                "zpool": zpool,
                "datasets": datasets
            }

            self.send_response(200)
            self.send_header("Content-Type", "application/json")
            self.end_headers()
            self.wfile.write(json.dumps(response).encode())

        else:
            self.send_response(404)
            self.end_headers()


if __name__ == "__main__":
    server = HTTPServer((HOST, PORT), Handler)
    server.serve_forever()

Відразу скрипт для rc.d/usr/local/etc/rc.d/glance_api:

#!/bin/sh

# PROVIDE: glance_api
# REQUIRE: NETWORKING
# KEYWORD: shutdown

. /etc/rc.subr

name="glance_api"
rcvar="glance_api_enable"

command="/usr/local/bin/python3.11"
command_args="/usr/local/bin/glance_api.py"
pidfile="/var/run/${name}.pid"

start_cmd="${name}_start"
stop_cmd="${name}_stop"

glance_api_start()
{
    echo "Starting glance_api..."
    daemon -p ${pidfile} ${command} ${command_args}
}

glance_api_stop()
{
    echo "Stopping glance_api..."
    if [ -f ${pidfile} ]; then
        kill `cat ${pidfile}`
        rm -f ${pidfile}
    fi
}

load_rc_config $name
: ${glance_api_enable:=no}

run_rc_command "$1"

Запускаємо:

root@setevoy-nas:~ # chmod +x /usr/local/etc/rc.d/glance_api
root@setevoy-nas:~ # sysrc glance_api_enable=YES
glance_api_enable:  -> YES

root@setevoy-nas:~ # service glance_api start
Starting glance_api...

В Glance додаємо ще один блок:

...
          - type: custom-api
            title: NAS Status
            url: http://nas.setevoy:9001/status
            template: |
              <div>
                <div><b>Uptime:</b> {{ .JSON.String "uptime" }}</div>

                <div style="margin-top:10px;">
                  <b>Load:</b>
                  {{ .JSON.String "load.1m" }} /
                  {{ .JSON.String "load.5m" }} /
                  {{ .JSON.String "load.15m" }}
                </div>

                <div style="margin-top:10px;">
                  <b>ZFS:</b>
                  <span class="color-positive">
                    {{ .JSON.String "zpool.health" }}
                  </span>
                  ({{ .JSON.String "zpool.capacity" }} used)
                </div>

                <div style="margin-top:10px;">
                  <b>Datasets:</b>
                  <ul class="list list-gap-5">
                  {{ range .JSON.Array "datasets" }}
                    <li>
                      {{ .String "name" }} —
                      {{ .String "used" }} used
                    </li>
                  {{ end }}
                  </ul>
                </div>
              </div>
...

І тепер NAS Page виглядає так:

Можна було б в glance_api.py додати і виконання дій через POST – але я не став заморачуватись, та і виконувати команди з дашборди – це вже трохи занадто.

Останній штрих – Chrome/Firefox extension Custom New Tab URL:

Ну а потім, яе Glance переїде на окремий хост – то замінити URL.

Єдиний мінус в Glance – що він не вміє в auto refresh 🙁 Але можна зробити теж через екстешени браузеру.

Loading