Продвинутый

Вайбкодерские идеи для домашнего сервера: 5 практических...

Алексей Кузнецов
Алексей Кузнецов
Системный администратор10 июня 2026 г.21 мин чтения

Соберите полезные сервисы на собственном сервере уже сегодня: медиаконтент, личный облачный хостинг, мониторинг, автоматизация и AI‑ассистент.

Вайбкодерские идеи для домашнего сервера: 5 практических приложений

Соберите полезные сервисы на собственном сервере уже сегодня: медиаконтент, личный облачный хостинг, мониторинг, автоматизация и AI-ассистент.

Медиа-центр с интеллектуальными рекомендациями

В домашней лаборатории я часто использую Plex как медиаплеер. Сам по себе Plex умеет транскодировать, хранить субтитры и отдавать контент по LAN/Internet, но без рекомендаций он напоминает обычный медиаплеер. Добавим к нему слой «умного» поиска — будем запрашивать TMDB API, кэшировать результаты в Redis и подсовывать их Plex через web-hooks. Всё это удобно собрать в Docker-Compose, а управление транскодировкой и субтитрами делаем через plextranscoder-контейнер.

Во-первых, убедитесь, что ваш сервер (у меня «Винни-Пух») имеет открытый порт 32400 и статический IP в локальной сети.
Во-вторых, подготовьте отдельный том /mnt/media — в нём будут лежать фильмы и сериалы. На TrueNAS это NFS-шар, экспортированный как media.

Развёртывание Plex + интеграция с TMDB API

  1. Создайте файл .env – в нём храним токен TMDB и параметры Redis.
dockerfile
# .env
TMDB_API_KEY=your_tmdb_api_key_here
REDIS_HOST=redis
REDIS_PORT=6379
  1. docker-compose.yml – три сервиса: Plex, Redis и небольшое приложение-посредник tmdb-proxy.
yaml
version: "3.9"

services:
  plex:
    image: linuxserver/plex:latest
    container_name: plex
    network_mode: host
    environment:
      - PUID=1000
      - PGID=1000
      - VERSION=docker
    volumes:
      - /mnt/media:/data
      - /opt/plex/config:/config
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    container_name: redis
    command: ["redis-server", "--save", "60", "1"]
    volumes:
      - redis-data:/data
    restart: unless-stopped

  tmdb-proxy:
    image: python:3.12-slim
    container_name: tmdb-proxy
    working_dir: /app
    environment:
      - TMDB_API_KEY=${TMDB_API_KEY}
      - REDIS_HOST=${REDIS_HOST}
      - REDIS_PORT=${REDIS_PORT}
    volumes:
      - ./tmdb-proxy:/app
    command: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
    restart: unless-stopped
    depends_on:
      - redis

volumes:
  redis-data:
  1. Код tmdb-proxy (папка tmdb-proxy). Простейший FastAPI-сервис, который получает запрос от Plex-вебхука, ищет фильм в TMDB, кэширует результат в Redis и отсылает обратно JSON-пейлоады.
python
# tmdb-proxy/main.py
import os
import aiohttp
import aioredis
from fastapi import FastAPI, Request

app = FastAPI()
redis = None

@app.on_event("startup")
async def startup():
    global redis
    redis = await aioredis.from_url(
        f"redis://{os.getenv('REDIS_HOST')}:{os.getenv('REDIS_PORT')}"
    )

@app.get("/recommend")
async def recommend(title: str):
    cache_key = f"tmdb:{title.lower()}"
    cached = await redis.get(cache_key)
    if cached:
        return {"source": "cache", "data": cached.decode()}

    async with aiohttp.ClientSession() as session:
        params = {
            "api_key": os.getenv("TMDB_API_KEY"),
            "query": title,
            "language": "ru-RU"
        }
        async with session.get(
            "https://api.themoviedb.org/3/search/movie", params=params
        ) as resp:
            data = await resp.json()
            await redis.set(cache_key, str(data), ex=86400)  # кэш на сутки
            return {"source": "api", "data": data}
  1. Подключаем webhook в Plex: в веб-интерфейсе Plex → Settings → Webhooks добавить URL http://<IP_сервера>:8000/recommend?title={title}. При каждом запросе Plex передаст название, а наш сервис вернёт рекомендации, которые можно отобразить в клиенте через пользовательскую панель (например, с помощью plexpy-плагина).

С практической точки зрения, такой подход даёт предсказуемость: если TMDB недоступен, Redis отдает старый результат, а Plex продолжает работать без задержек.

Docker-Compose для автоматической транскодировки и субтитров

