-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcompose.yml
More file actions
275 lines (267 loc) · 10.9 KB
/
Copy pathcompose.yml
File metadata and controls
275 lines (267 loc) · 10.9 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
name: typo3-demo
x-logging: &default-logging
driver: json-file
options:
max-size: "10m"
max-file: "3"
services:
web:
image: ghcr.io/netresearch/typo3-demo:latest
build:
context: .
dockerfile: docker/web/Dockerfile
logging: *default-logging
depends_on:
db:
condition: service_healthy
# The application user does not exist until db-init has run.
db-init:
condition: service_completed_successfully
valkey:
condition: service_healthy
environment:
MARIADB_HOST: db
MARIADB_DATABASE: ${MARIADB_DATABASE:-typo3}
MARIADB_USER: ${MARIADB_USER:-typo3}
MARIADB_PASSWORD: ${MARIADB_PASSWORD:-typo3}
TYPO3_CONTEXT: ${TYPO3_CONTEXT:-Development/Docker}
TYPO3_DOMAIN: ${TYPO3_DOMAIN:-localhost}
TYPO3_SITE_BASE: ${TYPO3_SITE_BASE:-/}
TYPO3_ENCRYPTION_KEY: ${TYPO3_ENCRYPTION_KEY:-}
TYPO3_INSTALL_TOOL_PASSWORD: ${TYPO3_INSTALL_TOOL_PASSWORD:-joh316}
# ai_filemetadata reads a plain OpenAI key from its own extension
# configuration and has no nr_vault support, so it cannot use the vault
# route the LLM modules take. The entrypoint writes it into
# config/system/settings.php - which is the file the extension actually
# reads; additional.php is not on its lookup path. Empty here keeps the
# key already stored there.
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
# autotranslate reads its DeepL key from the extension configuration and has
# no nr_vault support, so it cannot take the route OPENAI_API_KEY takes. The
# entrypoint writes it into config/system/additional.php, which lives in the
# typo3-config volume the worker mounts too — so only this service needs the
# variable. Empty keeps a previously stored key instead of wiping it.
DEEPL_API_KEY: ${DEEPL_API_KEY:-}
volumes:
- typo3-fileadmin:/var/www/public/fileadmin
- typo3-var:/var/www/var
- typo3-config:/var/www/config/system
healthcheck:
test: ["CMD-SHELL", "wget -q -O /dev/null --header=\"Host: $${TYPO3_DOMAIN:-localhost}\" http://127.0.0.1/ || exit 1"]
interval: 15s
timeout: 5s
retries: 5
start_period: 300s
restart: unless-stopped
security_opt:
- no-new-privileges:true
deploy:
resources:
limits:
memory: 1G
# Symfony Messenger consumer for nr-repurpose's async generation jobs
# (doctrine transport). Reuses the web image; the web service has already
# run the entrypoint setup by the time this starts. The consumer exits
# after its time/memory limits and on fatal errors — the restart policy
# recycles it either way, with Docker-visible backoff on crash loops.
worker:
image: ghcr.io/netresearch/typo3-demo:latest
logging: *default-logging
depends_on:
web:
condition: service_healthy
# www-data so generated artifacts (podcasts, PNGs) on the shared
# fileadmin/var volumes stay manageable by the web container's php-fpm.
user: www-data
entrypoint: ["sh", "-c"]
command:
# Consume three transports so content is both INDEXED and VECTORIZED:
# doctrine - nr-repurpose async generation jobs
# index - lochmueller/index page-index jobs; processing these emits
# the IndexPageEvent that nr_ai_search turns into embed jobs
# (without this the 'index' queue piles up and NOTHING is
# ever vectorized — the RAG store stays empty)
# nr_ai_search - RAG embedding jobs, written to the shared var/ vektor store
- >
exec vendor/bin/typo3 messenger:consume doctrine index nr_ai_search
--time-limit=3600 --memory-limit=512M --sleep=2
environment:
# Writable HOME for chromium (www-data has no home dir in the image);
# /tmp keeps browser cache litter off the persistent var volume.
HOME: /tmp
MARIADB_HOST: db
MARIADB_DATABASE: ${MARIADB_DATABASE:-typo3}
MARIADB_USER: ${MARIADB_USER:-typo3}
MARIADB_PASSWORD: ${MARIADB_PASSWORD:-typo3}
TYPO3_CONTEXT: ${TYPO3_CONTEXT:-Development/Docker}
TYPO3_DOMAIN: ${TYPO3_DOMAIN:-localhost}
TYPO3_SITE_BASE: ${TYPO3_SITE_BASE:-/}
volumes:
- typo3-fileadmin:/var/www/public/fileadmin
- typo3-var:/var/www/var
- typo3-config:/var/www/config/system
# Override the web image's baked-in nginx HEALTHCHECK (the worker runs no
# nginx) with a consumer-process check. `disable: true` is not enough:
# `up --wait` errors on services with no healthcheck configured.
healthcheck:
# [m] bracket trick: keeps the healthcheck shell's own cmdline from
# matching, which would report healthy even with a dead consumer.
test: ["CMD-SHELL", "pgrep -f '[m]essenger:consume' || exit 1"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
restart: unless-stopped
security_opt:
- no-new-privileges:true
deploy:
resources:
limits:
memory: 1G
# MariaDB as a Docker Hardened Image: minimal debian-13 base, no package
# manager and no shell tooling beyond what the server itself needs. Pulling it
# requires a Docker Hub account with DHI entitlement (`docker login dhi.io`) —
# dhi.io refuses anonymous pulls.
#
# Deliberately a floating tag where every other service here is digest-pinned:
# Renovate has no dhi.io credentials, so a digest would never be bumped and
# this stack would run a frozen "hardened" image — the opposite of the point.
# `make update` pulls on every deploy, so the tag stays current by itself.
#
# Two consequences of the hardened entrypoint, both handled here rather than
# by swapping the image out for a patched one:
# * it honours MARIADB_ROOT_PASSWORD and nothing else — no MARIADB_DATABASE
# /MARIADB_USER, no /docker-entrypoint-initdb.d. Database, application user
# and seed import live in the `db-init` service below.
# * it ships no configuration file, so the server falls back to its built-in
# collation, and its entrypoint execs a fixed `mariadbd --datadir=...`
# that drops any command arguments. docker/db/my.cnf is mounted to the one
# path the server still reads on its own.
db:
image: dhi.io/mariadb:12
logging: *default-logging
environment:
# Only MARIADB_ROOT_PASSWORD reaches the hardened entrypoint. The other
# three are ignored by it — db-init acts on them — but they stay declared
# here because `docker compose exec db` is how this repo opens a client
# (make db-shell, make export-seed, the diagnostics and BE-reset
# workflows) and those read the credentials from this environment.
MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD:-root}
MARIADB_DATABASE: ${MARIADB_DATABASE:-typo3}
MARIADB_USER: ${MARIADB_USER:-typo3}
MARIADB_PASSWORD: ${MARIADB_PASSWORD:-typo3}
volumes:
- typo3-db-v2:/var/lib/mysql
- ./docker/db/my.cnf:/etc/my.cnf:ro
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
restart: unless-stopped
security_opt:
- no-new-privileges:true
deploy:
resources:
limits:
memory: 512M
# What the hardened image's entrypoint does not do: create the application
# database and user, and import data/db.sql.gz on first boot. Runs once per
# stack start against the healthy server and is idempotent — the seed only
# enters a database that has no tables, so live data always wins.
db-init:
image: dhi.io/mariadb:12
logging: *default-logging
depends_on:
db:
condition: service_healthy
entrypoint: ["/bin/sh", "/init.sh"]
environment:
MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD:-root}
MARIADB_DATABASE: ${MARIADB_DATABASE:-typo3}
MARIADB_USER: ${MARIADB_USER:-typo3}
MARIADB_PASSWORD: ${MARIADB_PASSWORD:-typo3}
volumes:
- ./docker/db/init.sh:/init.sh:ro
- ./data/db.sql.gz:/seed/db.sql.gz:ro
restart: "no"
security_opt:
- no-new-privileges:true
deploy:
resources:
limits:
memory: 256M
valkey:
image: valkey/valkey:9-alpine@sha256:ee91f7a174ac4d6a6b0685b3a60e321f0a9dbbb691f9b0e285be2ba1d1be8328
logging: *default-logging
healthcheck:
test: ["CMD", "valkey-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
start_period: 5s
restart: unless-stopped
security_opt:
- no-new-privileges:true
deploy:
resources:
limits:
memory: 128M
# Caddy as a Docker Hardened Image, for the same reasons as the database and
# with the same floating tag (Renovate cannot resolve dhi.io, so a digest
# would freeze). The measured difference is not theoretical: caddy:2-alpine —
# pinned to the current upstream digest, which has not been rebuilt since
# 2026-06-22 — carries 26 fixable CVEs, ten of them CRITICAL or HIGH, in the
# Go standard library the caddy binary is built from and in curl. The
# hardened image reports none.
#
# It contains exactly one executable, /usr/local/bin/caddy: no shell, no
# wget, so the previous admin-API healthcheck cannot run. `caddy reload` is
# the probe the binary itself provides — it reaches the admin API and returns
# "config is unchanged" in ~0.1s, so it reports liveness without mutating
# anything in practice.
caddy:
image: dhi.io/caddy:2
logging: *default-logging
ports:
- "${CADDY_HTTP_PORT:-80}:80"
- "${CADDY_HTTPS_PORT:-443}:443"
- "${CADDY_HTTPS_PORT:-443}:443/udp"
volumes:
- ./docker/caddy/Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data-v2:/data
- caddy-config-v2:/config
environment:
TYPO3_DOMAIN: ${TYPO3_DOMAIN:-localhost}
depends_on:
web:
condition: service_healthy
healthcheck:
test: ["CMD", "/usr/local/bin/caddy", "reload", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
restart: unless-stopped
security_opt:
- no-new-privileges:true
deploy:
resources:
limits:
memory: 256M
volumes:
# -v2 because the hardened image runs as a different uid than the official one
# that created the previous volume; a rename gets every environment a fresh,
# correctly owned data directory (re-seeded from data/db.sql.gz) instead of a
# permission-denied crash loop. The demo database is disposable by design.
typo3-db-v2:
typo3-fileadmin:
typo3-var:
typo3-config:
# Same reason as above: the old caddy volumes were created by a root-running
# image and the hardened one cannot write them (verified: it exits 1 on
# "mkdir /data/caddy/pki: permission denied"). Caddy issues a fresh
# certificate into the new volume on first start.
caddy-data-v2:
caddy-config-v2: