Self-Hosting
Run Tindra on your own infrastructure. One binary, one Postgres database.
On Debian or Ubuntu? Skip Docker and install the native package from our APT repository. See Debian & Ubuntu.
Requirements
- Docker with the Docker Compose plugin
- 512 MB RAM minimum, 1 GB recommended
Install script
The fastest way to get up and running. Run this on the server you want to host Tindra on:
bash -c "$(curl -sSL https://install.tindra.sh)"
The script asks a few questions, then writes a docker-compose.yml, pulls the images, and optionally starts the stack:
Tindra self-hosted install
→ Checking prerequisites
✓ docker found
✓ docker compose found
→ Install directory
Path [/opt/tindra]:
→ Public URL
URL (e.g. https://tindra.example.com): https://tindra.yourcompany.com
→ Host port
Port [8080]:
Is your reverse proxy (Caddy, nginx, Traefik...) on this machine? [Y/n]:
✓ Listening on 127.0.0.1:8080
→ First account
Email: you@yourcompany.com
Name: Your Name
Password: (hidden)
Confirm: (hidden)
✓ Account details collected
→ Generating database password
✓ 48-character random password generated
→ Writing docker-compose.yml
✓ Written to /opt/tindra/docker-compose.yml
→ Pulling images
→ Starting
→ Creating your account
✓ Account created: you@yourcompany.com
Tindra is running.
Open https://tindra.yourcompany.com and log in.
The database password is auto-generated and written into docker-compose.yml, which only your user can read. There is no sign-up page. The installer creates your admin account through the CLI and pipes the password in on stdin, so it never shows up in a process list.
The proxy question decides how Tindra listens:
- Proxy on this machine (default): Tindra listens on
127.0.0.1only, andTRUSTED_PROXIEScovers loopback and Docker's private ranges. - No local proxy: the port is published on every interface. Docker's port rules bypass ufw and firewalld, so the port is reachable from other machines. If a proxy on another machine forwards to Tindra, give the installer its IP.
Reverse proxy: The installer does not set up a reverse proxy or TLS for you. See Reverse proxy below.
Manual install
If you prefer to set things up yourself, create a directory for the stack and a .env file beside docker-compose.yml:
POSTGRES_PASSWORD=replace-with-a-long-random-hex-password
PUBLIC_URL=https://tindra.example.com
PORT=8080
COOKIE_SECURE=true
TRUSTED_PROXIES=127.0.0.1,::1,172.16.0.0/12,192.168.0.0/16,10.0.0.0/8
For a new database, generate a URL-safe password with openssl rand -hex 24 and replace the placeholder. Keep this file private. If you are adopting this example for an existing database, use its current password: changing POSTGRES_PASSWORD does not change credentials in an initialized Postgres volume. Passwords containing URL-reserved characters need percent-encoding in DATABASE_URL; a hex password avoids that extra step.
The example binds the application to host loopback for a reverse proxy on the same host, and trusts the same proxy ranges the installer does. For a proxy on another host or a container network, adapt the bind address/network and set TRUSTED_PROXIES as described in Reverse proxy.
# docker-compose.yml
services:
postgres:
image: postgres:18-alpine
restart: unless-stopped
environment:
POSTGRES_DB: tindra
POSTGRES_USER: tindra
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD to a nonempty database password}"
volumes:
- postgres_data:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U tindra -d tindra"]
interval: 5s
timeout: 5s
retries: 5
tindra:
image: ghcr.io/blendbyte/tindra:latest
restart: unless-stopped
stop_grace_period: 75s
ports:
- "127.0.0.1:${PORT:-8080}:8080"
environment:
DATABASE_URL: "postgres://tindra:${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD}@postgres:5432/tindra?sslmode=disable"
PUBLIC_URL: "${PUBLIC_URL:?Set PUBLIC_URL}"
BIND_ADDR: ":8080"
DATA_DIR: /data
COOKIE_SECURE: "${COOKIE_SECURE:-false}"
LOG_FORMAT: json
TRUSTED_PROXIES: "${TRUSTED_PROXIES:-}"
UPTIME_ALLOW_PRIVATE_IPS: "${UPTIME_ALLOW_PRIVATE_IPS:-false}"
STATS_API_KEY: "${STATS_API_KEY:-}"
volumes:
- tindra_data:/data
depends_on:
postgres:
condition: service_healthy
volumes:
postgres_data:
tindra_data:
docker compose up -d
Migrations run automatically on startup. Inspect docker compose logs tindra and check http://127.0.0.1:8080/healthz from the host (adjust the port if needed) before creating your first account:
docker compose exec tindra /tindra users create \
--email you@example.com \
--name "Your Name"
It asks for the password twice without echoing it. Use at least 12 characters.
For scripts, pipe the password in with --password-stdin. Tindra reads the first line of stdin. Note the -T, which lets docker compose exec take piped input:
docker compose exec -T tindra /tindra users create \
--email you@example.com \
--name "Your Name" \
--password-stdin < admin-password.txt
--password still works, but the password lands in your shell history and the process list.
There is no sign-up page. users create creates an administrator with all management permissions. Bootstrap this account before configuring SSO, then arrange verified-email or explicit provider linking as described in Authentication.
Environment variables
Use Configuration for the complete server reference, including email, OAuth/SSO, independent retention limits, buffering, and metrics authentication. Add optional runtime variables to the service's environment block, then recreate the container with docker compose up -d.
The .env file beside Compose supplies interpolation values; variables are not automatically passed into the container unless the service references them. Production Postgres is reachable on the Compose network and does not publish a host port.
Running the binary directly, for example under systemd? Put your settings in /etc/tindra/tindra.env. The Debian & Ubuntu package sets this up for you. See Configuration file for the lookup order and permissions.
Persistent storage
Keep both named volumes. Postgres stores telemetry and account/configuration data; /data stores source maps and attachments. The image initializes /data for the application user, UID/GID 65532:65532, and checks writability at startup.
Existing volumes and host bind mounts retain their existing ownership. If startup reports that DATA_DIR cannot be written, correct access for UID/GID 65532 on that mount and restart. Changing the image alone does not repair a previously created volume. See Backup for preserving both stores.
Graceful shutdown
Keep stop_grace_period: 75s so Tindra can finish HTTP shutdown and drain its bounded ingestion queues. These queues are in memory; a forced stop or crash can lose accepted data that has not reached Postgres. Queue retries do not replace persistent storage or backups.
Reverse proxy
Run Tindra behind nginx or Caddy for TLS termination, and set COOKIE_SECURE=true for an HTTPS public URL. For client IP detection and rate limiting, pass X-Forwarded-For and set TRUSTED_PROXIES to the proxy addresses Tindra sees.
Get this wrong and every request looks like it comes from your proxy. Login rate limits are per IP, so a few failed attempts lock everyone out, and the audit log records the proxy instead of the real client.
A proxy on the host usually reaches a bridged container from the Docker network gateway, not 127.0.0.1. That's why the installer and the manual example trust Docker's private ranges (172.16.0.0/12,192.168.0.0/16,10.0.0.0/8) alongside loopback. This is safe only while the port is bound to 127.0.0.1, so nothing outside the host can connect. On a published port, anyone on those networks could fake their client IP. In that case, trust only your proxy's actual address, for example TRUSTED_PROXIES=10.0.0.5. For a directly run binary with a local proxy, the peer is 127.0.0.1 or ::1.
Tindra trusts forwarded headers only from a configured proxy and walks the chain right to left to the first untrusted hop. Leave TRUSTED_PROXIES unset when Tindra is exposed directly.
If you prefer a Unix socket over a TCP port, set BIND_ADDR=unix:/run/tindra/tindra.sock and point your proxy at the socket path instead.
Caddy
your-hostname.example.com {
reverse_proxy localhost:8080
}
Caddy handles TLS automatically.
nginx
server {
listen 443 ssl;
server_name your-hostname.example.com;
location / {
proxy_pass http://localhost:8080;
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;
}
}
Updating
docker compose pull tindra
docker compose up -d tindra
Migrations run on startup, and downtime depends on the database size and migrations in the target version. Back up first, allow graceful shutdown, and check startup logs and /healthz before returning traffic. See Upgrades for the authentication, storage, and networking changes to check on existing installations.
Local development
Development uses a separate loopback-bound Postgres service and a writable ./data directory. Follow the local development instructions in the repository README and contributor guide for the supported Go/Bun versions, frontend build, make db, make run, and CLI account creation. Keep the development database credentials and Compose file separate from production.
Next steps
- Configuration: full environment variable reference
- Authentication: set up OAuth and disable password login
- Backup: back up your Postgres data
Monitor ingestion health
Use Ingestion Monitoring for authenticated Prometheus scrapes, queue and write metrics, and a recovery checklist when accepted data is not appearing. Pending in-memory data is not part of a database backup.