настройка self-hosted, часть 5.1: vps-as-edge. настраиваем домашний сервер

Напоминаю, что мы идем к тому, чтобы получить безопасную, легко воспроизводимую и покрытую мониторингом self-hosted инфраструктуру для любых наших сервисов.

В нашей схеме это будет работать так. VPS:

Бэкенд-сервер - недоступен извне, подключен только по headscale (ну, можно еще оставить открытым 22 порт со всеми мерами безопасности, если хотите) и хостит нужные вам сервисы.

В предыдущих частях (1, 2, 3, 4) мы делали все на одном VPS - и сервисы, и наблюдаемость. В пятой части мы готовим бэкенд-ноду (эта часть, 5.1), а VPS становится edge-нодой (реверс-прокси и координатор tailnet-сети, будет в части 5.2). Большинство шагов опциональны и, возможно, вам не потребуются.

Я исхожу из того, что линукс у вас уже установлен. В статье все примеры основаны на Debian 13 trixie. В качестве железа - ThinkPad X270, i5-7200U, 16 GB RAM, SSD 256 GB, 2 батареи.

Содержание:


Главное


Чеклист

Железо и BIOS

Пост-инсталл

Сеть

Долговечность диска

Питание и режим работы

Безопасность платформы

Прочее

Docker


1. Диагностика железа

Этот шаг, как и большинство остальных, опционален. Если с железом у вас все ок или его состояние для вас не критично - проматывайте до следующего раздела.

Первое, что нужно выяснить перед использованием - состояние железа. Грузимся с live-образа.

1.1. SMART NVMe

sudo apt update && sudo apt install -y nvme-cli smartmontools
sudo nvme smart-log /dev/nvme0n1

smartctl (smartmontools) дает более подробный отчет, чем nvme smart-log:

sudo smartctl -a /dev/nvme0n1          # полный отчет
sudo smartctl -H /dev/nvme0n1          # health check: PASSED или FAILED
sudo smartctl -t short /dev/nvme0n1    # короткий self-test (2 минуты)
sudo smartctl -l selftest /dev/nvme0n1 # результаты прошлых self-test'ов

Если интересно, дока по утилите nvme (см. трактовку вывода в таблице на странице 206): https://nvmexpress.org/wp-content/uploads/NVM-Express-Base-Specification-Revision-2.2-2025.03.11-Ratified.pdf

На что смотреть сейчас:

В целом, метрики важны в динамике, поэтому дальше мы и настроим мониторинг.

Пример реального вывода с X270 (Toshiba 256 GB, 12220 часов работы):

Smart Log for NVME device:nvme0 namespace-id:ffffffff
critical_warning			: 0
temperature				: 122 °F (323 K)
available_spare				: 100%
available_spare_threshold		: 10%
percentage_used				: 41%
endurance group critical warning summary: 0
Data Units Read				: 51561369 (26.40 TB)
Data Units Written			: 50877300 (26.05 TB)
host_read_commands			: 1091082675
host_write_commands			: 798348055
controller_busy_time			: 10074
power_cycles				: 3335
power_on_hours				: 12220
unsafe_shutdowns			: 312
media_errors				: 0
num_err_log_entries			: 0
Warning Temperature Time		: 8
Critical Composite Temperature Time	: 0
Temperature Sensor 1			: 122 °F (323 K)
Thermal Management T1 Trans Count	: 0
Thermal Management T2 Trans Count	: 0
Thermal Management T1 Total Time	: 0
Thermal Management T2 Total Time	: 0

1.2. Memtest86+

Это опенсорс-утилита для теста состояния RAM. Идем на memtest.org, качаем “Linux ISO w/ GRUB (64 bits)” - скачается zip-архивом.

Распаковываем, пишем образ на флешку, ребутаемся:

unzip mt86plus_*_64.grub.iso.zip       # в архиве лежит .iso
lsblk                                  # ищем флешку, скорее всего вида sdX
sudo dd if=mt86plus_*_64.grub.iso of=/dev/sdX bs=4M status=progress oflag=sync

Можно запустить один полный проход (будет идти минут 30-50).

1.3. Состояние батарей

Ставим upower, потом:

