Заметки о домашнем сервере

sparn про Linux, Docker и всякий self-hosting. Пишу, чтобы не забыть.

Обещал в прошлый раз — рассказываю, как я бэкаплю домашний сервер. Тема нудная ровно до того момента, как у тебя умирает диск с фотками за десять лет. После этого она становится самой интересной темой в твоей жизни, но уже поздно. Так что давайте до.

Правило 3-2-1

Если коротко, это здравый смысл, оформленный в цифры:

  • 3 копии данных;
  • на 2 разных носителях;
  • 1 копия — вне дома (offsite).

У меня это раскладывается так. Оригинал живёт на сервере (раз). Бэкап едет на внешний HDD, который воткнут в сервер по USB (два, другой носитель). И копия этого репозитория уезжает offsite — на удалённое хранилище через тот же restic (три, вне дома). Если дома случится потоп, пожар или просто кто-то унесёт коробку — данные переживут.

Почему restic

Перепробовал я разное, остановился на restic. Один бинарь, дедупликация (одинаковые куски хранятся один раз — экономит прилично), всё шифруется на клиенте перед записью. Последнее особенно важно для offsite-копии: на удалённом хранилище лежит зашифрованный мусор, и мне всё равно, кто его теоретически увидит.

Инициализируем репозиторий на внешнем диске один раз:

export RESTIC_REPOSITORY=/mnt/backup-hdd/restic
export RESTIC_PASSWORD_FILE=/root/.restic-pass
restic init

Пароль я держу в файле и отдельно записал его в надёжное место. Потеряешь пароль — потеряешь весь репозиторий, restic в этом смысле безжалостен.

Сам бэкап

Бэкаплю каталог /app целиком — там и compose-рецепты, и данные сервисов, и тот самый дамп БД Nextcloud, который я делаю перед этим.

restic backup /app \
  --exclude-caches \
  --tag daily

Чтобы репозиторий не пух до бесконечности, навожу порядок политикой хранения — оставляю разумную глубину истории и удаляю старьё:

restic forget \
  --keep-daily 7 \
  --keep-weekly 4 \
  --keep-monthly 6 \
  --prune

То есть последние 7 дней, 4 недели и 6 месяцев. Этого с головой, а --prune физически освобождает место от выкинутых снапшотов.

По расписанию

Руками такое делать нельзя — забудешь на второй неделе. У меня это systemd timer (cron тоже годится, дело вкуса), который раз в сутки ночью дёргает скрипт: сначала дамп БД, потом restic backup, потом forget --prune, а в конце пушит копию в offsite-репозиторий командой restic copy.

Грубый набросок строки в cron, если без systemd:

30 3 * * *  /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1

Лог обязательно, потому что молчаливый бэкап — это бэкап, про который ты не знаешь, что он сломался месяц назад.

Самое главное: тест восстановления

А вот теперь то, ради чего весь пост. Бэкап, который ты ни разу не восстанавливал, — это не бэкап, а гипотеза. Я регулярно (раз в пару месяцев) проверяю, что из репозитория реально достаётся файл.

Смотрим, какие снапшоты есть:

restic snapshots

И разворачиваем последний во временную папку:

restic restore latest --target /tmp/restore-test

Дальше глазами проверяю, что внутри лежит то, что должно, и файлы не битые. Один раз так выяснил, что забыл в бэкап положить .env-файлы (исключил лишним паттерном) — и спокойно поправил это в мирное время, а не в три часа ночи под крик «всё пропало».

Вот, собственно, и всё. Скучно, надёжно, проверяемо. Спите спокойно.

Главный раздражитель, который и подтолкнул меня поднять хранилище дома — это вечное «в облаке кончилось место, доплатите». Фоток с телефона за годы накопилось столько, что бесплатных гигов не хватает примерно никогда. И я подумал: у меня же стоит коробка с диском, давай-ка я буду хозяином своим фоткам.

Поставил Nextcloud, по своему же правилу — отдельным стеком в /app/nextcloud/. Связка стандартная: сам Nextcloud + база (взял MariaDB) + Redis для кэша. Всё в одном compose.yml, данные смонтированы в ./data, которая лежит на SATA-диске под хранилище.

Автозагрузка фото с телефона

Ради этого всё и затевалось. На телефон ставится мобильный клиент Nextcloud, в нём включается «Автозагрузка»: выбираешь папку с камерой, и каждое новое фото само улетает на сервер по wifi. Поснимал за день — вечером дома всё уже на диске, без ручного копирования через провод.

Я ещё включил, чтобы заливалось только по wifi и оставлял оригиналы на телефоне до подтверждения — мобильный трафик и нервы целее.

По объёму: у меня сейчас наехало около 180 ГБ фото и видео, и это вполне комфортно лежит на отдельном диске. Видео жрёт несоизмеримо больше фоток, так что если вы снимаете 4K — закладывайте место с запасом.

Грабли, на которые я наступил

Без них не обошлось, записываю, чтобы не повторять.

Права на каталог data. Классика. Контейнер Nextcloud работает под пользователем веб-сервера (uid 33, www-data), а каталог с данными после первого запуска принадлежал кому попало. Симптом — Nextcloud ругается, что не может писать. Лечится приведением владельца в порядок:

sudo chown -R 33:33 /app/nextcloud/data

После этого жалобы пропали. Если потом руками кидать файлы в data мимо Nextcloud — он их не увидит, пока не сделаешь files:scan, но это уже другая история.

Генерация превью. Когда залил всю гору фоток разом, веб-морда стала открываться мучительно долго: Nextcloud пытался на лету генерировать превьюшки для тысяч картинок. Решается тем, чтобы прогнать их пачкой заранее, через occ внутри контейнера:

docker compose exec -u www-data nextcloud \
  php occ preview:generate-all

Один раз помучился — дальше галерея листается бодро. Тяжёлые форматы (HEIC, видео) требуют, чтобы в образе были нужные библиотеки; в актуальном официальном образе с этим уже норм.

Приложение Memories. Поставил его поверх — это нормальная лента «фото по датам», к которой привыкаешь в облаках. Хорошая штука, но у неё свой индекс, который надо один раз прогнать (memories:index), иначе лента пустая и ты сидишь и не понимаешь, почему. Прогнал — заработало.

Бэкап базы

Отдельно проговорю, потому что про это любят забывать: файлы фоток — это половина дела, вторая половина живёт в базе. В ней метаданные, шаринги, кто что куда залил. Потеряешь БД — получишь кучу файлов без структуры.

Поэтому дамп базы у меня делается отдельно и регулярно:

docker compose exec -T db \
  mysqldump -u nextcloud -p"$DB_PASS" nextcloud \
  > /app/nextcloud/backup/db-$(date +%F).sql

А вот как этот дамп вместе с фотками уезжает в нормальный бэкап по правилу 3-2-1 — будет в следующем посте. Потому что хранилище без бэкапа — это не хранилище, а бомба замедленного действия.

Когда сервисов на коробке становится больше трёх, наступает момент истины: либо у тебя есть система, либо у тебя бардак, в котором ты через полгода не вспомнишь, где чьи данные. У меня бардак уже был, поэтому в этот раз я завёл правило и держусь за него зубами.

Правило ровно одно: один docker compose стек = одна папка в /app/<сервис>. Всё, что относится к сервису, живёт внутри этой папки и нигде больше.

Как выглядит на диске

/app
├── writefreely/
│   ├── compose.yml
│   ├── .env
│   └── data/
├── nextcloud/
│   ├── compose.yml
│   ├── .env
│   └── data/
└── gitea/
    ├── compose.yml
    ├── .env
    └── data/

Никаких именованных докер-томов, разбросанных в недрах /var/lib/docker, которые потом фиг найдёшь. Все данные я монтирую относительными путями внутрь папки сервиса — в подкаталог data/. Хочу посмотреть, что сервис нажил — иду в его папку, и там всё.

Из чего состоит стек

Типичный compose.yml у меня выглядит так:

services:
  writefreely:
    image: writeas/writefreely:latest
    container_name: writefreely
    restart: unless-stopped
    env_file: .env
    ports:
      - "127.0.0.1:8080:8080"
    volumes:
      - ./data:/data

Несколько вещей, которые я делаю всегда и осознанно:

  • restart: unless-stopped на каждом сервисе. После ребута коробки (или скачка питания) всё поднимается само. Но если я сам остановил контейнер руками — он остаётся остановленным и не воскресает у меня за спиной. always ведёт себя навязчивее, поэтому именно unless-stopped.
  • Все секреты — в .env, не в compose.yml. Пароли БД, токены, ключи. Сам compose.yml тогда не стыдно показать кому угодно.
  • Порты вешаю на 127.0.0.1, наружу публикую отдельно через единую точку входа. Внутрь докера снаружи напрямую никто не лезет.

.env рядом, и в нём примерно вот это:

# /app/writefreely/.env
DOMAIN=blog.local
DATABASE_PASSWORD=...

Восстанавливаемость — ради неё всё и затевалось

Главная мысль всей этой раскладки: коробку я должен уметь собрать заново с нуля за вечер. Не «вспомнить, как было», а буквально развернуть.

Для этого мне нужны две вещи: сами compose.yml с .env (это рецепт) и data/ (это содержимое). Рецепты весят копейки и меняются редко, поэтому я их версионирую и складываю отдельно. Грубо — собрать все compose-файлы в одно место:

mkdir -p ~/server-config/app
rsync -av --include='*/' \
  --include='compose.yml' --include='.env' \
  --exclude='*' \
  /app/ ~/server-config/app/

Эту папку ~/server-config я кладу в git и в бэкап. Получается, что вся конфигурация сервера — это десяток текстовых файлов, которые читаются глазами и восстанавливаются за минуты.

Данные из data/ — отдельная история, они большие и про них будет отдельный разговор про бэкапы. Но рецепт от содержимого я держу раздельно сознательно: потерять конфиг и потерять данные — это две очень разные катастрофы, и готовиться к ним надо по-разному.

Скучно? Скучно. Но именно скучные системы переживают переезд на новое железо без седых волос.

Обещал написать про железо — пишу. Спойлер: ничего пафосного, и это сознательно.

Долгое время «домашний сервер» у меня жил в голове как стойка, гул вентиляторов и счёт за электричество размером с аренду. Потом я посмотрел, сколько реально потребляет то, что мне нужно, и понял, что городить огород незачем. Мне нужна тихая коробка, которая работает 24/7, не греет комнату и не пугает соседей по счётчику.

Что в итоге взял

Взял б/у Dell OptiPlex Micro — это такой мини-корпус размером чуть больше книжки. На вторичке их полно, потому что корпорации списывают их пачками после офисного цикла. Внутри обычный десктопный x86, а не мобильный огрызок.

Конфигурация после небольшого апгрейда:

  • CPU: Intel Core i5 (4 ядра, вполне хватает);
  • RAM: было 8 ГБ, докинул до 16 ГБ SO-DIMM (минут пять работы);
  • системный диск: NVMe 512 ГБ в M.2 слот;
  • плюс одна SATA 2.5” под данные — в этих корпусах есть посадочное место под 2.5” диск, чем я и воспользовался.

Альтернативой я всерьёз рассматривал свежие мини-ПК на Intel N100 — они новые, холодные, и идут с нормальным питанием из коробки. Если не хочется возиться с б/у, это отличный вариант, по деньгам сопоставимо. Я просто люблю запах списанного корпоративного железа.

Почему не Raspberry Pi

Этот вопрос мне задают чаще всего, поэтому отвечу один раз и со ссылкой на этот абзац.

