-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdocker-compose.selfhosted.yml
More file actions
617 lines (582 loc) · 23.6 KB
/
Copy pathdocker-compose.selfhosted.yml
File metadata and controls
617 lines (582 loc) · 23.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
# ─── Truss Self-Hosted Docker Compose ───
# One-command deployment: docker compose -f docker-compose.selfhosted.yml --env-file .env.selfhosted up -d
#
# Services:
# postgres — PostgreSQL 16 (shared by Truss API, Kratos, Keto)
# kratos-migrate — One-shot: runs Kratos DB migrations then exits
# kratos — Ory Kratos (authentication / identity management)
# keto-migrate — One-shot: runs Keto DB migrations then exits
# keto — Ory Keto (authorization / permissions)
# minio — MinIO S3-compatible object storage
# flagd-init — One-shot: seeds initial flag config
# flagd — Feature flag evaluation engine (CNCF/OpenFeature)
# truss-api — Truss backend API (Express / Node.js)
# truss-dashboard — Truss dashboard SPA (Nginx serving static build)
#
# Exposed ports:
# 3000 — Dashboard (the ONLY port that should face the internet)
# All other services are internal to the Docker network.
# See comments in each service section to expose additional ports if needed.
name: truss
services:
# ────────────────────────────────────────────
# PostgreSQL — shared database for all services
# ────────────────────────────────────────────
# Truss API, Kratos, and Keto all connect to this instance.
# Keto uses a separate `keto` search_path to avoid table collisions.
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: truss
POSTGRES_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD is required - set it in .env.selfhosted}
POSTGRES_DB: truss
healthcheck:
test: ["CMD-SHELL", "pg_isready -U truss -d truss"]
interval: 5s
timeout: 5s
retries: 20
volumes:
- truss_postgres_data:/var/lib/postgresql/data
# ────────────────────────────────────────────
# Ory Kratos — Authentication / Identity
# ────────────────────────────────────────────
# Handles user registration, login, password recovery, MFA, passkeys.
# Migration runs as a one-shot init container before Kratos starts.
kratos-migrate:
image: oryd/kratos:v1.2.0
restart: "no"
depends_on:
postgres:
condition: service_healthy
environment:
DSN: postgres://truss:${DB_PASSWORD:-truss}@postgres:5432/truss?sslmode=disable
command:
- migrate
- sql
- -e
- --yes
- postgres://truss:${DB_PASSWORD:-truss}@postgres:5432/truss?sslmode=disable
kratos:
image: oryd/kratos:v1.2.0
restart: unless-stopped
depends_on:
kratos-migrate:
condition: service_completed_successfully
entrypoint: ["/bin/sh", "-ec"]
command:
- |
# Bootstrap Kratos config inline — no bind mounts needed.
# This pattern works on any Docker host without volume mapping config files.
mkdir -p /tmp/kratos
cat >/tmp/kratos/identity.schema.json <<'JSON'
{
"$$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json",
"$$schema": "http://json-schema.org/draft-07/schema#",
"title": "Person",
"type": "object",
"properties": {
"traits": {
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email",
"ory.sh/kratos": {
"credentials": {
"password": { "identifier": true },
"code": { "identifier": true, "via": "email" },
"passkey": { "display_name": true },
"webauthn": { "identifier": true },
"totp": { "account_name": true }
},
"verification": { "via": "email" },
"recovery": { "via": "email" }
}
}
},
"required": ["email"],
"additionalProperties": false
}
}
}
JSON
cat >/tmp/kratos/kratos.yml <<'YAML'
version: v0.13.0
dsn: postgres://truss:${DB_PASSWORD:-truss}@postgres:5432/truss?sslmode=disable
serve:
public:
base_url: ${TRUSS_PUBLIC_URL:-http://localhost:3000}/.ory/kratos/public/
cors:
enabled: true
allowed_origins:
- "${TRUSS_PUBLIC_URL:-http://localhost:3000}"
allowed_methods: ["POST","GET","PUT","PATCH","DELETE"]
allowed_headers: ["Authorization","Cookie","Content-Type","X-CSRF-Token"]
exposed_headers: ["Content-Type","Set-Cookie"]
admin:
base_url: http://kratos:4434/
selfservice:
default_browser_return_url: ${TRUSS_PUBLIC_URL:-http://localhost:3000}/
methods:
password:
enabled: true
config:
haveibeenpwned_enabled: true
min_password_length: 8
identifier_similarity_check_enabled: true
code:
enabled: true
config:
lifespan: 15m
totp:
enabled: true
config:
issuer: Truss
passkey:
enabled: true
config:
rp:
display_name: Truss
id: localhost
origins:
- "${TRUSS_PUBLIC_URL:-http://localhost:3000}"
lookup_secret:
enabled: true
flows:
login:
ui_url: ${TRUSS_PUBLIC_URL:-http://localhost:3000}/login
lifespan: 10m
registration:
ui_url: ${TRUSS_PUBLIC_URL:-http://localhost:3000}/register
lifespan: 10m
after:
password:
hooks:
- hook: session
verification:
enabled: true
ui_url: ${TRUSS_PUBLIC_URL:-http://localhost:3000}/verification
use: code
recovery:
enabled: true
ui_url: ${TRUSS_PUBLIC_URL:-http://localhost:3000}/recovery
use: code
settings:
ui_url: ${TRUSS_PUBLIC_URL:-http://localhost:3000}/settings
required_aal: highest_available
session:
lifespan: 24h
cookie:
name: ory_kratos_session
same_site: Lax
domain: ""
courier:
smtp:
# Configure SMTP in .env.selfhosted for email verification/recovery.
# Without SMTP, verification and recovery emails will not be sent.
connection_uri: smtp://not-configured:25
from_address: noreply@truss.local
from_name: Truss
identity:
default_schema_id: default
schemas:
- id: default
url: file:///tmp/kratos/identity.schema.json
secrets:
cookie:
- "${KRATOS_COOKIE_SECRET:-change-me-cookie-secret-32chars!}"
cipher:
- "${KRATOS_CIPHER_SECRET:-change-me-cipher-secret-32chars!}"
log:
level: info
format: json
YAML
exec kratos serve all --config /tmp/kratos/kratos.yml
# Kratos public API is internal — Nginx proxies auth requests.
# Uncomment ONLY if external apps need direct Kratos access.
# ports:
# - "4433:4433"
expose:
- "4433"
- "4434"
# ────────────────────────────────────────────
# Ory Keto — Authorization / Permissions
# ────────────────────────────────────────────
# Relation-based access control (ReBAC). Uses a separate `keto` schema
# in the shared Postgres database to avoid table collisions with Kratos.
postgres-init:
# One-shot job: create the keto + hydra schemas so their migrations can use search_path.
image: postgres:16-alpine
restart: "no"
depends_on:
postgres:
condition: service_healthy
entrypoint: ["/bin/sh", "-ec"]
command:
- |
psql "postgres://truss:${DB_PASSWORD:-truss}@postgres:5432/truss?sslmode=disable" \
-c "CREATE SCHEMA IF NOT EXISTS keto; CREATE SCHEMA IF NOT EXISTS hydra;"
echo "keto + hydra schemas ready"
keto-migrate:
image: oryd/keto:v0.12.0-alpha.0
restart: "no"
depends_on:
postgres-init:
condition: service_completed_successfully
entrypoint: ["/bin/sh", "-ec"]
command:
- |
mkdir -p /tmp/keto
cat >/tmp/keto/keto.yml <<'YAML'
dsn: postgres://truss:${DB_PASSWORD:-truss}@postgres:5432/truss?sslmode=disable&search_path=keto
namespaces:
- id: 0
name: default
YAML
keto migrate up -c /tmp/keto/keto.yml --yes
keto:
image: oryd/keto:v0.12.0-alpha.0
restart: unless-stopped
depends_on:
keto-migrate:
condition: service_completed_successfully
entrypoint: ["/bin/sh", "-ec"]
command:
- |
mkdir -p /tmp/keto
cat >/tmp/keto/keto.yml <<'YAML'
dsn: postgres://truss:${DB_PASSWORD:-truss}@postgres:5432/truss?sslmode=disable&search_path=keto
serve:
read:
host: 0.0.0.0
port: 4466
write:
host: 0.0.0.0
port: 4467
namespaces:
- id: 0
name: default
log:
level: info
format: json
YAML
exec keto serve all --config /tmp/keto/keto.yml
# Keto ports are internal-only — accessed by the API service, not exposed to host.
expose:
- "4466"
- "4467"
# ────────────────────────────────────────────
# Ory Hydra — OAuth2 / OIDC
# ────────────────────────────────────────────
# Be your own OAuth2 / OpenID Connect provider. Uses a separate `hydra` schema
# in the shared Postgres. Login + consent are bridged to Truss via the API.
hydra-migrate:
image: oryd/hydra:v2.2.0
restart: "no"
depends_on:
postgres-init:
condition: service_completed_successfully
environment:
DSN: "postgres://truss:${DB_PASSWORD:-truss}@postgres:5432/truss?sslmode=disable&search_path=hydra"
command: migrate sql -e --yes
hydra:
image: oryd/hydra:v2.2.0
restart: unless-stopped
depends_on:
hydra-migrate:
condition: service_completed_successfully
environment:
DSN: "postgres://truss:${DB_PASSWORD:-truss}@postgres:5432/truss?sslmode=disable&search_path=hydra"
# Generate with: openssl rand -hex 16
SECRETS_SYSTEM: "${HYDRA_SECRETS_SYSTEM:?HYDRA_SECRETS_SYSTEM is required - generate with openssl rand -hex 16}"
URLS_SELF_ISSUER: "${HYDRA_ISSUER_URL:-http://localhost:4444}"
URLS_LOGIN: "${TRUSS_PUBLIC_URL:-http://localhost:3000}/api/hydra/bridge/login"
URLS_CONSENT: "${TRUSS_PUBLIC_URL:-http://localhost:3000}/api/hydra/bridge/consent"
# --dev allows the http issuer used by default; front Hydra with TLS and set
# HYDRA_ISSUER_URL to a public https URL in production.
command: serve all --dev
# OAuth2 public endpoints (token, auth, discovery, jwks) — apps reach Hydra here.
ports:
- "4444:4444"
# Admin API is internal-only — used by the Truss API, never exposed.
expose:
- "4445"
# ────────────────────────────────────────────
# Ory Oathkeeper — API Gateway
# ────────────────────────────────────────────
# Identity & Access proxy. Stateless — access rules are managed by the Truss
# API (truss_internal.gateway_rules) and synced to Oathkeeper at runtime.
oathkeeper:
image: oryd/oathkeeper:v0.40.7
restart: unless-stopped
depends_on:
kratos:
condition: service_started
entrypoint: ["/bin/sh", "-ec"]
command:
- |
mkdir -p /tmp/oathkeeper
cat >/tmp/oathkeeper/rules.json <<'JSON'
[]
JSON
cat >/tmp/oathkeeper/config.yaml <<'YAML'
log: { level: info, format: json }
serve:
proxy: { port: 4455 }
api: { port: 4456 }
access_rules:
repositories:
- file:///tmp/oathkeeper/rules.json
errors:
fallback: [json]
handlers:
json: { enabled: true, config: { verbose: true } }
authenticators:
noop: { enabled: true }
unauthorized: { enabled: true }
anonymous: { enabled: true, config: { subject: guest } }
cookie_session:
enabled: true
config:
check_session_url: http://kratos:4433/sessions/whoami
preserve_path: true
only: [ory_kratos_session]
authorizers:
allow: { enabled: true }
deny: { enabled: true }
mutators:
noop: { enabled: true }
header:
enabled: true
config:
headers:
X-User: "{{ print .Subject }}"
YAML
exec oathkeeper serve --config /tmp/oathkeeper/config.yaml
# Gateway proxy (4455) + admin API (4456) are internal — the Truss API manages rules.
expose:
- "4455"
- "4456"
# ────────────────────────────────────────────
# MinIO — S3-compatible object storage
# ────────────────────────────────────────────
# Provides file storage with S3 API. The console (port 9001) is exposed
# for direct bucket management; the S3 API (9000) is internal to Docker.
minio:
image: minio/minio:RELEASE.2024-09-22T00-33-43Z
restart: unless-stopped
command: server /data --console-address :9001
environment:
MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:?MINIO_ACCESS_KEY is required - set it in .env.selfhosted}
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:?MINIO_SECRET_KEY is required - set it in .env.selfhosted}
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 10s
timeout: 5s
retries: 5
# S3 API + Console are internal only — access via Truss dashboard.
# Uncomment ports below ONLY if you need direct S3 SDK access or MinIO console.
# WARNING: Secure with strong credentials before exposing to the network.
# ports:
# - "9000:9000" # S3 API (for presigned URLs from external apps)
# - "9001:9001" # MinIO Console (web UI)
expose:
- "9000"
- "9001"
volumes:
- truss_minio_data:/data
# ────────────────────────────────────────────
# flagd — Feature flag evaluation engine
# ────────────────────────────────────────────
# CNCF/OpenFeature feature flag daemon. Truss API syncs flag definitions
# to flagd; client SDKs evaluate flags via gRPC/HTTP on port 8013.
flagd-init:
image: alpine:3.20
restart: "no"
entrypoint: ["/bin/sh", "-ec"]
command:
- |
if [ ! -f /etc/flagd/flags.flagd.json ]; then
mkdir -p /etc/flagd
cat >/etc/flagd/flags.flagd.json <<'JSON'
{
"$schema": "https://flagd.dev/schema/v0/flags.json",
"flags": {
"truss-healthcheck": {
"state": "ENABLED",
"variants": { "on": true, "off": false },
"defaultVariant": "on"
}
}
}
JSON
echo "flagd: seeded initial flag config"
else
echo "flagd: config already exists, skipping seed"
fi
volumes:
- flagd_data:/etc/flagd
flagd:
image: ghcr.io/open-feature/flagd:v0.11.1
restart: unless-stopped
depends_on:
flagd-init:
condition: service_completed_successfully
command:
- start
- --uri
- file:/etc/flagd/flags.flagd.json
- --port
- "8013"
- --management-port
- "8014"
- --log-format
- json
volumes:
- flagd_data:/etc/flagd
expose:
- "8013"
- "8014"
# ────────────────────────────────────────────
# Valkey — Redis-compatible cache / KV store
# ────────────────────────────────────────────
# In-memory cache, key/value store, session store, rate-limit counters.
# Password-protected (requirepass); the Truss API connects with VALKEY_PASSWORD.
valkey:
image: valkey/valkey:8-alpine
restart: unless-stopped
environment:
VALKEY_PASSWORD: "${VALKEY_PASSWORD:?VALKEY_PASSWORD is required - set it in .env.selfhosted}"
# requirepass is read from the container env at runtime, not baked into the args.
# Ephemeral cache: persistence (RDB/AOF) disabled — restart = empty cache, which
# is the right default for a cache. Flip --save/--appendonly + add a volume for durability.
command: ["sh", "-c", "exec valkey-server --requirepass \"$$VALKEY_PASSWORD\" --save \"\" --appendonly no"]
healthcheck:
test: ["CMD-SHELL", "valkey-cli -a \"$$VALKEY_PASSWORD\" ping | grep -q PONG"]
interval: 10s
timeout: 5s
retries: 5
# Internal-only — accessed by the API service, not exposed to the host.
expose:
- "6379"
# ────────────────────────────────────────────
# Truss API — Express / Node.js backend
# ────────────────────────────────────────────
# Handles all dashboard API requests, SQL workbench, client API,
# WebSocket realtime, and orchestrates Kratos/Keto/MinIO.
truss-api:
# Published multi-arch image — no repo clone needed. To build from source instead
# (requires the repo cloned), comment `image:` and uncomment the `build:` block.
image: ghcr.io/binarysquadd/truss-api:latest
# build:
# context: .
# dockerfile: Dockerfile
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
kratos:
condition: service_started
keto:
condition: service_started
hydra:
condition: service_started
oathkeeper:
condition: service_started
minio:
condition: service_healthy
flagd:
condition: service_started
valkey:
condition: service_healthy
environment:
# ── Self-hosted mode flags ──
TRUSS_SELF_HOSTED: "true"
TRUSS_AUTH_REQUIRED: "true"
# Comma-separated Kratos identity IDs granted admin powers (DB roles, migrations,
# backups, authz rules). Register first, copy your ID from the Authentication panel.
TRUSS_ADMIN_IDENTITY_IDS: "${TRUSS_ADMIN_IDENTITY_IDS:-}"
# First-boot default admin (seeded only if no identity exists yet). If the
# password is left empty a random one is generated + printed to the API logs.
TRUSS_BOOTSTRAP_ADMIN_EMAIL: "${TRUSS_BOOTSTRAP_ADMIN_EMAIL:-admin@truss.local}"
TRUSS_BOOTSTRAP_ADMIN_PASSWORD: "${TRUSS_BOOTSTRAP_ADMIN_PASSWORD:-}"
NODE_ENV: production
API_PORT: "8787"
# Public URL users hit in the browser. Drives the session-cookie Secure flag:
# http:// (e.g. localhost) → no Secure (so the cookie isn't dropped over plain
# HTTP); https:// → Secure. Override with COOKIE_SECURE=true/false if needed.
TRUSS_PUBLIC_URL: "${TRUSS_PUBLIC_URL:-http://localhost:3000}"
# Tracing is opt-in. Empty = off. The bundled observability overlay sets this to the
# OTel Collector; or point it at your own collector. Metrics (/metrics) + JSON logs are
# always on regardless.
OTEL_EXPORTER_OTLP_ENDPOINT: "${OTEL_EXPORTER_OTLP_ENDPOINT:-}"
# ── Database ──
DATABASE_URL: postgres://truss:${DB_PASSWORD}@postgres:5432/truss?sslmode=disable
# ── Security ──
# Generate with: openssl rand -hex 32
ENCRYPTION_KEY: "${ENCRYPTION_KEY:?ENCRYPTION_KEY is required - generate with openssl rand -hex 32}"
# ── Ory Kratos (auth) — internal Docker DNS ──
KRATOS_PUBLIC_URL: http://kratos:4433
KRATOS_ADMIN_URL: http://kratos:4434
# ── Ory Keto (authz) — internal Docker DNS ──
KETO_READ_URL: http://keto:4466
KETO_WRITE_URL: http://keto:4467
# ── Ory Hydra (OAuth2/OIDC) — internal Docker DNS ──
HYDRA_PUBLIC_URL: http://hydra:4444
HYDRA_ADMIN_URL: http://hydra:4445
# ── Ory Oathkeeper (API gateway) — internal Docker DNS ──
OATHKEEPER_PROXY_URL: http://oathkeeper:4455
OATHKEEPER_ADMIN_URL: http://oathkeeper:4456
# ── MinIO (storage) — internal Docker DNS ──
MINIO_S3_ENDPOINT: http://minio:9000
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
# ── flagd (feature flags) — internal Docker DNS ──
FLAGD_HOST: http://flagd
FLAGD_PORT: "8013"
# ── Valkey (cache/KV) — internal Docker DNS ──
VALKEY_HOST: valkey
VALKEY_PORT: "6379"
VALKEY_PASSWORD: ${VALKEY_PASSWORD}
# API port is internal — Nginx proxies /api and /v1 to it.
expose:
- "8787"
# ────────────────────────────────────────────
# Truss Dashboard — React SPA served by Nginx
# ────────────────────────────────────────────
# Built with VITE_SELF_HOSTED=true (hides billing UI, org switcher).
# Nginx serves the static build and proxies API/WebSocket requests.
truss-dashboard:
image: ghcr.io/binarysquadd/truss-dashboard:latest
# build:
# context: .
# dockerfile: selfhosted/Dockerfile.dashboard
restart: unless-stopped
depends_on:
- truss-api
ports:
# Main entry point — users access the dashboard here.
- "3000:80"
# MCP server — lets an AI agent operate this instance over the Model Context Protocol
# at http://localhost:8765/mcp. Callers authenticate with a service_role key
# (Authorization: Bearer truss_sk_...). Talks to the API over the internal network.
# Optional, off by default. Enable with: docker compose --profile mcp up -d
# (the published truss-mcp image ships from v0.2.0 onward).
truss-mcp:
profiles: ["mcp"]
image: ghcr.io/binarysquadd/truss-mcp:latest
# build:
# context: ./apps/mcp
restart: unless-stopped
depends_on:
- truss-api
environment:
TRUSS_API_URL: http://truss-api:8787
MCP_PORT: "8765"
ports:
- "8765:8765"
volumes:
truss_postgres_data:
truss_minio_data:
flagd_data: