forked from libredb/libredb-studio
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdatabase-compose.yml
More file actions
752 lines (724 loc) · 31.7 KB
/
Copy pathdatabase-compose.yml
File metadata and controls
752 lines (724 loc) · 31.7 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
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
# Shared configuration for every Druid process. Druid is configured entirely through
# `druid_*` environment variables (the image's entrypoint translates them into
# runtime.properties), so one anchor keeps the five processes provably identical - a cluster
# whose processes disagree about ZooKeeper or the metadata store fails in confusing ways.
x-druid-env: &druid-env
# Sizes every process for a laptop. Without it each one claims cluster-scale heap and
# direct memory, and the five together will not fit.
DRUID_SINGLE_NODE_CONF: micro-quickstart
# druid-multi-stage-query is included on purpose: it is what makes `INSERT INTO ... SELECT`
# (MSQ) available on /druid/v2/sql/task. The provider does not use that endpoint - the point
# is that the live pass can prove the *synchronous* endpoint rejects INSERT even when the
# engine is fully capable of it, which is the honest reason supportsCreateTable is false.
druid_extensions_loadList: '["druid-histogram", "druid-datasketches", "druid-lookups-cached-global", "postgresql-metadata-storage", "druid-multi-stage-query"]'
druid_zk_service_host: druid-zookeeper
druid_metadata_storage_host: ""
druid_metadata_storage_type: postgresql
druid_metadata_storage_connector_connectURI: jdbc:postgresql://druid-metadata:5432/druid
druid_metadata_storage_connector_user: druid
druid_metadata_storage_connector_password: druid
# Deep storage on a shared local volume. Real deployments use S3/HDFS; local is what makes
# this a single-machine fixture, and the historical and middlemanager must see the same files.
druid_storage_type: local
druid_storage_storageDirectory: /opt/shared/segments
druid_indexer_logs_type: file
druid_indexer_logs_directory: /opt/shared/indexing-logs
druid_indexer_runner_javaOptsArray: '["-server", "-Xmx1g", "-Xms1g", "-XX:MaxDirectMemorySize=1g", "-Duser.timezone=UTC", "-Dfile.encoding=UTF-8", "-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager"]'
druid_indexer_fork_property_druid_processing_buffer_sizeBytes: 100MiB
druid_processing_numThreads: "2"
druid_processing_numMergeBuffers: "2"
# Lets the Router at 8888 proxy Coordinator and Overlord APIs, so the whole cluster is
# reachable through the one port the provider targets.
druid_router_managementProxy_enabled: "true"
# info, not the upstream compose's debug: the debug logger emits one line per internal
# request and buries the errors a live pass is looking for.
druid_emitter_logging_logLevel: info
services:
postgres:
image: postgres:18
container_name: libredb-postgres
restart: unless-stopped
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
ports:
- "5432:5432"
mongodb:
image: mongo:latest
container_name: libredb-mongodb
restart: unless-stopped
environment:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: admin
ports:
- "27017:27017"
mysql:
image: mysql:latest
container_name: libredb-mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: mysql
ports:
- "3306:3306"
mssql:
# Use 2022-latest to avoid SQL Server 2025 NUMA/processor topology assert in containers.
# Limit CPUs to avoid: ASSERT (result * DrtlGetProcessorCoreCount() == DrtlGetProcessorCount())
image: mcr.microsoft.com/mssql/server:2022-latest
container_name: libredb-mssql
restart: unless-stopped
environment:
ACCEPT_EULA: "Y"
MSSQL_SA_PASSWORD: Password123!
MSSQL_DATABASE: mssql
ports:
- "1433:1433"
deploy:
resources:
limits:
cpus: "4"
memory: 4G
oracle:
image: gvenzl/oracle-xe
container_name: libredb-oracle
restart: unless-stopped
environment:
ORACLE_PDB: ORCLPDB1
ORACLE_PASSWORD: Password123!
ports:
- "1521:1521"
couchbase:
# The image ships an uninitialized node: the cluster, the credentials below,
# the bucket and its primary index are all applied by the couchbase-init
# sidecar that follows.
image: couchbase:community-8.0.2
container_name: libredb-couchbase
restart: unless-stopped
environment:
COUCHBASE_USER: Administrator
COUCHBASE_PASSWORD: password123
ports:
# 8091 management REST, 8093 query service (SQL++). Those are the only two
# the provider needs: it speaks HTTP, never the binary KV protocol (11210).
- "8091:8091"
- "8093:8093"
ulimits:
# Couchbase warns at every start below this and degrades under load.
nofile:
soft: 200000
hard: 200000
healthcheck:
# Authenticated on purpose: /pools answers anonymously only until the
# cluster is initialized, and returns 401 for every later start, which
# would leave an initialized node permanently "unhealthy". With
# credentials it answers 200 in both states.
test:
- CMD-SHELL
- 'curl -sf -o /dev/null -u "$$COUCHBASE_USER:$$COUCHBASE_PASSWORD" http://localhost:8091/pools'
interval: 5s
timeout: 5s
retries: 40
couchbase-init:
# One-shot sidecar. A fresh node has no cluster, no bucket and no index, and
# SQL++ rejects a SELECT against an un-indexed keyspace with error 4000, so
# without this the service would come up unusable.
image: couchbase:community-8.0.2
container_name: libredb-couchbase-init
restart: "no"
depends_on:
couchbase:
condition: service_healthy
environment:
COUCHBASE_HOST: couchbase
COUCHBASE_USER: Administrator
COUCHBASE_PASSWORD: password123
COUCHBASE_BUCKET: travel
entrypoint: ["/bin/bash", "-c"]
command:
# $$ escapes compose interpolation: these expand in the container's shell.
- |
set -eu
couchbase-cli cluster-init -c "$$COUCHBASE_HOST" \
--cluster-username "$$COUCHBASE_USER" --cluster-password "$$COUCHBASE_PASSWORD" \
--cluster-name libredb --services data,index,query \
--cluster-ramsize 1024 --cluster-index-ramsize 512 --index-storage-setting default \
|| echo "cluster already initialized - existing settings left untouched"
# Community Edition rejects Magma, which bucket-create defaults to, and a
# single node cannot honour a replica.
couchbase-cli bucket-create -c "$$COUCHBASE_HOST" \
-u "$$COUCHBASE_USER" -p "$$COUCHBASE_PASSWORD" \
--bucket "$$COUCHBASE_BUCKET" --bucket-type couchbase --storage-backend couchstore \
--bucket-ramsize 256 --bucket-replica 0 --wait \
|| echo "bucket already exists - existing data left untouched"
# The query service accepts statements a few seconds after the node is
# initialized, so the index creation is retried rather than assumed.
for _ in $$(seq 1 30); do
if curl -sf -o /dev/null -u "$$COUCHBASE_USER:$$COUCHBASE_PASSWORD" \
"http://$$COUCHBASE_HOST:8093/query/service" \
--data-urlencode "statement=CREATE PRIMARY INDEX IF NOT EXISTS ON \`$$COUCHBASE_BUCKET\`"; then
echo "couchbase ready: bucket $$COUCHBASE_BUCKET has a primary index"
exit 0
fi
sleep 2
done
echo "couchbase-init: the query service never accepted CREATE PRIMARY INDEX" >&2
exit 1
clickhouse:
# Pinned to the exact build the provider was live-verified against, so the status
# codes, header names and mid-stream-failure trailer recorded in
# docs/providers/clickhouse.md stay reproducible for the next person.
image: clickhouse/clickhouse-server:26.7.1.1315
container_name: libredb-clickhouse
restart: unless-stopped
environment:
CLICKHOUSE_USER: libredb
CLICKHOUSE_PASSWORD: password123
CLICKHOUSE_DB: demo
# Lets the live pass GRANT/REVOKE a second, restricted user (needed to reproduce the
# ACCESS_DENIED-arrives-as-500 finding, spec section 2.5) without hand-editing users.xml.
CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: 1
ports:
# 8123 is HTTP, the only protocol the provider speaks. 9000 (native TCP) is deliberately
# NOT exposed: there is no native-protocol transport in this codebase to connect with it.
- "8123:8123"
ulimits:
# ClickHouse warns at every start below this and degrades under load, same class of
# issue as the couchbase nofile setting above.
nofile:
soft: 262144
hard: 262144
healthcheck:
# /ping needs no auth and returns "Ok." once the server can accept queries. The image has
# no curl (unlike couchbase's), so this uses wget, which it does ship.
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8123/ping"]
interval: 5s
timeout: 5s
retries: 40
# No seed sidecar: CLICKHOUSE_DB above creates `demo` on first boot, and unlike Couchbase
# there is no index prerequisite - a SELECT against a freshly created table works
# immediately. Do not add one.
# ---------------------------------------------------------------------------------------
# Apache Druid. Unlike every other service here, Druid is a distributed system and has no
# single-container mode: five Druid processes plus ZooKeeper plus a metadata database is
# the minimum that can answer a SQL query. That is why all seven carry `profiles: [druid]`
# - a bare `docker compose -f database-compose.yml up -d` would otherwise grow from 7
# containers to 14 and from roughly 6 GB of RAM to 12. Start it explicitly:
#
# docker compose -f database-compose.yml --profile druid up -d
#
# The provider only ever talks to the Router (8888) or the Broker (8082); the other five
# processes are cluster internals and deliberately publish no host port. 8091, the
# MiddleManager's usual port, is already taken by couchbase above - another reason not to.
# ---------------------------------------------------------------------------------------
druid-metadata:
# Druid's metadata store (segment table, task history, supervisors, config). Distinct
# from the `postgres` service above on purpose: sharing it would put Druid's ~20 internal
# tables into the demo database the studio's own postgres connection browses.
image: postgres:17.6
container_name: libredb-druid-metadata
profiles: ["druid"]
restart: unless-stopped
environment:
POSTGRES_USER: druid
POSTGRES_PASSWORD: druid
POSTGRES_DB: druid
# No published port: only the Druid Coordinator/Overlord reads it, over the compose
# network. Publishing it would collide with the `postgres` service on 5432.
healthcheck:
test: ["CMD-SHELL", "pg_isready -U druid -d druid"]
interval: 5s
timeout: 5s
retries: 30
druid-zookeeper:
# Druid's cluster coordination: process discovery, segment load queues, task assignment.
image: zookeeper:3.9.3
container_name: libredb-druid-zookeeper
profiles: ["druid"]
restart: unless-stopped
environment:
ZOO_MY_ID: "1"
# zkServer.sh's own status check uses the `srvr` four-letter word, which 3.9 refuses
# unless it is whitelisted.
ZOO_4LW_COMMANDS_WHITELIST: "srvr,ruok,conf"
healthcheck:
test: ["CMD-SHELL", "echo ruok | nc -w 2 localhost 2181 | grep -q imok"]
interval: 5s
timeout: 5s
retries: 30
druid-coordinator:
# Coordinator + Overlord in one process (the image's coordinator config sets
# druid.coordinator.asOverlord.enabled), so segment balancing and task management both
# live here. This is what accepts an ingestion task, which is how a datasource is
# created at all - Druid has no CREATE TABLE.
image: apache/druid:37.0.0
container_name: libredb-druid-coordinator
profiles: ["druid"]
restart: unless-stopped
command: ["coordinator"]
environment: *druid-env
volumes:
- druid_shared:/opt/shared
- druid_coordinator_var:/opt/druid/var
depends_on:
druid-metadata:
condition: service_healthy
druid-zookeeper:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8081/status/health"]
interval: 10s
timeout: 5s
retries: 40
druid-historical:
# Serves published segments. Without it a datasource exists in the metadata store but
# returns no rows, which is the "datasource with no segments" case the provider has to
# render honestly.
image: apache/druid:37.0.0
container_name: libredb-druid-historical
profiles: ["druid"]
restart: unless-stopped
command: ["historical"]
environment: *druid-env
volumes:
- druid_shared:/opt/shared
- druid_historical_var:/opt/druid/var
depends_on:
druid-coordinator:
condition: service_started
druid-zookeeper:
condition: service_healthy
druid-middlemanager:
# Runs ingestion tasks as forked peon processes. Needed for the live pass to load data.
image: apache/druid:37.0.0
container_name: libredb-druid-middlemanager
profiles: ["druid"]
restart: unless-stopped
command: ["middleManager"]
environment: *druid-env
volumes:
- druid_shared:/opt/shared
- druid_middlemanager_var:/opt/druid/var
depends_on:
druid-coordinator:
condition: service_started
druid-zookeeper:
condition: service_healthy
druid-broker:
# Plans and merges queries. POST /druid/v2/sql here is the same API the Router proxies,
# so the provider works against 8082 directly too - published so that can be proven
# rather than assumed.
image: apache/druid:37.0.0
container_name: libredb-druid-broker
profiles: ["druid"]
restart: unless-stopped
command: ["broker"]
environment: *druid-env
volumes:
- druid_broker_var:/opt/druid/var
ports:
- "8082:8082"
depends_on:
druid-coordinator:
condition: service_started
druid-zookeeper:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8082/status/health"]
interval: 10s
timeout: 5s
retries: 40
druid-router:
# The provider's default target. It fronts the SQL API, the web console and - because
# druid_router_managementProxy_enabled is set - the Coordinator and Overlord APIs, so a
# single port is enough for both querying and loading data.
image: apache/druid:37.0.0
container_name: libredb-druid-router
profiles: ["druid"]
restart: unless-stopped
command: ["router"]
environment: *druid-env
volumes:
- druid_router_var:/opt/druid/var
ports:
- "8888:8888"
depends_on:
druid-broker:
condition: service_started
druid-coordinator:
condition: service_started
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8888/status/health"]
interval: 10s
timeout: 5s
retries: 40
# Redis, as a service rather than a hand-started container. Its absence was the
# one hole in the shipped set (#424): `redis` had no compose entry, so its own
# gate-4 probe could not be reproduced on a clean machine.
redis:
image: redis:latest
container_name: libredb-redis
restart: unless-stopped
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 10
# ---------------------------------------------------------------------------------------
# Search engines (issue #424, Phase 1). Two type-ids, one provider module, so BOTH have to
# be runnable at once: every claim the provider records is a claim about which of the two
# answered differently, and that can only be re-measured side by side.
#
# This is the exact configuration the Phase 1 probes ran against (read back with
# `docker inspect`): single-node discovery, security off, and a 512 MB heap each - about
# 1.2 GB of RSS for the pair. No `profiles:` on purpose, unlike the druid block: these are
# SHIPPED providers, so a plain `docker compose up` has to be able to reproduce their
# integration pass, the same rule clickhouse and couchbase follow above.
#
# Security is DISABLED on both, which is what makes the fixtures reproducible - and it is
# also the limit of what they can prove: a bogus `Basic` header is IGNORED here (HTTP 200
# on both, measured), so no 401/403 body could ever be captured from these containers. The
# transport decides `auth` on the HTTP status alone for exactly that reason.
# ---------------------------------------------------------------------------------------
elasticsearch:
# Pinned to the exact build the provider was live-verified against, the same rule the
# clickhouse service follows: the status codes, fault names and paging behaviour recorded
# in docs/providers/elasticsearch.md are version-specific claims.
image: docker.elastic.co/elasticsearch/elasticsearch:9.1.4
container_name: libredb-elasticsearch
restart: unless-stopped
environment:
# One node forms its own cluster; without it the node waits for a seed host it will
# never find and the SQL endpoint never comes up.
discovery.type: single-node
# Off, so the endpoint is plain HTTP with no bootstrap password or enrolment token. The
# SQL API itself needs no licence beyond basic - that is what makes SQL, and not ES|QL,
# the query language both products can share.
xpack.security.enabled: "false"
# Sized for a laptop. The image otherwise claims a heap from the host's total memory.
ES_JAVA_OPTS: "-Xms512m -Xmx512m"
ports:
# 9200 is the HTTP API, the only protocol this provider speaks. 9300 (transport) is
# deliberately not published: there is no transport-protocol client in this codebase.
- "9200:9200"
healthcheck:
# `_cluster/health` answers before any index exists, so it does not need seed data. A
# single-node cluster with a replica requested is YELLOW and never green, so waiting
# for green here would hang forever. The image ships curl.
test: ["CMD", "curl", "-sf", "http://localhost:9200/_cluster/health?wait_for_status=yellow&timeout=3s"]
interval: 5s
timeout: 5s
retries: 40
opensearch:
# Pinned for the same reason, and here the reason is sharper: the SQL PLUGIN's fault
# names are Java class names that its own releases rename, and `latest` resolved to 3.8.0
# when the provider was measured (`GET /` reported distribution `opensearch`, number
# 3.8.0, build_date 2026-08-01).
image: opensearchproject/opensearch:3.8.0
container_name: libredb-opensearch
restart: unless-stopped
environment:
discovery.type: single-node
# The fork's own switch, and NOT the same key as Elasticsearch's: the security plugin
# is installed rather than a licensed feature, so it is disabled by name. Without this
# the node serves HTTPS with a self-signed certificate and a default admin password.
DISABLE_SECURITY_PLUGIN: "true"
OPENSEARCH_JAVA_OPTS: "-Xms512m -Xmx512m"
ports:
# 9201 on the host because both products ship on 9200 in the container and the
# elasticsearch service above already publishes it. The provider's default port stays
# 9200 - this is a collision on THIS machine, not a fact about the product.
- "9201:9200"
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:9200/_cluster/health?wait_for_status=yellow&timeout=3s"]
interval: 5s
timeout: 5s
retries: 40
# No seed sidecar, and one thing to expect instead: a stock node ships its own indices.
# Measured on an empty 3.8.0 cluster, `.plugins-ml-config` and `top_queries-<date>-<n>`
# are already there, so two thirds of the index listing is the engine's bookkeeping. The
# provider hides both; the second one carries no leading dot, so the dot convention alone
# does not catch it.
# ---------------------------------------------------------------------------------------
# Apache Trino (issue #424, Phase 2). One container is the whole cluster: the coordinator
# runs the worker in-process by default, which is enough to answer every statement the
# provider sends. Nothing else is needed to exercise it.
# ---------------------------------------------------------------------------------------
trino:
# Pinned to the exact build the provider was live-verified against, the same rule the
# clickhouse and elasticsearch services follow: the fault names, the page count of an
# exchange, the `SHOW STATS` shape and the 401 body recorded in docs/providers/trino.md
# are claims about 476, not about Trino in general. `version()` here returns the bare
# string "476", so the pin and the engine's own answer are the same token.
image: trinodb/trino:476
container_name: libredb-trino
restart: unless-stopped
ports:
# 8080 is the client protocol AND the web UI - one port, two surfaces, which is why
# the provider's default port is a generic number and the dialect is declared rather
# than inferred from it. There is no second protocol port to publish: Trino has no
# native wire beyond this HTTP one.
- "8080:8080"
healthcheck:
# The image's own script, not a bare `curl /v1/info`. Measured on a cold start: the
# statement endpoint answers at ~3.0s, `/v1/info` starts answering 200 at ~5.9s while
# still reporting `"starting": true`, and only at ~8.8s does it report false. A plain
# `curl -sf http://localhost:8080/v1/info` would therefore call the service healthy
# three seconds before it can plan a query. This script reads the port out of
# /etc/trino/config.properties and greps for `"starting": false`, which is the
# condition that actually matters.
test: ["CMD", "/usr/lib/trino/bin/health-check"]
interval: 5s
timeout: 5s
retries: 40
# No seed sidecar, and no `depends_on`: the image ships the `tpch` catalog configured, so
# `tpch.tiny.nation` (25 rows) and `tpch.tiny.lineitem` (60175 rows) are queryable the
# moment the healthcheck passes - generated on read, never stored. `tpcds`, `memory`,
# `system` and `jmx` are configured too. Point a connection's Database field at `tpch`;
# `jmx` is what makes the overview's uptime readable, so leave that catalog in place.
# ==========================================================================
# Wire-compatible engines (issue #424, Phase 0)
#
# These have no provider of their own: each speaks the wire protocol of a driver
# we already ship, and the entries below are the instances a gate-4 probe
# actually ran against. They exist so the compatibility claims in
# `src/lib/db/compatibility.ts` can be re-measured rather than trusted.
#
# All of them carry `profiles: ["compat"]`, so a plain `docker compose up` still
# starts only the shipped engines. Bring them up deliberately:
# docker compose -f database-compose.yml --profile compat up -d
# ==========================================================================
# -- PostgreSQL wire ------------------------------------------------------
citus:
image: citusdata/citus:latest
container_name: libredb-citus
profiles: ["compat"]
environment:
POSTGRES_PASSWORD: postgres
ports:
- "5435:5432"
# network_mode: host is not a shortcut here. CockroachDB 26.x refuses an insecure
# cluster whose --listen-addr is not 127.0.0.1/localhost, and a loopback bind
# inside the container namespace cannot be published with -p. Host networking is
# the only way to reach an insecure single node; the alternative is a secure
# cluster with --accept-sql-without-tls.
#
# LINUX ONLY. Docker Desktop (macOS, Windows) runs containers inside a VM, where
# `host` is the VM rather than your machine, so 127.0.0.1:26257 will not answer
# there. On those platforms run a secure cluster with --accept-sql-without-tls and
# a normal port mapping instead. The probe behind this entry ran on Linux.
cockroachdb:
image: cockroachdb/cockroach:latest
container_name: libredb-cockroachdb
profiles: ["compat"]
network_mode: host
command: start-single-node --insecure --listen-addr=localhost:26257 --http-addr=localhost:18080
healthcheck:
test: ["CMD", "./cockroach", "sql", "--insecure", "--host=127.0.0.1:26257", "-e", "SELECT 1"]
interval: 10s
timeout: 5s
retries: 12
materialize:
image: materialize/materialized:latest
container_name: libredb-materialize
profiles: ["compat"]
ports:
- "6875:6875"
risingwave:
image: risingwavelabs/risingwave:latest
container_name: libredb-risingwave
profiles: ["compat"]
command: single_node
ports:
- "4566:4566"
# TimescaleDB is an extension, not a fork: the server is a stock PostgreSQL 17 and
# `SELECT version()` says so. The extension version comes from pg_extension, which
# is why the probed version records both.
timescaledb:
image: timescale/timescaledb:latest-pg17
container_name: libredb-timescaledb
profiles: ["compat"]
environment:
POSTGRES_PASSWORD: probe
POSTGRES_DB: probe
ports:
- "15432:5432"
# `yugabyted` is the single-node launcher: it starts the master and the tserver in
# one container, and --background=false keeps it in the foreground so Docker owns
# the process and the container does not exit immediately. YSQL listens on 5433,
# NOT the PostgreSQL default 5432 - connecting to 5432 here reaches nothing. Sign
# in as user `yugabyte`, database `yugabyte`, no password.
yugabytedb:
image: yugabytedb/yugabyte:2.25.2.0-b359
container_name: libredb-yugabytedb
profiles: ["compat"]
command: bin/yugabyted start --background=false
ports:
- "15433:5433"
# THIRD-PARTY IMAGE, stated because it is unusual here: Apache publishes only
# BUILD images for Cloudberry - you compile the server inside them - so the
# project itself ships nothing runnable, and this is a community image. The probe
# behind this entry ran on it; nothing is claimed about an Apache-built server.
#
# Boot takes about two minutes before Postgres answers: the entrypoint initialises
# a coordinator and its segments first. `shm_size: 1gb` is what the probe ran with,
# the segment processes communicating through shared memory. Sign in as user
# `gpadmin`, password `probe`, database `probe`.
#
# `gpadmin` is a superuser, and the agent's execution profile refuses it as too
# broad, so an agent run here needs a least-privilege role created by hand - and
# even then the grounding read is refused, because gp_toolkit alone answers more
# rows than its budget allows (B52).
cloudberry:
image: woblerr/cloudberry:2.1.0-incubating
container_name: libredb-cloudberry
profiles: ["compat"]
shm_size: 1gb
environment:
CLOUDBERRY_PASSWORD: probe
CLOUDBERRY_DATABASE_NAME: probe
ports:
- "15435:5432"
# -- MySQL wire ----------------------------------------------------------
mariadb:
image: mariadb:latest
container_name: libredb-mariadb
profiles: ["compat"]
environment:
MARIADB_ROOT_PASSWORD: root
MARIADB_DATABASE: probe
ports:
- "3307:3306"
healthcheck:
test: ["CMD", "mariadb-admin", "ping", "-uroot", "-proot", "--silent"]
interval: 5s
timeout: 5s
retries: 20
# TiDB needs no PD and no TiKV to run here: --store=unistore is its embedded
# single-node storage engine, so this is one container like every other entry, and
# an empty --path keeps that store in memory. TiDB speaks the MySQL protocol on
# 4000, not 3306. Sign in as user `root`, no password. The probe behind this entry
# ran on unistore only, never against a PD + TiKV cluster.
tidb:
image: pingcap/tidb:v8.5.1
container_name: libredb-tidb
profiles: ["compat"]
command: ["--store=unistore", "--path=", "--host=0.0.0.0"]
ports:
- "14000:4000"
# allin1 is StarRocks in one container - the frontend and the backend together -
# and it is the form the probe behind this entry ran against. StarRocks speaks the
# MySQL protocol on 9030, not 3306. Sign in as user `root`, no password.
starrocks:
image: starrocks/allin1-ubuntu:3.3-latest
container_name: libredb-starrocks
profiles: ["compat"]
ports:
- "19030:9030"
# vttestserver is Vitess in one container: it brings up the topology, a vtctld, a
# vtgate and a mysqld for the keyspace named in KEYSPACES, so this entry needs no
# cluster of its own.
#
# The tag is PINNED deliberately. The rolling `:mysql80` tag serves a
# 25.0.0-SNAPSHOT built from main - not a release, and not reproducible - so the
# probe behind this entry ran on v24.0.2 and this file names it.
#
# PORT is the base of the range vttestserver allocates, and vtgate's MySQL port is
# 33577, which is what this entry publishes; 3306 reaches nothing. Sign in as user
# `root`, no password, database `probe`. NUM_SHARDS=1 is the shape that was probed:
# a sharded keyspace was not.
#
# This fixture cannot be used to test a permission error. vttestserver accepts any
# username with any password and grants it full rights, and CREATE USER is a vtgate
# parse error, so no restricted role can be made here. That is the fixture, not
# Vitess.
vitess:
image: vitess/vttestserver:v24.0.2-mysql80
container_name: libredb-vitess
profiles: ["compat"]
environment:
PORT: 33574
KEYSPACES: probe
NUM_SHARDS: 1
MYSQL_BIND_HOST: 0.0.0.0
ports:
- "33577:33577"
# -- Redis wire ----------------------------------------------------------
valkey:
image: valkey/valkey:latest
container_name: libredb-valkey
profiles: ["compat"]
ports:
- "6380:6379"
healthcheck:
test: ["CMD", "valkey-cli", "ping"]
interval: 5s
timeout: 3s
retries: 10
dragonfly:
image: docker.dragonflydb.io/dragonflydb/dragonfly:latest
container_name: libredb-dragonfly
profiles: ["compat"]
ulimits:
memlock: -1
ports:
- "6381:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 10
keydb:
image: eqalpha/keydb:latest
container_name: libredb-keydb
profiles: ["compat"]
ports:
- "6382:6379"
healthcheck:
test: ["CMD", "keydb-cli", "ping"]
interval: 5s
timeout: 3s
retries: 10
# -- MongoDB wire --------------------------------------------------------
# FerretDB is a proxy, not a database: it needs its own PostgreSQL-with-
# DocumentDB backend, which is why it is the only two-service entry here. Sign in
# with the BACKEND's PostgreSQL credentials - FerretDB rejects authMechanism=PLAIN.
ferretdb-backend:
image: ghcr.io/ferretdb/postgres-documentdb:latest
container_name: libredb-ferretdb-backend
profiles: ["compat"]
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
interval: 5s
timeout: 5s
retries: 20
ferretdb:
image: ghcr.io/ferretdb/ferretdb:latest
container_name: libredb-ferretdb
profiles: ["compat"]
depends_on:
ferretdb-backend:
condition: service_healthy
environment:
FERRETDB_POSTGRESQL_URL: postgres://postgres:postgres@ferretdb-backend:5432/postgres
ports:
- "27018:27017"
volumes:
# Named volumes only for Druid: it is the one service here whose processes must share
# state (deep storage) and whose task history is worth surviving a restart. Everything
# above uses the image's own anonymous volumes.
#
# druid_shared is the deep-storage + indexing-log volume, mounted by the three processes
# that write or read segments. A `down -v` resets the cluster to empty.
druid_shared: {}
druid_coordinator_var: {}
druid_historical_var: {}
druid_middlemanager_var: {}
druid_broker_var: {}
druid_router_var: {}