Для домашнего кластера я использую plextranscoder-контейнер, который следит за новыми медиа-файлами, запускает ffmpeg-транскодинг в нужный профиль и скачивает субтитры из OpenSubtitles. Всё это тоже укладывается в один docker-compose.yml.

  1. Дополнительный сервис в том же файле (добавляем после tmdb-proxy):
yaml
  transcoder:
    image: ghcr.io/jellyfin/jellyfin-ffmpeg:latest
    container_name: plex-transcoder
    volumes:
      - /mnt/media:/media
      - ./transcode-scripts:/scripts
    environment:
      - TZ=Europe/Moscow
    command: ["/scripts/watch.sh"]
    restart: unless-stopped
    depends_on:
      - plex
  1. Скрипт watch.sh – наблюдает за каталогом /media и при появлении нового файла запускает ffmpeg.
bash
#!/usr/bin/env bash
inotifywait -m -e close_write --format '%w%f' /media | while read FILE; do
  echo "Новый файл: $FILE"
  # Если файл > 4 ГБ, делаем 1080p-транскод
  if [[ $(stat -c%s "$FILE") -gt $((4*1024*1024*1024)) ]]; then
    out="${FILE%.*}_1080p.mkv"
    ffmpeg -i "$FILE" -c:v libx264 -preset fast -crf 23 -c:a aac -b:a 192k "$out"
    echo "Транскод завершён: $out"
  fi

  # Скачиваем субтитры (OpenSubtitles API)
  title=$(basename "$FILE" | sed -E 's/[\._].*//')
  curl -s "https://api.opensubtitles.com/api/v1/subtitles?query=${title}&languages=ru" \
    -H "Api-Key: your_opensubtitles_key" \
    -o "/media/${title}.srt"
done

Важно: перед запуском установите права выполнения chmod +x watch.sh и проверьте, что пользователь внутри контейнера имеет доступ к /mnt/media.

  1. Запуск
bash
docker compose up -d plex redis tmdb-proxy transcoder

После старта все новые фильмы автоматически транскодируются в 1080p (если исходный размер превышает 4 ГБ) и снабжаются русскими субтитрами. Plex сразу их «видит», а рекомендации от TMDB уже находятся в кэше, поэтому пользователь получает персонализированный список похожих фильмов без дополнительных кликов.


Вывод
Сочетание Plex, TMDB-проكسي и автоматического транскодера в одном docker-compose-стеке даёт предсказуемую, масштабируемую и полностью контролируемую медиаплатформу. Всё работает без GUI-инструментов, а конфиги находятся в обычных текстовых файлах – так легче поддерживать и документировать. При необходимости добавить новые сервисы (например, Jellyfin-клиент или Grafana-дашборд для мониторинга нагрузки) достаточно расширить docker-compose.yml и добавить соответствующие systemd-таймеры для резервных копий.

См. подробнее о подходе к «интеллектуальному» домашнему серверу в статье Что ещё полезного можно сделать со своим сервером — Журнал «Код».

Личный облачный хостинг Nextcloud с расширенной безопасностью

В домашней лаборатории Nextcloud часто становится «единой точкой входа» для файлов, календарей и контактов. Чтобы эта точка была предсказуемой и устойчива к внешним угрозам, я обычно делаю два шага: настраиваю HTTPS через Caddy с автоматическим сертификатом Let’s Encrypt и включаю двухфакторную аутентификацию + сканер вредоносных файлов ClamAV. Ниже – пошаговый набор команд и конфигов, проверенный на ноде «Винни-Пух» (Ubuntu 22.04 LTS).

Конфигурация HTTPS через Caddy с автоматическим сертификатом Let’s Encrypt

  1. Установить Caddy – в репозитории Ubuntu уже есть готовый пакет, но я предпочитаю официальные бинарники, чтобы сразу получить последнюю версию.
bash
# скачиваем и ставим последнюю стабильную сборку
curl -1Lf https://github.com/caddyserver/caddy/releases/download/v2.8.4/caddy_2.8.4_linux_amd64.tar.gz \
    -o caddy.tar.gz
sudo tar -xzf caddy.tar.gz -C /usr/local/bin caddy
sudo chmod +x /usr/local/bin/caddy

# создаём systemd-службу
cat <<'EOF' | sudo tee /etc/systemd/system/caddy.service
[Unit]
Description=Caddy web server
After=network.target

[Service]
User=root
Group=root
ExecStart=/usr/local/bin/caddy run --environ --config /etc/caddy/Caddyfile
ExecReload=/usr/local/bin/caddy reload --config /etc/caddy/Caddyfile
TimeoutStopSec=5s
LimitNOFILE=1048576
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
ReadWriteDirectories=/var/www /etc/caddy

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now caddy
  1. Создать Caddyfile – Caddy умеет получать сертификат Let’s Encrypt «из коробки». Важно, чтобы DNS-запись nextcloud.example.com уже указывала на ваш публичный IP.
bash
sudo mkdir -p /etc/caddy
cat <<'EOF' | sudo tee /etc/caddy/Caddyfile
nextcloud.example.com {
    reverse_proxy 127.0.0.1:8080               # порт, где работает контейнер Nextcloud
    encode gzip
    file_server

    # Принудительно включаем HSTS и безопасные заголовки
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "SAMEORIGIN"
        Referrer-Policy "no-referrer-when-downgrade"
    }

    # Автоматический редирект HTTP→HTTPS
    @http {
        protocol http
    }
    redir @http https://{host}{uri} permanent
}
EOF
  1. Перезапустить Caddy
bash
sudo systemctl reload caddy

После этого Caddy запросит сертификат у Let’s Encrypt, разместит его в /root/.caddy и будет автоматически обновлять каждые 90 дней. Проверить можно командой curl -I https://nextcloud.example.com, в заголовке Strict-Transport-Security должно быть значение из конфига.

Подробнее о возможностях Caddy см. в официальной документации — Getting Started with Caddy.

Включение двухфактории и сканера вредоносных файлов ClamAV

2.1 Двухфакторная аутентификация (2FA) в Nextcloud

Nextcloud уже поставляется с поддержкой OTP-токенов (Google Authenticator, Authy и т.п.). Чтобы включить её глобально:

  1. Зайти в админ-панель → «Безопасность» → «Двухфакторная аутентификация».
  2. Отметить «Включить двухфакторную аутентификацию» и выбрать провайдер «TOTP».
  3. Сохранить – каждый пользователь получит QR-код при первом входе.

Для автоматизации можно задать параметр в config.php:

php
<?php
$CONFIG = array (
  'twofactor_enforced' => true,   // обязательная 2FA для всех пользователей
  'twofactor_enforced_groups' => array('admin', 'family'), // если нужно только для групп
);

Перезапуск PHP-фпм (если используется) гарантирует применение:

bash
sudo systemctl reload php8.2-fpm

2.2 ClamAV + Nextcloud — сканирование загружаемых файлов

  1. Установить ClamAV и запустить демона.
bash
sudo apt update
sudo apt install -y clamav clamav-daemon
sudo systemctl enable --now clamav-daemon
  1. Обновить сигнатуры (можно добавить в cron, но clamav-freshclam уже ставит таймер).
bash
sudo freshclam
  1. Подключить приложение «Antivirus for files» в Nextcloud (Marketplace → «Antivirus»). После установки в настройках указываем:

    • BackendClamAV daemon
    • Host127.0.0.1
    • Port3310
    • Maximum file size100 MB (по желанию)
  2. Проверить работу – загрузите тестовый файл eicar.com (стандартный тест-вирус). Если всё настроено, Nextcloud откажет в загрузке и в журнале появится запись вида:

[2026-06-10 12:34:56.789] security WARNING: File eicar.com blocked by ClamAV (EICAR-Test-File)

2.3 Автоматический сканинг существующего хранилища

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

bash
#!/usr/bin/env bash
NEXTCLOUD_DATA="/var/www/nextcloud/data"
find "$NEXTCLOUD_DATA" -type f -print0 | while IFS= read -r -d '' file; do
    if clamscan -i "$file" | grep -q FOUND; then
        echo "Infected: $file"
        # перемещаем в карантин
        mv "$file" "${file}.quarantine"
    fi
done

Запускаем от пользователя www-data (или того, под которым работает PHP), чтобы иметь доступ к файлам:

bash
sudo -u www-data bash scan_nextcloud.sh

2.4 Мониторинг статуса ClamAV

Для предсказуемой работы я добавляю простой systemd-таймер, который проверяет, что clamav-daemon отвечает:

ini
# /etc/systemd/system/clamav-health.service
[Unit]
Description=Check ClamAV daemon health
After=network.target

[Service]
Type=oneshot
ExecStart=/usr/bin/clamdscan --ping
ExecStartPost=/usr/bin/logger -t clamav-health "Daemon OK"
ExecStartPost=-/usr/bin/logger -t clamav-health "Daemon NOT responding"

# /etc/systemd/system/clamav-health.timer
[Unit]
Description=Run ClamAV health check every 5 minutes

[Timer]
OnBootSec=1min
OnUnitActiveSec=5min
Persistent=true

[Install]
WantedBy=timers.target
bash
sudo systemctl enable --now clamav-health.timer

Теперь в journalctl -u clamav-health будет видно, когда демон недоступен, и можно настроить алерт в Grafana/Prometheus.


Итого: Caddy берёт на себя всё, что касается TLS, а встроенный в Nextcloud модуль 2FA + отдельный сканер ClamAV закрывают два главных вектора атак — краже учётных данных и загрузке вредоносного кода. Такой набор конфигураций легко масштабировать на остальные ноды кластера («Пятачок», «Сова»), а благодаря systemd-юнитам всё остаётся предсказуемым и документированным.

Система мониторинга и алертинга для домашней инфраструктуры

Prometheus + node_exporter + blackbox_exporter в k3s

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

1. Подготовка кластера

bash
# Установим k3s (скрипт от Rancher)
curl -sfL https://get.k3s.io | sh -

# Проверим, что кластер работает
kubectl get nodes

Совет: если планируете использовать несколько нод, добавьте их в файл /etc/rancher/k3s/k3s.yaml через параметр --server / --agent.

2. Деплой Prometheus и экспортеров через Helm

bash
# Добавим репозиторий prometheus-community
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update

# Установим chart с минимальными настройками
helm install monitoring prometheus-community/kube-prometheus-stack \
  --namespace monitoring --create-namespace \
  --set prometheus.prometheusSpec.serviceMonitorSelectorNilUsesHelmValues=false

Chart уже включает node_exporter. Для blackbox_exporter добавим отдельный ServiceMonitor.

yaml
# blackbox-exporter.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: blackbox-exporter
  namespace: monitoring
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: blackbox-exporter
  endpoints:
  - port: http
    path: /probe
    params:
      module: [http_2xx]
    relabelings:
    - sourceLabels: [__address__]
      targetLabel: __param_target
    - sourceLabels: [__param_target]
      targetLabel: instance
    - targetLabel: __address__
      replacement: blackbox-exporter.monitoring.svc.cluster.local:9115
bash
# Применяем манифест
kubectl apply -f blackbox-exporter.yaml

3. Пример правила проверки доступности домашнего Plex-сервера

yaml
# plex-availability.yaml
apiVersion: monitoring.coreos.com/v1
kind: Probe
metadata:
  name: plex-http
  namespace: monitoring
spec:
  jobName: "blackbox"
  metricsPath: /probe
  params:
    module: [http_2xx]
  staticConfig:
    static:
    - targets:
      - "plex.home.local:32400"
bash
kubectl apply -f plex-availability.yaml

После применения правило появится в Prometheus, и вы сможете построить график probe_success{instance="plex.home.local:32400"}.

4. Хранение метрик

Для небольших домашних кластеров хватает локального persistentVolume. В Helm-чарте укажите:

yaml
prometheus:
  prometheusSpec:
    storageSpec:
      volumeClaimTemplate:
        spec:
          storageClassName: local-path
          accessModes: ["ReadWriteOnce"]
          resources:
            requests:
              storage: 20Gi

Grafana-дашборды

Grafana уже включена в kube-prometheus-stack. Доступ получаем через kubectl port-forward:

bash
kubectl -n monitoring port-forward svc/grafana 3000:80
# логин admin / пароль admin (после первого входа сразу смените)

1. Импорт готового дашборда

В официальном репозитории Grafana есть набор «Home-Lab Overview» (ID = 1860). Чтобы импортировать:

  1. Откройте «Create → Import».
  2. Введите 1860 и нажмите «Load».
  3. Укажите datasource Prometheus и сохраните.

2. Настройка собственного дашборда для Plex

json
{
  "title": "Plex Availability",
  "panels": [
    {
      "type": "graph",
      "title": "HTTP-probe success",
      "targets": [
        {
          "expr": "probe_success{instance=\"plex.home.local:32400\"}",
          "legendFormat": "{{instance}}"
        }
      ],
      "yaxes": [{ "format": "percent", "min": "0", "max": "1" }]
    }
  ]
}

Сохраните JSON в файл plex-dashboard.json и импортируйте через UI.


Alertmanager + Telegram

Alertmanager отвечает за доставку уведомлений. В k3s он уже развернут в составе kube-prometheus-stack. Нужно лишь добавить канал Telegram.

1. Получаем Bot-токен

Создайте бота в @BotFather, запомните токен, затем запросите chat_id у @userinfobot.

2. Секрет с токеном

bash
kubectl -n monitoring create secret generic telegram-secret \
  --from-literal=bot-token='123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11'

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

yaml
# alertmanager-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: alertmanager-config
  namespace: monitoring
