Self-host
Finnly на своём сервере
Данные остаются на вашей машине и никуда не уходят — ни к нам, ни к кому-либо ещё.
Что вы получаете
Образ приложения
ghcr.io/vov4egspb/finnly для amd64 и arm64. Внутри один бинарь: API, обновление базы и веб-интерфейс.
Обычный Postgres
Стандартный postgres:16-alpine с Docker Hub, ничего своего.
Два файла с этой страницы
compose.yml и .env — копируются кнопкой, ничего скачивать не нужно.
Что понадобится
- Домашний ПК, старый ноутбук, Raspberry Pi 4+ или VPS за пару долларов.
- Docker. Windows и macOS — Docker Desktop с docker.com, установка «далее-далее». Linux — curl -fsSL https://get.docker.com | sh.
- 10 минут.
Знать Docker не нужно: все команды ниже даны целиком.
Установка
Шаг 1. Папка
Создайте где угодно папку finnly. В ней будут только два файла — базу и образы Docker хранит у себя.
Шаг 2. Файл .env с секретами
Создайте в папке файл с именем .env (да, начинается с точки):
POSTGRES_PASSWORD=замените-на-случайную-строку
JWT_SECRET=замените-на-другую-случайную-строкуСлучайные строки: openssl rand -hex 32. В PowerShell на Windows — -join ((1..64) | ForEach-Object { '{0:x}' -f (Get-Random -Max 16) }).
Запоминать их не нужно, но терять нельзя: без POSTGRES_PASSWORD не откроется база. JWT_SECRET подписывает сессии — если его сменить, всех разлогинит.
Шаг 3. Файл compose.yml
Рядом с .env создайте compose.yml и вставьте целиком:
services:
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: finnly
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?укажите в .env}
POSTGRES_DB: finnly
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U finnly']
interval: 10s
timeout: 5s
retries: 10
app:
image: ghcr.io/vov4egspb/finnly:latest
restart: unless-stopped
ports:
- '8080:8080'
environment:
DATABASE_URL: postgres://finnly:${POSTGRES_PASSWORD:?укажите в .env}@db:5432/finnly?sslmode=disable
JWT_SECRET: ${JWT_SECRET:?укажите в .env}
PORT: '8080'
# false — потому что открываем по http на своей машине.
# За HTTPS-прокси обязательно поменяйте на "true".
COOKIE_SECURE: 'false'
ALLOW_REGISTRATION: 'false'
FINNLY_ADMIN_EMAIL: admin@finnly.local
FINNLY_ADMIN_PASSWORD: finnly
# Почта — по желанию, для восстановления пароля. См. раздел «Почта» в SELFHOST.md.
# SMTP_HOST: 'smtp.example.com'
# SMTP_PORT: '465'
# SMTP_USERNAME: 'finnly@example.com'
# SMTP_PASSWORD: ${SMTP_PASSWORD}
# MAIL_FROM: 'Finnly <finnly@example.com>'
# APP_URL: 'https://finnly.example.com'
depends_on:
db:
condition: service_healthy
volumes:
pgdata:Шаг 4. Запуск
В терминале, находясь в этой папке:
docker compose up -dПервый раз это займёт минуту-две — Docker скачивает образы. -d значит «в фоне»: терминал можно закрыть.
Шаг 5. Вход
Откройте http://localhost:8080 — или http://<IP-сервера>:8080, если поднимали на другом устройстве.
- почта: admin@finnly.local
- пароль: finnly
Регистрация выключена: аккаунт один, он создаётся при первом старте вместе с набором категорий по умолчанию.
Шаг 6. Смените пароль — сразу
Пароль finnly знают все, кто читал эту страницу. Пока он не сменён, приложение при каждом запуске пишет предупреждение в лог.
Смены пароля в интерфейсе пока нет, поэтому она делается через compose.yml:
- поменяйте FINNLY_ADMIN_PASSWORD на свой;
- добавьте рядом строку FINNLY_ADMIN_PASSWORD_RESET: 'true';
- docker compose up -d;
- уберите строку с _RESET и снова docker compose up -d.
Обновление
docker compose pull
docker compose up -dДанные не трогаются, изменения базы применяются сами при старте. Чтобы обновление не приходило неожиданно, замените в compose.yml тег :latest на конкретную версию, например :1.4.
Бэкап
docker compose exec -T db pg_dump -U finnly finnly | gzip > finnly-$(date +%F).sql.gzВосстановление на пустую базу:
gunzip -c finnly-2026-09-08.sql.gz | docker compose exec -T db psql -U finnly finnlyКопию держите не на том же диске. Данные живут в томе Docker pgdata и переживают перезапуск, обновление и docker compose down — но не docker compose down -v: ключ -v сносит том вместе с базой.
Свой домен и HTTPS
Нужно, если открываете снаружи. Плюс две вещи работают только по HTTPS: сканирование чеков камерой и установка на домашний экран как приложения. На http://localhost они тоже работают — браузер считает localhost доверенным, — а по IP в локальной сети уже нет.
Заведите A-запись домена на IP сервера, откройте порты 80 и 443 и положите рядом с compose.yml файл Caddyfile:
{$DOMAIN} {
encode zstd gzip
header {
X-Content-Type-Options nosniff
Referrer-Policy strict-origin-when-cross-origin
}
reverse_proxy app:8080
}В .env добавьте DOMAIN=finnly.example.com. В compose.yml у сервиса app уберите блок ports (наружу теперь смотрит только прокси) и поставьте COOKIE_SECURE: 'true', а в конец добавьте сервис:
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- '80:80'
- '443:443'
environment:
DOMAIN: ${DOMAIN:?укажите в .env}
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
- appИ в списке volumes: внизу — ещё две строки: caddy_data: и caddy_config:. Сертификат Caddy получит и будет продлевать сам.
COOKIE_SECURE: 'true' здесь обязателен: иначе cookie сессии ходит открытым текстом, а живёт она 30 дней.
Напоминания в Telegram
В 9 утра по вашему времени бот пишет вам в личку накануне и в день запланированной операции, регулярного платежа и списания по кредиту. Уже проведённые платежи он пропускает. В настройках можно включить сводку за неделю (по понедельникам) и за месяц (1-го числа).
Для self-host важно, что бот сам ходит за сообщениями (long polling): публичный адрес, домен и проброс портов не нужны — работает и на домашней машине за NAT.
Шаг 1. Создать бота
Откройте @BotFather и отправьте /newbot. Он спросит две вещи:
- имя — любое, хоть Finnly; видно в заголовке чата;
- username — латиницей и обязательно заканчивается на bot, например my_finnly_bot.
В ответ придёт токен вида 8123456789:AAHm… — это пароль от бота, его не публикуют.
Шаг 2. Вписать их в compose.yml
Обе строки идут в блок environment: сервиса app — туда же, где JWT_SECRET и остальные переменные, и с тем же отступом:
app:
environment:
# ...остальные переменные оставьте как есть
TELEGRAM_BOT_TOKEN: '8123456789:AAHm-пример-вашего-токена'
TELEGRAM_BOT_USERNAME: 'my_finnly_bot'- username — без @ и без t.me/: из него собирается ссылка t.me/<username>?start=<код>, с лишним символом она не откроется;
- нужны обе переменные: без токена бот не стартует, без username кнопка привязки даст нерабочую ссылку;
- кавычки обязательны — в токене есть двоеточие, и без них YAML примет строку за пару «ключ: значение».
Шаг 3. Перезапустить и привязать
docker compose up -d, затем docker compose logs app. Там должно появиться telegram reminders enabled (bot @my_finnly_bot). Если вместо этого disabled: TELEGRAM_BOT_TOKEN is empty — переменные не доехали, проверьте отступы в compose.yml.
Дальше в приложении: Профиль → Telegram → Подключить. Откроется чат с ботом, нажмите Start — привязка готова. Код одноразовый и живёт 15 минут; если не успели, нажмите «Подключить» ещё раз.
Один чат — один аккаунт: привязка того же чата к другому аккаунту снимает старую. Если заблокировать бота, привязка снимется сама.
Без токена бот и рассылка просто не запускаются, остальное приложение работает как обычно.
Если что-то пошло не так
- Логи: docker compose logs -f app. Там же строка про созданный аккаунт и предупреждение о пароле.
- Порт 8080 занят: поменяйте в ports левое число, например '9090:8080', и открывайте :9090. Правое не трогайте.
- Забыли пароль: тот же приём с FINNLY_ADMIN_PASSWORD_RESET из шага 6.
- Сломалось, данных не жалко: docker compose down -v && docker compose up -d — чистый старт с нуля.