<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Заметки о домашнем сервере</title>
    <link>https://sparn.ru/</link>
    <description>sparn про Linux, Docker и всякий self-hosting. Пишу, чтобы не забыть.</description>
    <pubDate>Fri, 24 Jul 2026 21:04:12 +0000</pubDate>
    <item>
      <title>Год спустя — сколько намотал домашний сервер</title>
      <link>https://sparn.ru/god-spustya-schyot-elektrichestvo</link>
      <description>&lt;![CDATA[Когда я только собирал домашний сервер, главным аргументом «против» в моей голове был не шум и не место, а электричество. Казалось, что коробка, которая работает 24/7, намотает прилично, и счета вырастут заметно. Прошёл год — пора посчитать честно, а не на ощущениях.&#xA;&#xA;Воткнул сервер через умную розетку с замером мощности и снимал показания пару недель в разных режимах. Картина вышла такая:&#xA;&#xA;idle (что бывает большую часть суток): ~12–14 Вт;&#xA;обычная работа — пара контейнеров что-то делают: ~15–18 Вт;&#xA;редкие пики под нагрузкой (бэкап, обновления): кратковременно до ~30 Вт.&#xA;&#xA;То есть большую часть времени это около 15 Вт в среднем. Я специально брал низкое по энергопотреблению железо, без прожорливой видеокарты, и это окупилось именно тут.&#xA;&#xA;Дальше арифметика. Берём средние 15 Вт и считаем за год:&#xA;&#xA;15 Вт × 24 ч × 365 дней = 131 400 Вт·ч ≈ 131 кВт·ч в год&#xA;&#xA;В месяц это около 11 кВт·ч. Дальше умножаем на тариф. Возьму для примера 6 ₽ за кВт·ч (у каждого свой, подставьте):&#xA;&#xA;131 кВт·ч × 6 ₽ ≈ 790 ₽ в год&#xA;≈ 66 ₽ в месяц&#xA;&#xA;Я перепроверил — даже не поверил с первого раза. Получается, мой домашний сервер за год по электричеству стоит примерно как пара чашек кофе в кофейне. В месяц — меньше, чем я не глядя трачу на всякую ерунду.&#xA;&#xA;Сравнение, которое всё ставит на места: ноутбук в зарядке, чайник, который кипит несколько раз в день, тёплый пол — всё это ест кратно больше. Один час работы электрочайника по мощности сопоставим с тем, что сервер потребляет за половину суток. На фоне холодильника или стиральной машины сервер в счёте просто теряется.&#xA;&#xA;Вывод спустя год простой: страх перед «намотает» был необоснованным. Для типового маломощного домашнего сервера электричество — это статья расходов, которую можно округлить до нуля и забыть. Куда дороже обходится время, которое я на него радостно трачу, но это уже совсем другая бухгалтерия.]]&gt;</description>
      <content:encoded><![CDATA[<p>Когда я только собирал домашний сервер, главным аргументом «против» в моей голове был не шум и не место, а электричество. Казалось, что коробка, которая работает 24/7, намотает прилично, и счета вырастут заметно. Прошёл год — пора посчитать честно, а не на ощущениях.</p>

<p>Воткнул сервер через умную розетку с замером мощности и снимал показания пару недель в разных режимах. Картина вышла такая:</p>
<ul><li><strong>idle (что бывает большую часть суток):</strong> ~12–14 Вт;</li>
<li><strong>обычная работа</strong> — пара контейнеров что-то делают: ~15–18 Вт;</li>
<li><strong>редкие пики</strong> под нагрузкой (бэкап, обновления): кратковременно до ~30 Вт.</li></ul>

<p>То есть большую часть времени это около <strong>15 Вт в среднем</strong>. Я специально брал низкое по энергопотреблению железо, без прожорливой видеокарты, и это окупилось именно тут.</p>

<p>Дальше арифметика. Берём средние 15 Вт и считаем за год:</p>

<pre><code class="language-text">15 Вт × 24 ч × 365 дней = 131 400 Вт·ч ≈ 131 кВт·ч в год
</code></pre>

<p>В месяц это около <strong>11 кВт·ч</strong>. Дальше умножаем на тариф. Возьму для примера 6 ₽ за кВт·ч (у каждого свой, подставьте):</p>

<pre><code class="language-text">131 кВт·ч × 6 ₽ ≈ 790 ₽ в год
≈ 66 ₽ в месяц
</code></pre>

<p>Я перепроверил — даже не поверил с первого раза. Получается, мой домашний сервер за год по электричеству стоит примерно как пара чашек кофе в кофейне. В месяц — меньше, чем я не глядя трачу на всякую ерунду.</p>

<p>Сравнение, которое всё ставит на места: ноутбук в зарядке, чайник, который кипит несколько раз в день, тёплый пол — всё это ест кратно больше. Один час работы электрочайника по мощности сопоставим с тем, что сервер потребляет за половину суток. На фоне холодильника или стиральной машины сервер в счёте просто теряется.</p>

<p>Вывод спустя год простой: страх перед «намотает» был необоснованным. Для типового маломощного домашнего сервера электричество — это статья расходов, которую можно округлить до нуля и забыть. Куда дороже обходится время, которое я на него радостно трачу, но это уже совсем другая бухгалтерия.</p>
]]></content:encoded>
      <guid>https://sparn.ru/god-spustya-schyot-elektrichestvo</guid>
      <pubDate>Sun, 12 Jul 2026 15:58:56 +0000</pubDate>
    </item>
    <item>
      <title>Обновил сервер с 22.04 до Ubuntu 24.04</title>
      <link>https://sparn.ru/ubuntu-24-04-upgrade</link>
      <description>&lt;![CDATA[Дотянул-таки до обновления домашнего сервера с Ubuntu 22.04 LTS на 24.04. Откладывал, потому что система работала и трогать не хотелось, но поддержка 22.04 не вечная, да и хотелось свежее ядро. Записываю, как прошло и что насторожило.&#xA;&#xA;Сначала подготовка, потому что обновление мажорной версии — это всегда чуть-чуть лотерея. Сделал полный бэкап важных папок (/app, /etc, домашний каталог) через restic. Отдельно — снапшот всей машины на уровне гипервизора, чтобы в случае чего откатиться за минуту целиком, а не восстанавливать по кусочкам. Затем привёл текущую систему в порядок:&#xA;&#xA;sudo apt update &amp;&amp; sudo apt full-upgrade&#xA;sudo apt autoremove&#xA;sudo reboot&#xA;&#xA;Чистый старт без висящих обновлений — чтобы апгрейд не споткнулся о наполовину применённые пакеты.&#xA;&#xA;Сам процесс запускается так:&#xA;&#xA;sudo apt install update-manager-core&#xA;sudo do-release-upgrade&#xA;&#xA;Дальше — то, что насторожило по ходу.&#xA;&#xA;needrestart стал интерактивным. В 24.04 он по умолчанию во время апгрейда спрашивает, какие сервисы перезапустить. Посреди длинной установки это неожиданно: думаешь, что процесс идёт сам, а он встал и ждёт ввода. Если хочется тишины, можно заранее перевести его в автоматический режим, поправив /etc/needrestart/needrestart.conf ($nrconf{restart} = &#39;a&#39;;). Я оставил вручную, но имейте в виду — отойти и забыть не получится.&#xA;&#xA;Вопросы про изменённые конфиги. Несколько раз спросило, оставить мою версию конфига или взять версию мейнтейнера (тот самый диалог про *.dpkg-dist). По умолчанию надо оставлять своё, но я каждый раз смотрел дифф через предложенную опцию D, чтобы не пропустить важных изменений в дефолтах.&#xA;&#xA;Сторонние репозитории отключаются. do-release-upgrade закомментировал PPA и внешние списки в sources.list.d. Это нормально и правильно, но после апгрейда их надо осознанно вернуть, поправив на новый кодовое имя релиза (noble), а не вслепую раскомментировать.&#xA;&#xA;После перезагрузки первым делом проверил версию и что вообще живо:&#xA;&#xA;lsb_release -a&#xA;uname -r&#xA;&#xA;И главное для меня — контейнеры. Docker пережил обновление нормально, но я всё равно прошёлся по стекам:&#xA;&#xA;docker ps -a&#xA;docker compose -f /app/forgejo/compose.yml up -d&#xA;&#xA;Пара контейнеров не поднялась автоматически с первого раза — помогло docker compose up -d по каждому стеку вручную, дальше restart: unless-stopped подхватил. Никаких потерь данных, тома на месте.&#xA;&#xA;Из реально сломанного — практически ничего, что приятно удивило. Один мой самописный скрипт ругнулся на изменившееся поведение системного Python (в 24.04 он строже относится к установке пакетов мимо venv — привет externally-managed-environment), завернул его в нормальный venv и забыл. В остальном — гладко. Снапшот так и не пригодился, но без него я бы нервничал заметно сильнее.]]&gt;</description>
      <content:encoded><![CDATA[<p>Дотянул-таки до обновления домашнего сервера с Ubuntu 22.04 LTS на 24.04. Откладывал, потому что система работала и трогать не хотелось, но поддержка 22.04 не вечная, да и хотелось свежее ядро. Записываю, как прошло и что насторожило.</p>

<p>Сначала подготовка, потому что обновление мажорной версии — это всегда чуть-чуть лотерея. Сделал полный бэкап важных папок (<code>/app</code>, <code>/etc</code>, домашний каталог) через restic. Отдельно — снапшот всей машины на уровне гипервизора, чтобы в случае чего откатиться за минуту целиком, а не восстанавливать по кусочкам. Затем привёл текущую систему в порядок:</p>

<pre><code class="language-bash">sudo apt update &amp;&amp; sudo apt full-upgrade
sudo apt autoremove
sudo reboot
</code></pre>

<p>Чистый старт без висящих обновлений — чтобы апгрейд не споткнулся о наполовину применённые пакеты.</p>

<p>Сам процесс запускается так:</p>

<pre><code class="language-bash">sudo apt install update-manager-core
sudo do-release-upgrade
</code></pre>

<p>Дальше — то, что насторожило по ходу.</p>

<p><strong>needrestart стал интерактивным.</strong> В 24.04 он по умолчанию во время апгрейда спрашивает, какие сервисы перезапустить. Посреди длинной установки это неожиданно: думаешь, что процесс идёт сам, а он встал и ждёт ввода. Если хочется тишины, можно заранее перевести его в автоматический режим, поправив <code>/etc/needrestart/needrestart.conf</code> (<code>$nrconf{restart} = &#39;a&#39;;</code>). Я оставил вручную, но имейте в виду — отойти и забыть не получится.</p>

<p><strong>Вопросы про изменённые конфиги.</strong> Несколько раз спросило, оставить мою версию конфига или взять версию мейнтейнера (тот самый диалог про <code>*.dpkg-dist</code>). По умолчанию надо оставлять своё, но я каждый раз смотрел дифф через предложенную опцию <code>D</code>, чтобы не пропустить важных изменений в дефолтах.</p>

<p><strong>Сторонние репозитории отключаются.</strong> <code>do-release-upgrade</code> закомментировал PPA и внешние списки в <code>sources.list.d</code>. Это нормально и правильно, но после апгрейда их надо осознанно вернуть, поправив на новый кодовое имя релиза (<code>noble</code>), а не вслепую раскомментировать.</p>

<p>После перезагрузки первым делом проверил версию и что вообще живо:</p>

<pre><code class="language-bash">lsb_release -a
uname -r
</code></pre>

<p>И главное для меня — контейнеры. Docker пережил обновление нормально, но я всё равно прошёлся по стекам:</p>

<pre><code class="language-bash">docker ps -a
docker compose -f /app/forgejo/compose.yml up -d
</code></pre>

<p>Пара контейнеров не поднялась автоматически с первого раза — помогло <code>docker compose up -d</code> по каждому стеку вручную, дальше <code>restart: unless-stopped</code> подхватил. Никаких потерь данных, тома на месте.</p>

<p>Из реально сломанного — практически ничего, что приятно удивило. Один мой самописный скрипт ругнулся на изменившееся поведение системного Python (в 24.04 он строже относится к установке пакетов мимо venv — привет <code>externally-managed-environment</code>), завернул его в нормальный venv и забыл. В остальном — гладко. Снапшот так и не пригодился, но без него я бы нервничал заметно сильнее.</p>
]]></content:encoded>
      <guid>https://sparn.ru/ubuntu-24-04-upgrade</guid>
      <pubDate>Thu, 09 Jul 2026 13:40:24 +0000</pubDate>
    </item>
    <item>
      <title>Self-host Healthchecks — чтобы знать, что бэкап молчит</title>
      <link>https://sparn.ru/healthchecks-deadman</link>
      <description>&lt;![CDATA[У меня была классическая проблема: бэкап настроен, скрипт по таймеру отрабатывает, и я об этом... никак не узнаю. Пока всё хорошо — тишина. И пока однажды не понадобится восстановиться, ты искренне веришь, что бэкапы делаются. А потом выясняется, что три недели назад скрипт тихо падал на каждом запуске, и свежего архива нет. Классика.&#xA;&#xA;Проблема тут фундаментальная: обычный мониторинг проверяет, что сервис жив. А мне нужно обратное — узнать, что задача НЕ выполнилась. Это называется dead-man&#39;s switch: задача должна регулярно подавать признаки жизни, и если она замолчала — это и есть сигнал тревоги. Молчание = проблема.&#xA;&#xA;Для этого поднял у себя Healthchecks. Это сервис, который ждёт от твоей задачи ping по HTTP. Задача успешно отработала — дёрнула URL. Не дёрнула в отведённое окно — Healthchecks сам шлёт алерт. То есть мне не нужно ничего активно опрашивать; если бэкап молча перестал делаться, я об этом узнаю не через месяц, а на следующее же утро.&#xA;&#xA;Стек в Docker, /app/healthchecks/:&#xA;&#xA;services:&#xA;  healthchecks:&#xA;    image: healthchecks/healthchecks:latest&#xA;    containername: healthchecks&#xA;    restart: unless-stopped&#xA;    environment:&#xA;      SECRETKEY=${SECRETKEY}&#xA;      SITEROOT=http://hc.local:8000&#xA;      DEFAULTFROMEMAIL=hc@hc.local&#xA;      EMAILHOST=${SMTPHOST}&#xA;      EMAILPORT=587&#xA;      EMAILHOSTUSER=${SMTPUSER}&#xA;      EMAILHOSTPASSWORD=${SMTPPASS}&#xA;    volumes:&#xA;      ./data:/data&#xA;    ports:&#xA;      &#34;8000:8000&#34;&#xA;&#xA;В вебморде создаёшь check, задаёшь расписание (можно прямо cron-выражением) и grace period — сколько ждать опоздание, прежде чем паниковать. Получаешь уникальный ping-URL.&#xA;&#xA;Дальше прикручиваю его к бэкап-скрипту. Главное — пинговать в самом конце, после set -e, чтобы упавший скрипт не успел отрапортовать об успехе:&#xA;&#xA;!/bin/bash&#xA;set -e&#xA;&#xA;PINGURL=&#34;https://hc.local:8000/ping/xxxxxxxx-xxxx-xxxx&#34;&#xA;&#xA;... сам бэкап ...&#xA;restic backup /app /etc&#xA;&#xA;дошли сюда без ошибок — рапортуем&#xA;curl -fsS -m 10 --retry 3 &#34;$PINGURL&#34;   /dev/null&#xA;&#xA;Можно ещё красивее: пинговать $PINGURL/start в начале и $PING_URL/fail в блоке обработки ошибки — тогда в истории видно длительность задачи и явные падения отдельно от опозданий.&#xA;&#xA;Алерты вешаю на почту, но Healthchecks умеет и в кучу других каналов. Теперь у меня правило простое: если что-то важное должно происходить регулярно — оно дёргает Healthchecks. И отсутствие новостей перестало означать хорошие новости — теперь отсутствие пинга означает, что мне прилетит письмо. Спится спокойнее.]]&gt;</description>
      <content:encoded><![CDATA[<p>У меня была классическая проблема: бэкап настроен, скрипт по таймеру отрабатывает, и я об этом... никак не узнаю. Пока всё хорошо — тишина. И пока однажды не понадобится восстановиться, ты искренне веришь, что бэкапы делаются. А потом выясняется, что три недели назад скрипт тихо падал на каждом запуске, и свежего архива нет. Классика.</p>

<p>Проблема тут фундаментальная: обычный мониторинг проверяет, что сервис жив. А мне нужно обратное — узнать, что задача НЕ выполнилась. Это называется dead-man&#39;s switch: задача должна регулярно подавать признаки жизни, и если она замолчала — это и есть сигнал тревоги. Молчание = проблема.</p>

<p>Для этого поднял у себя Healthchecks. Это сервис, который ждёт от твоей задачи ping по HTTP. Задача успешно отработала — дёрнула URL. Не дёрнула в отведённое окно — Healthchecks сам шлёт алерт. То есть мне не нужно ничего активно опрашивать; если бэкап молча перестал делаться, я об этом узнаю не через месяц, а на следующее же утро.</p>

<p>Стек в Docker, <code>/app/healthchecks/</code>:</p>

<pre><code class="language-yaml">services:
  healthchecks:
    image: healthchecks/healthchecks:latest
    container_name: healthchecks
    restart: unless-stopped
    environment:
      - SECRET_KEY=${SECRET_KEY}
      - SITE_ROOT=http://hc.local:8000
      - DEFAULT_FROM_EMAIL=hc@hc.local
      - EMAIL_HOST=${SMTP_HOST}
      - EMAIL_PORT=587
      - EMAIL_HOST_USER=${SMTP_USER}
      - EMAIL_HOST_PASSWORD=${SMTP_PASS}
    volumes:
      - ./data:/data
    ports:
      - &#34;8000:8000&#34;
</code></pre>

<p>В вебморде создаёшь check, задаёшь расписание (можно прямо cron-выражением) и grace period — сколько ждать опоздание, прежде чем паниковать. Получаешь уникальный ping-URL.</p>

<p>Дальше прикручиваю его к бэкап-скрипту. Главное — пинговать в самом конце, после <code>set -e</code>, чтобы упавший скрипт не успел отрапортовать об успехе:</p>

<pre><code class="language-bash">#!/bin/bash
set -e

PING_URL=&#34;https://hc.local:8000/ping/xxxxxxxx-xxxx-xxxx&#34;

# ... сам бэкап ...
restic backup /app /etc

# дошли сюда без ошибок — рапортуем
curl -fsS -m 10 --retry 3 &#34;$PING_URL&#34; &gt; /dev/null
</code></pre>

<p>Можно ещё красивее: пинговать <code>$PING_URL/start</code> в начале и <code>$PING_URL/fail</code> в блоке обработки ошибки — тогда в истории видно длительность задачи и явные падения отдельно от опозданий.</p>

<p>Алерты вешаю на почту, но Healthchecks умеет и в кучу других каналов. Теперь у меня правило простое: если что-то важное должно происходить регулярно — оно дёргает Healthchecks. И отсутствие новостей перестало означать хорошие новости — теперь отсутствие пинга означает, что мне прилетит письмо. Спится спокойнее.</p>
]]></content:encoded>
      <guid>https://sparn.ru/healthchecks-deadman</guid>
      <pubDate>Tue, 07 Jul 2026 13:41:29 +0000</pubDate>
    </item>
    <item>
      <title>TIL — перевёл задачи с cron на systemd timers</title>
      <link>https://sparn.ru/til-cron-vs-systemd-timer</link>
      <description>&lt;![CDATA[Долго держал все периодические задачи в crontab по привычке. На днях надоело и перевёл их на systemd timers. Записываю плюсы, чтобы потом не гуглить то же самое.&#xA;&#xA;Что мне в итоге понравилось:&#xA;&#xA;логи задачи лежат в journalctl -u mybackup.service, а не где-то в почте root или вообще нигде;&#xA;можно навесить зависимости — например, запускать только After=docker.service;&#xA;OnCalendar читается человеком, а не как /15 3   1;&#xA;Persistent=true — если сервер спал в момент запуска, задача отработает после включения. Cron такое просто молча пропускает.&#xA;&#xA;Минимальная пара файлов. Сервис в /etc/systemd/system/backup.service:&#xA;&#xA;[Unit]&#xA;Description=Ночной бэкап&#xA;After=docker.service&#xA;&#xA;[Service]&#xA;Type=oneshot&#xA;ExecStart=/app/scripts/backup.sh&#xA;&#xA;Таймер рядом, backup.timer:&#xA;&#xA;[Unit]&#xA;Description=Запуск бэкапа раз в сутки&#xA;&#xA;[Timer]&#xA;OnCalendar=-- 03:30:00&#xA;Persistent=true&#xA;&#xA;[Install]&#xA;WantedBy=timers.target&#xA;&#xA;Включается так:&#xA;&#xA;sudo systemctl daemon-reload&#xA;sudo systemctl enable --now backup.timer&#xA;systemctl list-timers&#xA;&#xA;list-timers сразу показывает, когда следующий запуск и когда был прошлый — очень удобно, в cron такого обзора из коробки нет. Единственное, к чему привыкаешь: один таск — это два файла вместо одной строки. Для пары задач это перебор, но когда задач десяток и хочется логи и порядок — оно того стоит.]]&gt;</description>
      <content:encoded><![CDATA[<p>Долго держал все периодические задачи в crontab по привычке. На днях надоело и перевёл их на systemd timers. Записываю плюсы, чтобы потом не гуглить то же самое.</p>

<p>Что мне в итоге понравилось:</p>
<ul><li>логи задачи лежат в <code>journalctl -u mybackup.service</code>, а не где-то в почте root или вообще нигде;</li>
<li>можно навесить зависимости — например, запускать только <code>After=docker.service</code>;</li>
<li><code>OnCalendar</code> читается человеком, а не как <code>*/15 3 * * 1</code>;</li>
<li><code>Persistent=true</code> — если сервер спал в момент запуска, задача отработает после включения. Cron такое просто молча пропускает.</li></ul>

<p>Минимальная пара файлов. Сервис в <code>/etc/systemd/system/backup.service</code>:</p>

<pre><code class="language-ini">[Unit]
Description=Ночной бэкап
After=docker.service

[Service]
Type=oneshot
ExecStart=/app/scripts/backup.sh
</code></pre>

<p>Таймер рядом, <code>backup.timer</code>:</p>

<pre><code class="language-ini">[Unit]
Description=Запуск бэкапа раз в сутки

[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true

[Install]
WantedBy=timers.target
</code></pre>

<p>Включается так:</p>

<pre><code class="language-bash">sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
systemctl list-timers
</code></pre>

<p><code>list-timers</code> сразу показывает, когда следующий запуск и когда был прошлый — очень удобно, в cron такого обзора из коробки нет. Единственное, к чему привыкаешь: один таск — это два файла вместо одной строки. Для пары задач это перебор, но когда задач десяток и хочется логи и порядок — оно того стоит.</p>
]]></content:encoded>
      <guid>https://sparn.ru/til-cron-vs-systemd-timer</guid>
      <pubDate>Sun, 05 Jul 2026 13:06:31 +0000</pubDate>
    </item>
    <item>
      <title>Поднял Forgejo — свой git дома</title>
      <link>https://sparn.ru/forgejo-svoy-git</link>
      <description>&lt;![CDATA[Я давно держал все конфиги сервера и dotfiles на GitHub, и вроде всё работало. Но накопилось несколько вещей, которые меня в этой схеме не устраивали. Во-первых, часть репо — это compose-файлы и .env-шаблоны для домашнего сервера, и мне просто некомфортно, что всё это лежит на чужих серверах, пусть даже в приватных репозиториях. Во-вторых, хотелось, чтобы бэкап моих же конфигов жил рядом с тем, что они описывают, а не зависел от доступности внешнего сервиса. В общем, решил поднять свой git.&#xA;&#xA;Выбор был между Gitea и Forgejo. Forgejo — это форк Gitea, который пошёл своим путём после смены управления у Gitea, и сообщество мне там симпатичнее. По функционалу для моих задач они почти идентичны, так что взял Forgejo.&#xA;&#xA;Подняв в Docker, как и всё остальное у меня. Стек живёт в /app/forgejo/:&#xA;&#xA;services:&#xA;  forgejo:&#xA;    image: codeberg.org/forgejo/forgejo:9&#xA;    containername: forgejo&#xA;    restart: unless-stopped&#xA;    environment:&#xA;      USERUID=1000&#xA;      USERGID=1000&#xA;      FORGEJOserverDOMAIN=git.local&#xA;      FORGEJOserverROOTURL=http://git.local:3000/&#xA;    volumes:&#xA;      ./data:/data&#xA;      /etc/timezone:/etc/timezone:ro&#xA;      /etc/localtime:/etc/localtime:ro&#xA;    ports:&#xA;      &#34;3000:3000&#34;&#xA;      &#34;222:22&#34;&#xA;&#xA;SSH-порт контейнера прокинул на 222, чтобы не конфликтовать с системным sshd. После первого старта Forgejo открывает страницу установки — там выбрал SQLite (для одного пользователя База на postgres избыточна), задал админа и выключил открытую регистрацию, чтобы случайно никто не завёл аккаунт.&#xA;&#xA;Дальше добавил remote к своим dotfiles:&#xA;&#xA;git remote add home ssh://git@192.168.1.10:222/sparn/dotfiles.git&#xA;git push home main&#xA;&#xA;Я не стал полностью уходить с GitHub — там удобно показывать что-то людям и пользоваться Actions. Сделал так: home-репо у меня основной, а на GitHub push зеркалом через дополнительный remote, либо через встроенный в Forgejo механизм push-mirror. Получилось, что мои конфиги теперь в двух местах, и одно из них полностью под моим контролем.&#xA;&#xA;Отдельно радует, что веб-морда лёгкая и шустрая даже на скромном железе. Памяти контейнер ест мизер, на idle почти не виден. Для домашнего использования — то что надо: свой git, свои данные, и при этом всё привычно как на больших площадках. Бэкап самого Forgejo — это просто архив папки ./data, кладу его в общий ночной бэкап сервера, так что отдельной возни нет.]]&gt;</description>
      <content:encoded><![CDATA[<p>Я давно держал все конфиги сервера и dotfiles на GitHub, и вроде всё работало. Но накопилось несколько вещей, которые меня в этой схеме не устраивали. Во-первых, часть репо — это compose-файлы и <code>.env</code>-шаблоны для домашнего сервера, и мне просто некомфортно, что всё это лежит на чужих серверах, пусть даже в приватных репозиториях. Во-вторых, хотелось, чтобы бэкап моих же конфигов жил рядом с тем, что они описывают, а не зависел от доступности внешнего сервиса. В общем, решил поднять свой git.</p>

<p>Выбор был между Gitea и Forgejo. Forgejo — это форк Gitea, который пошёл своим путём после смены управления у Gitea, и сообщество мне там симпатичнее. По функционалу для моих задач они почти идентичны, так что взял Forgejo.</p>

<p>Подняв в Docker, как и всё остальное у меня. Стек живёт в <code>/app/forgejo/</code>:</p>

<pre><code class="language-yaml">services:
  forgejo:
    image: codeberg.org/forgejo/forgejo:9
    container_name: forgejo
    restart: unless-stopped
    environment:
      - USER_UID=1000
      - USER_GID=1000
      - FORGEJO__server__DOMAIN=git.local
      - FORGEJO__server__ROOT_URL=http://git.local:3000/
    volumes:
      - ./data:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    ports:
      - &#34;3000:3000&#34;
      - &#34;222:22&#34;
</code></pre>

<p>SSH-порт контейнера прокинул на 222, чтобы не конфликтовать с системным sshd. После первого старта Forgejo открывает страницу установки — там выбрал SQLite (для одного пользователя База на postgres избыточна), задал админа и выключил открытую регистрацию, чтобы случайно никто не завёл аккаунт.</p>

<p>Дальше добавил remote к своим dotfiles:</p>

<pre><code class="language-bash">git remote add home ssh://git@192.168.1.10:222/sparn/dotfiles.git
git push home main
</code></pre>

<p>Я не стал полностью уходить с GitHub — там удобно показывать что-то людям и пользоваться Actions. Сделал так: home-репо у меня основной, а на GitHub push зеркалом через дополнительный remote, либо через встроенный в Forgejo механизм push-mirror. Получилось, что мои конфиги теперь в двух местах, и одно из них полностью под моим контролем.</p>

<p>Отдельно радует, что веб-морда лёгкая и шустрая даже на скромном железе. Памяти контейнер ест мизер, на idle почти не виден. Для домашнего использования — то что надо: свой git, свои данные, и при этом всё привычно как на больших площадках. Бэкап самого Forgejo — это просто архив папки <code>./data</code>, кладу его в общий ночной бэкап сервера, так что отдельной возни нет.</p>
]]></content:encoded>
      <guid>https://sparn.ru/forgejo-svoy-git</guid>
      <pubDate>Fri, 03 Jul 2026 15:19:53 +0000</pubDate>
    </item>
    <item>
      <title>Переезд с HDD на SSD — и заодно шифрование LUKS</title>
      <link>https://sparn.ru/perenos-ssd-luks</link>
      <description>&lt;![CDATA[Сервер у меня жил на старом HDD, и это чувствовалось во всём: контейнеры стартовали медленно, база отвечала с ленцой, а сам диск по ночам тарахтел так, что слышно из другой комнаты. Купил SSD. И раз уж всё равно переезжать с нуля, решил сразу включить шифрование диска через LUKS — чтобы, если железо однажды уведут или я отнесу его в ремонт, данные не утекли вместе с ним.&#xA;&#xA;Сначала готовлю SSD. Создаю на нём раздел и накатываю LUKS:&#xA;&#xA;sudo cryptsetup luksFormat /dev/sdb1&#xA;YES, придумываем пароль&#xA;sudo cryptsetup open /dev/sdb1 cryptdata&#xA;sudo mkfs.ext4 /dev/mapper/cryptdata&#xA;sudo mount /dev/mapper/cryptdata /mnt/ssd&#xA;&#xA;Теперь /dev/mapper/cryptdata — это расшифрованный том, с которым работаешь как с обычным разделом. Под капотом всё на диске лежит зашифрованным.&#xA;&#xA;Перенос данных — через rsync, чтобы сохранить права, владельцев и симлинки. Сервисы перед этим гашу, иначе утащу базу в неконсистентном состоянии:&#xA;&#xA;docker compose -f /app/paperless/docker-compose.yml down   # и остальные стеки&#xA;sudo rsync -aAXv --info=progress2 /app/ /mnt/ssd/app/&#xA;&#xA;Флаги тут важные: -a тянет права и время, -A ACL, -X расширенные атрибуты. Для контейнерных томов это критично — иначе потом ловишь permission denied на ровном месте.&#xA;&#xA;Дальше — чтобы том сам подключался при загрузке. Прописываю его в /etc/crypttab и /etc/fstab. Сначала узнаю UUID раздела:&#xA;&#xA;sudo blkid /dev/sdb1&#xA;UUID=&#34;xxxx-xxxx-...&#34;&#xA;&#xA;/etc/crypttab&#xA;cryptdata  UUID=xxxx-xxxx-...  none  luks&#xA;&#xA;/etc/fstab&#xA;/dev/mapper/cryptdata  /app  ext4  defaults  0  2&#xA;&#xA;И тут главный вопрос, на котором спотыкаются все: как разблокировать диск при загрузке. С none в crypttab система при старте просит пароль. На обычном десктопе — без проблем, ввёл с клавиатуры. Но у меня сервер стоит без монитора, и тянуться к нему при каждой перезагрузке — так себе удовольствие.&#xA;&#xA;Я оставил ввод пароля, но через консоль по сети. На сервере поднят dropbear-initramfs — это крошечный SSH-сервер, который живёт ещё до загрузки основной системы, в initramfs. После ребута я захожу на него с ноута и ввожу пароль LUKS:&#xA;&#xA;ssh -p 222 root@192.168.1.10&#xA;попадаю в initramfs&#xA;cryptroot-unlock&#xA;вводим пароль -  система продолжает загрузку&#xA;&#xA;Да, это значит, что полностью автономно после отключения света сервер не поднимется — он будет ждать, пока я введу пароль. Для меня это осознанный компромисс: смысл шифрования как раз в том, что без пароля диск не открыть, даже забрав его. Хранить ключ на самой машине рядом с диском — это как запереть дверь и приклеить ключ к косяку.&#xA;&#xA;Что в итоге. SSD оживил сервер до неузнаваемости: контейнеры стартуют мгновенно, база летает, тишина в комнате. А LUKS дал спокойствие — теперь украденный или потерянный диск это просто кусок зашифрованного мусора, а не мой архив документов и бэкапов нараспашку. Переезд занял вечер, и ни о чём не жалею.]]&gt;</description>
      <content:encoded><![CDATA[<p>Сервер у меня жил на старом HDD, и это чувствовалось во всём: контейнеры стартовали медленно, база отвечала с ленцой, а сам диск по ночам тарахтел так, что слышно из другой комнаты. Купил SSD. И раз уж всё равно переезжать с нуля, решил сразу включить шифрование диска через LUKS — чтобы, если железо однажды уведут или я отнесу его в ремонт, данные не утекли вместе с ним.</p>

<p>Сначала готовлю SSD. Создаю на нём раздел и накатываю LUKS:</p>

<pre><code class="language-bash">sudo cryptsetup luksFormat /dev/sdb1
# YES, придумываем пароль
sudo cryptsetup open /dev/sdb1 cryptdata
sudo mkfs.ext4 /dev/mapper/cryptdata
sudo mount /dev/mapper/cryptdata /mnt/ssd
</code></pre>

<p>Теперь <code>/dev/mapper/cryptdata</code> — это расшифрованный том, с которым работаешь как с обычным разделом. Под капотом всё на диске лежит зашифрованным.</p>

<p>Перенос данных — через rsync, чтобы сохранить права, владельцев и симлинки. Сервисы перед этим гашу, иначе утащу базу в неконсистентном состоянии:</p>

<pre><code class="language-bash">docker compose -f /app/paperless/docker-compose.yml down   # и остальные стеки
sudo rsync -aAXv --info=progress2 /app/ /mnt/ssd/app/
</code></pre>

<p>Флаги тут важные: <code>-a</code> тянет права и время, <code>-A</code> ACL, <code>-X</code> расширенные атрибуты. Для контейнерных томов это критично — иначе потом ловишь permission denied на ровном месте.</p>

<p>Дальше — чтобы том сам подключался при загрузке. Прописываю его в <code>/etc/crypttab</code> и <code>/etc/fstab</code>. Сначала узнаю UUID раздела:</p>

<pre><code class="language-bash">sudo blkid /dev/sdb1
# UUID=&#34;xxxx-xxxx-...&#34;
</code></pre>

<pre><code class="language-bash"># /etc/crypttab
cryptdata  UUID=xxxx-xxxx-...  none  luks
</code></pre>

<pre><code class="language-bash"># /etc/fstab
/dev/mapper/cryptdata  /app  ext4  defaults  0  2
</code></pre>

<p>И тут главный вопрос, на котором спотыкаются все: как разблокировать диск при загрузке. С <code>none</code> в crypttab система при старте просит пароль. На обычном десктопе — без проблем, ввёл с клавиатуры. Но у меня сервер стоит без монитора, и тянуться к нему при каждой перезагрузке — так себе удовольствие.</p>

<p>Я оставил ввод пароля, но через консоль по сети. На сервере поднят dropbear-initramfs — это крошечный SSH-сервер, который живёт ещё до загрузки основной системы, в initramfs. После ребута я захожу на него с ноута и ввожу пароль LUKS:</p>

<pre><code class="language-bash">ssh -p 222 root@192.168.1.10
# попадаю в initramfs
cryptroot-unlock
# вводим пароль -&gt; система продолжает загрузку
</code></pre>

<p>Да, это значит, что полностью автономно после отключения света сервер не поднимется — он будет ждать, пока я введу пароль. Для меня это осознанный компромисс: смысл шифрования как раз в том, что без пароля диск не открыть, даже забрав его. Хранить ключ на самой машине рядом с диском — это как запереть дверь и приклеить ключ к косяку.</p>

<p>Что в итоге. SSD оживил сервер до неузнаваемости: контейнеры стартуют мгновенно, база летает, тишина в комнате. А LUKS дал спокойствие — теперь украденный или потерянный диск это просто кусок зашифрованного мусора, а не мой архив документов и бэкапов нараспашку. Переезд занял вечер, и ни о чём не жалею.</p>
]]></content:encoded>
      <guid>https://sparn.ru/perenos-ssd-luks</guid>
      <pubDate>Tue, 30 Jun 2026 15:55:39 +0000</pubDate>
    </item>
    <item>
      <title>TIL: красивые имена для сервисов в локалке через split-DNS</title>
      <link>https://sparn.ru/til-split-dns</link>
      <description>&lt;![CDATA[[TIL] Надоело лазить по сервисам через 192.168.1.10:8000, :8080, :9000 и помнить, кто на каком порту. Хотелось открывать их по-человечески: paperless.home.lan, nas.home.lan и так далее. Решается это split-DNS — когда твой локальный DNS отдаёт на эти имена внутренний IP сервера.&#xA;&#xA;У меня в роли локального DNS уже стоит AdGuard Home, так что добавил всё в его DNS-rewrites. В UI это Filters → DNS rewrites, а если в конфиге AdGuardHome.yaml, то так:&#xA;&#xA;filtering:&#xA;  rewrites:&#xA;    domain: &#39;*.home.lan&#39;&#xA;      answer: 192.168.1.10&#xA;&#xA;Один wildcard — и любое имя в зоне home.lan резолвится в сервер. Удобно: завёл новый сервис, придумал ему имя, и оно сразу работает, без правки DNS.&#xA;&#xA;Если AdGuard нет, ровно то же делается на dnsmasq одной строкой:&#xA;&#xA;/etc/dnsmasq.conf&#xA;address=/home.lan/192.168.1.10&#xA;&#xA;Проверка, что отдаёт нужный IP:&#xA;&#xA;nslookup paperless.home.lan 192.168.1.1&#xA;Address: 192.168.1.10&#xA;&#xA;Один нюанс: чтобы это работало на всех устройствах, в роутере DHCP должен раздавать именно этот DNS, иначе телефон пойдёт спрашивать у публичного резолвера и ничего не найдёт. Я взял зону .home.lan (не .local — её занимает mDNS, и не настоящий домен — чтобы не пересекаться с внешним миром).&#xA;&#xA;Дальше повесил перед сервисами reverse-proxy, чтобы порты тоже спрятать, и теперь paperless.home.lan открывается просто в браузере. Маленькое удобство, а пользуюсь каждый день.]]&gt;</description>
      <content:encoded><![CDATA[<p>[TIL] Надоело лазить по сервисам через <code>192.168.1.10:8000</code>, <code>:8080</code>, <code>:9000</code> и помнить, кто на каком порту. Хотелось открывать их по-человечески: <code>paperless.home.lan</code>, <code>nas.home.lan</code> и так далее. Решается это split-DNS — когда твой локальный DNS отдаёт на эти имена внутренний IP сервера.</p>

<p>У меня в роли локального DNS уже стоит AdGuard Home, так что добавил всё в его DNS-rewrites. В UI это Filters → DNS rewrites, а если в конфиге <code>AdGuardHome.yaml</code>, то так:</p>

<pre><code class="language-yaml">filtering:
  rewrites:
    - domain: &#39;*.home.lan&#39;
      answer: 192.168.1.10
</code></pre>

<p>Один wildcard — и любое имя в зоне <code>home.lan</code> резолвится в сервер. Удобно: завёл новый сервис, придумал ему имя, и оно сразу работает, без правки DNS.</p>

<p>Если AdGuard нет, ровно то же делается на dnsmasq одной строкой:</p>

<pre><code># /etc/dnsmasq.conf
address=/home.lan/192.168.1.10
</code></pre>

<p>Проверка, что отдаёт нужный IP:</p>

<pre><code class="language-bash">nslookup paperless.home.lan 192.168.1.1
# Address: 192.168.1.10
</code></pre>

<p>Один нюанс: чтобы это работало на всех устройствах, в роутере DHCP должен раздавать именно этот DNS, иначе телефон пойдёт спрашивать у публичного резолвера и ничего не найдёт. Я взял зону <code>.home.lan</code> (не <code>.local</code> — её занимает mDNS, и не настоящий домен — чтобы не пересекаться с внешним миром).</p>

<p>Дальше повесил перед сервисами reverse-proxy, чтобы порты тоже спрятать, и теперь <code>paperless.home.lan</code> открывается просто в браузере. Маленькое удобство, а пользуюсь каждый день.</p>
]]></content:encoded>
      <guid>https://sparn.ru/til-split-dns</guid>
      <pubDate>Sun, 28 Jun 2026 15:40:40 +0000</pubDate>
    </item>
    <item>
      <title>Поставил ИБП и научил сервер гаситься сам</title>
      <link>https://sparn.ru/ups-nut-shutdown</link>
      <description>&lt;![CDATA[Свет у меня моргает редко, но метко. Пару раз в год — короткое отключение на минуту-две, и каждый раз сервер падал жёстко, как будто выдернули вилку. А он так и было, по сути. После одного такого падения ext4 ругнулся при загрузке, а у Postgres пришлось чистить WAL. Ничего не потерял, но понервничал. После второго — поехал за ИБП.&#xA;&#xA;Купил обычный линейно-интерактивный UPS с USB. Задача простая: продержать сервер пару минут и, если свет не вернулся, корректно его погасить — чтобы файловая система и базы закрывались штатно, а не аварийно.&#xA;&#xA;Софт для этого — NUT (Network UPS Tools). Он общается с ИБП по USB, понимает заряд и состояние сети, и дёргает shutdown по заданным правилам.&#xA;&#xA;Ставится из репозитория:&#xA;&#xA;sudo apt install nut&#xA;&#xA;Дальше три конфига. Сначала описываем сам ИБП в /etc/nut/ups.conf:&#xA;&#xA;[homeups]&#xA;    driver = usbhid-ups&#xA;    port = auto&#xA;    desc = &#34;Home server UPS&#34;&#xA;&#xA;Проверяем, что NUT его видит:&#xA;&#xA;sudo upsdrvctl start&#xA;upsc homeups&#xA;battery.charge: 100&#xA;ups.status: OL        &lt;- OL = на сети, OB = на батарее&#xA;&#xA;Самое интересное — логика выключения. Живёт в /etc/nut/upssched.conf и в скрипте-обработчике. Я не глушу сервер при первом же чихе сети: даю отлежаться, и реагирую либо по таймеру на батарее, либо по проценту заряда — что наступит раньше.&#xA;&#xA;/etc/nut/upssched.conf&#xA;CMDSCRIPT /etc/nut/upssched-cmd&#xA;&#xA;свет пропал — НЕ паникуем, ждём 60 секунд&#xA;AT ONBATT  START-TIMER onbattgrace 60&#xA;свет вернулся раньше — отменяем таймер&#xA;AT ONLINE  CANCEL-TIMER onbattgrace&#xA;UPS сам кричит &#34;заряд низкий&#34; — гасимся немедленно&#xA;AT LOWBATT * EXECUTE upsshutdown&#xA;&#xA;А вот сам обработчик, где добавляю ещё и порог по проценту — не хочу дожидаться, пока батарея сядет в ноль:&#xA;&#xA;!/bin/bash&#xA;/etc/nut/upssched-cmd&#xA;case $1 in&#xA;    onbattgrace)&#xA;        CHARGE=$(upsc homeups battery.charge)&#xA;        logger &#34;NUT: на батарее, заряд ${CHARGE}%&#34;&#xA;        # ниже 50% — не рискуем, гасимся&#xA;        if [ &#34;$CHARGE&#34; -le 50 ]; then&#xA;            /usr/sbin/upsmon -c fsd   # инициируем shutdown&#xA;        fi&#xA;        ;;&#xA;    ups_shutdown)&#xA;        logger &#34;NUT: LOWBATT, аварийное выключение&#34;&#xA;        /usr/sbin/upsmon -c fsd&#xA;        ;;&#xA;esac&#xA;&#xA;Идея такая: пропал свет — ждём минуту (вдруг это короткое моргание и всё вернётся). Если через минуту мы всё ещё на батарее и заряд просел до 50% — гасим заранее, с запасом. А если ИБП сам сигналит LOWBATT — выключаемся не раздумывая.&#xA;&#xA;Команда upsmon -c fsd (forced shutdown) запускает штатное выключение: systemd останавливает сервисы, контейнеры получают SIGTERM, базы закрываются как надо, ext4 размонтируется чисто.&#xA;&#xA;Проверять это удобно, выдернув ИБП из розетки и глядя в логи (journalctl -fu nut-monitor). У меня сервер на батарее в простое живёт минут двадцать, так что 50%-порог срабатывает сильно раньше — времени на корректное гашение вагон.&#xA;&#xA;С тех пор отключения света — не событие. Свет мигнул, сервер пережил, я даже не заметил. А если надолго — он сам аккуратно ляжет спать и утром поднимется без единого fsck.]]&gt;</description>
      <content:encoded><![CDATA[<p>Свет у меня моргает редко, но метко. Пару раз в год — короткое отключение на минуту-две, и каждый раз сервер падал жёстко, как будто выдернули вилку. А он так и было, по сути. После одного такого падения ext4 ругнулся при загрузке, а у Postgres пришлось чистить WAL. Ничего не потерял, но понервничал. После второго — поехал за ИБП.</p>

<p>Купил обычный линейно-интерактивный UPS с USB. Задача простая: продержать сервер пару минут и, если свет не вернулся, корректно его погасить — чтобы файловая система и базы закрывались штатно, а не аварийно.</p>

<p>Софт для этого — <strong>NUT</strong> (Network UPS Tools). Он общается с ИБП по USB, понимает заряд и состояние сети, и дёргает shutdown по заданным правилам.</p>

<p>Ставится из репозитория:</p>

<pre><code class="language-bash">sudo apt install nut
</code></pre>

<p>Дальше три конфига. Сначала описываем сам ИБП в <code>/etc/nut/ups.conf</code>:</p>

<pre><code class="language-ini">[homeups]
    driver = usbhid-ups
    port = auto
    desc = &#34;Home server UPS&#34;
</code></pre>

<p>Проверяем, что NUT его видит:</p>

<pre><code class="language-bash">sudo upsdrvctl start
upsc homeups
# battery.charge: 100
# ups.status: OL        &lt;- OL = на сети, OB = на батарее
</code></pre>

<p>Самое интересное — логика выключения. Живёт в <code>/etc/nut/upssched.conf</code> и в скрипте-обработчике. Я не глушу сервер при первом же чихе сети: даю отлежаться, и реагирую либо по таймеру на батарее, либо по проценту заряда — что наступит раньше.</p>

<pre><code class="language-ini"># /etc/nut/upssched.conf
CMDSCRIPT /etc/nut/upssched-cmd

# свет пропал — НЕ паникуем, ждём 60 секунд
AT ONBATT * START-TIMER onbatt_grace 60
# свет вернулся раньше — отменяем таймер
AT ONLINE * CANCEL-TIMER onbatt_grace
# UPS сам кричит &#34;заряд низкий&#34; — гасимся немедленно
AT LOWBATT * EXECUTE ups_shutdown
</code></pre>

<p>А вот сам обработчик, где добавляю ещё и порог по проценту — не хочу дожидаться, пока батарея сядет в ноль:</p>

<pre><code class="language-bash">#!/bin/bash
# /etc/nut/upssched-cmd
case $1 in
    onbatt_grace)
        CHARGE=$(upsc homeups battery.charge)
        logger &#34;NUT: на батарее, заряд ${CHARGE}%&#34;
        # ниже 50% — не рискуем, гасимся
        if [ &#34;$CHARGE&#34; -le 50 ]; then
            /usr/sbin/upsmon -c fsd   # инициируем shutdown
        fi
        ;;
    ups_shutdown)
        logger &#34;NUT: LOWBATT, аварийное выключение&#34;
        /usr/sbin/upsmon -c fsd
        ;;
esac
</code></pre>

<p>Идея такая: пропал свет — ждём минуту (вдруг это короткое моргание и всё вернётся). Если через минуту мы всё ещё на батарее и заряд просел до 50% — гасим заранее, с запасом. А если ИБП сам сигналит <code>LOWBATT</code> — выключаемся не раздумывая.</p>

<p>Команда <code>upsmon -c fsd</code> (forced shutdown) запускает штатное выключение: systemd останавливает сервисы, контейнеры получают SIGTERM, базы закрываются как надо, ext4 размонтируется чисто.</p>

<p>Проверять это удобно, выдернув ИБП из розетки и глядя в логи (<code>journalctl -fu nut-monitor</code>). У меня сервер на батарее в простое живёт минут двадцать, так что 50%-порог срабатывает сильно раньше — времени на корректное гашение вагон.</p>

<p>С тех пор отключения света — не событие. Свет мигнул, сервер пережил, я даже не заметил. А если надолго — он сам аккуратно ляжет спать и утром поднимется без единого fsck.</p>
]]></content:encoded>
      <guid>https://sparn.ru/ups-nut-shutdown</guid>
      <pubDate>Thu, 25 Jun 2026 13:49:26 +0000</pubDate>
    </item>
    <item>
      <title>Paperless-ngx: как я перестал бояться бумажек</title>
      <link>https://sparn.ru/paperless-ngx-skany</link>
      <description>&lt;![CDATA[У меня дома был ящик. Обычный ящик, куда годами падали договоры, чеки на технику, квитанции, справки и прочая бумага, которую «вдруг понадобится». Найти что-то в нём было невозможно. Решение — Paperless-ngx: сервис, который сканы бумажек превращает в нормальный искабельный архив.&#xA;&#xA;Поднял в Docker, как всё остальное, в /app/paperless/. Минимальный стек — само приложение, Redis и Postgres:&#xA;&#xA;services:&#xA;  paperless:&#xA;    image: paperlessngx/paperless-ngx:2.14.7&#xA;    dependson: [db, redis]&#xA;    environment:&#xA;      PAPERLESSREDIS: redis://redis:6379&#xA;      PAPERLESSDBHOST: db&#xA;      PAPERLESSOCRLANGUAGE: rus+eng&#xA;      PAPERLESSCONSUMERPOLLING: 30&#xA;    volumes:&#xA;      ./data:/usr/src/paperless/data&#xA;      ./media:/usr/src/paperless/media&#xA;      ./consume:/usr/src/paperless/consume&#xA;    ports:&#xA;      &#34;8000:8000&#34;&#xA;&#xA;Главная магия — папка consume. Кидаешь туда PDF или картинку, и Paperless её сам подхватывает: прогоняет через OCR (у меня rus+eng, потому что половина бумаг — на русском), распознаёт текст и складывает документ в архив. После этого по бумажке можно искать как по тексту: вбил «договор аренды 2023» — и вот он.&#xA;&#xA;Сканер у меня выдаёт в сетевую папку, которую я просто примонтировал в consume. То есть процесс целиком: положил лист в сканер, нажал кнопку — через минуту документ уже в архиве, распознан и проиндексирован. Руками не трогаю вообще.&#xA;&#xA;Дальше — организация. Тут две вещи, без которых архив быстро превращается в свалку:&#xA;&#xA;Корреспонденты — от кого/кому документ. Налоговая, банк, работодатель, магазин.&#xA;Теги — чеки, гарантия, медицина, авто. Можно навесить несколько.&#xA;&#xA;Самое приятное — Paperless умеет назначать их автоматически по правилам. Я один раз вручную разметил десяток чеков из одного магазина, и теперь он сам ставит тег чеки и нужного корреспондента на всё похожее. Чем больше документов — тем умнее угадывает.&#xA;&#xA;Отдельно про дату. Paperless вытаскивает дату прямо из текста документа, так что чек двухлетней давности встаёт в архив под своей реальной датой, а не под датой сканирования. Мелочь, а порядок наводит.&#xA;&#xA;Ну и про бэкап, потому что архив документов терять нельзя. Тут есть встроенный экспортёр, который выгружает всё в человекочитаемый вид (PDF + метаданные в JSON), а не только дамп базы:&#xA;&#xA;docker compose -f /app/paperless/docker-compose.yml \&#xA;  exec paperless documentexporter ../export&#xA;&#xA;Эту папку export я гоню в свой обычный ночной бэкап вместе с остальными томами. Если завтра сервер сгорит — восстановлю не дамп непонятного формата, а нормальные PDF с тегами.&#xA;&#xA;Бумажный ящик, кстати, я не выбросил. Но открываю его теперь раз в год, чтобы отсканировать накопившееся и снова про него забыть.]]&gt;</description>
      <content:encoded><![CDATA[<p>У меня дома был ящик. Обычный ящик, куда годами падали договоры, чеки на технику, квитанции, справки и прочая бумага, которую «вдруг понадобится». Найти что-то в нём было невозможно. Решение — Paperless-ngx: сервис, который сканы бумажек превращает в нормальный искабельный архив.</p>

<p>Поднял в Docker, как всё остальное, в <code>/app/paperless/</code>. Минимальный стек — само приложение, Redis и Postgres:</p>

<pre><code class="language-yaml">services:
  paperless:
    image: paperlessngx/paperless-ngx:2.14.7
    depends_on: [db, redis]
    environment:
      PAPERLESS_REDIS: redis://redis:6379
      PAPERLESS_DBHOST: db
      PAPERLESS_OCR_LANGUAGE: rus+eng
      PAPERLESS_CONSUMER_POLLING: 30
    volumes:
      - ./data:/usr/src/paperless/data
      - ./media:/usr/src/paperless/media
      - ./consume:/usr/src/paperless/consume
    ports:
      - &#34;8000:8000&#34;
</code></pre>

<p>Главная магия — папка <code>consume</code>. Кидаешь туда PDF или картинку, и Paperless её сам подхватывает: прогоняет через OCR (у меня <code>rus+eng</code>, потому что половина бумаг — на русском), распознаёт текст и складывает документ в архив. После этого по бумажке можно искать как по тексту: вбил «договор аренды 2023» — и вот он.</p>

<p>Сканер у меня выдаёт в сетевую папку, которую я просто примонтировал в <code>consume</code>. То есть процесс целиком: положил лист в сканер, нажал кнопку — через минуту документ уже в архиве, распознан и проиндексирован. Руками не трогаю вообще.</p>

<p>Дальше — организация. Тут две вещи, без которых архив быстро превращается в свалку:</p>
<ul><li><strong>Корреспонденты</strong> — от кого/кому документ. Налоговая, банк, работодатель, магазин.</li>
<li><strong>Теги</strong> — <code>чеки</code>, <code>гарантия</code>, <code>медицина</code>, <code>авто</code>. Можно навесить несколько.</li></ul>

<p>Самое приятное — Paperless умеет назначать их автоматически по правилам. Я один раз вручную разметил десяток чеков из одного магазина, и теперь он сам ставит тег <code>чеки</code> и нужного корреспондента на всё похожее. Чем больше документов — тем умнее угадывает.</p>

<p>Отдельно про дату. Paperless вытаскивает дату прямо из текста документа, так что чек двухлетней давности встаёт в архив под своей реальной датой, а не под датой сканирования. Мелочь, а порядок наводит.</p>

<p>Ну и про бэкап, потому что архив документов терять нельзя. Тут есть встроенный экспортёр, который выгружает всё в человекочитаемый вид (PDF + метаданные в JSON), а не только дамп базы:</p>

<pre><code class="language-bash">docker compose -f /app/paperless/docker-compose.yml \
  exec paperless document_exporter ../export
</code></pre>

<p>Эту папку <code>export</code> я гоню в свой обычный ночной бэкап вместе с остальными томами. Если завтра сервер сгорит — восстановлю не дамп непонятного формата, а нормальные PDF с тегами.</p>

<p>Бумажный ящик, кстати, я не выбросил. Но открываю его теперь раз в год, чтобы отсканировать накопившееся и снова про него забыть.</p>
]]></content:encoded>
      <guid>https://sparn.ru/paperless-ngx-skany</guid>
      <pubDate>Tue, 23 Jun 2026 13:29:19 +0000</pubDate>
    </item>
    <item>
      <title>Как Watchtower сломал мне сервис в три часа ночи</title>
      <link>https://sparn.ru/watchtower-vs-ruchnoe</link>
      <description>&lt;![CDATA[Долгое время у меня в стеке крутился Watchtower — штука, которая сама следит за обновлениями образов и тихо подтягивает новые версии. Идея красивая: ты ничего не делаешь, а контейнеры всегда свежие. На практике эта красота однажды разбудила меня в три часа ночи уведомлением «сервис недоступен».&#xA;&#xA;Что произошло. У одного из контейнеров вышел новый тег latest, и это была мажорная версия — со сменой формата конфига и схемы базы. Watchtower честно сделал свою работу: выкачал новый образ, снёс старый контейнер, поднял новый. Новый стартанул, увидел старый конфиг, не понял его и упал. И так по кругу — рестарт, падение, рестарт. К утру лог был размером с небольшую книгу.&#xA;&#xA;Сам виноват, конечно. latest — это не версия, это лотерея. Ты подписываешься на «что разработчик зальёт следующим», а он может залить что угодно.&#xA;&#xA;После этого Watchtower я выключил. Совсем:&#xA;&#xA;docker compose -f /app/watchtower/docker-compose.yml down&#xA;&#xA;Теперь обновляюсь руками и осознанно. Во-первых, везде прибил конкретные теги вместо latest:&#xA;&#xA;services:&#xA;  paperless:&#xA;    # было: image: paperlessngx/paperless-ngx:latest&#xA;    image: paperlessngx/paperless-ngx:2.14.7&#xA;&#xA;Во-вторых, завёл простой ритуал. Раз в пару недель смотрю, что вышло:&#xA;&#xA;что вообще доступно&#xA;docker compose -f /app/сервис/docker-compose.yml pull --dry-run&#xA;&#xA;а потом обязательно читаю changelog/release notes на гитхабе&#xA;&#xA;И только если в release notes нет слов про «breaking changes», «migration» и «backup your data before upgrade» — обновляюсь. А если есть — сначала делаю бэкап томов, потом обновляю, потом проверяю, что всё живо.&#xA;&#xA;бэкап перед апгрейдом — стало рефлексом&#xA;docker run --rm -v paperlessdata:/data -v $(pwd):/backup \&#xA;  alpine tar czf /backup/paperlessdata_$(date +%F).tar.gz -C /data .&#xA;&#xA;Да, это медленнее. Да, иногда я неделю сижу на «старой» версии. Зато я точно знаю, что именно и когда поменялось, и если что-то сломается — это будет в удобное мне время, а не в 3:00 под аккомпанемент пуш-уведомлений.&#xA;&#xA;Автоматизация хороша ровно до того момента, пока она не автоматизирует катастрофу. Обновление сервисов — как раз тот случай, где я предпочитаю держать руку на рубильнике сам.]]&gt;</description>
      <content:encoded><![CDATA[<p>Долгое время у меня в стеке крутился Watchtower — штука, которая сама следит за обновлениями образов и тихо подтягивает новые версии. Идея красивая: ты ничего не делаешь, а контейнеры всегда свежие. На практике эта красота однажды разбудила меня в три часа ночи уведомлением «сервис недоступен».</p>

<p>Что произошло. У одного из контейнеров вышел новый тег <code>latest</code>, и это была мажорная версия — со сменой формата конфига и схемы базы. Watchtower честно сделал свою работу: выкачал новый образ, снёс старый контейнер, поднял новый. Новый стартанул, увидел старый конфиг, не понял его и упал. И так по кругу — рестарт, падение, рестарт. К утру лог был размером с небольшую книгу.</p>

<p>Сам виноват, конечно. <code>latest</code> — это не версия, это лотерея. Ты подписываешься на «что разработчик зальёт следующим», а он может залить что угодно.</p>

<p>После этого Watchtower я выключил. Совсем:</p>

<pre><code class="language-bash">docker compose -f /app/watchtower/docker-compose.yml down
</code></pre>

<p>Теперь обновляюсь руками и осознанно. Во-первых, везде прибил конкретные теги вместо <code>latest</code>:</p>

<pre><code class="language-yaml">services:
  paperless:
    # было: image: paperlessngx/paperless-ngx:latest
    image: paperlessngx/paperless-ngx:2.14.7
</code></pre>

<p>Во-вторых, завёл простой ритуал. Раз в пару недель смотрю, что вышло:</p>

<pre><code class="language-bash"># что вообще доступно
docker compose -f /app/&lt;сервис&gt;/docker-compose.yml pull --dry-run

# а потом обязательно читаю changelog/release notes на гитхабе
</code></pre>

<p>И только если в release notes нет слов про «breaking changes», «migration» и «backup your data before upgrade» — обновляюсь. А если есть — сначала делаю бэкап томов, потом обновляю, потом проверяю, что всё живо.</p>

<pre><code class="language-bash"># бэкап перед апгрейдом — стало рефлексом
docker run --rm -v paperless_data:/data -v $(pwd):/backup \
  alpine tar czf /backup/paperless_data_$(date +%F).tar.gz -C /data .
</code></pre>

<p>Да, это медленнее. Да, иногда я неделю сижу на «старой» версии. Зато я точно знаю, что именно и когда поменялось, и если что-то сломается — это будет в удобное мне время, а не в 3:00 под аккомпанемент пуш-уведомлений.</p>

<p>Автоматизация хороша ровно до того момента, пока она не автоматизирует катастрофу. Обновление сервисов — как раз тот случай, где я предпочитаю держать руку на рубильнике сам.</p>
]]></content:encoded>
      <guid>https://sparn.ru/watchtower-vs-ruchnoe</guid>
      <pubDate>Sun, 21 Jun 2026 15:02:15 +0000</pubDate>
    </item>
  </channel>
</rss>