Repository navigation
Expand file tree
/
Copy pathdocker-compose.yml
More file actions
300 lines (289 loc) · 12.5 KB
/
Copy pathdocker-compose.yml
File metadata and controls
300 lines (289 loc) · 12.5 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
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: fora
POSTGRES_PASSWORD: fora
POSTGRES_DB: fora
ports:
# Override by setting FORA_PG_PORT in the environment (or via --env-file
# from scripts/devstack.sh). Default 55432 avoids clashing with local
# Postgres or testcontainers running on 5432.
- "${FORA_PG_PORT:-55432}:5432"
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U fora -d fora"]
interval: 5s
timeout: 3s
retries: 5
redis:
image: redis:7-alpine
restart: unless-stopped
ports:
- "${FORA_REDIS_PORT:-56379}:6379"
command: ["redis-server", "--save", "", "--loglevel", "warning"]
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
# Local-dev TigerBeetle for running the Exchange binary against a real ledger.
# (Integration tests do NOT use this service — they spin up their own container
# via internal/testutil.StartSharedTigerBeetle.) A single-replica cluster: the
# data file is formatted on first boot only (the volume persists it across
# restarts), then started. Point the Exchange at it with
# EXCHANGE_BILLING_TB_ADDRESS=127.0.0.1:${FORA_TB_PORT:-53000}.
#
# --development relaxes Direct IO (Docker/overlay/macOS volumes) and shrinks
# caches. security_opt/cap_add are mandatory: Docker >= 25 blocks io_uring
# without seccomp=unconfined (error: PermissionDenied), and TigerBeetle locks
# memory (macOS error: SystemResources without IPC_LOCK).
tigerbeetle:
image: ghcr.io/tigerbeetle/tigerbeetle:0.17.8
restart: unless-stopped
entrypoint: ["sh", "-c"]
command:
- |
set -e
[ -f /data/0_0.tigerbeetle ] || /tigerbeetle format --cluster=0 --replica=0 --replica-count=1 --development /data/0_0.tigerbeetle
exec /tigerbeetle start --addresses=0.0.0.0:3000 --cache-grid=256MiB --development /data/0_0.tigerbeetle
security_opt:
- "seccomp=unconfined"
cap_add:
- "IPC_LOCK"
# The image is busybox-based (it already runs `sh -c` above), so `nc` is
# available for a TCP probe. This proves the replica is accepting connections,
# which is what callers wait for — not that the cluster answers correctly; only
# a real request does that. Formatting runs before `start`, so the port opening
# is the honest readiness edge.
healthcheck:
test: ["CMD", "nc", "-z", "127.0.0.1", "3000"]
interval: 5s
timeout: 3s
retries: 5
ports:
- "${FORA_TB_PORT:-53000}:3000"
volumes:
- tbdata:/data
# Local-dev Vault for running the Identity Service against a real key store.
# (Integration tests do NOT use this service — they spin up their own container
# via internal/testutil.StartSharedVault.) Dev mode is in-memory and starts
# UNSEALED with a fixed root token: perfect for a laptop, unacceptable anywhere
# else, which is why nothing outside this file knows that token. The keys the
# service mints here vanish when the container stops, exactly as dev keys should.
vault:
image: hashicorp/vault:1.20
restart: unless-stopped
environment:
VAULT_DEV_ROOT_TOKEN_ID: ${FORA_VAULT_DEV_TOKEN:-dev-root-token}
VAULT_DEV_LISTEN_ADDRESS: 0.0.0.0:8200
# IPC_LOCK lets Vault lock its memory so secrets are never swapped to disk.
cap_add:
- "IPC_LOCK"
# Bound to the loopback interface, not 0.0.0.0: this Vault boots unsealed with a
# root token that is a published default in this file, and it is the one service
# here holding agents' private keys. On a shared network an unbound port hands
# every one of them to anyone who can reach the host.
ports:
- "127.0.0.1:${FORA_VAULT_PORT:-58200}:8200"
healthcheck:
test: ["CMD", "vault", "status", "-address=http://127.0.0.1:8200"]
interval: 5s
timeout: 3s
retries: 5
# ── Zitadel: the upstream OIDC provider, with two instances: developers sign
# up at http://localhost:58080, publishers at http://127.0.0.1:58080 (two
# host names, so two separate browser sessions). Integration tests do not use
# these; the `zitadel` test tier starts its own pair via
# internal/testutil.StartSharedZitadel. Bring them up with `make zitadel-up`,
# which runs deploy/zitadel/bootstrap/bootstrap.sh for each instance — and,
# when GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET are exported, Google
# federation on the developers' side. Sign-in then runs through the Identity
# service's and the console's own flows.
#
# The `zitadel` profile keeps all of them out of the default set, so a bare
# `docker compose up` skips their ~30-45s boot. Corollary: a bare
# `docker compose down` does not stop them either — use `make zitadel-down`.
zitadel-db:
image: postgres:17-alpine
restart: unless-stopped
profiles: ["zitadel"]
environment:
POSTGRES_USER: zitadel
POSTGRES_PASSWORD: zitadel
POSTGRES_DB: zitadel
# Not published: only Zitadel talks to it, over the compose network.
volumes:
- zitadeldata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U zitadel -d zitadel"]
interval: 5s
timeout: 3s
retries: 5
# v3.4.9 is PINNED: v4.x has an open migration-03 boot crash, and v3's Login V1
# fixes the external-IdP callback path Google must be told about (see runbook).
zitadel:
image: ghcr.io/zitadel/zitadel:v3.4.9
restart: unless-stopped
profiles: ["zitadel"]
command:
- start-from-init
- --masterkey
- ${FORA_ZITADEL_MASTERKEY:-MasterkeyNeedsToHave32Characters}
- --tlsMode
- disabled
- --steps
- /etc/zitadel/init-steps.yaml
environment:
# ExternalDomain + ExternalPort are baked into the issuer, JWKS, and endpoint
# URLs AT INIT, not derived from the request Host. So the published port is
# fixed, never ephemeral, and changing it after first boot needs a
# `make zitadel-down` (which drops the volumes) first.
ZITADEL_EXTERNALDOMAIN: ${FORA_ZITADEL_DOMAIN:-localhost}
ZITADEL_EXTERNALPORT: ${FORA_ZITADEL_PORT:-58080}
ZITADEL_EXTERNALSECURE: "false"
ZITADEL_PORT: "8080"
ZITADEL_TLS_ENABLED: "false"
ZITADEL_DATABASE_POSTGRES_HOST: zitadel-db
ZITADEL_DATABASE_POSTGRES_PORT: "5432"
ZITADEL_DATABASE_POSTGRES_DATABASE: zitadel
ZITADEL_DATABASE_POSTGRES_USER_USERNAME: zitadel
ZITADEL_DATABASE_POSTGRES_USER_PASSWORD: zitadel
ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE: disable
ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME: zitadel
ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD: zitadel
ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE: disable
# The System API user that creates the publisher instance; its key is
# zitadel-system-key's, read at the user's first call.
ZITADEL_SYSTEMAPIUSERS: '{"fora-instance-admin":{"Path":"/systemapi/public.pem"}}'
# Every instance's mail goes to mailpit, whose inbox is at
# http://127.0.0.1:58025. Zitadel reads these settings when it creates
# an instance, so a change needs `make zitadel-down` first.
ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_SMTP_HOST: mailpit:1025
ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_SMTP_USER: ""
ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_SMTP_PASSWORD: ""
ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_TLS: "false"
ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_FROM: noreply@zitadel.example
ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_FROMNAME: FORA
# root, so FirstInstance can write the admin PAT into the shared volume.
user: "0"
volumes:
- ./deploy/zitadel/init-steps.yaml:/etc/zitadel/init-steps.yaml:ro
- zitadel-bootstrap:/bootstrap
- zitadel-systemapi:/systemapi:ro
depends_on:
zitadel-db:
condition: service_healthy
mailpit:
condition: service_healthy
# Same host-specific stylesheet routes as the deployed Caddy front end.
# The public port stays the same; Zitadel still owns every authentication route.
zitadel-login:
image: caddy:2-alpine
restart: unless-stopped
profiles: ["zitadel"]
ports:
- "127.0.0.1:${FORA_ZITADEL_PORT:-58080}:8080"
volumes:
- ./deploy/zitadel/Caddyfile:/etc/caddy/Caddyfile:ro
- ./deploy/zitadel/developer-login.css:/srv/branding/developer-login.css:ro
- ./deploy/zitadel/publisher-login.css:/srv/branding/publisher-login.css:ro
depends_on:
- zitadel
# Catches every email both Zitadel instances send: sign-up codes, password
# resets. Read them at http://127.0.0.1:58025.
mailpit:
image: axllent/mailpit:v1.31.4
profiles: ["zitadel"]
ports:
- "127.0.0.1:58025:8025"
healthcheck:
test: ["CMD", "/mailpit", "readyz"]
interval: 2s
timeout: 3s
retries: 30
# One-shot provisioning of the developer instance; see
# deploy/zitadel/bootstrap/bootstrap.sh. It dials zitadel:8080 over the
# compose network while ExternalDomain is `localhost`, so it overrides the
# Host header — Zitadel routes instances by it.
zitadel-init:
image: curlimages/curl:8.11.1
profiles: ["zitadel"]
entrypoint: ["sh", "/zitadel/bootstrap.sh"]
# root, to match the zitadel service: FirstInstance creates /bootstrap owned by
# root, and this image's default uid 100 cannot write the client id/secret there.
user: "0"
environment:
ZITADEL_INSTANCE_ROLE: developers
ZITADEL_BASE_URL: http://zitadel:8080
ZITADEL_INSTANCE_HOST: ${FORA_ZITADEL_DOMAIN:-localhost}
ZITADEL_ISSUER: http://${FORA_ZITADEL_DOMAIN:-localhost}:${FORA_ZITADEL_PORT:-58080}
IDENTITY_AUTH_ISSUER: ${IDENTITY_AUTH_ISSUER:-http://localhost:8083}
EXTRA_REDIRECT_URIS: ${EXTRA_REDIRECT_URIS:-}
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}
# `ROTATE_CLIENT_SECRET=1 make zitadel-up` mints new secrets; without it a
# re-run keeps every secret Zitadel still accepts.
ROTATE_CLIENT_SECRET: ${ROTATE_CLIENT_SECRET:-}
volumes:
- ./deploy/zitadel/bootstrap:/zitadel:ro
- zitadel-bootstrap:/bootstrap
depends_on:
- zitadel
# One-shot: the System API key this stack's Zitadel trusts, and a token from
# it that lives five minutes, minted right before the publisher instance's
# step uses it.
zitadel-system-key:
image: alpine/openssl:3.3.2
profiles: ["zitadel"]
entrypoint: ["sh", "/zitadel/local-system-key.sh"]
user: "0"
environment:
AUDIENCE: http://${FORA_ZITADEL_DOMAIN:-localhost}:${FORA_ZITADEL_PORT:-58080}
KEY_DIR: /systemapi
volumes:
- ./deploy/zitadel/bootstrap:/zitadel:ro
- zitadel-systemapi:/systemapi
# One-shot (publishers.sh): creates the publisher instance at 127.0.0.1
# through the System API (or finds it), then provisions it: the console's
# OIDC client — its /auth/callback and /signed-out on CONSOLE_PUBLIC_ORIGIN,
# which must match what the console is configured with, exactly — the admin
# and alice.
zitadel-publishers-init:
image: curlimages/curl:8.11.1
profiles: ["zitadel"]
entrypoint: ["sh", "/zitadel/publishers.sh"]
user: "0"
environment:
ZITADEL_BASE_URL: http://zitadel:8080
ZITADEL_INSTANCE_HOST: 127.0.0.1
ZITADEL_PAT_FILE: /publishers/bootstrap.pat
ZITADEL_SYSTEM_TOKEN_FILE: /systemapi/token
ZITADEL_ISSUER: http://127.0.0.1:${FORA_ZITADEL_PORT:-58080}
CONSOLE_PUBLIC_ORIGIN: ${CONSOLE_PUBLIC_ORIGIN:-http://localhost:8085}
CONSOLE_OUT_DIR: /console-oidc
# Local-stack value in a tracked file, like the developer instance's
# admin password in deploy/zitadel/init-steps.yaml.
ZITADEL_ADMIN_PASSWORD: Publisher-Admin-1!
ROTATE_CLIENT_SECRET: ${ROTATE_CLIENT_SECRET:-}
volumes:
- ./deploy/zitadel/bootstrap:/zitadel:ro
- zitadel-systemapi:/systemapi:ro
- zitadel-publishers:/publishers
- zitadel-console-oidc:/console-oidc
# Named volumes are scoped by docker compose project name (`-p <project>`),
# so two concurrent projects get independent data dirs automatically.
volumes:
pgdata:
tbdata:
zitadeldata:
# Carries the admin PAT from FirstInstance to the bootstrap, and the resulting
# client id/secret back out to `make zitadel-creds`.
zitadel-bootstrap:
# The System API key and token, the publisher instance's admin token, and the
# console's client id/secret. Named zitadel-* so `make zitadel-down` wipes them.
zitadel-systemapi:
zitadel-publishers:
zitadel-console-oidc: