Repository navigation
Expand file tree
/
Copy pathdocker-compose.dev.yml
More file actions
972 lines (939 loc) · 38.6 KB
/
Copy pathdocker-compose.dev.yml
File metadata and controls
972 lines (939 loc) · 38.6 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
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
name: bloom_v2_dev
services:
bloom-web:
# web/.env is optional: every var bloom-web needs is already supplied via
# `environment`/`args` below from the root .env.dev. A fresh clone has no
# web/.env, so mark it not-required (Compose v2) instead of failing to start.
env_file:
- path: ./web/.env
required: false
build:
context: .
dockerfile: ./web/Dockerfile.bloom-web.dev
args:
NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL:-http://localhost:8000}
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${ANON_KEY}
ports:
- "${BLOOM_WEB_PORT}:3000"
environment:
NODE_ENV: development
# Browser-facing: must be an address the *browser* can reach.
NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL:-http://localhost:8000}
# Server-facing: stays on the Docker network.
SUPABASE_URL: http://kong:8000
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${ANON_KEY}
SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY}
JWT_SECRET: ${JWT_SECRET}
# Starting cylinder pipeline runs from Bloom (web/lib/cyl-pipeline/trigger-enabled.ts).
CYL_PIPELINE_TRIGGER_ENABLED: ${CYL_PIPELINE_TRIGGER_ENABLED:-true}
# Bloom's AWS user (bloomv2-workflows-job), shown on the scRNA job form (optional).
SCRNA_FOLDER_READER_ARN: ${SCRNA_FOLDER_READER_ARN:-}
volumes:
- ./web:/app/web
- ./packages:/app/packages
- /app/node_modules
networks:
- supanet
command: sh -c "apt-get update >/dev/null && apt-get install -y --no-install-recommends socat >/dev/null && socat TCP-LISTEN:8000,fork,reuseaddr TCP:kong:8000 &npm run dev"
langchain-agent:
build:
context: ./langchain
dockerfile: Dockerfile
ports:
- "${LANGCHAIN_PORT}:5002"
environment:
SUPABASE_URL: http://kong:8000
SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
OPENAI_API_KEY: ${OPENAI_API_KEY}
OPENAI_MODEL: gpt-4o-mini
BLOOM_MCP_URL: http://bloommcp:8811/mcp
BLOOMMCP_API_KEY: ${BLOOMMCP_API_KEY}
BLOOM_PLOTS_DIR: ${BLOOM_PLOTS_DIR}
BLOOM_PLOTS_URL: ${BLOOM_PLOTS_URL}
LANGCHAIN_TRACING_V2: ${LANGCHAIN_TRACING_V2}
LANGCHAIN_API_KEY: ${LANGCHAIN_API_KEY}
LANGCHAIN_PROJECT: ${LANGCHAIN_PROJECT}
JWT_SECRET: ${JWT_SECRET}
BLOOM_AGENT_KEY: ${BLOOM_AGENT_KEY}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_HOST: ${POSTGRES_HOST}
POSTGRES_PORT: ${POSTGRES_PORT}
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
LOCAL_LLM_URL: ${LOCAL_LLM_URL}
LOCAL_LLM_MODEL: ${LOCAL_LLM_MODEL}
CORS_ORIGINS: "http://localhost:3000"
volumes:
- ./langchain:/app
- ./bloommcp/data/PLOTS_DIR:/app/data/PLOTS_DIR
networks:
- supanet
healthcheck:
# Probe with python, not curl — curl is not installed in this image.
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:5002/health')"]
interval: 30s
timeout: 5s
retries: 3
depends_on:
- kong
- bloommcp
- db-dev
restart: unless-stopped
workflows:
build:
context: ./services/workflows
dockerfile: Dockerfile
# Sandbox the service in dev too — it runs ffmpeg + Pillow/numpy on
# attacker-influenced image bytes. Matches prod hardening. The source
# bind-mount below stays writable for --reload; only the container's own
# filesystem is read-only, with a size-capped tmpfs for the temp MP4.
read_only: true
tmpfs:
- /tmp:mode=1777,size=512m
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
ports:
- "${WORKFLOWS_PORT:-5100}:5100"
environment:
WORKFLOWS_CORS_ORIGINS: "http://localhost:3000"
# Logs in as a dedicated least-privilege Supabase app user; that user's
# grants + storage policies bound everything the service can read/write.
SUPABASE_URL: http://kong:8000
SUPABASE_ANON_KEY: ${ANON_KEY}
WORKFLOWS_SUPABASE_EMAIL: ${WORKFLOWS_SUPABASE_EMAIL}
WORKFLOWS_SUPABASE_PASSWORD: ${WORKFLOWS_SUPABASE_PASSWORD}
# Public base for rewriting signed URLs off the internal kong host so
# download_url is usable by outside callers.
WORKFLOWS_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL}
# Optional in dev: without it GET /model-cards answers 503 and the pipeline
# confirm dialog shows "Couldn't check the models' age ranges." (bloom#971).
WANDB_API_KEY: ${WANDB_API_KEY:-}
# Bloom's AWS user (bloomv2-workflows-job), for a folder a scientist shared with it (optional).
WORKFLOWS_S3_READER_ACCESS_KEY_ID: ${WORKFLOWS_S3_READER_ACCESS_KEY_ID:-}
WORKFLOWS_S3_READER_SECRET_ACCESS_KEY: ${WORKFLOWS_S3_READER_SECRET_ACCESS_KEY:-}
volumes:
- ./services/workflows:/app
command: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "5100", "--reload"]
networks:
- supanet
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:5100/health')"]
interval: 30s
timeout: 5s
retries: 3
depends_on:
- db-dev
- kong
restart: unless-stopped
# Consumes the cyl_pipeline_dispatch pgmq queue (bloom #11/#404 Phase 2) and
# submits each claimed batch to Argo as a Kubernetes Workflow CRD. Same image
# as workflows; scale by replicas (pgmq's claim is concurrency-safe).
cyl-pipeline-worker:
build:
context: ./services/workflows
dockerfile: Dockerfile
read_only: true
# Submits and polls, and writes nothing to /tmp, so a small RAM disk
# well inside the limit — tmpfs pages are charged to this same cgroup.
mem_limit: 1g
memswap_limit: 1g
tmpfs:
- /tmp:mode=1777,size=128m
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
environment:
SUPABASE_URL: http://kong:8000
SUPABASE_ANON_KEY: ${ANON_KEY}
WORKFLOWS_SUPABASE_EMAIL: ${WORKFLOWS_SUPABASE_EMAIL}
WORKFLOWS_SUPABASE_PASSWORD: ${WORKFLOWS_SUPABASE_PASSWORD}
# Real credentials for the bloom-pipeline ServiceAccount — deploy-time
# secrets, not filled in here. Namespace/TTL have safe code defaults
# (runai-busch-lab / 3600s) so they're not required to be set.
WORKFLOWS_K8S_TOKEN: ${WORKFLOWS_K8S_TOKEN}
WORKFLOWS_K8S_CA_CERT: ${WORKFLOWS_K8S_CA_CERT}
WORKFLOWS_K8S_API_URL: ${WORKFLOWS_K8S_API_URL}
WORKFLOWS_K8S_NAMESPACE: ${WORKFLOWS_K8S_NAMESPACE:-runai-busch-lab}
WORKFLOWS_K8S_TTL_SECONDS: ${WORKFLOWS_K8S_TTL_SECONDS:-3600}
WORKFLOWS_K8S_ENV_LABEL: ${WORKFLOWS_K8S_ENV_LABEL:-dev}
# Blank by default: dev numbers its scans independently, so it must not
# share staging's directories. Unset, every claimed batch fails "not
# configured" (bloom#863); set both to a dev-only tree to dispatch.
WORKFLOWS_K8S_PIPELINE_HOSTPATH_ROOT: ${WORKFLOWS_K8S_PIPELINE_HOSTPATH_ROOT:-}
WORKFLOWS_K8S_PIPELINE_SECRET_NAME: ${WORKFLOWS_K8S_PIPELINE_SECRET_NAME:-}
WORKFLOWS_RNASEQ_CELLRANGER_TEMPLATE: ${WORKFLOWS_RNASEQ_CELLRANGER_TEMPLATE:-}
CYL_PIPELINE_TRIGGER_ENABLED: ${CYL_PIPELINE_TRIGGER_ENABLED:-true}
volumes:
- ./services/workflows:/app
command: ["python", "dispatch_worker.py"]
networks:
- supanet
depends_on:
- db-dev
- kong
# See docker-compose.prod.yml's cyl-pipeline-worker for why this isn't
# Docker's 10s default — the in-flight K8s submission POST alone can take
# up to 15s (k8s_client.py's httpx timeout).
stop_grace_period: 30s
restart: unless-stopped
# Periodically polls Argo for each in-flight run's real Workflow phase(s)
# and progresses cyl_pipeline_runs.status past dispatch outcome (bloom #11
# Phase 3) — a separate process from cyl-pipeline-worker above (event-driven
# dispatch vs. this poller's fixed wall-clock sweep). Same image; read-only
# use of the K8s API (get, not create), so only TOKEN/CA_CERT/API_URL/
# NAMESPACE are needed, plus TTL_SECONDS: the poller never treats a workflow
# as removed sooner than that after the workflow was dispatched, because Argo
# can't have garbage-collected it yet (bloom#1042). It also needs ENV_LABEL:
# it trusts a Workflow as a run's own only if its `environment` label matches,
# so this must be the value cyl-pipeline-worker stamps (prod and staging share
# a namespace). The PIPELINE_* pair is submission-only (build_workflow_body). Like the poll
# interval below, WORKFLOWS_NOT_FOUND_GRACE_SECONDS is left to its code
# default (600s).
# WORKFLOWS_STATUS_POLL_SECONDS is deliberately NOT referenced here — its
# Python-side default (15s) governs it, matching WORKFLOWS_WORKER_POLL_SECONDS's
# own precedent above; adding it here without a real default-file entry would
# make compose inject an empty string and silently override that default.
# Also safe to scale by replicas, for a different reason than cyl-pipeline-
# worker above: each sweep is an idempotent recompute from Argo's current
# state (see design.md's "rollup computation happens in Python" decision),
# not a claimed, exactly-once event — redundant concurrent sweeps just
# mean harmless, wasted-work overlap, not a correctness risk. Each replica
# keeps its own in-memory count of a workflow's NotFound lookups, so each
# must see a workflow's removal conditions for itself before closing its rows.
cyl-status-poller:
build:
context: ./services/workflows
dockerfile: Dockerfile
read_only: true
# Submits and polls, and writes nothing to /tmp, so a small RAM disk
# well inside the limit — tmpfs pages are charged to this same cgroup.
mem_limit: 1g
memswap_limit: 1g
tmpfs:
- /tmp:mode=1777,size=128m
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
environment:
SUPABASE_URL: http://kong:8000
SUPABASE_ANON_KEY: ${ANON_KEY}
WORKFLOWS_SUPABASE_EMAIL: ${WORKFLOWS_SUPABASE_EMAIL}
WORKFLOWS_SUPABASE_PASSWORD: ${WORKFLOWS_SUPABASE_PASSWORD}
WORKFLOWS_K8S_TOKEN: ${WORKFLOWS_K8S_TOKEN}
WORKFLOWS_K8S_CA_CERT: ${WORKFLOWS_K8S_CA_CERT}
WORKFLOWS_K8S_API_URL: ${WORKFLOWS_K8S_API_URL}
WORKFLOWS_K8S_NAMESPACE: ${WORKFLOWS_K8S_NAMESPACE:-runai-busch-lab}
WORKFLOWS_K8S_TTL_SECONDS: ${WORKFLOWS_K8S_TTL_SECONDS:-3600}
WORKFLOWS_K8S_ENV_LABEL: ${WORKFLOWS_K8S_ENV_LABEL:-dev}
volumes:
- ./services/workflows:/app
command: ["python", "status_poller.py"]
networks:
- supanet
depends_on:
- db-dev
- kong
# Larger than cyl-pipeline-worker's 30s (found during /review-pr round 1,
# not a blind copy): one sweep can issue one get_workflow_status GET per
# distinct workflow name across EVERY candidate run this cycle, not a
# single call per claim, so 30s isn't necessarily enough headroom for
# _stop()'s documented "let the in-flight sweep finish" guarantee.
stop_grace_period: 60s
restart: unless-stopped
# Sends queued RNA-seq runs (Cell Ranger first) to Argo; see rnaseq_workflows.py.
# Same image, credentials and limits as cyl-pipeline-worker, with its own queues.
rnaseq-worker:
build:
context: ./services/workflows
dockerfile: Dockerfile
read_only: true
mem_limit: 1g
memswap_limit: 1g
tmpfs:
- /tmp:mode=1777,size=128m
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
environment:
SUPABASE_URL: http://kong:8000
SUPABASE_ANON_KEY: ${ANON_KEY}
WORKFLOWS_SUPABASE_EMAIL: ${WORKFLOWS_SUPABASE_EMAIL}
WORKFLOWS_SUPABASE_PASSWORD: ${WORKFLOWS_SUPABASE_PASSWORD}
WORKFLOWS_K8S_TOKEN: ${WORKFLOWS_K8S_TOKEN}
WORKFLOWS_K8S_CA_CERT: ${WORKFLOWS_K8S_CA_CERT}
WORKFLOWS_K8S_API_URL: ${WORKFLOWS_K8S_API_URL}
WORKFLOWS_K8S_NAMESPACE: ${WORKFLOWS_K8S_NAMESPACE:-runai-busch-lab}
WORKFLOWS_K8S_TTL_SECONDS: ${WORKFLOWS_K8S_TTL_SECONDS:-3600}
WORKFLOWS_K8S_ENV_LABEL: ${WORKFLOWS_K8S_ENV_LABEL:-dev}
# The three keys below are cyl-pipeline-worker's; this worker ignores
# them (RNA-seq dispatch is not gated by them). They're here because
# this environment must equal cyl-pipeline-worker's.
WORKFLOWS_K8S_PIPELINE_HOSTPATH_ROOT: ${WORKFLOWS_K8S_PIPELINE_HOSTPATH_ROOT:-}
WORKFLOWS_K8S_PIPELINE_SECRET_NAME: ${WORKFLOWS_K8S_PIPELINE_SECRET_NAME:-}
WORKFLOWS_RNASEQ_CELLRANGER_TEMPLATE: ${WORKFLOWS_RNASEQ_CELLRANGER_TEMPLATE:-}
CYL_PIPELINE_TRIGGER_ENABLED: ${CYL_PIPELINE_TRIGGER_ENABLED:-true}
command: ["python", "rnaseq_worker.py"]
networks:
- supanet
depends_on:
- db-dev
- kong
# Lets a submission in progress (up to 15s) finish on shutdown.
stop_grace_period: 30s
restart: unless-stopped
# Follows submitted and running RNA-seq runs on Argo and records their status.
# Same image, credentials and limits as cyl-status-poller; read-only use of the K8s API.
rnaseq-status-poller:
build:
context: ./services/workflows
dockerfile: Dockerfile
read_only: true
mem_limit: 1g
memswap_limit: 1g
tmpfs:
- /tmp:mode=1777,size=128m
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
environment:
SUPABASE_URL: http://kong:8000
SUPABASE_ANON_KEY: ${ANON_KEY}
WORKFLOWS_SUPABASE_EMAIL: ${WORKFLOWS_SUPABASE_EMAIL}
WORKFLOWS_SUPABASE_PASSWORD: ${WORKFLOWS_SUPABASE_PASSWORD}
WORKFLOWS_K8S_TOKEN: ${WORKFLOWS_K8S_TOKEN}
WORKFLOWS_K8S_CA_CERT: ${WORKFLOWS_K8S_CA_CERT}
WORKFLOWS_K8S_API_URL: ${WORKFLOWS_K8S_API_URL}
WORKFLOWS_K8S_NAMESPACE: ${WORKFLOWS_K8S_NAMESPACE:-runai-busch-lab}
WORKFLOWS_K8S_TTL_SECONDS: ${WORKFLOWS_K8S_TTL_SECONDS:-3600}
WORKFLOWS_K8S_ENV_LABEL: ${WORKFLOWS_K8S_ENV_LABEL:-dev}
# The run-finished email; unset in dev, so nothing is sent.
SMTP_HOST: ${SMTP_HOST:-}
SMTP_PORT: ${SMTP_PORT:-}
SMTP_ADMIN_EMAIL: ${SMTP_ADMIN_EMAIL:-}
SMTP_SENDER_NAME: ${SMTP_SENDER_NAME:-}
SITE_URL: ${SITE_URL:-}
command: ["python", "rnaseq_status_poller.py"]
networks:
- supanet
depends_on:
- db-dev
- kong
# Lets the run in progress (a 15 s status read plus its update) finish on shutdown.
stop_grace_period: 60s
restart: unless-stopped
# Renders one plate on command. Behind a profile, so `up -d` never starts it.
# Dev-only until the render queue lands; see services/workflows/README.md.
plate-video-worker:
build:
context: ./services/workflows
dockerfile: Dockerfile
profiles:
- plate-video-worker
read_only: true
tmpfs:
- /tmp:mode=1777,size=512m
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
# The workflows service's own limit, until a render here is measured.
mem_limit: 8g
memswap_limit: 8g
environment:
SUPABASE_URL: http://kong:8000
SUPABASE_ANON_KEY: ${ANON_KEY}
WORKFLOWS_SUPABASE_EMAIL: ${WORKFLOWS_SUPABASE_EMAIL}
WORKFLOWS_SUPABASE_PASSWORD: ${WORKFLOWS_SUPABASE_PASSWORD}
WORKFLOWS_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL}
networks:
- supanet
depends_on:
- db-dev
- kong
# Rendering takes its arguments, so the default command only says how.
command:
- python
- plate_video_worker.py
- --help
# Renders one cyl scan on command. Behind a profile, so `up -d` never starts it.
# Dev-only until the render queue lands; see services/workflows/README.md.
cyl-video-worker:
build:
context: ./services/workflows
dockerfile: Dockerfile
profiles:
- cyl-video-worker
read_only: true
tmpfs:
- /tmp:mode=1777,size=512m
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
# A scan is capped at MAX_IMAGES decimated frames, far under a plate; this
# stays at the plate's limit until a render here is measured.
mem_limit: 8g
memswap_limit: 8g
environment:
SUPABASE_URL: http://kong:8000
SUPABASE_ANON_KEY: ${ANON_KEY}
WORKFLOWS_SUPABASE_EMAIL: ${WORKFLOWS_SUPABASE_EMAIL}
WORKFLOWS_SUPABASE_PASSWORD: ${WORKFLOWS_SUPABASE_PASSWORD}
WORKFLOWS_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL}
networks:
- supanet
depends_on:
- db-dev
- kong
command:
- python
- cyl_video_worker.py
- --help
bloommcp:
build:
context: ./bloommcp
dockerfile: Dockerfile
ports:
- "${BLOOMMCP_PORT}:8811"
environment:
SUPABASE_URL: http://kong:8000
# Public base for rewriting signed URLs (bloom#581's output_links) off
# the internal kong host so they're usable by outside callers — mirrors
# the workflows service's identical WORKFLOWS_PUBLIC_SUPABASE_URL above.
BLOOM_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL}
# bloommcp authenticates to PostgREST + Storage as bloom_agent — same
# JWT langchain-agent uses. SERVICE_ROLE_KEY is deliberately NOT passed
# in so a future code path can't silently bypass the
# agent_insert_bloommcp_data / agent_update_bloommcp_data policies.
BLOOM_AGENT_KEY: ${BLOOM_AGENT_KEY}
BLOOM_TRAITS_DIR: /app/data/TRAITS_DIR
BLOOM_OUTPUT_DIR: /app/data/ANALYSIS_OUTPUT
BLOOM_PLOTS_DIR: /app/data/PLOTS_DIR
BLOOM_PLOTS_URL: ${BLOOM_PLOTS_URL}
BLOOMMCP_API_KEY: ${BLOOMMCP_API_KEY}
# Verifies an optional X-Bloom-Identity header for usage tracking (#406)
# — never used as DB/Storage authority. Same secret GoTrue/PostgREST/
# langchain-agent already use; validated lazily (only if/when a request
# actually carries the header), so leaving it set here has no effect
# until a client sends that header.
JWT_SECRET: ${JWT_SECRET}
# OAuth login for external MCP clients. Both URLs must be reachable by
# the *client* (which runs outside Docker), so they use host-facing
# addresses, not the internal kong hostname. Setting both turns on the
# /.well-known/oauth-protected-resource. Also reused (bloom#642) as the
# self-serve base for local-backend /plots URLs below — see
# bloommcp/docs/storage-backends.md.
BLOOMMCP_PUBLIC_URL: ${BLOOMMCP_PUBLIC_URL:-http://localhost:8811}
BLOOMMCP_OAUTH_AUTHORIZATION_SERVER: ${BLOOMMCP_OAUTH_AUTHORIZATION_SERVER:-http://localhost:8000/auth/v1}
# Set once Supabase signs asymmetrically; unset falls back to JWT_SECRET.
BLOOMMCP_OAUTH_JWKS_URI: ${BLOOMMCP_OAUTH_JWKS_URI:-}
# Storage backend (opt-in, default off — empty here behaves exactly like
# unset). Set BLOOM_STORAGE_BACKEND=local in your own .env.dev, or run
# `make dev-up-local`, to run FULLY LOCAL (offline) — reads inputs via
# LocalReader AND writes outputs as real files on disk instead of Supabase,
# with no SUPABASE_URL / BLOOM_AGENT_KEY needed to boot. Dev/power-user
# only — see bloommcp/docs/storage-backends.md. The output root defaults to
# BLOOM_OUTPUT_DIR and the input root to BLOOM_TRAITS_DIR (both mounted
# below), so `local` needs no extra config in dev. Do NOT mix backends for
# one experiment (no cross-store view; version ids can collide).
# BLOOM_LOCAL_ROOT (#479) is a single-var alternative to
# BLOOM_STORAGE_LOCAL_ROOT/BLOOM_EXPERIMENT_LOCAL_ROOT: set it (with
# BLOOM_STORAGE_BACKEND=local) to point all three subpaths — input,
# output, plots — at one root instead of the split vars. If you point it
# somewhere not already mounted into the container, add your own bind
# mount — the auto-created subfolders otherwise live only in the
# container's writable layer and vanish on the next rebuild.
BLOOM_STORAGE_BACKEND: ${BLOOM_STORAGE_BACKEND:-}
BLOOM_STORAGE_LOCAL_ROOT: ${BLOOM_STORAGE_LOCAL_ROOT:-}
BLOOM_EXPERIMENT_LOCAL_ROOT: ${BLOOM_EXPERIMENT_LOCAL_ROOT:-}
BLOOM_LOCAL_ROOT: ${BLOOM_LOCAL_ROOT:-}
# Served-URL base for LocalStorageBackend.create_signed_url (bloom#581) —
# NOT used by bloommcp's own output_links (bloom#642: those carry a
# direct filesystem path for the local backend, no URL needed at all).
# Only relevant if you're deliberately calling create_signed_url
# yourself against a server you've separately stood up to serve
# BLOOM_STORAGE_LOCAL_ROOT over HTTP; leave unset otherwise.
BLOOM_STORAGE_URL: ${BLOOM_STORAGE_URL:-}
# Kept in sync with docker-compose.prod.yml for dev/prod parity, even
# though dev has a writable root FS. See the prod compose for rationale.
NUMBA_CACHE_DIR: /tmp/numba
PYTHONPYCACHEPREFIX: /tmp/pycache
volumes:
- ./bloommcp:/app
- ./bloommcp/data/TRAITS_DIR:/app/data/TRAITS_DIR
- ./bloommcp/data/PLOTS_DIR:/app/data/PLOTS_DIR
- ./bloommcp/data/ANALYSIS_OUTPUT:/app/data/ANALYSIS_OUTPUT
networks:
- supanet
healthcheck:
# Probe the unauthenticated /health route (server.py), NOT /mcp — /mcp
# requires a Bearer API key and returns 401, which would mark the
# (otherwise healthy) container unhealthy.
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8811/health')"]
interval: 30s
timeout: 5s
retries: 3
depends_on:
- kong
restart: unless-stopped
studio:
image: ghcr.io/salk-harnessing-plants-initiative/supabase-studio:2026.03.30-sha-12a43e5@sha256:d73df17a81b897bb61326b3160b29558ca4c2cdbe32d25b7bb11e15a2cd13488
restart: unless-stopped
networks:
- supanet
ports:
- "${STUDIO_PORT}:3000"
healthcheck:
test:
[
"CMD",
"node",
"-e",
"fetch('http://studio:3000/api/platform/profile').then((r) => {if (r.status !== 200) throw new Error(r.status)})"
]
timeout: 10s
interval: 5s
retries: 3
environment:
STUDIO_PG_META_URL: http://meta:8080
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
# Required from studio 2026.03.30 onward; without it the container exits on boot.
EDGE_FUNCTIONS_MANAGEMENT_FOLDER: /tmp/edge-functions
# SQL editor saved queries; unset, the editor errors on every load.
SNIPPETS_MANAGEMENT_FOLDER: /app/snippets
DEFAULT_ORGANIZATION_NAME: ${STUDIO_DEFAULT_ORGANIZATION}
DEFAULT_PROJECT_NAME: ${STUDIO_DEFAULT_PROJECT}
OPENAI_API_KEY: ${OPENAI_API_KEY}
SUPABASE_URL: http://kong:8000
SUPABASE_PUBLIC_URL: http://studio.localhost
SUPABASE_ANON_KEY: ${ANON_KEY}
SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
AUTH_JWT_SECRET: ${JWT_SECRET}
NEXT_PUBLIC_ENABLE_LOGS: true
NEXT_ANALYTICS_BACKEND_PROVIDER: postgres
volumes:
- ./volumes/snippets:/app/snippets:z
kong:
image: ghcr.io/salk-harnessing-plants-initiative/kong:2.8.1@sha256:1b53405d8680a09d6f44494b7990bf7da2ea43f84a258c59717d4539abf09f6d
restart: unless-stopped
networks:
- supanet
ports:
- ${KONG_HTTP_PORT}:8000/tcp
- ${KONG_HTTPS_PORT}:8443/tcp
volumes:
- ./volumes/api/kong.yml:/home/kong/temp.yml:ro,z
environment:
KONG_DATABASE: "off"
KONG_DECLARATIVE_CONFIG: /home/kong/kong.yml
KONG_DNS_ORDER: LAST,A,CNAME
KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth
KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k
KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k
SUPABASE_ANON_KEY: ${ANON_KEY}
SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
BLOOM_AGENT_KEY: ${BLOOM_AGENT_KEY}
DASHBOARD_USERNAME: ${DASHBOARD_USERNAME}
DASHBOARD_PASSWORD: ${DASHBOARD_PASSWORD}
entrypoint: bash -c 'eval "echo \"$$(cat ~/temp.yml)\"" > ~/kong.yml && /docker-entrypoint.sh kong docker-start'
auth:
image: ghcr.io/salk-harnessing-plants-initiative/supabase-gotrue:v2.188.1@sha256:87db8c737af49a64236c461882ed3925f8b1e5c2c47176c64694dedc65153573
restart: unless-stopped
networks:
- supanet
healthcheck:
test:
[
"CMD",
"wget",
"--no-verbose",
"--tries=1",
"--spider",
"http://localhost:9999/health"
]
timeout: 5s
interval: 5s
retries: 3
start_period: 20s
depends_on:
db-dev:
condition: service_healthy
environment:
GOTRUE_API_HOST: 0.0.0.0
GOTRUE_API_PORT: 9999
API_EXTERNAL_URL: ${API_EXTERNAL_URL}
GOTRUE_DB_DRIVER: postgres
GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
GOTRUE_SITE_URL: ${SITE_URL}
GOTRUE_URI_ALLOW_LIST: ${ADDITIONAL_REDIRECT_URLS}
GOTRUE_DISABLE_SIGNUP: ${DISABLE_SIGNUP}
GOTRUE_JWT_ADMIN_ROLES: service_role
GOTRUE_JWT_AUD: authenticated
GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated
GOTRUE_JWT_EXP: ${JWT_EXPIRY}
GOTRUE_JWT_SECRET: ${JWT_SECRET}
# Asymmetric signing keys. ID tokens (the `openid` scope) cannot be
# signed with the shared HS256 secret — a public client has no way to
# verify one without also holding the secret — so GoTrue refuses and the
# token exchange fails. This key set carries an ES256 private key for
# new tokens plus JWT_SECRET as a symmetric JWK, so tokens already
# signed with the secret keep verifying.
GOTRUE_JWT_KEYS: ${JWT_KEYS:-[]}
GOTRUE_EXTERNAL_EMAIL_ENABLED: ${ENABLE_EMAIL_SIGNUP}
GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: ${ENABLE_ANONYMOUS_USERS}
GOTRUE_MAILER_AUTOCONFIRM: ${ENABLE_EMAIL_AUTOCONFIRM}
# Custom access token hook — assigns bloom_user/bloom_admin role via JWT
GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED: "true"
GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_URI: "pg-functions://postgres/public/custom_access_token_hook"
# OAuth discovery must advertise absolute URLs. Without an issuer GoTrue
# emits relative endpoints a conforming client cannot resolve — discovery
# returns 200 and the flow dead-ends before registration.
GOTRUE_API_EXTERNAL_URL: ${API_EXTERNAL_URL}
# Deliberately NOT ${API_EXTERNAL_URL}/auth/v1 (http://localhost:9999/auth/v1
# by default): that's GoTrue's own direct port, which this compose file
# never publishes to the host (no `ports:` on the `auth` service, only
# reachable container-to-container via `supanet`). An MCP client
# validating `iss` per RFC 9207 would reject every dev-issued token
# against an issuer it cannot even resolve. Routed through Kong's
# published port instead, matching BLOOMMCP_OAUTH_AUTHORIZATION_SERVER's
# own default (http://localhost:8000/auth/v1) below — both must name the
# same externally-reachable authorization server.
GOTRUE_JWT_ISSUER: http://localhost:${KONG_HTTP_PORT}/auth/v1
GOTRUE_OAUTH_SERVER_ENABLED: ${GOTRUE_OAUTH_SERVER_ENABLED:-true}
GOTRUE_OAUTH_SERVER_AUTHORIZATION_PATH: ${GOTRUE_OAUTH_SERVER_AUTHORIZATION_PATH:-/oauth/consent}
GOTRUE_OAUTH_SERVER_ALLOW_DYNAMIC_REGISTRATION: ${GOTRUE_OAUTH_SERVER_ALLOW_DYNAMIC_REGISTRATION:-true}
GOTRUE_OAUTH_SERVER_AUTHORIZATION_TTL: ${GOTRUE_OAUTH_SERVER_AUTHORIZATION_TTL:-600s}
rest:
image: ghcr.io/salk-harnessing-plants-initiative/postgrest:v12.2.12@sha256:5f4ce744539bbba786b4e24dbbd95bdb2a956dcf568c5374995a0ff4a68f5bd2
restart: unless-stopped
networks:
- supanet
depends_on:
db-dev:
condition: service_healthy
environment:
PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
PGRST_DB_SCHEMAS: ${PGRST_DB_SCHEMAS}
PGRST_DB_ANON_ROLE: anon
PGRST_JWT_SECRET: ${JWT_JWKS:-${JWT_SECRET}}
PGRST_DB_USE_LEGACY_GUCS: "false"
PGRST_APP_SETTINGS_JWT_SECRET: ${JWT_SECRET}
PGRST_APP_SETTINGS_JWT_EXP: ${JWT_EXPIRY}
command:
[
"postgrest"
]
realtime:
image: ghcr.io/salk-harnessing-plants-initiative/supabase-realtime:v2.34.47@sha256:4d2460cb6eb081b7cfec0e6b0af57f547497f60cf897621555303992d299f6e8
networks:
supanet:
# realtime-dev is the seeded tenant; Realtime reads it from the Host header's first label.
aliases:
- realtime-dev.supabase-realtime
restart: unless-stopped
depends_on:
db-dev:
condition: service_healthy
healthcheck:
test:
[
"CMD",
"curl",
"-sSfL",
"--head",
"-o",
"/dev/null",
"-H",
"Authorization: Bearer ${ANON_KEY}",
"http://localhost:4000/api/tenants/realtime-dev/health"
]
timeout: 5s
interval: 5s
retries: 3
start_period: 60s
environment:
PORT: 4000
DB_HOST: ${POSTGRES_HOST}
DB_PORT: ${POSTGRES_PORT}
DB_USER: ${POSTGRES_USER}
DB_PASSWORD: ${POSTGRES_PASSWORD}
DB_NAME: ${POSTGRES_DB}
DB_AFTER_CONNECT_QUERY: 'SET search_path TO _realtime'
DB_ENC_KEY: ${DB_ENC_KEY}
# Realtime takes the JWKS via its own API_JWT_JWKS, not API_JWT_SECRET (#646).
# Default is `null`, not empty: seeds.exs guards with `if v, do: Jason.decode!(v)`
# and "" is truthy in Elixir, so an empty value crashes the boot seed.
API_JWT_SECRET: ${JWT_SECRET}
API_JWT_JWKS: ${JWT_JWKS:-null}
SECRET_KEY_BASE: ${SECRET_KEY_BASE}
ERL_AFLAGS: -proto_dist inet_tcp
DNS_NODES: "''"
RLIMIT_NOFILE: "10000"
APP_NAME: realtime
SEED_SELF_HOST: true
RUN_JANITOR: true
supabase-minio:
networks:
- supanet
image: ghcr.io/salk-harnessing-plants-initiative/minio:RELEASE.2025-01-20T14-49-07Z@sha256:ed9be66eb5f2636c18289c34c3b725ddf57815f2777c77b5938543b78a44f144
ports:
- "${MINIO_PORT}:9000"
- "${MINIO_CONSOLE_PORT}:9001"
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
command: server --console-address ":9001" /data
volumes:
- ${MINIO_DATA_PATH}:/data
minio-init:
networks:
- supanet
image: ghcr.io/salk-harnessing-plants-initiative/minio-mc:RELEASE.2025-08-13T08-35-41Z@sha256:a7fe349ef4bd8521fb8497f55c6042871b2ae640607cf99d9bede5e9bdf11727
depends_on:
- supabase-minio
entrypoint: ["/bin/sh", "/usr/local/bin/init/create-buckets.sh"]
volumes:
- ./minio/init:/usr/local/bin/init
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
storage:
networks:
- supanet
image: ghcr.io/salk-harnessing-plants-initiative/supabase-storage-api:v1.48.14@sha256:14a7e837e44a5f1d59c4af0a7fb0066a6ab3b9146690a48f8e018dd60f3bf91f
restart: unless-stopped
ports:
- "${STORAGE_PORT}:5000"
volumes:
- ./volumes/storage:/var/lib/storage:z
healthcheck:
test:
[
"CMD",
"wget",
"--no-verbose",
"--tries=1",
"--spider",
"http://storage:5000/status"
]
timeout: 5s
interval: 5s
retries: 3
start_period: 30s
depends_on:
db-dev:
condition: service_healthy
rest:
condition: service_started
imgproxy:
condition: service_started
environment:
ANON_KEY: ${ANON_KEY}
SERVICE_KEY: ${SERVICE_ROLE_KEY}
POSTGREST_URL: http://rest:3000
# storage-api reads asymmetric keys from JWT_JWKS only — it treats
# PGRST_JWT_SECRET as an opaque symmetric secret, so the JWKS must not go
# there. Without JWT_JWKS its accepted-alg list stays HS256-only and every
# ES256 session token is refused (#646).
PGRST_JWT_SECRET: ${JWT_SECRET}
JWT_JWKS: ${JWT_JWKS:-null}
DATABASE_URL: postgres://supabase_storage_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
# 500 MB cap for GraviScan jpegs (single sensor image). Per-bucket
# `storage.buckets.file_size_limit` further constrains where stricter caps are needed.
FILE_SIZE_LIMIT: 524288000
STORAGE_BACKEND: s3
STORAGE_S3_BUCKET: bloom-storage
STORAGE_S3_ENDPOINT: ${MINIO_STORAGE_S3_ENDPOINT}
STORAGE_S3_FORCE_PATH_STYLE: "true"
STORAGE_S3_REGION: ${MINIO_S3_STORAGE_REGION}
STORAGE_S3_ACCESS_KEY: ${MINIO_ROOT_USER}
STORAGE_S3_SECRET_KEY: ${MINIO_ROOT_PASSWORD}
AWS_ACCESS_KEY_ID: ${MINIO_ROOT_USER}
AWS_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD}
ENABLE_IMAGE_TRANSFORMATION: "true"
IMGPROXY_URL: http://imgproxy:5001
imgproxy:
image: ghcr.io/salk-harnessing-plants-initiative/darthsim-imgproxy:v3.31.1@sha256:2b7a56dbf9c8a8e12e7109a5bdd27d31a8c1aa49f2116c927c6aedc37e18db98
restart: unless-stopped
networks:
- supanet
volumes:
- ./volumes/storage:/var/lib/storage:z
healthcheck:
test:
[
"CMD",
"imgproxy",
"health"
]
timeout: 5s
interval: 5s
retries: 3
start_period: 20s
environment:
IMGPROXY_BIND: ":5001"
IMGPROXY_LOCAL_FILESYSTEM_ROOT: /
IMGPROXY_USE_ETAG: "true"
IMGPROXY_ENABLE_WEBP_DETECTION: ${IMGPROXY_ENABLE_WEBP_DETECTION}
meta:
image: ghcr.io/salk-harnessing-plants-initiative/supabase-postgres-meta:v0.96.2@sha256:4ed8f7c5d3b2e25aa4aa29794605b49799b66da4b36a3c70894cd5241af63f97
restart: unless-stopped
networks:
- supanet
depends_on:
db-dev:
condition: service_healthy
environment:
PG_META_PORT: 8080
PG_META_DB_HOST: ${POSTGRES_HOST}
PG_META_DB_PORT: ${POSTGRES_PORT}
PG_META_DB_NAME: ${POSTGRES_DB}
PG_META_DB_USER: supabase_admin
PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD}
# NOTE: analytics (Logflare) removed — makes unauthorized outbound
# connections to DataDog. See: https://github.com/supabase/cli/issues/3254
db-dev:
# Pinned to the same tag as docker-compose.prod.yml. Behaviour that differs between
# Postgres builds is otherwise invisible locally and only surfaces in the one CI job
# that builds from the prod compose file. See issue #692.
#
# Upgrading an existing checkout requires clearing TWO things once, or the container
# restart-loops on `configuration file "/etc/postgresql/postgresql.conf" contains errors`:
#
# make dev-down
# rm -rf ./volumes/db/data # bind mount; both tags are PG 15, so the
# # cluster is otherwise never re-initialised
# docker volume rm bloom_v2_dev_db-config # holds supautils.conf et al. written by
# # the OLD image; the new one rejects it
#
# The db-config volume is the non-obvious one — it survives `docker compose down` and is
# shared by every worktree using the bloom_v2_dev project name.
image: ghcr.io/salk-harnessing-plants-initiative/supabase-postgres:15.14.1.104@sha256:bc49dde9849c1f8ee5c48f08600b49e2b0911f17c1b055a696d610cea48b6ba1
restart: unless-stopped
volumes:
- ./volumes/db/_supabase.sh:/docker-entrypoint-initdb.d/96-_supabase.sh:Z
- ./volumes/db/data:/var/lib/postgresql/data:Z
- ./volumes/db/realtime.sql:/docker-entrypoint-initdb.d/migrations/99-realtime.sql:Z
- ./volumes/db/webhooks.sql:/docker-entrypoint-initdb.d/init-scripts/98-webhooks.sql:Z
- ./volumes/db/roles.sql:/docker-entrypoint-initdb.d/init-scripts/99-roles.sql:Z
- ./volumes/db/jwt.sql:/docker-entrypoint-initdb.d/init-scripts/99-jwt.sql:Z
- ./volumes/db/logs.sql:/docker-entrypoint-initdb.d/migrations/99-logs.sql:Z
- ./volumes/db/pooler.sql:/docker-entrypoint-initdb.d/migrations/99-pooler.sql:Z
- db-config:/etc/postgresql-custom
networks:
- supanet
ports:
# Host-exposed Postgres port. Canonical name POSTGRES_HOST_PORT (matches
# CI, conftest, and docker-compose.prod.yml). Override to e.g. 5433 when
# host 5432 is shadowed by another Postgres (e.g. a WSL relay).
- "${POSTGRES_HOST_PORT:-5432}:5432"
healthcheck:
test:
[
"CMD",
"pg_isready",
"-U",
"supabase_admin",
"-h",
"localhost"
]
interval: 5s
timeout: 5s
retries: 10
start_period: 60s
environment:
POSTGRES_HOST: /var/run/postgresql
PGPORT: ${POSTGRES_PORT}
POSTGRES_PORT: ${POSTGRES_PORT}
PGPASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
PGDATABASE: ${POSTGRES_DB}
POSTGRES_DB: ${POSTGRES_DB}
JWT_SECRET: ${JWT_SECRET}
JWT_EXP: ${JWT_EXPIRY}
command:
[
"postgres",
"-c",
"config_file=/etc/postgresql/postgresql.conf",
"-c",
"log_min_messages=fatal"
]
swagger-ui:
image: ghcr.io/salk-harnessing-plants-initiative/swaggerapi-swagger-ui:v5.18.2@sha256:bcac11f186d874111edc7ff2db7850eeb6bcbda383b5742a6cfc9b150e80e30d
restart: unless-stopped
networks:
- supanet
ports:
- "${META_PORT}:8080"
environment:
SWAGGER_JSON_URL: http://localhost:8000/rest/v1/?apikey=${ANON_KEY}
BASE_URL: /
DOC_EXPANSION: list
FILTER: true
TRY_IT_OUT_ENABLED: true
supavisor:
image: ghcr.io/salk-harnessing-plants-initiative/supabase-supavisor:2.7.4@sha256:466297ba00956e2d1cbd56a4362fbd5dbc9c2a47ad83e8ca9bdb877f743315ee
restart: unless-stopped
networks:
- supanet
ports:
- ${POOLER_PROXY_PORT_TRANSACTION}:6543
volumes:
- ./volumes/pooler/pooler.exs:/etc/pooler/pooler.exs:ro,z
healthcheck:
test:
[
"CMD",
"curl",
"-sSfL",
"--head",
"-o",
"/dev/null",
"http://127.0.0.1:4000/api/health"
]
interval: 10s
timeout: 5s
retries: 5
start_period: 60s
depends_on:
db-dev:
condition: service_healthy
environment:
PORT: 4000
SUPAVISOR_ENC_KEY: ${SUPAVISOR_ENC_KEY}
POSTGRES_PORT: ${POSTGRES_PORT}
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
DATABASE_URL: ecto://supabase_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/_supabase
CLUSTER_POSTGRES: true
SECRET_KEY_BASE: ${SECRET_KEY_BASE}
VAULT_ENC_KEY: ${VAULT_ENC_KEY}
# Supavisor has no documented JWKS input, so it keeps the symmetric secret
# it used before the asymmetric migration rather than an unreadable blob (#646).
API_JWT_SECRET: ${JWT_SECRET}
METRICS_JWT_SECRET: ${JWT_SECRET}
REGION: local
ERL_AFLAGS: -proto_dist inet_tcp
POOLER_TENANT_ID: ${POOLER_TENANT_ID}
POOLER_DEFAULT_POOL_SIZE: ${POOLER_DEFAULT_POOL_SIZE}
POOLER_MAX_CLIENT_CONN: ${POOLER_MAX_CLIENT_CONN}
POOLER_POOL_MODE: transaction
DB_POOL_SIZE: ${POOLER_DB_POOL_SIZE}
command:
[
"/bin/sh",
"-c",
"/app/bin/migrate && /app/bin/supavisor eval \"$$(cat /etc/pooler/pooler.exs)\" && /app/bin/server"
]
volumes:
db-config:
networks:
supanet:
driver: bridge