Self-hosted
Minimal self-hosted setup for trip2g: ghcr.io/trip2g/trip2g:latest + MinIO + Caddy.
This setup is for a single server, a single docker-compose.yml, and a straightforward docker compose up -d.
What this setup runs
trip2gruns the site, admin UI, auth flow, and notes git repository.miniostores uploaded assets and simple SQLite backups.caddyaccepts externalHTTP/HTTPStraffic and proxies requests inside the compose network.- Vector search can stay disabled, or you can enable it later with OpenAI or another OpenAI-compatible embeddings API.
Easy-to-miss requirements
- A public server should use
HTTPS. Otherwise secure auth cookies will not work correctly. - Email sign-in needs a
resend.comaccount, an API key, and a verified sender domain or subdomain. - In production you must set your own
JWT_SECRETandDATA_ENCRYPTION_KEY. - In practice, only
80and443should be exposed externally bycaddy.
Prerequisites
You need:
- a Linux server with Docker and the Docker Compose plugin
- a site domain such as
docs.example.com - a sender subdomain such as
mg.example.com - DNS access
Create a directory such as /opt/trip2g and put two files there: docker-compose.yml and .env.
Check your server before setup
If the server is not fresh, verify the following before starting:
- Ports
80and443are free — compose hands them to Caddy:ss -tlnp | grep -E ':80 |:443 ' - No other Caddy, Nginx, or Traefik is already listening on those ports. If there is, stop it or move it to a different port.
- No conflicting Docker networks from other projects (rare, but can happen with non-default bridge or overlay setups).
If those ports are already claimed by an existing reverse proxy (Nginx, Caddy, Traefik), there is no need to remove it — just add trip2g as an upstream in your existing config, drop the caddy service from docker-compose.yml, and publish port 8081 directly. The same applies to MinIO: if you already have your own object storage, skip the minio service entirely and point .env at your existing bucket.
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:
Why these choices matter:
caddyis the only service exposing80and443.trip2g-datakeeps trip2g data and the internal bare git repository.minio-datakeeps MinIO objects.trip2gandminiostay internal to the compose network.
.env
Minimal production .env:
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"}}
What each setting does
PUBLIC_URLis the external URL of your site. trip2g uses it for links, email flows, and integrations.LISTEN_ADDRis the main HTTP listen address.INTERNAL_LISTEN_ADDRis the internal address used for health checks and service endpoints.DB_FILEis the data file path inside the container.GIT_API_REPO_PATHis the path to trip2g's built-in git repository.LOG_LEVELsets server log verbosity.DEV=falseenables production behavior.OWNER_EMAILis the owner account email.MAIL_FROMis the sender address. It must belong to a domain verified in Resend.RESEND_API_KEYis used to send email sign-in codes.JWT_SECRETsigns user session tokens. Rotating it invalidates existing sessions.DATA_ENCRYPTION_KEYis a 32-byte key used to encrypt sensitive stored data. To generate one:openssl rand -base64 32 | head -c 32MINIO_ROOT_USER/MINIO_ROOT_PASSWORDare the MinIO root credentials.MINIO_ENDPOINTis the MinIO address as seen from thetrip2gcontainer.MINIO_PUBLIC_URLis the public MinIO hostname used in presigned file URLs.MINIO_ACCESS_KEY_ID/MINIO_SECRET_KEYare the credentials trip2g uses for MinIO.MINIO_BUCKETis the S3 bucket for assets and backup objects.MINIO_REGIONis the S3 region string. For MinIO,us-east-1is fine.MINIO_USE_SSL=falseis normal for container-to-container traffic on one host.MINIO_INIT_TIMEOUTcontrols how long startup waits for MinIO.MINIO_URL_EXPIRES_INcontrols presigned URL lifetime for file downloads.SIMPLE_BACKUP=trueenables simple SQLite backups to MinIO.FEATURESis the JSON feature-flag config. Vector search lives here.OPENAI_API_KEYis the key used for OpenAI or a compatible embeddings provider.
Optional HTTP-only smoke test:
PUBLIC_URL=http://SERVER_IP:8081
USER_TOKEN_INSECURE=true
Use that only for temporary testing. For a public instance, keep secure cookies and use TLS.
Caddyfile
For a clean public setup, give the site and file storage separate hostnames:
docs.example.com→ trip2gfiles.example.com→ MinIO
Create a Caddyfile next to docker-compose.yml:
docs.example.com {
encode zstd gzip
reverse_proxy trip2g:8081
}
files.example.com {
encode zstd gzip
reverse_proxy minio:9000
}
# Optional, if you want the MinIO console externally:
# minio-admin.example.com {
# reverse_proxy minio:9001
# }
With that setup, these values in .env should match:
PUBLIC_URL=https://docs.example.com
MINIO_PUBLIC_URL=https://files.example.com
Why this matters:
- trip2g stays on the main site hostname;
- file URLs use a public MinIO hostname;
caddyreachestrip2gandminioby service name inside the docker network.
External object storage instead of MinIO
By default MinIO runs on the same server as trip2g. That is convenient to start but offers no protection against server loss: if the disk dies, files and backups go with it.
For production, we recommend moving storage to a separate S3-compatible service: Backblaze B2, Hetzner Object Storage, Timeweb S3, or similar.
In that case you can remove the minio service from docker-compose.yml entirely and point .env at the external service:
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
With that in place, SIMPLE_BACKUP=true stores SQLite backups on the external service automatically — no extra work, and protected from server-level failure.
SQLite replication with Litestream
SIMPLE_BACKUP=true takes periodic SQLite snapshots to MinIO. For continuous replication with a one-second interval, add Litestream.
Litestream runs on the host as a systemd service and streams the database file directly to any S3-compatible target. The infra/ directory already has a ready-made setup:
infra/generate-litestream-config.sh— generates/etc/litestream.ymlfrom environment variablesinfra/litestream.service— the systemd unit
The config reads the same variables as trip2g's .env: MINIO_ACCESS_KEY_ID, MINIO_SECRET_KEY, MINIO_ENDPOINT, MINIO_BUCKET, DB_FILE. After installing 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 and SIMPLE_BACKUP can run together — they do not conflict. The combination is especially useful with external object storage: both files and the database then live outside the server.
Create a free Resend account
On resend.com:
- Create a free account.
- Add a sending domain, preferably a subdomain such as
mg.example.com. - Add the DNS records Resend asks for.
- Create an API key.
- Put that key into
RESEND_API_KEY. - Set
MAIL_FROMto an address inside the verified domain, for exampleno-reply@mg.example.com.
Why a subdomain is better:
- it isolates sender reputation;
- it keeps trip2g transactional mail separate from your main domain mail.
If you do not verify a sender domain in Resend, email delivery is effectively just for your own address. That is enough if only the owner signs in by email. If other users need email login, verify the sender domain.
Enable vector search with OpenAI or another compatible service
trip2g works fine without vector search. Full-text search still works.
If you want semantic search:
OpenAI
OPENAI_API_KEY=sk-...
FEATURES={"vector_search":{"enabled":true,"model":"text-embedding-3-small"}}
Recommended starting 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"}}
Important: trip2g validates the embedding model name. The currently supported values are:
text-embedding-3-smalltext-embedding-3-largetext-embedding-ada-002multilingual-e5-basebge-m3
So “any OpenAI-compatible service” only works if it exposes a compatible /v1 embeddings API and you configure one of those supported model names.
Start the stack
Inside the directory with docker-compose.yml:
docker compose up -d
If you still use the old syntax:
docker-compose up -d
Check the result:
docker compose ps
docker compose logs -f caddy trip2g
After startup:
- open
https://docs.example.com - sign in with the owner email from
OWNER_EMAIL - on an empty instance, the service itself will offer a link to download a preconfigured vault ZIP
- configure the Obsidian plugin with your
PUBLIC_URL
From there, continue with Getting started.
Things people often forget
A/AAAADNS record forPUBLIC_URLA/AAAADNS record forMINIO_PUBLIC_URL- TLS certificate for the site domain
- Resend DNS records for the sender domain
- persistent storage for
trip2g-data - do not publish the MinIO console on
9001unless you actually need it - checking logs after the first login and the first outbound email
For the smoothest rollout, start without vector search, verify email sign-in first, and only then enable embeddings in FEATURES.