The llm container mounted the whole ./data directory read-only just to read one snapshot. Snapshots now get their own directory, published with mkdir -p, and compose mounts only ./data/replica — the primary databases are no longer visible inside the container.
1085 lines
34 KiB
YAML
1085 lines
34 KiB
YAML
networks:
|
|
gateway:
|
|
name: gateway
|
|
driver: bridge
|
|
internal: false
|
|
ipam:
|
|
config:
|
|
- subnet: 192.168.5.0/24
|
|
storage:
|
|
name: storage
|
|
driver: bridge
|
|
internal: true
|
|
ipam:
|
|
config:
|
|
- subnet: 192.168.6.0/24
|
|
data:
|
|
name: data
|
|
driver: bridge
|
|
internal: true
|
|
ipam:
|
|
config:
|
|
- subnet: 192.168.7.0/24
|
|
observability:
|
|
name: observability
|
|
driver: bridge
|
|
internal: true
|
|
ipam:
|
|
config:
|
|
- subnet: 192.168.8.0/24
|
|
ci:
|
|
name: ci
|
|
driver: bridge
|
|
internal: false
|
|
ipam:
|
|
config:
|
|
- subnet: 192.168.9.0/24
|
|
hostpub:
|
|
# Docker cannot publish ports from a container whose networks are all
|
|
# internal. This non-internal bridge exists solely to carry loopback
|
|
# port publishes (e.g. postgres 127.0.0.1:5432 for host-side llm batch
|
|
# runs); no service-to-service traffic should use it.
|
|
name: hostpub
|
|
driver: bridge
|
|
internal: false
|
|
ipam:
|
|
config:
|
|
- subnet: 192.168.10.0/24
|
|
|
|
services:
|
|
coredns:
|
|
image: coredns/coredns:1.12.0
|
|
container_name: coredns
|
|
command: ["-conf", "/etc/coredns/Corefile"]
|
|
networks:
|
|
gateway:
|
|
ipv4_address: 192.168.5.53
|
|
volumes:
|
|
- ./infra/coredns/Corefile:/etc/coredns/Corefile:ro
|
|
- ./infra/coredns/hosts:/etc/coredns/hosts:ro
|
|
labels:
|
|
- "promtail=true"
|
|
restart: unless-stopped
|
|
|
|
traefik:
|
|
image: traefik:v3.3
|
|
container_name: traefik
|
|
networks:
|
|
- gateway
|
|
- observability
|
|
- storage
|
|
- ci
|
|
ports:
|
|
- "80:80"
|
|
- "443:443"
|
|
- "8081:8080"
|
|
volumes:
|
|
- ./infra/traefik/traefik.yml:/etc/traefik/traefik.yml:ro
|
|
- ./infra/traefik/dynamic:/etc/traefik/dynamic:ro
|
|
- ./infra/traefik/certs:/etc/traefik/certs:ro
|
|
- ./infra/traefik/plugins:/plugins-local:ro
|
|
environment:
|
|
- DOMAIN=${DOMAIN:-fhirworx.io}
|
|
- OTEL_SERVICE_NAME=traefik
|
|
- TRAEFIK_ACCESSLOG=true
|
|
- TRAEFIK_ACCESSLOG_FORMAT=json
|
|
- TRAEFIK_ACCESSLOG_FIELDS_DEFAULTMODE=keep
|
|
- TRAEFIK_ACCESSLOG_FIELDS_HEADERS_DEFAULTMODE=keep
|
|
labels:
|
|
- "promtail=true"
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
restart: unless-stopped
|
|
|
|
rustfs:
|
|
image: ${IMAGE_PREFIX:-fhirworx}/rustfs:latest
|
|
pull_policy: if_not_present
|
|
container_name: rustfs
|
|
user: "10001:10001"
|
|
networks:
|
|
- storage
|
|
- ci
|
|
environment:
|
|
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY}
|
|
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY}
|
|
volumes:
|
|
- rustfs_data:/data
|
|
- rustfs_logs:/logs
|
|
labels:
|
|
- "promtail=true"
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
restart: unless-stopped
|
|
|
|
mc:
|
|
image: ${IMAGE_PREFIX:-fhirworx}/mc:${COMMIT_SHA:-latest}
|
|
pull_policy: if_not_present
|
|
container_name: mc
|
|
build:
|
|
context: infra/rustfs
|
|
dockerfile: ../images/mc.Dockerfile
|
|
networks:
|
|
- storage
|
|
environment:
|
|
- MC_HOST_local=http://${RUSTFS_ACCESS_KEY}:${RUSTFS_SECRET_KEY}@rustfs:9000
|
|
user: "10002:10002"
|
|
read_only: true
|
|
tmpfs:
|
|
- /home/mc/.mc:uid=10002,gid=10002
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
cap_drop:
|
|
- ALL
|
|
entrypoint: ["sleep", "infinity"]
|
|
depends_on:
|
|
- rustfs
|
|
restart: unless-stopped
|
|
|
|
postgres:
|
|
# Patched pgvector (AVX-512-free) built FROM the upstream mirror — the
|
|
# stock vector.so SIGILLs this Zen2 host on hnsw/ivfflat index builds (#580).
|
|
image: ${IMAGE_PREFIX:-fhirworx}/postgresql:pgvector
|
|
pull_policy: if_not_present
|
|
build:
|
|
context: .
|
|
dockerfile: infra/images/postgresql.Dockerfile
|
|
args:
|
|
BASE: ${IMAGE_PREFIX:-fhirworx}/postgresql:latest
|
|
container_name: postgres
|
|
networks:
|
|
- storage
|
|
- hostpub
|
|
ports:
|
|
# Loopback-only: host-side llm batch runs (indexer/tagger) need
|
|
# pgvector; everything else still uses the storage net.
|
|
- "127.0.0.1:5432:5432"
|
|
environment:
|
|
- POSTGRESQL_PASSWORD=${POSTGRES_PASSWORD:-changeme}
|
|
- POSTGRESQL_DATABASE=gitea
|
|
volumes:
|
|
- postgres_data:/bitnami/postgresql
|
|
labels:
|
|
- "promtail=true"
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
healthcheck:
|
|
# Gate dependents (git, nessie, polaris, api) on Postgres actually
|
|
# accepting connections, not just the container having started.
|
|
test: ["CMD-SHELL", "pg_isready -U postgres -d gitea"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 30s
|
|
restart: unless-stopped
|
|
|
|
git:
|
|
build:
|
|
context: ./infra/gitea
|
|
dockerfile: Dockerfile
|
|
args:
|
|
GITEA_VERSION: v1.25.4
|
|
# BUILD_TAG bumps `?v=...` on every asset URL → busts browser/CDN
|
|
# cache. Use BUILD_TAG=$(date +%s) when invoking docker compose build,
|
|
# or the Dockerfile defaults to current epoch.
|
|
BUILD_TAG: "${BUILD_TAG:-}"
|
|
image: fhirworx/git:v1.25.4
|
|
container_name: git
|
|
networks:
|
|
- gateway
|
|
- storage
|
|
- ci
|
|
env_file:
|
|
# Written by `stack mail wire-git` (or `stack mail provision`) from
|
|
# the mail droplet's credentials cache. Marked optional so cold-start
|
|
# works before the mail droplet exists; once it does, re-running
|
|
# wire-git populates this file and `docker compose up -d git`
|
|
# picks it up.
|
|
- path: .state/git/mailer.env
|
|
required: false
|
|
environment:
|
|
- GITEA__database__DB_TYPE=postgres
|
|
- GITEA__database__HOST=postgres:5432
|
|
- GITEA__database__NAME=gitea
|
|
- GITEA__database__USER=git
|
|
- GITEA__database__PASSWD=${GITEA_DB_PASSWORD:-git_password}
|
|
- GITEA__storage__STORAGE_TYPE=minio
|
|
- GITEA__storage__MINIO_ENDPOINT=rustfs:9000
|
|
- GITEA__storage__MINIO_ACCESS_KEY_ID=${GITEA_S3_ACCESS_KEY:-git}
|
|
- GITEA__storage__MINIO_SECRET_ACCESS_KEY=${GITEA_S3_SECRET_KEY}
|
|
- GITEA__storage__MINIO_BUCKET=gitea
|
|
- GITEA__storage__MINIO_USE_SSL=false
|
|
- GITEA__lfs__STORAGE_TYPE=minio
|
|
- GITEA__lfs__MINIO_ENDPOINT=rustfs:9000
|
|
- GITEA__lfs__MINIO_ACCESS_KEY_ID=${GITEA_S3_ACCESS_KEY:-git}
|
|
- GITEA__lfs__MINIO_SECRET_ACCESS_KEY=${GITEA_S3_SECRET_KEY}
|
|
- GITEA__lfs__MINIO_BUCKET=gitea-lfs
|
|
- GITEA__lfs__MINIO_USE_SSL=false
|
|
- GITEA__packages__ENABLED=true
|
|
- GITEA__packages__STORAGE_TYPE=minio
|
|
- GITEA__packages__MINIO_ENDPOINT=rustfs:9000
|
|
- GITEA__packages__MINIO_ACCESS_KEY_ID=${GITEA_S3_ACCESS_KEY:-git}
|
|
- GITEA__packages__MINIO_SECRET_ACCESS_KEY=${GITEA_S3_SECRET_KEY}
|
|
- GITEA__packages__MINIO_BUCKET=gitea-packages
|
|
- GITEA__packages__MINIO_USE_SSL=false
|
|
- GITEA__server__DOMAIN=git.${DOMAIN:-fhirworx.io}
|
|
- GITEA__server__ROOT_URL=https://git.${DOMAIN:-fhirworx.io}/
|
|
- GITEA__server__SSH_DOMAIN=git.${DOMAIN:-fhirworx.io}
|
|
- GITEA__webhook__ALLOWED_HOST_LIST=ci.${DOMAIN:-fhirworx.io},${HOST_IP:-192.168.1.192},172.19.0.0/16
|
|
- GITEA__ui__THEMES=fhirworx,fhirworx-dark
|
|
- GITEA__ui__DEFAULT_THEME=fhirworx
|
|
volumes:
|
|
- gitea_data:/var/lib/gitea
|
|
- gitea_config:/etc/gitea
|
|
- ./infra/gitea/custom:/var/lib/gitea/custom
|
|
ports:
|
|
- "2222:2222"
|
|
- "3000:3000"
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
rustfs:
|
|
condition: service_started
|
|
labels:
|
|
- "promtail=true"
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
restart: unless-stopped
|
|
|
|
act-runner:
|
|
image: gitea/act_runner:latest
|
|
container_name: act-runner
|
|
networks:
|
|
- ci
|
|
- storage
|
|
environment:
|
|
- GITEA_INSTANCE_URL=http://git:3000
|
|
- GITEA_RUNNER_REGISTRATION_TOKEN=${ACT_RUNNER_TOKEN}
|
|
- GITEA_RUNNER_NAME=homelab-runner
|
|
- GITEA_RUNNER_LABELS=ubuntu-latest:docker://catthehacker/ubuntu:act-latest
|
|
- CONFIG_FILE=/config.yaml
|
|
volumes:
|
|
- ${DOCKER_SOCK:-/run/user/1000/docker.sock}:/var/run/docker.sock
|
|
- act_runner_data:/data
|
|
- ./infra/act-runner/config.yaml:/config.yaml:ro
|
|
depends_on:
|
|
- git
|
|
labels:
|
|
- "promtail=true"
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
restart: unless-stopped
|
|
|
|
notebooks:
|
|
image: ${IMAGE_PREFIX:-fhirworx}/notebooks:${HEAVY_TAG:-latest}
|
|
pull_policy: if_not_present
|
|
build:
|
|
context: .
|
|
dockerfile: infra/images/notebooks.Dockerfile
|
|
container_name: notebooks
|
|
networks:
|
|
- gateway
|
|
- data
|
|
- storage
|
|
environment:
|
|
- PYTHONPATH=/home/kert/src
|
|
- RUSTFS_ENDPOINT=http://rustfs:9000
|
|
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY}
|
|
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY}
|
|
# Read-only DuckLake catalog access — conf.connect.ducklake()
|
|
# reads reference data from the lake (M5, #514).
|
|
- DUCKLAKE_RO_PASSWORD=${DUCKLAKE_RO_PASSWORD}
|
|
- NESSIE_S3_ACCESS_KEY=${NESSIE_S3_ACCESS_KEY}
|
|
- NESSIE_S3_SECRET_KEY=${NESSIE_S3_SECRET_KEY}
|
|
- POLARIS_ROOT_SECRET=${POLARIS_ROOT_SECRET}
|
|
volumes:
|
|
- ./notebooks:/home/kert/notebooks
|
|
# Mount only user-editable config files, not the whole infra/marimo
|
|
# directory — the theme and source lives inside the baked image now.
|
|
- ./infra/marimo/marimo.toml:/home/kert/.config/marimo/marimo.toml:ro
|
|
- ./infra/marimo/snippets:/home/kert/.config/marimo/snippets:ro
|
|
- ./data:/home/kert/data
|
|
- ./data/zotero/data:/home/kert/zotero:ro
|
|
- ./src:/home/kert/src:ro
|
|
- ./assets:/home/kert/assets:ro
|
|
- ./stack.toml:/home/kert/stack.toml:ro
|
|
deploy:
|
|
resources:
|
|
reservations:
|
|
devices:
|
|
- driver: nvidia
|
|
count: all
|
|
capabilities: [gpu]
|
|
shm_size: "1g"
|
|
labels:
|
|
- "promtail=true"
|
|
restart: unless-stopped
|
|
|
|
ollama:
|
|
image: ollama/ollama:latest
|
|
container_name: ollama
|
|
networks:
|
|
- data
|
|
- hostpub # port publish requires a non-internal net (data is internal)
|
|
ports:
|
|
# Host + LAN: the rack's Ollama is one endpoint in the multi-host
|
|
# pool (rig 4090 / laptop 5080 serve the same API on the LAN).
|
|
- "11434:11434"
|
|
environment:
|
|
# Unload models after idle so the shared 3060 frees VRAM for
|
|
# notebooks/zotero.
|
|
- OLLAMA_KEEP_ALIVE=5m
|
|
volumes:
|
|
- ollama_models:/root/.ollama
|
|
deploy:
|
|
resources:
|
|
reservations:
|
|
devices:
|
|
- driver: nvidia
|
|
count: 1
|
|
capabilities: [gpu]
|
|
healthcheck:
|
|
test: ["CMD", "ollama", "ls"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 3
|
|
start_period: 30s
|
|
labels:
|
|
- "promtail=true"
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
restart: unless-stopped
|
|
|
|
# Watches the notebooks service for errors (container logs via the
|
|
# docker socket + __marimo__/session snapshots) and files deduplicated,
|
|
# auto-closing Gitea issues via dev/scripts/nb_issue_filer.py. See
|
|
# docs/superpowers/specs/2026-07-09-notebook-quality-gates-design.md.
|
|
nb-watcher:
|
|
image: python:3.13-alpine
|
|
container_name: nb-watcher
|
|
# `gateway` to reach git:3000 for the issue API.
|
|
networks:
|
|
- gateway
|
|
environment:
|
|
- PYTHONUNBUFFERED=1
|
|
- GITEA_TOKEN=${GITEA_TOKEN}
|
|
- GITEA_API_BASE=http://git:3000/api/v1
|
|
- NB_POLL_S=${NB_POLL_S:-60}
|
|
volumes:
|
|
- ./dev/scripts:/scripts:ro
|
|
- ./notebooks:/notebooks:ro
|
|
- ./.state:/state
|
|
- ${DOCKER_SOCK:-/run/user/1000/docker.sock}:/var/run/docker.sock:ro
|
|
command: python /scripts/nb_watcher.py
|
|
depends_on:
|
|
notebooks:
|
|
condition: service_started
|
|
healthcheck:
|
|
test:
|
|
- "CMD-SHELL"
|
|
- "test -f /tmp/heartbeat && test $$(( $$(date +%s) - $$(stat -c %Y /tmp/heartbeat) )) -lt $$(( $${NB_POLL_S:-60} * 5 ))"
|
|
interval: 60s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 120s
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
labels:
|
|
- "promtail=true"
|
|
restart: unless-stopped
|
|
|
|
zotero:
|
|
image: ${IMAGE_PREFIX:-fhirworx}/zotero:${HEAVY_TAG:-latest}
|
|
pull_policy: if_not_present
|
|
build:
|
|
context: .
|
|
dockerfile: infra/images/zotero.Dockerfile
|
|
container_name: zotero
|
|
networks:
|
|
- gateway
|
|
runtime: nvidia
|
|
stdin_open: true
|
|
tty: true
|
|
ports:
|
|
- "3478:3478"
|
|
- "3478:3478/udp"
|
|
volumes:
|
|
- ./data/zotero/data:/home/ubuntu/Zotero
|
|
- ./data/zotero/profiles/zotero:/home/ubuntu/.zotero
|
|
- ./data/zotero/profiles/mozilla:/home/ubuntu/.mozilla
|
|
- ./data:/home/ubuntu/data
|
|
tmpfs:
|
|
- /dev/shm:rw
|
|
environment:
|
|
- TZ=UTC
|
|
- DISPLAY_SIZEW=1920
|
|
- DISPLAY_SIZEH=1080
|
|
- DISPLAY_REFRESH=60
|
|
- DISPLAY_DPI=96
|
|
- DISPLAY_CDEPTH=24
|
|
- PASSWD=zotero
|
|
- KASMVNC_ENABLE=true
|
|
- SELKIES_ENABLE_BASIC_AUTH=false
|
|
# Skip the entrypoint's `dig @ns1.google.com TXT o-o.myaddr.l.google.com`
|
|
# public-IP probe — on networks where the resolver returns an
|
|
# edns0-client-subnet hint instead of a bare IP, the value lands in
|
|
# network.udp.public_ip and kasmvnc rejects it as invalid, leaving
|
|
# the UI in a FATAL restart loop. "auto" tells kasmvnc to figure it
|
|
# out itself.
|
|
- TURN_EXTERNAL_IP=auto
|
|
# OTLP tracing of the desktop-bridge lifecycle is OFF by default. To
|
|
# enable: add `observability` to `networks` below and uncomment the
|
|
# endpoint. Left off deliberately — zotero is a GPU desktop kept on
|
|
# `gateway` only; attaching it to `observability` widens its blast
|
|
# radius. The structured Loki logs + Grafana alert cover the same
|
|
# failure without crossing that isolation boundary.
|
|
# - OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
|
|
deploy:
|
|
resources:
|
|
reservations:
|
|
devices:
|
|
- driver: nvidia
|
|
count: 1
|
|
capabilities: [gpu]
|
|
# Detects the blank-desktop failure (dead :20->:21 kasmxproxy bridge) that
|
|
# container/nginx/Xvnc liveness all miss. The image bakes an equivalent
|
|
# HEALTHCHECK; this override tunes timing for the GPU desktop's slow boot
|
|
# (first start may install the NVIDIA userspace driver).
|
|
healthcheck:
|
|
test: ["CMD", "/usr/local/bin/zotero-healthcheck"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 5
|
|
start_period: 180s
|
|
labels:
|
|
- "promtail=true"
|
|
restart: unless-stopped
|
|
|
|
webdav:
|
|
image: rclone/rclone:latest
|
|
container_name: webdav
|
|
networks:
|
|
- storage
|
|
- gateway
|
|
environment:
|
|
- RCLONE_CONFIG_S3_TYPE=s3
|
|
- RCLONE_CONFIG_S3_PROVIDER=Minio
|
|
- RCLONE_CONFIG_S3_ACCESS_KEY_ID=${RUSTFS_ACCESS_KEY}
|
|
- RCLONE_CONFIG_S3_SECRET_ACCESS_KEY=${RUSTFS_SECRET_KEY}
|
|
- RCLONE_CONFIG_S3_ENDPOINT=http://rustfs:9000
|
|
- RCLONE_CONFIG_S3_FORCE_PATH_STYLE=true
|
|
command:
|
|
- serve
|
|
- webdav
|
|
- "s3:zotero"
|
|
- --addr=:8080
|
|
- --user=${WEBDAV_USER:-zotero}
|
|
- --pass=${WEBDAV_PASS}
|
|
- --vfs-cache-mode=full
|
|
tmpfs:
|
|
- /tmp/rclone-cache
|
|
labels:
|
|
- "promtail=true"
|
|
restart: unless-stopped
|
|
depends_on:
|
|
- rustfs
|
|
|
|
nessie:
|
|
image: ghcr.io/projectnessie/nessie:latest
|
|
container_name: nessie
|
|
networks:
|
|
- gateway
|
|
- storage
|
|
- data
|
|
- observability
|
|
environment:
|
|
- NESSIE_VERSION_STORE_TYPE=JDBC
|
|
- QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://postgres:5432/nessie
|
|
- QUARKUS_DATASOURCE_USERNAME=nessie
|
|
- QUARKUS_DATASOURCE_PASSWORD=${NESSIE_DB_PASSWORD}
|
|
# Iceberg REST Catalog settings.
|
|
# default-warehouse is a NAME that must reference a defined
|
|
# warehouse; pointing it straight at the s3 URI leaves the REST
|
|
# endpoint 500ing with "Default warehouse ... is not defined".
|
|
- NESSIE_CATALOG_DEFAULT_WAREHOUSE=warehouse
|
|
- nessie.catalog.warehouses.warehouse.location=${S3_WAREHOUSE:-s3://lakehouse/}
|
|
- NESSIE_CATALOG_SERVICE_S3_DEFAULT_OPTIONS_ENDPOINT=${S3_ENDPOINT:-http://rustfs:9000}
|
|
# S3 credentials go through Nessie's secrets manager: the option
|
|
# takes a URN referencing quarkus config keys with .name/.secret.
|
|
# (The former ACCESS_KEY_ID/SECRET_ACCESS_KEY env style is ignored
|
|
# by current Nessie — "Missing access key and secret for STATIC
|
|
# authentication mode".)
|
|
- nessie.catalog.service.s3.default-options.access-key=urn:nessie-secret:quarkus:s3creds
|
|
- s3creds.name=${NESSIE_S3_ACCESS_KEY}
|
|
- s3creds.secret=${NESSIE_S3_SECRET_KEY}
|
|
- NESSIE_CATALOG_SERVICE_S3_DEFAULT_OPTIONS_PATH_STYLE_ACCESS=true
|
|
- NESSIE_CATALOG_SERVICE_S3_DEFAULT_OPTIONS_REGION=${S3_REGION:-us-east-1}
|
|
# OpenTelemetry tracing
|
|
- QUARKUS_OTEL_ENABLED=true
|
|
- QUARKUS_OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://otel-collector:4317
|
|
- QUARKUS_OTEL_SERVICE_NAME=nessie
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
rustfs:
|
|
condition: service_started
|
|
labels:
|
|
- "promtail=true"
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
restart: unless-stopped
|
|
|
|
trino:
|
|
image: trinodb/trino:latest
|
|
container_name: trino
|
|
networks:
|
|
- gateway
|
|
- storage
|
|
- data
|
|
- observability
|
|
environment:
|
|
- NESSIE_S3_ACCESS_KEY=${NESSIE_S3_ACCESS_KEY}
|
|
- NESSIE_S3_SECRET_KEY=${NESSIE_S3_SECRET_KEY}
|
|
- NESSIE_API_URI=${NESSIE_API_URI:-http://nessie:19120/api/v2}
|
|
- S3_ENDPOINT=${S3_ENDPOINT:-http://rustfs:9000}
|
|
- S3_REGION=${S3_REGION:-us-east-1}
|
|
- S3_WAREHOUSE=${S3_WAREHOUSE:-s3://lakehouse/}
|
|
volumes:
|
|
- ./infra/trino/etc:/etc/trino:ro
|
|
depends_on:
|
|
- nessie
|
|
- rustfs
|
|
labels:
|
|
- "promtail=true"
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
restart: unless-stopped
|
|
|
|
polaris:
|
|
image: apache/polaris:latest
|
|
container_name: polaris
|
|
networks:
|
|
- gateway
|
|
- storage
|
|
- data
|
|
- observability
|
|
environment:
|
|
# PostgreSQL backend
|
|
- POLARIS_PERSISTENCE_TYPE=relational-jdbc
|
|
- QUARKUS_DATASOURCE_DB_KIND=postgresql
|
|
- QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://postgres:5432/polaris
|
|
- QUARKUS_DATASOURCE_USERNAME=polaris
|
|
- QUARKUS_DATASOURCE_PASSWORD=${POLARIS_DB_PASSWORD}
|
|
# Enable Flyway migrations for schema creation
|
|
- QUARKUS_FLYWAY_MIGRATE_AT_START=true
|
|
# Bootstrap credentials: realm,clientId,clientSecret
|
|
- POLARIS_BOOTSTRAP_CREDENTIALS=default-realm,root,${POLARIS_ROOT_SECRET}
|
|
# S3 storage defaults
|
|
- AWS_ACCESS_KEY_ID=${POLARIS_S3_ACCESS_KEY}
|
|
- AWS_SECRET_ACCESS_KEY=${POLARIS_S3_SECRET_KEY}
|
|
- AWS_REGION=${S3_REGION:-us-east-1}
|
|
# OpenTelemetry
|
|
- QUARKUS_OTEL_ENABLED=true
|
|
- QUARKUS_OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://otel-collector:4317
|
|
- QUARKUS_OTEL_SERVICE_NAME=polaris
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
rustfs:
|
|
condition: service_started
|
|
labels:
|
|
- "promtail=true"
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
restart: unless-stopped
|
|
|
|
dashboard:
|
|
image: nginx:alpine
|
|
container_name: dashboard
|
|
networks:
|
|
- gateway
|
|
volumes:
|
|
- ./infra/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
|
- ./infra/nginx/index.html:/usr/share/nginx/html/index.html:ro
|
|
- ./assets/css/dashboard.css:/usr/share/nginx/html/dashboard.css:ro
|
|
- ./assets/css/inject.css:/usr/share/nginx/html/fhirworx.css:ro
|
|
- ./assets/icons/fav32.png:/usr/share/nginx/html/fav32.png:ro
|
|
labels:
|
|
- "promtail=true"
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
restart: unless-stopped
|
|
|
|
docs:
|
|
image: ${IMAGE_PREFIX:-fhirworx}/docs:${COMMIT_SHA:-latest}
|
|
pull_policy: if_not_present
|
|
build:
|
|
context: .
|
|
dockerfile: infra/images/docs.Dockerfile
|
|
container_name: docs
|
|
networks:
|
|
- gateway
|
|
labels:
|
|
- "promtail=true"
|
|
restart: unless-stopped
|
|
|
|
api:
|
|
image: ${IMAGE_PREFIX:-fhirworx}/api:${COMMIT_SHA:-latest}
|
|
pull_policy: if_not_present
|
|
build:
|
|
context: .
|
|
dockerfile: infra/images/api.Dockerfile
|
|
container_name: api
|
|
networks:
|
|
- gateway
|
|
- storage
|
|
- data
|
|
- observability
|
|
environment:
|
|
- STACK_API_SECRET=${STACK_API_SECRET}
|
|
- GITEA_TOKEN=${GITEA_TOKEN}
|
|
# Postgres
|
|
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-changeme}
|
|
# S3 / RustFS
|
|
- NESSIE_S3_ACCESS_KEY=${NESSIE_S3_ACCESS_KEY}
|
|
- NESSIE_S3_SECRET_KEY=${NESSIE_S3_SECRET_KEY}
|
|
- S3_ENDPOINT=${S3_ENDPOINT:-http://rustfs:9000}
|
|
- S3_REGION=${S3_REGION:-us-east-1}
|
|
- S3_WAREHOUSE=${S3_WAREHOUSE:-s3://lakehouse/}
|
|
# OpenTelemetry
|
|
- OTEL_SERVICE_NAME=api
|
|
- OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
|
|
volumes:
|
|
- ./data:/app/data
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
rustfs:
|
|
condition: service_started
|
|
nessie:
|
|
condition: service_started
|
|
labels:
|
|
- "promtail=true"
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
restart: unless-stopped
|
|
|
|
# SSO-guarded RAG chat UI (P34). Routed at llm.fhirworx.io via Traefik's
|
|
# git-sso middleware ($reef in infra/traefik/dynamic/services.yml). Joins
|
|
# gateway (Traefik), storage (postgres:5432 / pgvector), and data
|
|
# (ollama:11434). Query-only — the batch indexer runs on the host.
|
|
llm:
|
|
image: ${IMAGE_PREFIX:-fhirworx}/llm:${COMMIT_SHA:-latest}
|
|
pull_policy: if_not_present
|
|
build:
|
|
context: .
|
|
dockerfile: infra/images/llm.Dockerfile
|
|
container_name: llm
|
|
networks:
|
|
- gateway
|
|
- storage
|
|
- data
|
|
- observability
|
|
volumes:
|
|
- ./data/replica:/app/data/replica:ro # DuckDB read replica only (directory mount survives replica re-publish)
|
|
environment:
|
|
- LLM_OLLAMA_HOSTS=${LLM_OLLAMA_HOSTS_IN_CONTAINER:-http://ollama:11434}
|
|
- LLM_PG_HOST=postgres
|
|
- LLM_DB_PASSWORD=${LLM_DB_PASSWORD}
|
|
- LLM_DUCKDB_REPLICA=/app/data/replica/aco.ro.duckdb
|
|
- OTEL_SERVICE_NAME=llm
|
|
- OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
ollama:
|
|
condition: service_started
|
|
labels:
|
|
- "promtail=true"
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
restart: unless-stopped
|
|
|
|
# IMAP → bib poller. Pulls UNSEEN mail from `cmsupdates@mail.fhirworx.io`
|
|
# every MAIL_POLL_INTERVAL seconds and upserts each as a Source item.
|
|
# Idempotent (server-side `\Seen` flag), so frequency is purely a
|
|
# latency knob — 600s = ~10min average from arrival → bib.
|
|
mail-poller:
|
|
image: ${IMAGE_PREFIX:-fhirworx}/api:${COMMIT_SHA:-latest}
|
|
pull_policy: if_not_present
|
|
container_name: mail-poller
|
|
# Needs `gateway` for outbound DNS + IMAPS to mail.fhirworx.io;
|
|
# `data` to share bib.sqlite with the api/lake services.
|
|
networks:
|
|
- gateway
|
|
- data
|
|
environment:
|
|
- MAIL_POLL_INTERVAL=${MAIL_POLL_INTERVAL:-600}
|
|
volumes:
|
|
- ./data:/app/data
|
|
- ./.state:/app/.state
|
|
# Mount source live so adding mailboxes / tweaking the poller
|
|
# doesn't require an image rebuild — the api image's baked venv
|
|
# provides interpreter + deps; --no-sync keeps `uv run` from
|
|
# going back to pypi.
|
|
- ./src:/app/src:ro
|
|
- ./pyproject.toml:/app/pyproject.toml:ro
|
|
command: >
|
|
sh -c 'while true; do
|
|
uv run --no-sync stack bib ingest-mail || true;
|
|
touch /tmp/heartbeat;
|
|
sleep $${MAIL_POLL_INTERVAL};
|
|
done'
|
|
# Override the inherited HEALTHCHECK from the api image (which
|
|
# probes http://localhost:8000/health — wrong for this loop-only
|
|
# container). Mark healthy when the poll loop has completed an
|
|
# iteration within the last 2 * MAIL_POLL_INTERVAL seconds.
|
|
healthcheck:
|
|
test:
|
|
- "CMD-SHELL"
|
|
- "test -f /tmp/heartbeat && test $$(( $$(date +%s) - $$(stat -c %Y /tmp/heartbeat) )) -lt $$(( $${MAIL_POLL_INTERVAL:-600} * 2 ))"
|
|
interval: 60s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 120s
|
|
labels:
|
|
- "promtail=true"
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
restart: unless-stopped
|
|
|
|
# Observability Stack
|
|
loki:
|
|
image: grafana/loki:latest
|
|
container_name: loki
|
|
networks:
|
|
- observability
|
|
volumes:
|
|
- ./infra/loki/loki-config.yml:/etc/loki/local-config.yaml:ro
|
|
- loki_data:/loki
|
|
command: -config.file=/etc/loki/local-config.yaml
|
|
healthcheck:
|
|
test: ["CMD", "/usr/bin/loki", "--version"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 5
|
|
start_period: 60s
|
|
labels:
|
|
- "promtail=true"
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
restart: unless-stopped
|
|
|
|
tempo:
|
|
image: grafana/tempo:2.7.2
|
|
container_name: tempo
|
|
networks:
|
|
- observability
|
|
volumes:
|
|
- ./infra/tempo/tempo.yml:/etc/tempo/config.yaml:ro
|
|
- tempo_data:/var/tempo
|
|
command: -config.file=/etc/tempo/config.yaml
|
|
healthcheck:
|
|
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3200/ready"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 30s
|
|
labels:
|
|
- "promtail=true"
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
restart: unless-stopped
|
|
|
|
nvidia-exporter:
|
|
image: nvcr.io/nvidia/k8s/dcgm-exporter:3.3.9-3.6.1-ubuntu22.04
|
|
# Prefixed: corwins.media compose also defines a "nvidia-exporter"
|
|
# container; Docker container names are global. Service DNS
|
|
# ("nvidia-exporter:9400") still resolves on the observability net.
|
|
container_name: stack-nvidia-exporter
|
|
networks:
|
|
- observability
|
|
runtime: nvidia
|
|
environment:
|
|
- DCGM_EXPORTER_NO_HOSTNAME=1
|
|
deploy:
|
|
resources:
|
|
reservations:
|
|
devices:
|
|
- driver: nvidia
|
|
count: 1
|
|
capabilities: [gpu]
|
|
cap_add:
|
|
- SYS_ADMIN
|
|
labels:
|
|
- "promtail=true"
|
|
restart: unless-stopped
|
|
|
|
promtail:
|
|
image: grafana/promtail:latest
|
|
container_name: promtail
|
|
networks:
|
|
- observability
|
|
volumes:
|
|
- ./infra/loki/promtail-config.yml:/etc/promtail/config.yml:ro
|
|
- ${DOCKER_SOCK:-/run/user/1000/docker.sock}:/var/run/docker.sock:ro
|
|
command: -config.file=/etc/promtail/config.yml
|
|
depends_on:
|
|
- loki
|
|
group_add:
|
|
- "${DOCKER_GID:-985}"
|
|
labels:
|
|
- "promtail=true"
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
restart: unless-stopped
|
|
|
|
otel-collector:
|
|
image: otel/opentelemetry-collector-contrib:latest
|
|
container_name: otel-collector
|
|
networks:
|
|
- observability
|
|
volumes:
|
|
- ./infra/otel/otel-collector.yml:/etc/otelcol-contrib/config.yaml:ro
|
|
depends_on:
|
|
- tempo
|
|
- loki
|
|
healthcheck:
|
|
test:
|
|
[
|
|
"CMD",
|
|
"/otelcol-contrib",
|
|
"validate",
|
|
"--config",
|
|
"file:/etc/otelcol-contrib/config.yaml",
|
|
]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 10s
|
|
labels:
|
|
- "promtail=true"
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
restart: unless-stopped
|
|
|
|
prometheus:
|
|
image: prom/prometheus:latest
|
|
container_name: prometheus
|
|
networks:
|
|
- gateway
|
|
- observability
|
|
volumes:
|
|
- ./infra/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
|
- ./infra/prometheus/targets:/etc/prometheus/targets:ro
|
|
- prometheus_data:/prometheus
|
|
command:
|
|
- "--config.file=/etc/prometheus/prometheus.yml"
|
|
- "--storage.tsdb.path=/prometheus"
|
|
- "--web.enable-lifecycle"
|
|
healthcheck:
|
|
test:
|
|
[
|
|
"CMD-SHELL",
|
|
"wget --no-verbose --tries=1 --spider http://localhost:9090/-/healthy || exit 1",
|
|
]
|
|
interval: 15s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 15s
|
|
labels:
|
|
- "promtail=true"
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
restart: unless-stopped
|
|
|
|
grafana:
|
|
image: grafana/grafana:latest
|
|
container_name: grafana
|
|
networks:
|
|
- gateway
|
|
- observability
|
|
# Theme injection: Grafana 12 is a heavy SPA, and Traefik's
|
|
# rewrite-body fires on the upstream HTML response but Grafana
|
|
# re-renders the head client-side. Patching index.html in place at
|
|
# startup guarantees the <link> survives. Runs as root so the patch
|
|
# can write the root-owned template; grafana-server itself doesn't
|
|
# require a specific UID.
|
|
user: "0:0"
|
|
entrypoint:
|
|
- /bin/sh
|
|
- -c
|
|
- |
|
|
# Inject base palette + grafana-specific overlay. Order matters:
|
|
# fhirworx.css first (palette), grafana.css second (overrides).
|
|
grep -q grafana-fhirworx.css /usr/share/grafana/public/views/index.html || \
|
|
sed -i 's|</head>|<link rel="stylesheet" type="text/css" href="/public/fhirworx.css"><link rel="stylesheet" type="text/css" href="/public/grafana-fhirworx.css"></head>|' /usr/share/grafana/public/views/index.html
|
|
exec /run.sh
|
|
env_file:
|
|
- path: .state/gitea/grafana.env
|
|
required: false
|
|
environment:
|
|
- GF_SECURITY_ADMIN_USER=admin
|
|
- GF_SECURITY_ADMIN_PASSWORD=${GF_ADMIN_PASSWORD:-admin}
|
|
- GF_USERS_ALLOW_SIGN_UP=false
|
|
- GF_AUTH_ANONYMOUS_ENABLED=true
|
|
- GF_AUTH_ANONYMOUS_ORG_ROLE=Viewer
|
|
- GF_SERVER_ROOT_URL=https://grafana.${DOMAIN:-fhirworx.io}
|
|
- GF_AUTH_GENERIC_OAUTH_ENABLED=true
|
|
- GF_AUTH_GENERIC_OAUTH_NAME=Gitea
|
|
- GF_AUTH_GENERIC_OAUTH_SCOPES=openid profile email
|
|
- GF_AUTH_GENERIC_OAUTH_AUTH_URL=https://git.${DOMAIN:-fhirworx.io}/login/oauth/authorize
|
|
- GF_AUTH_GENERIC_OAUTH_TOKEN_URL=http://git:3000/login/oauth/access_token
|
|
- GF_AUTH_GENERIC_OAUTH_API_URL=http://git:3000/api/v1/user
|
|
- GF_AUTH_GENERIC_OAUTH_ALLOW_SIGN_UP=true
|
|
- GF_AUTH_GENERIC_OAUTH_AUTO_LOGIN=false
|
|
- GF_AUTH_OAUTH_ALLOW_INSECURE_EMAIL_LOOKUP=true
|
|
- GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_PATH=is_admin && 'GrafanaAdmin'
|
|
- GF_AUTH_GENERIC_OAUTH_ALLOW_ASSIGN_GRAFANA_ADMIN=true
|
|
# Strip Grafana's stock home-page cruft (news feed, "welcome",
|
|
# tutorials, update nags, telemetry). Our homelab-overview
|
|
# dashboard is the default landing page.
|
|
- GF_NEWS_NEWS_FEED_ENABLED=false
|
|
- GF_ANALYTICS_REPORTING_ENABLED=false
|
|
- GF_ANALYTICS_CHECK_FOR_UPDATES=false
|
|
- GF_ANALYTICS_CHECK_FOR_PLUGIN_UPDATES=false
|
|
- GF_ANALYTICS_FEEDBACK_LINKS_ENABLED=false
|
|
- GF_HELP_ENABLED=false
|
|
- GF_PLUGINS_PUBLIC_KEY_RETRIEVAL_DISABLED=true
|
|
- GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH=/var/lib/grafana/dashboards/homelab-overview.json
|
|
# Light theme is the fhirworx baseline (cream background, deep
|
|
# navy primary). Theme overrides in grafana.css target the
|
|
# light-theme compiled CSS — dark would need a separate pass.
|
|
- GF_USERS_DEFAULT_THEME=light
|
|
volumes:
|
|
- ./infra/grafana/provisioning:/etc/grafana/provisioning:ro
|
|
- ./infra/grafana/dashboards:/var/lib/grafana/dashboards:ro
|
|
- grafana_data:/var/lib/grafana
|
|
- ./assets/icons/favicon.svg:/usr/share/grafana/public/img/grafana_icon.svg:ro
|
|
- ./assets/icons/fav32.png:/usr/share/grafana/public/img/fav32.png:ro
|
|
- ./assets/icons/apple-touch-icon.png:/usr/share/grafana/public/img/apple-touch-icon.png:ro
|
|
- ./assets/css/inject.css:/usr/share/grafana/public/fhirworx.css:ro
|
|
- ./assets/css/grafana.css:/usr/share/grafana/public/grafana-fhirworx.css:ro
|
|
depends_on:
|
|
- loki
|
|
- tempo
|
|
- prometheus
|
|
healthcheck:
|
|
test:
|
|
[
|
|
"CMD-SHELL",
|
|
"wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1",
|
|
]
|
|
interval: 15s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 30s
|
|
labels:
|
|
- "promtail=true"
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
restart: unless-stopped
|
|
|
|
# wire — ephemeral bootstrap container. Creates Gitea admin, OAuth2 app,
|
|
# oauth2-proxy credentials, tunnel config, DNS records. Idempotent.
|
|
# docker compose run --rm wire
|
|
wire:
|
|
image: ghcr.io/astral-sh/uv:python3.13-bookworm-slim
|
|
container_name: wire
|
|
networks:
|
|
- gateway
|
|
- storage
|
|
working_dir: /app
|
|
env_file: .env
|
|
volumes:
|
|
- .:/app
|
|
- ${DOCKER_SOCK:-/run/user/1000/docker.sock}:/var/run/docker.sock:ro
|
|
environment:
|
|
- UV_PROJECT_ENVIRONMENT=/tmp/.venv
|
|
entrypoint: ["uv", "run", "python", "dev/scripts/bootstrap_sso.py"]
|
|
profiles: ["tools"]
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
|
|
auth-handler:
|
|
image: nginx:alpine
|
|
container_name: auth-handler
|
|
networks:
|
|
- gateway
|
|
volumes:
|
|
- ./infra/oauth2-proxy/auth-handler.conf:/etc/nginx/conf.d/default.conf:ro
|
|
depends_on:
|
|
- oauth2-proxy
|
|
labels:
|
|
- "promtail=true"
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
restart: unless-stopped
|
|
|
|
oauth2-proxy:
|
|
image: quay.io/oauth2-proxy/oauth2-proxy:latest
|
|
container_name: oauth2-proxy
|
|
networks:
|
|
- gateway
|
|
env_file:
|
|
- path: .state/gitea/oauth2-proxy.env
|
|
required: false
|
|
environment:
|
|
- OAUTH2_PROXY_HTTP_ADDRESS=0.0.0.0:4180
|
|
- OAUTH2_PROXY_PROVIDER=oidc
|
|
- OAUTH2_PROXY_PROVIDER_DISPLAY_NAME=fhirworx
|
|
- OAUTH2_PROXY_OIDC_ISSUER_URL=https://git.${DOMAIN:-fhirworx.io}/
|
|
- OAUTH2_PROXY_SKIP_OIDC_DISCOVERY=true
|
|
- OAUTH2_PROXY_LOGIN_URL=https://git.${DOMAIN:-fhirworx.io}/login/oauth/authorize
|
|
- OAUTH2_PROXY_REDEEM_URL=http://git:3000/login/oauth/access_token
|
|
- OAUTH2_PROXY_OIDC_JWKS_URL=http://git:3000/login/oauth/keys
|
|
- OAUTH2_PROXY_INSECURE_OIDC_SKIP_ISSUER_VERIFICATION=true
|
|
- OAUTH2_PROXY_REDIRECT_URL=https://auth.${DOMAIN:-fhirworx.io}/oauth2/callback
|
|
- OAUTH2_PROXY_COOKIE_DOMAINS=.${DOMAIN:-fhirworx.io}
|
|
- OAUTH2_PROXY_WHITELIST_DOMAINS=.${DOMAIN:-fhirworx.io}
|
|
- OAUTH2_PROXY_COOKIE_SECURE=true
|
|
- OAUTH2_PROXY_COOKIE_SAMESITE=lax
|
|
- OAUTH2_PROXY_COOKIE_NAME=_fhirworx_auth
|
|
- OAUTH2_PROXY_SET_XAUTHREQUEST=true
|
|
- OAUTH2_PROXY_REVERSE_PROXY=true
|
|
- OAUTH2_PROXY_EMAIL_DOMAINS=*
|
|
- OAUTH2_PROXY_SKIP_PROVIDER_BUTTON=true
|
|
- OAUTH2_PROXY_CUSTOM_SIGN_IN_LOGO=-
|
|
depends_on:
|
|
- git
|
|
healthcheck:
|
|
test: ["CMD", "oauth2-proxy", "--version"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 15s
|
|
labels:
|
|
- "promtail=true"
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
restart: unless-stopped
|
|
|
|
cloudflared:
|
|
image: cloudflare/cloudflared:latest
|
|
container_name: cloudflared
|
|
command: tunnel --config /home/nonroot/.cloudflared/config.yml run
|
|
networks:
|
|
- gateway
|
|
volumes:
|
|
- ./infra/cloudflared:/home/nonroot/.cloudflared:ro
|
|
labels:
|
|
- "promtail=true"
|
|
restart: unless-stopped
|
|
|
|
volumes:
|
|
postgres_data:
|
|
rustfs_data:
|
|
rustfs_logs:
|
|
gitea_data:
|
|
gitea_config:
|
|
act_runner_data:
|
|
loki_data:
|
|
prometheus_data:
|
|
tempo_data:
|
|
grafana_data:
|
|
ollama_models:
|