MAATRIX / Блог / Actual Budget на сервере: частые ошибки и решения

Actual Budget на сервере: частые ошибки и решения

MAATRIX

Actual Budget поднимается одним docker-compose файлом за пять минут, и в этом его ловушка: кажется, что дальше всё «просто работает», а потом клиент внезапно не видит сервер, бюджет исчезает после перезапуска контейнера или синхронизация ломается после обновления. Ниже — конкретные причины таких сбоев и то, как их закрыть раз и навсегда, без танцев с бубном при каждом деплое.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Как устроен self-hosted Actual Budget и что вообще может сломаться

Actual Budget — это envelope-бюджетирование (метод конвертов) с быстрым локальным интерфейсом: приложение работает как PWA/desktop-клиент, хранит копию бюджета у себя и синхронизирует зашифрованные снэпшоты через ваш сервер. Сервер здесь — не «облако с логикой», а тонкий релей: он принимает и раздаёт зашифрованные блоки данных, сам их не читает. Это важно держать в голове, потому что часть ошибок ниже — не баги Actual, а следствия этой архитектуры (нужен HTTPS для криптографии в браузере, сервер не может «расшифровать и починить» ваш бюджет).

Официальный образ — actualbudget/actual-server, слушает порт 5006, все данные лежат в одном томе /data: своя SQLite-база для пароля администратора и папки с синхронизированными файлами бюджета. Минимальный рабочий docker-compose.yml:

services:
  actual:
    image: docker.io/actualbudget/actual-server:latest
    restart: unless-stopped
    ports:
      - "5006:5006"
    volumes:
      - ./actual-data:/data

Этого хватает для локального теста. Для прод-эксплуатации на арендованном сервере нужны ещё три вещи: TLS перед приложением, правильные права на том с данными и осознанная стратегия бэкапов и апдейтов — именно тут возникает большинство обращений в поддержку. Если сравниваете с альтернативами, у нас есть отдельный разбор Firefly III — более «бухгалтерского» инструмента с двойной записью, если envelope-подход не подходит.

«Unable to connect to server»: почему без HTTPS клиент не подключится

Самая частая жалоба — при добавлении custom-сервера в приложении бесконечная загрузка или прямая ошибка соединения, хотя curl http://ваш-ip:5006 с сервера отвечает нормально. Причина почти всегда одна: клиент Actual — это PWA, и часть криптографии (Web Crypto API), на которой держится шифрование бюджета, браузер разрешает выполнять только в «безопасном контексте» — то есть на HTTPS (или на localhost). Отдать сервер по голому HTTP на внешний IP — верный способ получить рабочий бэкенд и молчаливо отваливающийся клиент.

Решение — reverse proxy с автоматическим TLS перед контейнером. Пример на Caddy (самый безболезненный вариант для одного домена):

budget.example.com {
    reverse_proxy localhost:5006
}

Caddy сам получит и продлит сертификат Let's Encrypt. Если уже используете nginx для других сайтов на сервере, конфиг для Actual выглядит так:

server {
    listen 443 ssl http2;
    server_name budget.example.com;

    ssl_certificate     /etc/letsencrypt/live/budget.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/budget.example.com/privkey.pem;

    client_max_body_size 20m;
    proxy_read_timeout 120s;

    location / {
        proxy_pass http://127.0.0.1:5006;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Две детали, которые часто забывают и потом ловят непонятные обрывы: client_max_body_size — при импорте большого бюджета или загрузке файла сервер получает multipart-запрос заметно больше дефолтного лимита nginx в 1 МБ; и proxy_read_timeout — на слабом сервере первичная синхронизация крупного бюджета может занять больше стандартных 60 секунд. Если самостоятельная настройка TLS и прокси кажется избыточной возней, у нас есть пошаговый разбор Caddy с авто-SSL: частые ошибки и решения — закрывает почти все грабли с сертификатами разом.

Отдельно: если сервер стоит за Cloudflare или другим CDN-проксированием, добавьте переменную ACTUAL_TRUSTED_PROXIES со списком IP или подсетей прокси в окружение контейнера — иначе Actual может некорректно определять исходный IP клиента и это иногда мешает работе rate-limit'а на логине.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Permission denied при первом запуске тома с данными

Контейнер actual-server работает не под root, а под непривилегированным пользователем внутри образа. Если вы просто указали bind-mount на директорию, которую Docker создал автоматически (владелец — root хоста), процесс внутри контейнера не сможет писать в /data и в логах будет что-то вроде:

Error: EACCES: permission denied, open '/data/account.sqlite'

Контейнер при этом может даже подняться и слушать порт, но любая попытка сохранить бюджет или создать первого пользователя провалится. Фикс — создать каталог заранее и выставить права под UID контейнера (обычно 1000):

mkdir -p ./actual-data
sudo chown -R 1000:1000 ./actual-data
docker compose up -d
docker compose logs -f actual

Если не хотите гадать с UID — используйте именованный том вместо bind-mount, Docker сам управляет правами внутри него:

volumes:
  actual-data:

services:
  actual:
    image: docker.io/actualbudget/actual-server:latest
    restart: unless-stopped
    ports:
      - "127.0.0.1:5006:5006"
    volumes:
      - actual-data:/data

Обратите внимание и на пробрасывание порта только на 127.0.0.1 — если TLS уже терминируется на reverse proxy, слушать 5006 «наружу» на всех интерфейсах незачем, это лишняя поверхность атаки.

Бюджет «пропадает» после перезапуска контейнера

Классический сценарий: развернули Actual командой docker run для теста без -v, всё поработало день, потом сервер перезагрузили (обновление ОС, ребут VPS) — и бюджет пуст, будто вы только что установили приложение. Без явного тома данные живут в writable-слое контейнера и исчезают при его пересоздании (а не только при удалении — пересоздание при docker run без --name и повторном docker run тоже создаёт новый контейнер).

Проверить, что том реально примонтирован, а не потерян где-то по пути:

docker inspect actual | grep -A 5 '"Mounts"'

Если в выводе нет вашего пути или именованного тома — деплой не персистентный, чините compose-файл как в разделе выше и переносите то, что осталось, из старого writable-слоя контейнера (docker cp <container>:/data ./actual-data) до того, как контейнер будет удалён. Также проверьте, что на сервере не крутится случайно два инстанса Actual на одном порту из-за незакрытого предыдущего деплоя — второй контейнер с портом 5006 просто не поднимется, но при других портах путаница между «старым» и «новым» инстансом с разными томами данных — частая причина ощущения «бюджет пропал», хотя на деле он просто в другом месте.

После обновления сервер не синхронизируется с клиентом

Actual развивается быстро, и протокол синхронизации иногда меняется вместе с версией. Если обновили сервер (docker compose pull && docker compose up -d) командой, которая тянет :latest, а клиент в браузере остался на закешированной старой версии service worker'а — получите ошибку вида «This budget was created with a different version of Actual» или зависшую синхронизацию без внятного текста.

Порядок действий при апдейте, который снижает риск:

  1. Перед обновлением сделайте бэкап /data (раздел ниже) — это дешевле, чем откатываться по памяти.
  2. Обновите сервер и проверьте логи на ошибки миграции: docker compose logs -f actual.
  3. На клиенте сделайте жёсткое обновление страницы (Ctrl+Shift+R) — PWA держит service worker, обычный F5 его не всегда сбрасывает.
  4. Если используете мобильное или desktop-приложение отдельно от веба — обновите и его, версии клиента и сервера не должны расходиться сильно.

Если держите Actual в проде и не хотите сюрпризов от :latest, зафиксируйте версию тегом при обновлении вручную, а не через автообновление образов — так апдейт происходит осознанно, а не в 3 часа ночи вместе с десятком других контейнеров. Если для автообновлений всё же используете Watchtower, стоит явно исключить сервис Actual из его области действия или свериться с нашим разбором автообновления Watchtower — там как раз про то, как не обновить критичный сервис незаметно для себя.

Бэкап и восстановление данных

Здесь легко ошибиться дважды: сделать бэкап не того, что нужно, и не проверить, что из бэкапа реально можно восстановиться. Экспорт бюджета через интерфейс (File → Export Data → архив .zip) — это снимок конкретного бюджета на конкретный момент, удобно для переноса между инстансами, но это не полноценный бэкап сервера: он не включает пароль администратора и служебную базу авторизации.

Правильный минимум — бэкапить весь том /data целиком:

docker compose stop actual
tar czf actual-backup-$(date +%F).tar.gz -C ./actual-data .
docker compose start actual

Для регулярного автоматического бэкапа без остановки сервиса на живом проде надёжнее полагаться на снапшот-инструменты, которые умеют консистентно бэкапить SQLite без блокировки приложения — у нас есть пошаговые разборы под restic и Borg, если хотите не изобретать cron-скрипт с нуля. Общая логика бэкапа именно docker-томов (не только для Actual) разобрана в статье бэкап Docker volume: частые ошибки и решения — там же про типичную ошибку «бэкапили не тот путь» и про проверку restore.

Восстановление — обратная операция: останавливаем контейнер, разворачиваем архив в пустой ./actual-data, поднимаем сервис заново. Раз в квартал стоит реально проверять восстановление на тестовом сервере, а не полагаться на то, что архив «наверное целый» — это касается любого self-hosted сервиса с финансовыми данными, не только Actual.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Можно ли запустить Actual Budget без Docker?

Да, это обычное Node.js-приложение, его можно поставить напрямую и запускать через systemd или pm2. Docker удобнее тем, что версия сервера и её зависимости изолированы от системного Node на сервере и обновление — это просто смена образа.

Сколько ресурсов сервера нужно под Actual Budget?

Приложение лёгкое, ориентировочно хватает 512 МБ — 1 ГБ оперативной памяти под сам сервис даже с несколькими синхронизирующимися клиентами; точная цифра зависит от размера истории транзакций и от того, что ещё крутится на том же сервере. Гонять его на отдельном VPS ради одного Actual обычно избыточно — удобнее держать его на общем сервере за reverse proxy вместе с другими лёгкими self-hosted сервисами.

Что будет, если забыть пароль шифрования бюджета?

Actual использует end-to-end шифрование с паролем, который сервер не хранит и не знает. Если пароль потерян и нет локальной копии клиента с расшифрованными данными — бюджет не восстановить в принципе, это не «забыл пароль от сайта». Отдельно храните пароль шифрования там же, где храните бэкапы /data.

Безопасно ли открывать порт 5006 напрямую в интернет без reverse proxy?

Не стоит: без TLS ломается сама возможность подключиться клиентом (см. раздел выше), а открытый порт без ограничений — лишняя точка для брутфорса логина. Держите порт на 127.0.0.1, работайте через прокси с HTTPS и настройте файрвол на сервере — базовые правила разобраны в статье про UFW: частые ошибки и решения.

Как перенести бюджет с облачной версии Actual на self-hosted сервер?

Через тот же механизм экспорта/импорта: экспортируете .zip с бюджетом из облачного аккаунта, поднимаете свой сервер, при первом входе создаёте нового пользователя и импортируете файл. История транзакций и настройки категорий переносятся вместе с файлом.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →