Self-hosted

Минимальный self-hosted вариант trip2g: ghcr.io/trip2g/trip2g:latest + MinIO + Caddy.

Этот вариант подходит, если вам нужен один сервер, один docker-compose.yml и запуск через docker compose up -d.

Что поднимется

  • trip2g поднимает сайт, админку, авторизацию и git-репозиторий заметок.
  • minio хранит ассеты и простые резервные копии SQLite.
  • caddy принимает внешний HTTP/HTTPS и проксирует запросы внутрь compose-сети.
  • Векторный поиск можно оставить выключенным или включить через OpenAI / OpenAI-compatible embeddings API.

Что важно не забыть

  • Для публичного сервера нужен HTTPS. Иначе secure-cookie для входа не будет работать.
  • Для входа по email нужен аккаунт в resend.com, API key и подтвержденный домен или поддомен отправителя.
  • Для production обязательно задайте свои JWT_SECRET и DATA_ENCRYPTION_KEY. Со значениями по умолчанию сервер падать или работать небезопасно.
  • Наружу обычно публикуются только 80 и 443 у caddy.

Подготовка

Нужны:

  • Linux-сервер с Docker и Docker Compose plugin
  • домен для сайта, например docs.example.com
  • поддомен для email-отправителя, например mg.example.com
  • доступ к DNS

Создайте директорию, например /opt/trip2g, и положите туда два файла: docker-compose.yml и .env.

Проверьте сервер перед установкой

Если сервер не чистый, убедитесь до запуска:

  • Порты 80 и 443 свободны — compose отдаёт их Caddy:
    ss -tlnp | grep -E ':80 |:443 '
    
  • Нет другого Caddy, Nginx или Traefik, уже слушающего эти порты. Если есть — его нужно остановить или перенести.
  • Нет конфликтующих Docker-сетей от других проектов (редко, но бывает при нестандартном overlay).

Если порты заняты существующим reverse proxy (Nginx, Caddy, Traefik), убирать его не нужно — достаточно прописать trip2g как upstream в нём. Тогда сервис caddy из docker-compose.yml можно убрать, а порт 8081 опубликовать напрямую. То же самое касается MinIO: если у вас уже есть своё объектное хранилище — MinIO поднимать не нужно, достаточно указать его реквизиты в .env.

docker-compose.yml

services:
  caddy:
    image: caddy:2
    restart: unless-stopped
    depends_on:
      trip2g:
        condition: service_started
      minio:
        condition: service_healthy
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy-data:/data
      - caddy-config:/config

  minio:
    image: minio/minio:latest
    restart: unless-stopped
    command: server /data --console-address ":9001"
    environment:
      MINIO_ROOT_USER: ${MINIO_ROOT_USER}
      MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
    volumes:
      - minio-data:/data
    expose:
      - "9000"
      - "9001"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
      interval: 5s
      timeout: 5s
      retries: 20

  trip2g:
    image: ghcr.io/trip2g/trip2g:latest
    restart: unless-stopped
    depends_on:
      minio:
        condition: service_healthy
    env_file:
      - .env
    volumes:
      - trip2g-data:/data
    expose:
      - "8081"
    healthcheck:
      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8082/healthz"]
      interval: 10s
      timeout: 5s
      retries: 12
      start_period: 15s

volumes:
  caddy-data:
  caddy-config:
  trip2g-data:
  minio-data:

Что здесь важно:

  • caddy публикует только 80 и 443.
  • trip2g-data хранит данные trip2g и внутренний bare git-репозиторий.
  • minio-data хранит MinIO-объекты.
  • trip2g и minio доступны только внутри compose-сети.

.env

Минимальный .env для production:

PUBLIC_URL=https://docs.example.com
LISTEN_ADDR=0.0.0.0:8081
INTERNAL_LISTEN_ADDR=:8082
DB_FILE=/data/data.sqlite3
GIT_API_REPO_PATH=/data/git
LOG_LEVEL=info
DEV=false

OWNER_EMAIL=owner@example.com
MAIL_FROM=no-reply@mg.example.com
RESEND_API_KEY=re_xxxxxxxxx

JWT_SECRET=replace-with-long-random-secret
DATA_ENCRYPTION_KEY=0123456789abcdef0123456789abcdef

MINIO_ROOT_USER=trip2g
MINIO_ROOT_PASSWORD=replace-with-long-random-password
MINIO_ENDPOINT=minio:9000
MINIO_PUBLIC_URL=https://files.example.com
MINIO_ACCESS_KEY_ID=trip2g
MINIO_SECRET_KEY=replace-with-long-random-password
MINIO_BUCKET=trip2g
MINIO_REGION=us-east-1
MINIO_USE_SSL=false
MINIO_INIT_TIMEOUT=30s
MINIO_URL_EXPIRES_IN=10m

SIMPLE_BACKUP=true

FEATURES={}

# Для OpenAI embeddings:
# OPENAI_API_KEY=sk-...
# FEATURES={"vector_search":{"enabled":true,"model":"text-embedding-3-small"}}

# Для OpenAI-compatible embeddings API:
# OPENAI_API_KEY=provider-token-if-needed
# FEATURES={"vector_search":{"enabled":true,"model":"bge-m3","base_url":"https://embeddings.example.com/v1"}}

Что означает каждая настройка

  • PUBLIC_URL — внешний адрес вашего сайта. Используется в ссылках, email и интеграциях.
  • LISTEN_ADDR — адрес, на котором слушает основной HTTP-сервер.
  • INTERNAL_LISTEN_ADDR — внутренний адрес для healthcheck и служебных endpoint'ов.
  • DB_FILE — путь к файлу данных внутри контейнера.
  • GIT_API_REPO_PATH — путь к встроенному git-репозиторию trip2g.
  • LOG_LEVEL — уровень логов сервера.
  • DEV=false — production-режим. Не включайте DEV=true на публичном сервере.
  • OWNER_EMAIL — email владельца инстанса.
  • MAIL_FROM — адрес отправителя. Он должен принадлежать домену, который вы подтвердили в Resend.
  • RESEND_API_KEY — API key для отправки кодов входа по email.
  • JWT_SECRET — секрет для пользовательских сессий. После смены старые сессии станут недействительными.
  • DATA_ENCRYPTION_KEY — 32-байтовый ключ для шифрования чувствительных данных. Сгенерировать:
    openssl rand -base64 32 | head -c 32
    
  • MINIO_ROOT_USER / MINIO_ROOT_PASSWORD — root-учетка самого MinIO.
  • MINIO_ENDPOINT — адрес MinIO из контейнера trip2g.
  • MINIO_PUBLIC_URL — публичный адрес MinIO, который попадет в presigned URL для файлов.
  • MINIO_ACCESS_KEY_ID / MINIO_SECRET_KEY — ключи, которыми trip2g ходит в MinIO.
  • MINIO_BUCKET — bucket для ассетов и backup-объектов.
  • MINIO_REGION — регион S3-совместимого API. Для MinIO обычно оставляют us-east-1.
  • MINIO_USE_SSL=false — между контейнерами обычно не нужен TLS.
  • MINIO_INIT_TIMEOUT — сколько ждать MinIO на старте.
  • MINIO_URL_EXPIRES_IN — срок жизни presigned URL для скачивания файлов.
  • SIMPLE_BACKUP=true — включает простые backup'ы SQLite в MinIO.
  • FEATURES — JSON-конфиг feature flags. Здесь включается векторный поиск.
  • OPENAI_API_KEY — ключ для OpenAI или совместимого embeddings API.

Опционально для локального smoke-test без HTTPS:

PUBLIC_URL=http://SERVER_IP:8081
USER_TOKEN_INSECURE=true

Но это только для временной проверки. Для публичного сервера оставляйте secure cookies и ставьте TLS.

Caddyfile

Если хотите нормальный публичный setup, дайте сайту и файлам отдельные домены:

  • docs.example.com → trip2g
  • files.example.com → MinIO

Положите рядом с docker-compose.yml файл Caddyfile:

docs.example.com {
	encode zstd gzip
	reverse_proxy trip2g:8081
}

files.example.com {
	encode zstd gzip
	reverse_proxy minio:9000
}

# Опционально, если нужна MinIO console снаружи:
# minio-admin.example.com {
# 	reverse_proxy minio:9001
# }

Для такой схемы в .env должны совпасть:

PUBLIC_URL=https://docs.example.com
MINIO_PUBLIC_URL=https://files.example.com

Зачем это нужно:

  • trip2g работает на основном домене;
  • ссылки на файлы отдаются с публичного MinIO-домена;
  • caddy ходит к сервисам по именам trip2g и minio внутри docker-сети.

Внешнее объектное хранилище вместо MinIO

По умолчанию MinIO работает на том же сервере, что и trip2g. Это удобно для старта, но не защищает от потери сервера: если диск умрёт, пропадут и файлы, и резервные копии.

Для production рекомендуем вынести хранилище на отдельный S3-совместимый сервис: Backblaze B2, Hetzner Object Storage, Timeweb S3 и другие.

В этом случае сервис minio из docker-compose.yml можно убрать полностью, а в .env указать реквизиты внешнего сервиса:

MINIO_ENDPOINT=s3.us-east-005.backblazeb2.com
MINIO_PUBLIC_URL=https://files.example.com
MINIO_ACCESS_KEY_ID=your-key-id
MINIO_SECRET_KEY=your-secret
MINIO_BUCKET=trip2g
MINIO_REGION=us-east-005
MINIO_USE_SSL=true

Тогда SIMPLE_BACKUP=true будет складывать резервные копии SQLite уже на внешний сервис — автоматически, без дополнительных усилий и с защитой от потери сервера.

Репликация SQLite через Litestream

SIMPLE_BACKUP=true делает периодические snapshot'ы базы в MinIO. Если нужна непрерывная репликация SQLite с интервалом в 1 секунду, добавьте Litestream.

Litestream запускается на хосте как systemd-сервис и реплицирует файл базы напрямую в S3-совместимое хранилище. В infra/ уже есть готовая конфигурация:

  • infra/generate-litestream-config.sh — генерирует /etc/litestream.yml из переменных окружения
  • infra/litestream.service — systemd unit

Конфигурация читает те же переменные, что и .env trip2g: MINIO_ACCESS_KEY_ID, MINIO_SECRET_KEY, MINIO_ENDPOINT, MINIO_BUCKET, DB_FILE. После установки litestream:

sudo cp infra/generate-litestream-config.sh /usr/local/bin/generate-litestream-config.sh
sudo chmod +x /usr/local/bin/generate-litestream-config.sh
sudo cp infra/litestream.service /etc/systemd/system/litestream.service
sudo systemctl enable --now litestream

Litestream и SIMPLE_BACKUP можно использовать одновременно — они не конфликтуют. Особенно полезна эта связка с внешним объектным хранилищем: тогда и файлы, и база данных хранятся вне сервера.

Как создать бесплатный аккаунт Resend

На resend.com:

  1. Зарегистрируйте бесплатный аккаунт.
  2. Добавьте домен или, лучше, поддомен отправителя, например mg.example.com.
  3. Подтвердите DNS-записи, которые покажет Resend.
  4. Создайте API key.
  5. Поставьте этот ключ в RESEND_API_KEY.
  6. Укажите MAIL_FROM на адрес внутри подтвержденного домена, например no-reply@mg.example.com.

Почему лучше поддомен:

  • проще изолировать почтовую репутацию;
  • не нужно смешивать транзакционные письма trip2g с основной почтой домена.

Если домен в Resend не подтверждать, письма будут приходить только вам самому. Этого достаточно, если email-вход нужен только владельцу инстанса. Если по email должны входить другие пользователи, домен отправителя нужно подтвердить.

Векторный поиск через OpenAI или совместимый сервис

По умолчанию trip2g прекрасно работает и без этого. Полнотекстовый поиск останется доступен.

Если нужен семантический поиск:

OpenAI

OPENAI_API_KEY=sk-...
FEATURES={"vector_search":{"enabled":true,"model":"text-embedding-3-small"}}

Рекомендуемая модель для старта: text-embedding-3-small.

OpenAI-compatible embeddings API

OPENAI_API_KEY=provider-token-if-needed
FEATURES={"vector_search":{"enabled":true,"model":"bge-m3","base_url":"https://embeddings.example.com/v1"}}

Важно: trip2g валидирует имя embedding-модели. Сейчас поддерживаются:

  • text-embedding-3-small
  • text-embedding-3-large
  • text-embedding-ada-002
  • multilingual-e5-base
  • bge-m3

То есть “любой OpenAI-compatible сервис” подойдет только если он умеет отдавать embeddings через совместимый /v1 API и вы используете одно из поддерживаемых имен моделей.

Запуск

В директории с docker-compose.yml:

docker compose up -d

Если у вас старый синтаксис Compose:

docker-compose up -d

Проверка:

docker compose ps
docker compose logs -f caddy trip2g

После старта:

  • откройте https://docs.example.com
  • войдите по email владельца из OWNER_EMAIL
  • на пустом инстансе сервис сам предложит ссылку на скачивание ZIP с настроенным vault
  • настройте Obsidian plugin на ваш PUBLIC_URL
    Дальше можно идти в Начало работы и продолжать уже с плагином Obsidian.

Что еще легко забыть

  • DNS A/AAAA запись для PUBLIC_URL
  • DNS A/AAAA запись для MINIO_PUBLIC_URL
  • TLS-сертификат на домен сайта
  • DNS-записи Resend для домена отправителя
  • сохранность volume trip2g-data
  • не публикуйте MinIO console (9001) без необходимости
  • мониторинг логов после первого входа и первой отправки письма

Если нужен максимально простой старт, сначала поднимите инстанс без векторного поиска, проверьте вход по email, и только потом добавляйте FEATURES для embeddings.