Repository navigation
Expand file tree
/
Copy pathdocker-compose.yml
More file actions
381 lines (366 loc) · 17.1 KB
/
Copy pathdocker-compose.yml
File metadata and controls
381 lines (366 loc) · 17.1 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
# multidb-server — a thin orchestrator. Selection and wiring only.
#
# Everything that configures an engine lives in its image (SPEC decision 11), so
# this file chooses which engines run and how they are exposed. Nothing here is
# published to the host by default: apps join the mdb network by service name.
#
# Internal container ports are fixed and never change. Every host-side binding
# is ${MDB_*_HOST_PORT}-driven, so resolving a port collision on any machine is
# a one-line .env change and never an edit to this file (SPEC section 7).
#
# ENGINE SERVICES ARE NOT HERE. They are generated into compose.engines.yml
# from engines/*/engine.conf (SPEC section 22.1), and compose picks that file
# up via COMPOSE_FILE in .env. This file holds the services there is exactly
# one of: the UIs, Caddy, the exporters and the backup sidecars.
name: ${COMPOSE_PROJECT_NAME:-mdb}
x-logging: &default-logging
driver: json-file
options:
max-size: ${MDB_LOG_MAX_SIZE:-10m}
max-file: "${MDB_LOG_MAX_FILE:-3}"
x-hardening: &hardening
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
services:
phpmyadmin:
profiles: [ui]
image: phpmyadmin:5.2.3@sha256:5e2289bcd500868ed4ac9261ddd7cbcc6e20036f83031ade72667b15dca31c60
restart: unless-stopped
<<: *hardening
environment:
# A comma-separated list, which is what lets a single instance offer both
# servers on the login screen (SPEC section 18).
PMA_HOSTS: ${MDB_PMA_HOSTS:-mysql,mariadb}
PMA_PORTS: ${MDB_PMA_PORTS:-3306,3306}
UPLOAD_LIMIT: ${MDB_PMA_UPLOAD_LIMIT:-256M}
ports:
- "${MDB_BIND_ADDR:-127.0.0.1}:${MDB_PMA_HOST_PORT:-8081}:80"
networks: [mdb]
logging: *default-logging
# ---------------------------------------------------------------------------
# Adminer — speaks PostgreSQL, MySQL, and MariaDB
# ---------------------------------------------------------------------------
adminer:
profiles: [ui]
image: adminer:5.4.1@sha256:a3167350c4eb9ae4473b8ea0f49c8e5ae74c87b240ee2f6086521dba2a6bf243
restart: unless-stopped
<<: *hardening
environment:
# Pre-wired so the UI opens ready to log in rather than asking for a
# hostname (SPEC section 7).
ADMINER_DEFAULT_SERVER: ${MDB_ADMINER_DEFAULT_SERVER:-pg}
ports:
- "${MDB_BIND_ADDR:-127.0.0.1}:${MDB_ADMINER_HOST_PORT:-8080}:8080"
networks: [mdb]
logging: *default-logging
# ---------------------------------------------------------------------------
# pgAdmin — pre-provisioned with the toolkit's PostgreSQL
# ---------------------------------------------------------------------------
pgadmin:
profiles: [ui]
image: dpage/pgadmin4:9.9@sha256:5d9624a93634d1c5e595619cc57b1d330758120d1baf445fa97300c0c1fc3c0a
restart: unless-stopped
environment:
PGADMIN_DEFAULT_EMAIL: ${MDB_PGADMIN_EMAIL:-admin@db.localhost}
PGADMIN_DEFAULT_PASSWORD_FILE: /run/secrets/pgadmin_password.txt
PGADMIN_LISTEN_PORT: "80"
PGADMIN_CONFIG_SERVER_MODE: "False"
# pgAdmin validates the login address and rejects reserved special-use
# domains, so the natural admin@db.localhost default makes it crash-loop
# on first boot with "does not appear to be a valid email address".
# This is the supported allowlist for exactly that case.
PGADMIN_CONFIG_ALLOW_SPECIAL_EMAIL_DOMAINS: "['${MDB_UI_DOMAIN:-db.localhost}','localhost']"
volumes:
# Rendered by scripts/env-render so the UI opens with the server already
# listed rather than asking for a hostname (SPEC section 7).
- ./caddy/servers.json:/pgadmin4/servers.json:ro
- pgadmin_data:/var/lib/pgadmin
# A compose SECRET rather than the ./secrets bind mount every other service
# uses, because pgAdmin is the only one that reads a secret as a non-root
# user (uid 5050) with no root phase -- the engines and poolers read as root
# and then drop. Mounting the FILE steps around secrets/ being 0700, which
# uid 5050 cannot traverse.
#
# No uid/gid/mode here, although compose accepts them: for file-backed
# secrets outside swarm they are silently IGNORED (docker/compose#9648; the
# PR to honour them was closed unmerged). The file arrives with its HOST
# owner and mode -- a first fix set uid: 5050 here, verified only that
# compose accepted the syntax, and changed nothing on Linux. What makes the
# file readable is scripts/init generating it 0644, with the 0700 directory
# as the guard on the host.
secrets:
- source: pgadmin_password
target: pgadmin_password.txt
ports:
- "${MDB_BIND_ADDR:-127.0.0.1}:${MDB_PGADMIN_HOST_PORT:-8082}:80"
networks: [mdb]
logging: *default-logging
# ---------------------------------------------------------------------------
# Caddy — the web front door, and in prod the only published service
# ---------------------------------------------------------------------------
caddy:
profiles: [ui, prod]
# Built here: stock Caddy proxies HTTP, and the database engines speak raw
# TCP. images/caddy/Dockerfile adds caddy-l4 so ONE front door can own the
# host ports for every engine. See IMAGE-PROVENANCE.md.
build:
context: ./images
dockerfile: caddy/Dockerfile
image: ghcr.io/simtabi/multidb-server-caddy:dev
restart: unless-stopped
<<: *hardening
# Binding 80/443 needs this one capability back; everything else stays
# dropped.
cap_add:
- NET_BIND_SERVICE
volumes:
# Generated from the engine descriptors by scripts/gen-compose; empty
# unless MDB_PUBLISH=proxy.
- ./caddy/layer4.caddy:/etc/caddy/layer4.caddy:ro
# Rendered from caddy/Caddyfile.tmpl (dev) or Caddyfile.prod.tmpl (prod).
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
# Persisted so the internal CA and any issued certificates survive a
# restart; without this every `make up` would mint a new local CA and
# browsers would warn again each time.
- caddy_data:/data
- caddy_config:/config
ports:
- "${MDB_BIND_ADDR:-127.0.0.1}:${MDB_CADDY_HTTP_PORT:-80}:80"
- "${MDB_BIND_ADDR:-127.0.0.1}:${MDB_CADDY_HTTPS_PORT:-443}:443"
networks: [mdb]
logging: *default-logging
# ---------------------------------------------------------------------------
# Scheduled backups — one sidecar per engine.
#
# Per-engine rather than one shared sidecar so each gets its OWN native dump
# client from its own image: mariadb-dump against a MySQL 8.4 server is the
# kind of thing that works until it does not. The alternative, a single
# container with all three clients, is the cli image, and reaching into other
# containers would mean mounting the docker socket -- a far larger grant than
# a backup job needs.
#
# Every one of these runs scripts/backup, the SAME file the make targets run,
# so the dump flags cannot drift between the nightly path and the manual one
# (SPEC section 11).
# ---------------------------------------------------------------------------
backup-pg:
profiles: [backup]
image: ghcr.io/simtabi/multidb-server-pg:${MDB_PG_VERSION:-17}
restart: unless-stopped
<<: *hardening
entrypoint: ["/mdb/scripts/backup", "--engine", "pg", "--schedule"]
# The sidecar runs on the engine's image but is NOT the engine, so the
# inherited healthcheck (pg_isready / mysqladmin ping) would probe a server
# that is not running here and report the container unhealthy forever. It is
# REPLACED rather than disabled: `healthcheck: {disable: true}` makes
# `compose up --wait` fail outright with "has no healthcheck configured"
# (docker/compose#13522), which is why every profile carrying a backup
# sidecar failed to boot on CI while the databases themselves reported
# healthy. This machine's compose is new enough not to, so it only ever
# reproduced on a runner.
#
# It checks liveness only: the scheduler is still PID 1. It reads /proc, so
# no engine image needs an extra package.
#
# It deliberately does NOT assert that /backups is writable, though that is
# the failure worth catching. ./backups is created on the host by make init
# as the invoking user, and these containers run hardened under the engine's
# own uid, so writability depends on host uid mapping and differs between a
# laptop and a CI runner -- a healthcheck that fails there is reporting the
# environment, not the container. Enforcing it belongs in scripts/backup,
# which can say which path and which uid; check 39 already requires that a
# backup which cannot be written fails loudly rather than silently.
healthcheck:
test: ["CMD-SHELL", "grep -q backup /proc/1/cmdline"]
interval: 10s
timeout: 5s
retries: 6
start_period: 30s
environment:
MDB_IN_CONTAINER: "1"
MDB_BACKUP_DIR: /backups
PGHOST: pg
PGUSER: postgres
volumes:
- ./scripts:/mdb/scripts:ro
# engines/ is not optional. scripts/backup sources engine-lib.sh, which
# resolves MDB_ENGINES_DIR as scripts/../engines and exits non-zero when
# that directory is absent -- so without this mount the sidecar died on
# startup, every time, and restart: unless-stopped turned that into a
# silent crash loop. It went unnoticed because the container's healthcheck
# was disabled: nothing ever asked whether it was running.
- ./engines:/mdb/engines:ro
- ./secrets:/run/secrets:ro
- ./backups:/backups
- ./.env:/mdb/.env:ro
working_dir: /mdb
networks: [mdb]
logging: *default-logging
backup-mysql:
profiles: [backup]
image: ghcr.io/simtabi/multidb-server-mysql:${MDB_MYSQL_VERSION:-8.4}
restart: unless-stopped
<<: *hardening
entrypoint: ["/mdb/scripts/backup", "--engine", "mysql", "--schedule"]
# The sidecar runs on the engine's image but is NOT the engine, so the
# inherited healthcheck (pg_isready / mysqladmin ping) would probe a server
# that is not running here and report the container unhealthy forever. It is
# REPLACED rather than disabled: `healthcheck: {disable: true}` makes
# `compose up --wait` fail outright with "has no healthcheck configured"
# (docker/compose#13522), which is why every profile carrying a backup
# sidecar failed to boot on CI while the databases themselves reported
# healthy. This machine's compose is new enough not to, so it only ever
# reproduced on a runner.
#
# It checks liveness only: the scheduler is still PID 1. It reads /proc, so
# no engine image needs an extra package.
#
# It deliberately does NOT assert that /backups is writable, though that is
# the failure worth catching. ./backups is created on the host by make init
# as the invoking user, and these containers run hardened under the engine's
# own uid, so writability depends on host uid mapping and differs between a
# laptop and a CI runner -- a healthcheck that fails there is reporting the
# environment, not the container. Enforcing it belongs in scripts/backup,
# which can say which path and which uid; check 39 already requires that a
# backup which cannot be written fails loudly rather than silently.
healthcheck:
test: ["CMD-SHELL", "grep -q backup /proc/1/cmdline"]
interval: 10s
timeout: 5s
retries: 6
start_period: 30s
environment:
MDB_IN_CONTAINER: "1"
MDB_BACKUP_DIR: /backups
MYSQL_HOST: mysql
volumes:
- ./scripts:/mdb/scripts:ro
# engines/ is not optional. scripts/backup sources engine-lib.sh, which
# resolves MDB_ENGINES_DIR as scripts/../engines and exits non-zero when
# that directory is absent -- so without this mount the sidecar died on
# startup, every time, and restart: unless-stopped turned that into a
# silent crash loop. It went unnoticed because the container's healthcheck
# was disabled: nothing ever asked whether it was running.
- ./engines:/mdb/engines:ro
- ./secrets:/run/secrets:ro
- ./backups:/backups
- ./.env:/mdb/.env:ro
working_dir: /mdb
networks: [mdb]
logging: *default-logging
backup-mariadb:
profiles: [backup]
image: ghcr.io/simtabi/multidb-server-mariadb:${MDB_MARIADB_VERSION:-11.4}
restart: unless-stopped
<<: *hardening
entrypoint: ["/mdb/scripts/backup", "--engine", "mariadb", "--schedule"]
# The sidecar runs on the engine's image but is NOT the engine, so the
# inherited healthcheck (pg_isready / mysqladmin ping) would probe a server
# that is not running here and report the container unhealthy forever. It is
# REPLACED rather than disabled: `healthcheck: {disable: true}` makes
# `compose up --wait` fail outright with "has no healthcheck configured"
# (docker/compose#13522), which is why every profile carrying a backup
# sidecar failed to boot on CI while the databases themselves reported
# healthy. This machine's compose is new enough not to, so it only ever
# reproduced on a runner.
#
# It checks liveness only: the scheduler is still PID 1. It reads /proc, so
# no engine image needs an extra package.
#
# It deliberately does NOT assert that /backups is writable, though that is
# the failure worth catching. ./backups is created on the host by make init
# as the invoking user, and these containers run hardened under the engine's
# own uid, so writability depends on host uid mapping and differs between a
# laptop and a CI runner -- a healthcheck that fails there is reporting the
# environment, not the container. Enforcing it belongs in scripts/backup,
# which can say which path and which uid; check 39 already requires that a
# backup which cannot be written fails loudly rather than silently.
healthcheck:
test: ["CMD-SHELL", "grep -q backup /proc/1/cmdline"]
interval: 10s
timeout: 5s
retries: 6
start_period: 30s
environment:
MDB_IN_CONTAINER: "1"
MDB_BACKUP_DIR: /backups
MYSQL_HOST: mariadb
volumes:
- ./scripts:/mdb/scripts:ro
# engines/ is not optional. scripts/backup sources engine-lib.sh, which
# resolves MDB_ENGINES_DIR as scripts/../engines and exits non-zero when
# that directory is absent -- so without this mount the sidecar died on
# startup, every time, and restart: unless-stopped turned that into a
# silent crash loop. It went unnoticed because the container's healthcheck
# was disabled: nothing ever asked whether it was running.
- ./engines:/mdb/engines:ro
- ./secrets:/run/secrets:ro
- ./backups:/backups
- ./.env:/mdb/.env:ro
working_dir: /mdb
networks: [mdb]
logging: *default-logging
# ---------------------------------------------------------------------------
# Metrics — exporters on the internal network, published to nothing.
#
# Prometheus and Grafana are out of scope (SPEC section 12); these expose the
# endpoints and it is up to your monitoring stack to scrape them over the
# mdb network.
# ---------------------------------------------------------------------------
pg-exporter:
profiles: [metrics]
image: quay.io/prometheuscommunity/postgres-exporter:v0.20.1@sha256:ac5ec343104fae0e2d84a27bb8d69b38430a11910c5382cad85d478d2bab713e
restart: unless-stopped
<<: *hardening
environment:
# The exporter reads the password from a file rather than taking it in
# the connection string, so no credential appears in `docker inspect`.
DATA_SOURCE_URI: "pg:5432/${MDB_PG_DEFAULT_DB:-postgres}?sslmode=disable"
DATA_SOURCE_USER: postgres
DATA_SOURCE_PASS_FILE: /run/secrets/pg_superuser_password.txt
volumes:
- ./secrets:/run/secrets:ro
networks: [mdb]
logging: *default-logging
mysqld-exporter:
profiles: [metrics]
image: prom/mysqld-exporter:v0.19.0@sha256:eacb4b18e2ec1e0abdf2d64851b68526c964f6d9cb3e9458fb5d5f5062ea94c1
restart: unless-stopped
<<: *hardening
command:
- "--mysqld.address=mysql:3306"
- "--mysqld.username=root"
environment:
MYSQLD_EXPORTER_PASSWORD_FILE: /run/secrets/mysql_root_password.txt
volumes:
- ./secrets:/run/secrets:ro
networks: [mdb]
logging: *default-logging
networks:
mdb:
name: ${COMPOSE_PROJECT_NAME:-mdb}_net
ipam:
config:
- subnet: ${MDB_NETWORK_SUBNET:-172.28.0.0/16}
volumes:
# Per-engine, per-major volumes. Pointing a new PostgreSQL major at an old
# data directory fails hard ("database files are incompatible with server"),
# so the major is part of the volume name and versions coexist safely
# (SPEC decision 2).
pgadmin_data:
name: ${COMPOSE_PROJECT_NAME:-mdb}_pgadmin_data
caddy_data:
name: ${COMPOSE_PROJECT_NAME:-mdb}_caddy_data
caddy_config:
name: ${COMPOSE_PROJECT_NAME:-mdb}_caddy_config
# -----------------------------------------------------------------------------
# Secrets
# -----------------------------------------------------------------------------
# Only pgAdmin uses this mechanism; every other service bind-mounts ./secrets
# and reads it as root before dropping privileges. See the note on the pgadmin
# service for why it cannot.
secrets:
pgadmin_password:
file: ./secrets/pgadmin_password.txt