-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathdocker-compose.yml
More file actions
318 lines (302 loc) · 13.4 KB
/
Copy pathdocker-compose.yml
File metadata and controls
318 lines (302 loc) · 13.4 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
services:
db:
image: mariadb:11
restart: unless-stopped
environment:
MARIADB_ROOT_PASSWORD: ${WP_DB_ROOT_PASSWORD}
MARIADB_DATABASE: ${WP_DB_NAME:-wordpress}
MARIADB_USER: ${WP_DB_USER:-wordpress}
MARIADB_PASSWORD: ${WP_DB_PASSWORD}
volumes:
- db_data:/var/lib/mysql
redis:
image: redis:7-alpine
restart: unless-stopped
volumes:
- redis_data:/data
# ── Local identity provider ────────────────────────────────────────────
# Local development authenticates against THIS Zitadel, not the production
# one at auth.catholicdigitalcommons.org. See
# docs/superpowers/specs/2026-08-17-local-zitadel-stack-design.md.
#
# Its own Postgres: Zitadel requires PostgreSQL and this stack's `db` is
# MariaDB, so it cannot be shared the way martyrology-api shares its.
zitadel-db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${ZITADEL_DB_PASSWORD:-postgres}
POSTGRES_DB: zitadel
volumes:
- zitadel_db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d zitadel"]
interval: 5s
timeout: 3s
retries: 10
zitadel:
# PINNED. cdcf-infra/auth/setup-zitadel.sh calls the management API by
# versioned path (/zitadel.application.v2.ApplicationService/…), so a
# silent major bump breaks provisioning with no change in this repo.
image: ghcr.io/zitadel/zitadel:v4.15.0
restart: unless-stopped
# The master key must be EXACTLY 32 chars and must never change: Zitadel
# encrypts instance data with it and cannot decrypt after a rotation.
# Losing it means deleting zitadel_db_data and re-provisioning, which
# invalidates the client IDs in .env.local.
command: start-from-init --masterkey "${ZITADEL_MASTERKEY:-MasterkeyNeedsToHave32Characters}"
# The image is scratch-based; without uid 0 the PAT lands in the bind
# mount with ownership the host-run provisioning script cannot read.
user: "0"
# No published port. zitadel-proxy owns ${ZITADEL_PORT:-8090} and fronts
# both this backend and the v2 login UI on a single origin — the same
# shape production runs, so local reproduces its cookie/CSRF/Host
# behaviour instead of a dev-only two-origin variant.
expose:
- "8080"
environment:
# EXTERNALPORT must equal the published port above — issuer and
# discovery URLs are minted from these two.
ZITADEL_EXTERNALDOMAIN: localhost
ZITADEL_EXTERNALPORT: ${ZITADEL_PORT:-8090}
ZITADEL_EXTERNALSECURE: "false"
ZITADEL_TLS_ENABLED: "false"
# Points at zitadel-db above, NOT the stack's mariadb `db`.
ZITADEL_DATABASE_POSTGRES_HOST: zitadel-db
ZITADEL_DATABASE_POSTGRES_PORT: 5432
ZITADEL_DATABASE_POSTGRES_DATABASE: zitadel
ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME: postgres
ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD: ${ZITADEL_DB_PASSWORD:-postgres}
# Both SSL modes must be `disable`: zitadel-db serves no TLS, and
# omitting these leaves Zitadel retrying TLS and failing during
# migration, before it ever answers a request.
ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE: disable
ZITADEL_DATABASE_POSTGRES_USER_USERNAME: zitadel
ZITADEL_DATABASE_POSTGRES_USER_PASSWORD: ${ZITADEL_DB_PASSWORD:-zitadel}
ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE: disable
# Login V2, matching production (cdcf-infra auth/docker-compose.prod.yml).
# Local dev must exercise the same login UI production serves; running V1
# here would mean sign-in bugs only ever surface after deploy. The v2 UI
# is the zitadel-login service below, reached through zitadel-proxy at
# /ui/v2/login. These URLs are where Zitadel sends the browser, so they
# must be the PUBLIC origin, not the internal service name.
ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED: "true"
ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_BASEURI: http://localhost:${ZITADEL_PORT:-8090}/ui/v2/login
ZITADEL_OIDC_DEFAULTLOGINURLV2: http://localhost:${ZITADEL_PORT:-8090}/ui/v2/login/login?authRequest=
ZITADEL_OIDC_DEFAULTLOGOUTURLV2: http://localhost:${ZITADEL_PORT:-8090}/ui/v2/login/logout?post_logout_redirect=
ZITADEL_SAML_DEFAULTLOGINURLV2: http://localhost:${ZITADEL_PORT:-8090}/ui/v2/login/login?samlRequest=
# These four are one unit. PATPATH alone names a file that is never
# written — without the machine user there is no token, and the
# host-run provisioning script has nothing to authenticate with.
ZITADEL_FIRSTINSTANCE_PATPATH: /zitadel-data/automation-user.pat
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_USERNAME: automation-user
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_NAME: Automation User
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_PAT_EXPIRATIONDATE: "2030-01-01T00:00:00Z"
# A SECOND machine user, distinct from the one above: the v2 login UI
# authenticates to the Zitadel API as this identity. Like the block
# above it only takes effect on an empty database, so switching a
# already-initialised stack to Login V2 requires removing
# zitadel_db_data — the flag alone will not mint this token.
ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_MACHINE_USERNAME: login-client
ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_MACHINE_NAME: Login V2 Client
ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_PAT_EXPIRATIONDATE: "2030-01-01T00:00:00Z"
ZITADEL_FIRSTINSTANCE_LOGINCLIENTPATPATH: /zitadel-data/login-client.pat
volumes:
# Bind mount, not a named volume: the host-run cdcf-infra script must
# be able to read the PAT.
- ./.zitadel-data:/zitadel-data:delegated
depends_on:
zitadel-db:
condition: service_healthy
healthcheck:
# Zitadel's own readiness check, so dependents wait for migrations
# rather than merely for the port to open.
test: ["CMD", "/app/zitadel", "ready"]
interval: 10s
timeout: 5s
retries: 20
# Zitadel Login V2 UI (Next.js). Serves /ui/v2/login/* — the sign-in
# screens end users and the admin console both land on. Version-pinned to
# the backend: mixing zitadel and zitadel-login versions is unsupported.
zitadel-login:
image: ghcr.io/zitadel/zitadel-login:v4.15.0
restart: unless-stopped
depends_on:
zitadel:
condition: service_healthy
environment:
# Internal API call over the compose network, not the public origin.
ZITADEL_API_URL: http://zitadel:8080
NEXT_PUBLIC_BASE_PATH: /ui/v2/login
# Written by the LOGINCLIENT first-instance block on the zitadel
# service. If this file is missing the UI starts but every request
# fails unauthenticated — the usual symptom of enabling Login V2 on a
# database that was initialised before the setting existed.
ZITADEL_SERVICE_USER_TOKEN_FILE: /zitadel-data/login-client.pat
# Zitadel selects the instance by Host header, so API calls from this
# container must present the public host rather than `zitadel:8080`.
CUSTOM_REQUEST_HEADERS: "Host:localhost:${ZITADEL_PORT:-8090}"
# Without this the UI omits email.verification.send_code on
# AddHumanUser, so signups land ACTIVE with an unverified email and no
# verification mail is ever queued. Matches production.
EMAIL_VERIFICATION: "true"
volumes:
# Read-only: this service only consumes login-client.pat.
- ./.zitadel-data:/zitadel-data:ro
expose:
- "3000"
# Single public entry point for the Zitadel stack, mirroring the internal
# proxy production runs. Routes /ui/v2/login* to the v2 UI and everything
# else to the backend, so the login UI and the OIDC/API endpoints share one
# origin exactly as they do on auth.catholicdigitalcommons.org.
zitadel-proxy:
image: nginx:alpine
restart: unless-stopped
depends_on:
zitadel:
condition: service_healthy
zitadel-login:
condition: service_started
volumes:
- ./nginx/zitadel.conf:/etc/nginx/conf.d/default.conf:ro
ports:
# 8090, not 8080: martyrology-api and LiturgicalCalendarFrontend both
# default their local Zitadel to 8080, and a collision presents as an
# opaque bind failure. This must equal ZITADEL_EXTERNALPORT above.
- "127.0.0.1:${ZITADEL_PORT:-8090}:80"
healthcheck:
# 127.0.0.1, not localhost: nginx listens on IPv4 only, while localhost
# inside the container resolves to ::1 first and the probe would fail
# with "connection refused" against a perfectly healthy proxy.
test:
["CMD", "wget", "-q", "--spider", "http://127.0.0.1/debug/healthz"]
interval: 10s
timeout: 5s
retries: 10
wordpress:
image: wordpress:beta-7.0-php8.4-apache
restart: unless-stopped
depends_on:
- db
- redis
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_NAME: ${WP_DB_NAME:-wordpress}
WORDPRESS_DB_USER: ${WP_DB_USER:-wordpress}
WORDPRESS_DB_PASSWORD: ${WP_DB_PASSWORD}
WORDPRESS_CONFIG_EXTRA: |
define('FS_METHOD', 'direct');
define('REDIS_QUEUE_HOST', 'redis');
define('REDIS_QUEUE_PORT', 6379);
define('SMTP_HOST', '${SMTP_HOST:-}');
define('SMTP_PORT', '${SMTP_PORT:-465}');
define('SMTP_SECURE', '${SMTP_SECURE:-ssl}');
define('SMTP_USER', '${SMTP_USER:-}');
define('SMTP_PASS', '${SMTP_PASS:-}');
define('SMTP_FROM', '${SMTP_FROM:-webmaster@catholicdigitalcommons.org}');
define('SMTP_FROM_NAME', '${SMTP_FROM_NAME:-Catholic Digital Commons Foundation}');
volumes:
- wordpress_data:/var/www/html
- ./wordpress/themes/cdcf-headless:/var/www/html/wp-content/themes/cdcf-headless
- ./wordpress/plugins/cdcf-redis-translations:/var/www/html/wp-content/plugins/cdcf-redis-translations
wp-init:
image: wordpress:cli
user: "0:0"
depends_on:
- db
- wordpress
- redis
volumes:
- wordpress_data:/var/www/html
- ./wordpress/themes/cdcf-headless:/var/www/html/wp-content/themes/cdcf-headless
- ./wordpress/plugins/cdcf-redis-translations:/var/www/html/wp-content/plugins/cdcf-redis-translations
- ./wordpress/init.sh:/usr/local/bin/init.sh:ro
- ./wordpress/translate-all.php:/usr/local/bin/translate-all.php:ro
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_NAME: ${WP_DB_NAME:-wordpress}
WORDPRESS_DB_USER: ${WP_DB_USER:-wordpress}
WORDPRESS_DB_PASSWORD: ${WP_DB_PASSWORD}
WP_URL: ${WP_URL:-http://localhost}
WP_ADMIN_USER: ${WP_ADMIN_USER:-admin}
WP_ADMIN_PASSWORD: ${WP_ADMIN_PASSWORD:-admin}
WP_ADMIN_EMAIL: ${WP_ADMIN_EMAIL:-admin@cdcf.dev}
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
OPENAI_MODEL: ${OPENAI_MODEL:-gpt-4o-mini}
entrypoint: ["sh", "/usr/local/bin/init.sh"]
restart: "no"
redis-worker:
image: wordpress:cli
user: "0:0"
depends_on:
- db
- wordpress
- redis
volumes:
- wordpress_data:/var/www/html
- ./wordpress/themes/cdcf-headless:/var/www/html/wp-content/themes/cdcf-headless
- ./wordpress/plugins/cdcf-redis-translations:/var/www/html/wp-content/plugins/cdcf-redis-translations
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_NAME: ${WP_DB_NAME:-wordpress}
WORDPRESS_DB_USER: ${WP_DB_USER:-wordpress}
WORDPRESS_DB_PASSWORD: ${WP_DB_PASSWORD}
entrypoint: ["sh", "-c", "echo 'Waiting for database...'; until wp db check --allow-root >/dev/null 2>&1; do sleep 2; done; echo 'Database ready — starting worker loop'; while true; do wp eval 'if(function_exists(\"redis_queue\")){redis_queue()->get_job_processor()->process_jobs([\"default\"], 20);}' --allow-root 2>&1; sleep 10; done"]
restart: unless-stopped
wpcli:
image: wordpress:cli
user: "0:0"
depends_on:
- db
- wordpress
volumes:
- wordpress_data:/var/www/html
- ./wordpress/themes/cdcf-headless:/var/www/html/wp-content/themes/cdcf-headless
- ./wordpress/plugins/cdcf-redis-translations:/var/www/html/wp-content/plugins/cdcf-redis-translations
- ./wordpress/translate-all.php:/usr/local/bin/translate-all.php:ro
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_NAME: ${WP_DB_NAME:-wordpress}
WORDPRESS_DB_USER: ${WP_DB_USER:-wordpress}
WORDPRESS_DB_PASSWORD: ${WP_DB_PASSWORD}
profiles:
- cli
nextjs:
# When Next.js runs inside the compose network it must reach WordPress
# via the docker DNS name `wordpress`, not localhost. The host-side
# values in .env.local (http://localhost:8000/...) would not resolve
# here, so the URLs are hard-coded to the container-perspective ones.
build:
context: .
args:
WP_GRAPHQL_URL: http://wordpress/graphql
image: cdcf-website:latest
restart: unless-stopped
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- WP_GRAPHQL_URL=http://wordpress/graphql
- WP_REST_URL=http://wordpress/wp-json
- WP_PREVIEW_SECRET=${WP_PREVIEW_SECRET}
profiles:
- production
nginx:
image: nginx:alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- nextjs
- wordpress
profiles:
- production
volumes:
db_data:
redis_data:
wordpress_data:
zitadel_db_data: