Finnly

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:

  1. поменяйте FINNLY_ADMIN_PASSWORD на свой;
  2. добавьте рядом строку FINNLY_ADMIN_PASSWORD_RESET: 'true';
  3. docker compose up -d;
  4. уберите строку с _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 — чистый старт с нуля.