data:
  alertmanager.yml: |
    global:
      resolve_timeout: 5m
    route:
      receiver: telegram
      group_wait: 30s
      group_interval: 5m
      repeat_interval: 1h
    receivers:
    - name: telegram
      webhook_configs:
      - url: 'https://api.telegram.org/bot{{ .Values.telegram.botToken }}/sendMessage'
        send_resolved: true
        http_config:
          bearer_token_file: /etc/alertmanager/secrets/telegram-token
        relabel_configs:
        - source_labels: [__alert_name__]
          target_label: text
          replacement: '⚠️ {{ .AlertName }}'
        - source_labels: [__alert_message__]
          target_label: chat_id
          replacement: '123456789'   # ваш chat_id
bash
# Применяем ConfigMap
kubectl apply -f alertmanager-config.yaml

# Перезапускаем Alertmanager, чтобы он подхватил изменения
kubectl -n monitoring rollout restart deployment/alertmanager-monitoring-alertmanager

4. Пример правила алерта для Plex

yaml
# plex-alert.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: plex-alert
  namespace: monitoring
spec:
  groups:
  - name: plex.rules
    rules:
    - alert: PlexDown
      expr: probe_success{instance="plex.home.local:32400"} == 0
      for: 2m
      labels:
        severity: critical
      annotations:
        summary: "Plex недоступен"
        description: "HTTP-пробе к Plex не возвращает 2xx более 2 минут"
bash
kubectl apply -f plex-alert.yaml

После того как правило сработает, Alertmanager отправит сообщение в Telegram:

⚠️ PlexDown
Plex недоступен
HTTP-пробе к Plex не возвращает 2xx более 2 минут

Итоги

  • Prometheus + node_exporter + blackbox_exporter в k3s дают полную видимость как системных метрик, так и доступности сервисов.
  • Grafana превращает сырые метрики в удобные дашборды; импорт готовых шаблонов ускоряет старт.
  • Alertmanager с Telegram-каналом обеспечивает мгновенную реакцию на сбои, а хранение правил в виде PrometheusRule делает конфигурацию декларативной и версионируемой.

Эта комбинация проверена в моей домашней лаборатории — сервер «Сова» уже более года собирает метрики без простоев, а уведомления в Telegram позволяют реагировать в реальном времени, не теряя предсказуемости инфраструктуры.

Автоматический бэкап семейных фото и видео на ZFS-пул

Автоматический бэкап семейных фото и видео на ZFS-пул

Семейные медиа-файлы часто становятся самым ценным «активом» домашней лаборатории. Их потеря — это не просто неудобство, а реальная эмоциональная нагрузка. Ниже описываю проверенный процесс: снимки ZFS-пула реплицируются в удалённое S3-совместимое хранилище, а systemd-таймер проверяет целостность и отсылает отчёт на почту.


Настройка репликации sнимка между TrueNAS и удалённым S3-совместимым хранилищем

1. Подготовка ZFS-пула
На TrueNAS (у меня сервер «Винни-Пух») пул называется tank. В нём хранится каталог photos — точка монтирования /mnt/tank/photos. Чтобы репликация была предсказуемой, включаем автоматическое создание снимков каждый день:

bash
# /etc/cron.d/zfs-snapshots
0 3 * * * root zfs snapshot -r tank/photos@$(date +\%Y-\%m-\%d)

Почему cron, а не systemd?
Снимок — это одноразовое действие, а cron гарантирует, что он будет выполнен даже если система будет перезагружена в момент запуска таймера.

2. Установка s3cmd
Для отправки данных в S3-совместимый бакет (я использую Backblaze B2) ставим лёгкий клиент s3cmd:

bash
apt-get update && apt-get install -y s3cmd
s3cmd --configure
# В интерактивном режиме вводим:
# Access Key, Secret Key, Endpoint (https://s3.us-west-002.backblazeb2.com), …

3. Скрипт репликации
Создаём отдельный скрипт, который будет искать новые снимки, экспортировать их в zfs send, а затем загружать в бакет. Файл сохраняем как /usr/local/bin/zfs-s3-replicate.sh и делаем исполняемым.

bash
#!/usr/bin/env bash
set -euo pipefail

POOL="tank"
DATASET="photos"
REMOTE_BUCKET="s3://myfamily-backup/photos"
LOGFILE="/var/log/zfs-s3-replicate.log"

# Получаем список уже отправленных снимков
sent=$(s3cmd ls "${REMOTE_BUCKET}/" | awk '{print $4}' | sed 's#.*/##' | sort)

# Находим новые локальные снимки
for snap in $(zfs list -t snapshot -o name -H "${POOL}/${DATASET}" | sort); do
    name=$(basename "${snap}")
    if [[ " ${sent[@]} " =~ " ${name} " ]]; then
        continue
    fi

    echo "$(date +%F\ %T) – отправляю ${snap}" >> "${LOGFILE}"
    zfs send -R "${snap}" | s3cmd put - "${REMOTE_BUCKET}/${name}.zfs"
    echo "$(date +%F\ %T) – завершено ${snap}" >> "${LOGFILE}"
done

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

4. Systemd-служба и таймер
Чтобы запускать репликацию каждый вечер, описываем unit-файл:

ini
# /etc/systemd/system/zfs-s3-replicate.service
[Unit]
Description=Репликация ZFS-сайпшотов в S3
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/zfs-s3-replicate.sh
ini
# /etc/systemd/system/zfs-s3-replicate.timer
[Unit]
Description=Таймер для zfs-s3-replicate.service

[Timer]
OnCalendar=*-*-* 04:00:00
Persistent=true

[Install]
WantedBy=timers.target
bash
systemctl daemon-reload
systemctl enable --now zfs-s3-replicate.timer

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

5. Проверка
После первого запуска убедитесь, что в бакете появились файлы вида 2024-11-02.zfs. Можно быстро проверить целостность, скачав один из них и сравнив контрольные суммы:

bash
s3cmd get s3://myfamily-backup/photos/2024-11-02.zfs /tmp/
zfs receive -Fv tank/photos < /tmp/2024-11-02.zfs

Скрипт systemd-таймера, который проверяет целостность и отправляет отчёт

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

1. Генерация и хранение контрольных сумм
При каждой репликации скрипт выше уже пишет лог. Добавим в него запись SHA-256 хеша:

bash
# Внутри цикла после отправки
hash=$(zfs send -R "${snap}" | sha256sum | awk '{print $1}')
echo "hash=${hash}" >> "${LOGFILE}"

Для удобства сохраняем хеши в отдельный файл на TrueNAS:

bash
# После завершения репликации
echo "${name} ${hash}" >> /var/lib/zfs-s3-replicate/hashes.db

2. Проверочный скрипт

bash
#!/usr/bin/env bash
set -euo pipefail

REMOTE_BUCKET="s3://myfamily-backup/photos"
HASH_DB="/var/lib/zfs-s3-replicate/hashes.db"
TMPDIR=$(mktemp -d)
REPORT="/tmp/zfs-backup-report.txt"
MAILTO="alex@example.com"

> "${REPORT}"

while read -r name hash; do
    echo "Проверяю ${name}…" >> "${REPORT}"
    s3cmd get "${REMOTE_BUCKET}/${name}.zfs" "${TMPDIR}/${name}.zfs" >/dev/null 2>&1
    calc=$(sha256sum "${TMPDIR}/${name}.zfs" | awk '{print $1}')
    if [[ "${calc}" == "${hash}" ]]; then
        echo "  OK (hash=${calc})" >> "${REPORT}"
    else
        echo "  ❗ Ошибка: ожидается ${hash}, получено ${calc}" >> "${REPORT}"
    fi
    rm -f "${TMPDIR}/${name}.zfs"
done < "${HASH_DB}"

# Отправляем отчёт
if command -v mailx >/dev/null; then
    mailx -s "Отчёт о бэкапе ZFS ($(date +%F))" "${MAILTO}" < "${REPORT}"
else
    echo "mailx не установлен, отчёт сохранён в ${REPORT}"
fi

rm -rf "${TMPDIR}"

Сохраняем как /usr/local/bin/zfs-s3-verify.sh и делаем исполняемым.

3. Таймер для проверки

ini
# /etc/systemd/system/zfs-s3-verify.service
[Unit]
Description=Проверка целостности ZFS-сайпшотов в S3
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/zfs-s3-verify.sh
ini
# /etc/systemd/system/zfs-s3-verify.timer
[Unit]
Description=Таймер для zfs-s3-verify.service

[Timer]
OnCalendar=*-*-* 06:00:00
Persistent=true

[Install]
WantedBy=timers.target
bash
systemctl daemon-reload
systemctl enable --now zfs-s3-verify.timer

4. Что проверять в отчёте

  • отсутствие строк ❗ Ошибка — полный успех;
  • наличие новых записей в hashes.db — это значит, что репликация прошла, но проверка ещё не выполнена;
  • время выполнения — если процесс занимает более 30 минут, стоит увеличить интервал между репликациями.

Итоги

  • Репликация снимков в S3 обеспечивает географическую изоляцию и экономит место на локальном пуле.
  • Systemd-таймеры гарантируют предсказуемый запуск независимо от состояния системы.
  • Хеш-контроль и автоматический e-mail-отчёт позволяют быстро реагировать на любые сбои, а журнал zfs-s3-replicate.log служит единой точкой аудита.

Эти небольшие, но надёжные кусочки инфраструктуры уже несколько лет защищают наши семейные фото от «провалов» и дают уверенность в том, что ни один кадр не исчезнет без следа.

AI-ассистент для домашней автоматизации и контент-генерации

AI-ассистент для домашней автоматизации и контент-генерации

В этом разделе я покажу, как превратить домашний сервер в «мозг» вашего умного дома и одновременно в генератор текста/кода. Всё разворачивается в контейнерах, управляется systemd и интегрируется с Home Assistant через webhook.


Развёртывание локального LLM (llama.cpp) в контейнере

Почему именно llama.cpp – лёгкая сборка, не требует CUDA, а значит работает даже на старой ноде «Сова» (Intel Xeon E5-2670, 64 ГБ RAM). При желании можно подключить GPU-пасс-тру, но для большинства бытовых задач (генерация ответов, коротких статей, автокомплит) 4 ГБ VRAM достаточно.

1. Подготовка образа

Создаём Dockerfile в каталоге ~/llama:

Dockerfile
# syntax=docker/dockerfile:1.4
FROM debian:bookworm-slim AS builder