upower -e    # будет список батарей
upower -i /org/freedesktop/UPower/devices/battery_BAT0   # информация по конкретной батарее

Из вывода важны:

capacity:             85.1%     - здоровье батареи
charge-cycles:        453       - циклы перезарядки

1.4. Стресс-тест CPU

sudo apt install -y lm-sensors stress-ng
sudo sensors-detect --auto
sudo modprobe coretemp
echo 'coretemp' | sudo tee -a /etc/modules
sensors

В выводе sensors смотрим температуру CPU не под нагрузкой:

coretemp-isa-0000
Adapter: ISA adapter
Package id 0:  +35.0°C  (high = +100.0°C, crit = +100.0°C)

Можно запустить стресс-тест на 5 минут. В одной сессии запуск (--cpu 0 - использовать все доступные CPU):

stress-ng --cpu 0 --timeout 300

Во второй - наблюдение:

watch -n 2 sensors

2. Обновление BIOS

После диагностики с live-образа ставим ОС на диск, дальше BIOS обновляем уже из установленной системы.

Текущую версию BIOS до обновления можно узнать так:

cat /sys/class/dmi/id/bios_version
cat /sys/class/dmi/id/bios_date

После установки ставим fwupd - утилиту, которая качает прошивки из репозитория LVFS (Linux Vendor Firmware Service). Перед запуском проверьте, что ноут заряжается от сети, и не выключайте его на время обновления, иначе можно окирпичить.

sudo apt update
sudo apt install -y fwupd
sudo fwupdmgr refresh --force
sudo fwupdmgr get-devices
sudo fwupdmgr get-updates
sudo fwupdmgr update    # обновление прошивок

3. Шаги после установки

Узнаем IP и коннектимся по ssh:

ip a
ssh root@IP     # IP из предыдущего вывода

На свежем Debian вход под root по SSH-паролю обычно закрыт (PermitRootLogin prohibit-password - дефолт OpenSSH). Если при установке вы оставляли пароль root пустым, инсталлятор уже добавил первого пользователя в sudo - заходите под ним и шаг с установкой sudo ниже пропускаете. Если root-пароль задан - root доступен только по ключу или с физической консоли.

3.1. Sudo

Под root:

apt update && apt install -y sudo
usermod -aG sudo youruser

sudo apt upgrade -y    # заодно обновляемся
exit

Перезаходим в SSH под обычным юзером.

3.2. Hostname, timezone, locale

Делаем как в части 1, раздел 7, плюс генерируем локаль (на минимальном Debian ее обычно нет, прогоняем dpkg-reconfigure locales вручную; инсталлятор Ubuntu обычно делает это сам по выбранному при установке языку):

sudo hostnamectl set-hostname mio
sudo timedatectl set-timezone Europe/Moscow
sudo dpkg-reconfigure locales   # отметить en_US.UTF-8, остальное снять

3.3. SSH-ключи

Базовая настройка SSH - в части 1, раздел 2.

Не закрывайте текущую сессию - сначала откройте новую и убедитесь, что вход работает.

Доставка ключа:

ssh-keygen -t ed25519        # на клиенте, откуда подключаемся
ssh-copy-id -i ~/.ssh/id_ed25519.pub youruser@192.168.0.NNN   # переносим публичный ключ на сервер

4. Fail2ban: почему не нужен

В части 1, раздел 4 на VPS мы ставили fail2ban.

Домашний сервер находится за NAT, связан через tailnet, ssh только по ключам - публичного входа нет, fail2ban здесь не нужен. Если не доверяете локальной сети и хотите доп. защиту от перебора SSH с других устройств в той же подсети - имеет смысл включить.


5. Сеть

5.1. Ethernet или Wi-Fi

Для сервера предпочтительно проводное соединение (раздел 5.5). Но я выбрал Wi-Fi.

В Debian 13 без DE сеть управляется через ifupdown. Конфиг в /etc/network/interfaces.

5.2. Имена интерфейсов

ip link show

5.3. Выбор IP для статики

Смотрим на своем роутере DHCP-пул адресов. Выбираем за пределами этого пула, иначе может быть конфликт адресов.

