Self-hosting trimset¶
trimset is one image, run as three processes, with Postgres beside it. This page is for the person who keeps it running: what it needs, how to expose it safely, how to back it up and upgrade it. To try it first, use the quickstart.
What runs¶
| Container | What it does | Port |
|---|---|---|
proxy |
The front door. Forwards model calls to your upstream unchanged, records them off the request path, and passes every other path to the server. | 8800, all interfaces |
server |
The API (FastAPI): ingest, outcomes, fits, compiles, policies, the audit log. | 8810, inside the compose network only |
worker |
Background jobs: joining late outcomes, default labels by rule, fits and compiles, scheduled refits, partition upkeep. | none |
postgres |
The only store. No Redis, no queue. Decisions live in monthly partitions. | none |
The proxy, server and worker are the same image with different commands. Aim for 2 vCPU and 4 GB on one machine, with Postgres on the same box; disk grows with your decision volume (a decision record is a few hundred bytes plus its hashes).
Before you expose it¶
- Put TLS in front. Port 8800 speaks plain HTTP and serves both the model proxy and
the admin API. Terminate TLS in a reverse proxy and publish only that. A Caddy
example, with automatic certificates:
Then publish trimset's own port on loopback only: in
trimset.example.com { reverse_proxy 127.0.0.1:8800 log { output discard # see the next point } }compose.yaml, change the proxy service'sports:line to"127.0.0.1:8800:8800". - Prefer the header to the path form of the proxy key. Calls can authenticate with
X-Trimset-Key: <key>or with the key in the URL,/p/<key>/v1/.... trimset redacts the key from its own logs, but a reverse proxy's access log would record the path. Use the header, or turn access logging off as above. - Keep
.envprivate. It holdsTRIMSET_SECRETand the database password. - Hand out the narrowest key.
proxyfor the app's model calls,ingestfor the backend and the SDK,reviewfor reviewers,adminfor people who approve policies. Keys are stored as hashes and shown once.trimset-server revoke-key <key_id>(orDELETE /v1/admin/keys/<id>) ends one; a revoked key is refused everywhere within 30 seconds.
Configuration¶
Everything is an environment variable on the compose services (.env is read by all of
them). Only the first two are required.
| Variable | Default | Meaning |
|---|---|---|
TRIMSET_DB_PASSWORD |
required | Postgres password. Letters and digits only. |
TRIMSET_SECRET |
required | At least 32 characters (openssl rand -hex 32). Derives each project's key for state hashes (ADR 0009). Keep it stable: with a new secret, new decisions get hashes that no longer match older ones. |
TRIMSET_UPSTREAM_<NAME>_URL |
none | A model server. <NAME> is yours, e.g. JEV, OLLAYA. Only the operator can set these. |
TRIMSET_UPSTREAM_<NAME>_FORMAT |
systemone |
systemone or openai-decisions. |
TRIMSET_UPSTREAM_<NAME>_KEY |
none | A key sent to that URL only, as Authorization: Bearer. The URL must be https. |
TRIMSET_UPSTREAM_<NAME>_PROJECTS |
every project | Comma list of project ids allowed to use it. |
TRIMSET_WORKERS |
2 |
Server processes. |
TRIMSET_LOG_LEVEL |
INFO |
|
TRIMSET_DB_POOL_MAX |
10 |
Connections per server process. |
TRIMSET_AUTO_MIGRATE |
true in compose |
Migrate the database on start. |
TRIMSET_PROXY_HOST, TRIMSET_PROXY_PORT |
0.0.0.0, 8800 |
Where the proxy listens inside its container. Leave them; limit who can reach it with the ports: line in compose.yaml. |
TRIMSET_PROXY_BUFFER |
50000 |
Records held in memory while the database is slow or down. When full, the oldest are dropped (and counted); the proxy keeps forwarding. |
TRIMSET_KEY_CACHE_TTL_S |
30 (max 60) |
How long a key check is cached; also how long a revoked key may still work. |
TRIMSET_JOIN_INTERVAL_S, TRIMSET_RULES_INTERVAL_S, TRIMSET_JOBS_INTERVAL_S, TRIMSET_MAINTENANCE_INTERVAL_S |
60, 3600, 1, 3600 | How often the worker joins held outcomes, applies default labels, runs queued jobs, and creates partitions. |
OTEL_EXPORTER_OTLP_ENDPOINT |
none | Send OpenTelemetry traces to a collector. Off unless set. |
Per-project settings (retention, prices, tag allow-list, the refit schedule) live in the
database and are set through the API: PATCH /v1/admin/project. Changes are audited.
What leaves the machine¶
The proxy connects out only to the upstreams you configured. The server makes no outbound
connections of its own. Nothing is sent to anyone else, and there is no usage reporting.
If you set OTEL_EXPORTER_OTLP_ENDPOINT, traces go there.
Day to day¶
- Health.
GET /healthanswers if the process is up.GET /readyalso checks Postgres and answers 503 when it can't reach it. Neither needs a key or returns data. - Logs. Every container logs one JSON object per line to standard output
(
docker compose logs -f). Keys are redacted, including in the proxy's URL form. - If Postgres is down, the proxy keeps forwarding your model calls and holds the
records it can't write (up to
TRIMSET_PROXY_BUFFER); it writes them when the database returns. The SDK keeps deciding from its cached policies. - More than one worker is safe (jobs are claimed one at a time, and a refit is queued once per project and minute), but one is enough.
Backups¶
All state is in the Postgres volume. A logical dump is enough at this size:
docker compose exec -T postgres pg_dump -U trimset trimset | gzip > "trimset-$(date +%F).sql.gz"
To restore, stop the writers, replace the database with an empty one, load the dump, and start again (the server and worker find the schema in place):
docker compose stop proxy server worker
docker compose exec -T postgres psql -U trimset -d postgres \
-c 'DROP DATABASE trimset WITH (FORCE)' -c 'CREATE DATABASE trimset'
gunzip -c trimset-2026-12-01.sql.gz | docker compose exec -T postgres psql -U trimset trimset
docker compose start server worker proxy
Try a restore on a spare machine before you need one. TRIMSET_SECRET is not in the dump:
keep it with your other secrets, and restore with the same value.
Upgrading¶
git pull # or set the image tag to the version you want
docker compose pull && docker compose up -d
The server and the worker migrate the database when they start, under an advisory lock so that only one does. Migrations only move forward, so back up first, and read the release's notes in CHANGELOG.md. There is no downgrade: to go back, restore the backup you took.
While trimset is 0.x, keep the SDK and the server on the same version.
Not yet¶
Retention (dropping months older than a project's retention_days) and deleting all data
for one external reference are planned for v0.1 and are not enforced yet: today, nothing
is deleted automatically. Drift and silent-upgrade alerts are planned too. See the
plan.