# Утилиты
RUN apt-get update && apt-get install -y --no-install-recommends \
        git ca-certificates build-essential cmake \
    && rm -rf /var/lib/apt/lists/*

# Скачиваем исходники
WORKDIR /src
RUN git clone --depth 1 https://github.com/ggerganov/llama.cpp.git .
RUN make -j$(nproc)

# Финальный слой – только исполняемый файл
FROM debian:bookworm-slim
COPY --from=builder /src/llama.cpp /usr/local/bin/llama.cpp
RUN apt-get update && apt-get install -y --no-install-recommends \
        libstdc++6 ca-certificates \
    && rm -rf /var/lib/apt/lists/*

# Порт для HTTP-интерфейса
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/llama.cpp"]
CMD ["-m", "/models/ggml-model-q4_0.bin", "-p", "You are a helpful assistant.", "-t", "4", "-ngl", "0", "-fa", "0", "-c", "2048", "-n", "128", "-b", "256", "-a", "0.7", "-r", "0.0", "-s", "0", "-temp", "0.8", "-ctx", "2048", "--port", "8080"]

Примечание. Путь /models/ggml-model-q4_0.bin – это место, куда мы позже положим модель. Формат q4_0 экономит RAM в 4-5 раз.

2. Скачивание модели

bash
mkdir -p /opt/llama/models
cd /opt/llama/models
# Пример модели LLaMA-2-7B-q4_0 (GPL-compatible)
wget -O ggml-model-q4_0.bin https://huggingface.co/TheBloke/Llama-2-7B-GGML/resolve/main/llama-2-7b.Q4_0.ggmlv3.bin

3. Systemd-юнит

ini
# /etc/systemd/system/llama.service
[Unit]
Description=Local LLM (llama.cpp) container
After=network-online.target
Wants=network-online.target

[Service]
Restart=on-failure
ExecStartPre=/usr/bin/docker pull alpine:latest   # гарантируем наличие базового образа
ExecStart=/usr/bin/docker run --rm \
    --name llama \
    -p 8080:8080 \
    -v /opt/llama/models:/models:ro \
    -v /opt/llama/data:/data \
    --cpus=4 \
    --memory=8g \
    ghcr.io/llama-cpp/llama:latest
ExecStop=/usr/bin/docker stop llama

[Install]
WantedBy=multi-user.target
bash
systemctl daemon-reload
systemctl enable --now llama.service

Проверьте, что API отвечает:

bash
curl -X POST http://localhost:8080/completions \
    -H "Content-Type: application/json" \
    -d '{"prompt":"Привет, расскажи про ZFS"}'

Ответ придёт в JSON-формате, готовый к дальнейшей обработке.


Интеграция с Home Assistant через webhook

Home Assistant (HA) уже у меня работает в контейнере homeassistant на ноде «Винни-Пух». Мы будем использовать простой webhook, который отправляет запрос к LLM и получает ответ, а затем передаёт его в автоматизацию HA.

1. Создаём webhook-скрипт

bash
mkdir -p /opt/ha-webhook
cat > /opt/ha-webhook/llama_webhook.py <<'PY'
#!/usr/bin/env python3
import os, sys, json, requests
from flask import Flask, request, abort

app = Flask(__name__)

LLAMA_URL = os.getenv('LLAMA_URL', 'http://localhost:8080/completions')
HEADERS = {'Content-Type': 'application/json'}

@app.route('/api/llama', methods=['POST'])
def handle():
    data = request.get_json()
    if not data or 'prompt' not in data:
        abort(400, 'Missing prompt')
    payload = {
        "prompt": data['prompt'],
        "max_tokens": 150,
        "temperature": 0.8,
        "stop": ["\n"]
    }
    resp = requests.post(LLAMA_URL, headers=HEADERS, json=payload, timeout=10)
    resp.raise_for_status()
    result = resp.json()
    return json.dumps({"response": result.get('content', '')})

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5005)
PY
chmod +x /opt/ha-webhook/llama_webhook.py

2. Docker-композит для webhook-сервиса

yaml
# /opt/ha-webhook/docker-compose.yml
version: "3.9"
services:
  llama-webhook:
    image: python:3.12-slim
    container_name: llama-webhook
    restart: unless-stopped
    ports:
      - "5005:5005"
    volumes:
      - ./llama_webhook.py:/app/llama_webhook.py:ro
    working_dir: /app
    command: ["python", "llama_webhook.py"]
    environment:
      - LLAMA_URL=http://llama:8080/completions
    depends_on:
      - llama
  llama:
    image: ghcr.io/llama-cpp/llama:latest
    container_name: llama
    restart: unless-stopped
    ports:
      - "8080:8080"
    volumes:
      - /opt/llama/models:/models:ro
    command: ["-m", "/models/ggml-model-q4_0.bin", "-p", "You are a helpful assistant.", "-t", "4", "--port", "8080"]

Запускаем:

bash
cd /opt/ha-webhook
docker compose up -d

3. Добавляем webhook в Home Assistant

В configuration.yaml (или через UI → Интеграции → Webhook) создаём REST Command:

yaml
rest_command:
  llama_prompt:
    url: http://{{ states('sensor.host_ip') }}:5005/api/llama
    method: post
    payload: '{"prompt":"{{ prompt }}"}'
    content_type: application/json
    timeout: 10

4. Пример автоматизации: включить свет и включить музыку по голосовой команде

yaml
automation:
  - alias: "AI-управление светом и музыкой"
    trigger:
      - platform: event
        event_type: ios.action_fired
        event_data:
          actionName: "turn_on_party_mode"
    action:
      - service: rest_command.llama_prompt
        data:
          prompt: "Включи свет в гостиной и поставь плейлист «Вечеринка 2024»"
      - wait_template: "{{ is_state('media_player.living_room', 'playing') }}"
        timeout: '00:00:15'
      - service: notify.mobile_app_alexey_phone
        data:
          message: "Свет включён, музыка играет"

Когда вы вызываете действие turn_on_party_mode (например, через Siri Shortcut), Home Assistant отправит запрос к нашему webhook, LLM сформирует естественный ответ, а HA выполнит команды, закодированные в ответе (можно добавить парсер в llama_webhook.py, который будет извлекать entity_id и service из текста).


Краткий чек-лист

ШагЧто делаемГде проверяем
1Скачиваем модель в /opt/llama/modelsls -lh /opt/llama/models
2Запускаем llama.service через systemdsystemctl status llama
3Тестируем LLM APIcurl -X POST …
4Поднимаем webhook-контейнер (docker compose up -d)docker ps
5Добавляем rest_command в HAUI → Настройки → Автоматизации
6Создаём простую автоматизациюautomation editor

Почему так работает

  • Предсказуемость – каждый компонент (LLM, webhook, HA) живёт в отдельном контейнере, управляется systemd или Docker-композит. При падении контейнера systemd автоматически перезапустит сервис.
  • Разделение ответственности – LLM отвечает только за генерацию текста, webhook – за трансляцию в JSON, HA – за исполнение действий. Это упрощает отладку и масштабирование.
  • Локальность – всё находится в вашей домашней сети, никаких облачных API-ключей, никаких внешних точек отказа.

Что дальше

  • Подключить GPU-пасс-тру на ноде «Пятачок» (NVIDIA RTX 3060) – заменив --gpus all в docker-композит.yml.
  • Добавить кэширование запросов в Redis, чтобы ускорять повторяющиеся запросы («Как включить свет?», «Какая погода?`).
  • Интегрировать с Grafana-Alertmanager, чтобы получать алерт, если LLM начинает «залипать» (latency > 5 сек).

Эти шаги позволят превратить ваш домашний сервер из «мелкой коробки» в надёжный, предсказуемый центр умного дома и креативного контент-генератора.

Поделиться:TelegramX / TwitterVK