Смотрим, какой интерфейс включен в локальной сети:

ip route show default                   # на самом сервере (Linux)
route -n get default | grep interface   # если смотрите с мака

Проверяем с другого устройства в той же сети (например, с мака), что адрес свободен. Сначала поставьте `arp-scan` (на маке `brew install arp-scan`, на linux-клиенте `sudo apt install -y arp-scan`) и подставьте интерфейс этого устройства:
```bash
sudo arp-scan --interface=en0 --localnet    # en0 - пример для мака; видим занятые адреса

5.4. Wi-Fi со статическим IP

sudo apt update
sudo apt install -y wpasupplicant resolvconf

resolvconf нужен отдельно: строка dns-nameservers в /etc/network/interfaces парсится не самим ifupdown, а пакетом resolvconf. Без него она просто игнорируется, /etc/resolv.conf остается пустым, DNS не работает.

Генерим конфиг с хешем пароля:

wpa_passphrase "Имя сети" "Пароль сети" | sudo tee /etc/wpa_supplicant/wpa_supplicant.conf
sudo chmod 600 /etc/wpa_supplicant/wpa_supplicant.conf

В сгенерированном блоке wpa_passphrase оставляет исходный пароль строкой-комментарием #psk="..." - удалите ее, чтобы пароль не лежал открытым. chmod 600 закрывает файл от чтения остальными. Пароль также остается в истории шелла (передан аргументом) - при желании почистите ее.

/etc/network/interfaces:

auto lo
iface lo inet loopback

auto wlp4s0
iface wlp4s0 inet static
    address 192.168.0.5/24
    gateway 192.168.0.1
    dns-nameservers 1.1.1.1 9.9.9.9
    wpa-conf /etc/wpa_supplicant/wpa_supplicant.conf

5.5. Ethernet со статическим IP (если у вас проводное соединение)

Аналогично, без wpa-conf:

auto enp0s31f6
iface enp0s31f6 inet static
    address 192.168.0.5/24
    gateway 192.168.0.1
    dns-nameservers 1.1.1.1 9.9.9.9

5.6. Применить

Рестартим сервис и коннектимся заново:

systemctl restart networking

Если у вас на клиенте уже есть конфиг для коннекта к этому серверу, не забудьте там обновить IP: .ssh/config.

5.7. Проверка

ip -4 addr show
cat /etc/resolv.conf    # должны быть nameserver-строки
ping -c 3 1.1.1.1
ping -c 3 deb.debian.org

5.8. Частые проблемы

Если при apt update получаете Temporary failure resolving, значит DNS не настроен. Смотрим конфиг cat /etc/resolv.conf - если пусто, не установлен resolvconf. Временно добавьте в resolv.conf запись nameserver 1.1.1.1 руками и поставьте resolvconf.

Если Wi-Fi не подключается: sudo journalctl -u networking -b. Возможно, опечатались в SSID или пароле в wpa_supplicant.conf.


6. TLP - управление зарядом батареи

TLP - это утилита для управления питанием на ноутбуках. Согласно их доке, если ноут большую часть времени подключен к сети, продлить жизнь батареи можно, держа заряд в диапазоне 40-50%:

If the laptop is plugged most of the time and rarely unplugged, maximizing battery lifetime at the cost of a greatly reduced runtime may be acceptable, with values like starting charge at 40% and stopping at 50%.

Ставим и включаем службу:

sudo apt install -y tlp
sudo systemctl enable --now tlp

6.1. Конфигурация

Нужные нам параметры: START/STOP_CHARGE_THRESH_BAT0/BAT1, дока: https://linrunner.de/tlp/settings/battery.html

sudo nano /etc/tlp.d/01-server.conf

Ставим пороги:

START_CHARGE_THRESH_BAT0=40
STOP_CHARGE_THRESH_BAT0=50
START_CHARGE_THRESH_BAT1=40
STOP_CHARGE_THRESH_BAT1=50

START_CHARGE_THRESH_BATX=40 / STOP_CHARGE_THRESH_BATX=50 - зарядка начинается, если уровень опустился ниже 40%, и останавливается на 50%.

Применяем конфиг:

sudo systemctl restart tlp

6.2. Проверка

cat /sys/class/power_supply/BAT0/charge_control_start_threshold
cat /sys/class/power_supply/BAT0/charge_control_end_threshold
cat /sys/class/power_supply/BAT1/charge_control_start_threshold
cat /sys/class/power_supply/BAT1/charge_control_end_threshold

Должно вернуть 40 и 50. Полная диагностика:

sudo tlp-stat -b

Главное что проверить в выводе:


7. Поддержание скорости SSD с помощью TRIM

Хорошая статья про TRIM

SSD не умеет перезаписывать блок напрямую - может только записать в пустой. TRIM сообщает SSD список пустых блоков, чтобы он их заблаговременно очистил.

7.1. fstrim.timer

Проверка, что TRIM включен:

systemctl is-enabled fstrim.timer        # ожидаем enabled
# если disabled - включить таймер и сразу прогнать один раз
sudo systemctl enable --now fstrim.timer
sudo systemctl start fstrim.service
sudo journalctl -u fstrim.service -n 20 --no-pager

Пример с моего X270:

fstrim[5406]: /boot/efi: 965.3 MiB (1012195328 bytes) trimmed on /dev/nvme0n1p1
fstrim[5406]: /: 219 GiB (235143614464 bytes) trimmed on /dev/nvme0n1p2
systemd[1]: Finished fstrim.service.

7.2. journald

В части 1, раздел 14 мы ставили лимит 500M. На домашнем сервере места больше:

sudo mkdir -p /etc/systemd/journald.conf.d
sudo nano /etc/systemd/journald.conf.d/00-server.conf
[Journal]
SystemMaxUse=1G
MaxRetentionSec=2weeks

Применить и подрезать накопившееся:

sudo systemctl restart systemd-journald
sudo journalctl --vacuum-size=1G

7.3. Swap и vm.swappiness

Swap-файл создается как в части 1, раздел 8.

Проверяем своп:

sudo swapon --show

Если пусто - делаем как в части 1: fallocate -l 4G /swapfile, chmod 600, mkswap, swapon, строчка в /etc/fstab.

Что добавляем для сервера на ноуте: vm.swappiness.

По умолчанию ядро выгружает страницы памяти в swap даже при свободной RAM (vm.swappiness=60). Для серверов рекомендуемое значение - 10-20 (использовать своп только в крайнем случае). Это уменьшает запись на SSD и снижает нагрузку на диск.

echo 'vm.swappiness=10' | sudo tee /etc/sysctl.d/99-server.conf
sudo sysctl --system     # будет vm.swappiness = 10

7.4. man-db.timer

man-db.timer - ежедневный таймер, обновляющий базу man-страниц. Мелочь, но я man не использую, поэтому отключаю.

sudo systemctl disable --now man-db.timer

8. Отключаем закрытую крышку и настраиваем сон

8.1. Игнорировать закрытую крышку

systemd-logind управляет реакцией на аппаратные события: закрытие крышки, кнопка питания, бездействие. По умолчанию закрытие крышки -> sleep. У нас сервер, поэтому отключаем.

sudo mkdir -p /etc/systemd/logind.conf.d
sudo nano /etc/systemd/logind.conf.d/00-server.conf
[Login]
HandleLidSwitch=ignore                  # закрыли крышку на батарее - не уходить в сон
HandleLidSwitchDocked=ignore            # то же, если в док-станции
HandleLidSwitchExternalPower=ignore     # то же, если работаем от сети
HandlePowerKey=poweroff                 # кнопка питания - ставим выключение
IdleAction=ignore                       # на длительный idle не реагировать
sudo systemctl restart systemd-logind

Проверка:

systemd-analyze cat-config systemd/logind.conf | grep -E 'HandleLid|IdleAction'

8.2. Маскировать sleep

Выше мы отключили сон на закрытие крышки. Но его могут вызвать и другие причины - например, другие сервисы или скрипты. Сейчас мы явно запретим сон.

За сон в линуксе отвечают 5 таргетов: sleep.target, suspend.target, hibernate.target (suspend-to-disk), hybrid-sleep.target, suspend-then-hibernate.target.

Подробнее: https://wiki.archlinux.org/title/Power_management/Suspend_and_hibernate

Чтобы заблокировать все пять путей, используем systemctl mask (mask создает symlink на /dev/null):

sudo systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target suspend-then-hibernate.target

Откатить, если когда-нибудь понадобится:

sudo systemctl unmask sleep.target suspend.target hibernate.target hybrid-sleep.target suspend-then-hibernate.target

8.3. Тепловой контроль (thermald)

Если у вас Intel, можно поставить thermald для предотвращения перегрева. Если CPU не греется и ноут нормально вентилируется, смысла нет. Я себе поставил, поэтому упомяну:

sudo apt install -y thermald
sudo systemctl enable --now thermald

Работает как демон: при перегреве ограничивает частоту и мощность CPU (троттлинг), удерживая температуру в норме. Вентилятором он не управляет - это делает встроенный контроллер.


9. Автообновления

Базовая настройка unattended-upgrades (security-патчи + чистка старых ядер) - в части 1, раздел 5.

Для домашнего сервера я добавил автоматическую перезагрузку в 4 утра.

needrestart - если не стоит, ставим. После обновления он показывает, какие запущенные процессы используют старые версии библиотек (полезно знать и до ребута):

sudo apt install -y needrestart

Ставим ту же опцию, что в части 1, но добавляем еще две:

sudo nano /etc/apt/apt.conf.d/52unattended-upgrades-local
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:00";

10. Брандмауэр

В части 1, раздел 3 на VPS мы UFW включали - и это правильно, потому что у VPS публичный IP, без firewall он открыт всем.

Домашний сервер за NAT, поэтому по умолчанию UFW не нужен. NAT не пропускает входящие, tailnet шифрован.

Аргумент “за” тот же, что и в разделе 4 с fail2ban - если не доверяете локальной сети. Минимальный UFW на этот случай:

sudo apt install -y ufw
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow from 192.168.0.0/24 to any port 22 proto tcp
sudo ufw enable
sudo ufw status
Status: active

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW       192.168.0.0/24

11. Прочее

11.1. Выключаем Bluetooth

Bluetooth не нужен для сервера:

sudo systemctl disable --now bluetooth.service
sudo apt purge -y bluez

После apt purge bluez в журнале при загрузке может появиться:

kernel: Bluetooth: hci0: Reading supported features failed (-16)

Исправляем, чтобы Bluetooth-модули не грузились вообще:

sudo tee /etc/modprobe.d/no-bluetooth.conf <<'EOF'
blacklist btusb
blacklist bluetooth
blacklist btintel
blacklist btbcm
EOF
sudo update-initramfs -u
sudo reboot

update-initramfs -u обновляет initramfs, чтобы blacklist применился.

Проверка после ребута:

lsmod | grep -iE 'bluetooth|btusb'   # пусто
sudo journalctl -p err -b            # строка про hci0 исчезла

12. Self-hosted сервисы

Правила для добавления описаны в части 2, раздел 13 - Памятка для добавления нового сервиса.

Отличие локального сервера от VPS - у него нет публичного IP, поэтому абсолютного требования “только loopback” тут нет. По умолчанию сервисы держим на 127.0.0.1, а то, что должно слушать шире (ssh, а в части 5.2 - экспортеры node_exporter/cAdvisor в host-режиме на 0.0.0.0 ради скрейпа с VPS), закрываем UFW (default deny incoming, пускаем только tailnet и SSH из локалки) - детали в части 5.2, раздел 12.2.

12.1. Docker + docker-compose

По части 1, раздел 9, но для Debian.

Документация docker: https://docs.docker.com/engine/install/debian/

# Add Docker's official GPG key:
sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/debian
Suites: $(. /etc/os-release && echo "$VERSION_CODENAME")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF

sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

sudo usermod -aG docker $USER
# logout/login или перезагрузка

Проверка:

docker run --rm hello-world
docker compose version

Что дальше

В следующей части (5.2) настраиваем VPS как edge-ноду: поднимаем headscale-координатор за Caddy, связываем домашний сервер с VPS по tailnet и собираем его метрики (node_exporter, cAdvisor) в Grafana с дашбордами и алертами на координатор.