«Малинку» я очень уважаю, но для домашнего сервера она мне не зашла по трём причинам:

  1. Архитектура. Тут x86-64, а значит весь софт и все докер-образы просто работают. Не надо искать arm-сборки и спотыкаться об «а вот это под arm не собрали».
  2. Диски. NVMe и SATA напрямую, а не microSD, которая дохнет от постоянной записи, и не USB-костыли. Для хранилища это критично.
  3. Цена. Бэушный мини-ПК с уже стоящей памятью и местом под диск выходит сопоставимо с Pi в сборе (плата + нормальный корпус + питание + накопитель), но сразу даёт полноценную машину.

Pi прекрасен как маленький контроллер чего-нибудь. Но как рабочая лошадка с дисками — мини-ПК удобнее.

Потребление

Главный приятный сюрприз — аппетит. Замерил ваттметром из розетки:

  • в простое — около 10–15 Вт;
  • под нагрузкой (когда что-то реально молотит) — поднимается до ~30–35 Вт, но это редко.

15 Вт в простое — это меньше, чем светодиодная лампочка в люстре. За такое не жалко платить за круглосуточную работу.

Система

Поставил Ubuntu Server 24.04 LTS. Без графики, чисто консоль. Выбор простой: LTS до 2029-го, гигантское комьюнити, любой гайд в интернете по умолчанию написан под Ubuntu/Debian. Когда что-то ломается, ответ находится за тридцать секунд.

Разметку сделал максимально скучную: NVMe под систему, SATA-диск отдельным разделом под данные сервисов, который потом монтирую в докер-тома. Всё, дальше уже софт.

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

Привет. Я sparn, и это, кажется, мой четвёртый подход к ведению блога. Первые три тихо умерли где-то между «надо бы написать» и «потом допишу». Но в этот раз повод другой, и он чисто утилитарный.

Я ковыряю домашний сервер. Не потому что мне за это платят, а потому что мне нравится, когда дома стоит коробка, которая делает полезные вещи и принадлежит только мне. И вот какая штука: каждый раз, когда я что-то настраиваю, я решаю одну и ту же проблему по второму разу. Полгода назад я уже чинил права на каталоге с данными. Я даже помню, что чинил. Но как именно — нет. И сижу, гуглю то, что сам же делал. Бесит.

Так что этот блог — в первую очередь моя внешняя память. Записал команду — не надо помнить. Записал, на какие грабли наступил — не наступлю второй раз. Если из этого кому-то ещё будет польза, отлично, но писать я буду честно для себя из будущего, которое всё забыло.

Почему WriteFreely

Я довольно долго смотрел, на чём вести. Варианты были очевидные: завести аккаунт на каком-нибудь готовом сервисе, поднять WordPress, или генератор статики типа Hugo.

Готовый сервис отпал сразу — я и так весь смысл затеи вижу в том, чтобы держать своё у себя. WordPress я уважаю, но это PHP, база, плагины, и через год оно превращается в зоопарк, который надо отдельно обслуживать. А я хочу обслуживать сервер, а не блог про сервер.

Статику я почти выбрал, но мне не понравилось, что для каждой заметки нужно лезть в репозиторий, собирать, деплоить. Хотелось просто открыть, написать, сохранить.

WriteFreely попал ровно в это ощущение:

  • один бинарь на Go, никаких интерпретаторов и зоопарка зависимостей;
  • self-hosted, лежит у меня в /app/writefreely/ в докере;
  • пишешь в чистом markdown, без визуального редактора, который норовит вставить мусорный html;
  • на выходе страница без тонны JS — открывается мгновенно даже с телефона в метро.

Минимализм тут не маркетинговое слово, а буквально: текст, заголовок, кнопка «опубликовать». Мне больше ничего и не надо.

О чём это всё будет

План на ближайшее время простой — писать про то, что реально делаю руками:

  • железо, на котором всё крутится (спойлер: ничего пафосного);
  • как я раскладываю сервисы по докер-стекам, чтобы потом не плакать при переезде;
  • хранилище, бэкапы, и почему бэкап без проверки восстановления — это не бэкап;
  • всякие мелкие настройки, которые я обязательно забуду через месяц.

Без расписания, без «контент-плана». Настроил что-то интересное — записал. Вот и весь движок.

Поехали.