Files
SpecterOps-Nemesis/compose.yaml
T
Will Schroeder 79682c8aa9 Titus scanner + EVTX parsing (#105)
* Conversion of NoseyParker scanner to Titus

- Old `noseyparker_scanner` project removed, new `titus_scaner` Golang project built
- Replaced `noseyparker` references with `titus` as appropriate
- Bumped various built-in docker image versions (Dapr, Gotenberg, etc.)

* Replace ZIP-only extraction with Titus library archive support

Replace custom ZIP extraction code with Titus library's enum.ExtractText()
to support scanning secrets inside zip, jar, war, ear, apk, ipa, xpi, crx,
tar, tar.gz/tgz, and 7z archives. Document formats (xlsx, docx, pdf, etc.)
are intentionally excluded since Nemesis handles those via document_conversion.

- Add original_path to TitusInput so Titus can dispatch by file extension
  (MinIO stores files by UUID without extensions)
- Add magic-byte fallback detection for ZIP, 7z, gzip, and tar
- Preserve git-repo-in-ZIP detection as a special case before archive scan
- Rename env vars: DECOMPRESS_ZIPS -> EXTRACT_ARCHIVES,
  MAX_EXTRACT_SIZE_MB -> EXTRACT_MAX_TOTAL_SIZE_MB, and add
  EXTRACT_MAX_FILE_SIZE_MB and EXTRACT_MAX_DEPTH
- Add unit tests for archive detection and config loading

* Integrated (optional) Titus secret validation

- Integrated (optional) Titus secret validation that can be enabled

* Added Titus rule ID display and ability to disable specific rules

- Added Titus rule ID display and ability to disable specific built-in rules (by ID)

* `titus_scanner` fixes

- One scanner per concurrent file to prevent timeouts
- Fix custom rule regexes to prevent timeouts
- Exclude a few default run-away rules causing timeouts
- Bumped cores for titus scanner to 2

* Enable Vectorscan/Hyperscan acceleration for titus_scanner

Switch from the pure-Go regexp2 engine to the Vectorscan (Hyperscan)
SIMD-accelerated engine, using a fork with parallelized regexp2
confirmation and content-based deduplication

Key changes:
- Dockerfile: install libvectorscan-dev/libvectorscan5, build with
  CGO_ENABLED=1 and -tags vectorscan
- go.mod: replace praetorian-inc/titus with HarmJ0y/titus fork that
  parallelizes the regexp2 confirmation loop and uses content-based
  dedup (matching portable engine behavior)
- Add bounded-concurrency semaphore in handler to prevent OOM from
  unbounded goroutines while still returning 200 to Dapr immediately
- Add content-based deduplication, match count cap, snippet truncation,
  and payload size logging to prevent oversized publishes
- Replace O(N*M) line/column computation with precomputed line index
  using binary search
- Add GOMEMLIMIT and memory limit to container resources

* Update README.md

* remove `noseyparker_scanner` container

* Add EVTX (Windows Event Log) enrichment module

Parses .evtx files and extracts security-relevant data:
- Markdown summary transform with event counts, timeline, unique accounts/IPs
- Downloadable CSV transforms for account changes, group membership changes,
  explicit credential use (4648), process creation (4688), and task changes
- Findings for high-signal events: audit log cleared, new services, admin group
  changes, account changes, explicit creds, scheduled tasks, PS script blocks
- PowerShell 4104 script blocks reassembled and resubmitted to enrichment
  pipeline as child .ps1 files for full Titus/YARA scanning

- Added de-duping for duplicate PS scripts carved from EVTXs (hash-based)

* EVTX mods

- Power timelining done for SYSTEM
- Inbound/outbound auth for SECURITY
- Filtering for default system accounts
- CSV viewer now can download files, transforms for EVTX transformed to the viewer

* Convert titus scanner to Dapr bulk subscribe for batch processing

- Add bulk subscribe types to models (BulkMessagePayload, BulkResponse, etc.)
- Replace async single-event handler with synchronous HandleBulkEvent
- Process batches with bounded concurrency via semaphore + WaitGroup
- Handle edge cases: duplicate entryId, empty objectID/entryId, panic recovery
- Add BulkMaxMessages and BulkMaxAwaitDurationMs config with clamping
- Set app-max-concurrency to 1 (parallelism within work batch via scanner pool)

---------

Co-authored-by: Lee Chagolla-Christensen <lee@localhost>
2026-03-03 15:34:56 -08:00

1234 lines
44 KiB
YAML

name: nemesis
networks:
nemesis:
driver: bridge
volumes:
grafana_data:
jaeger_data:
loki_data:
minio_data:
phoenix_data:
postgres_data:
prometheus_data:
rabbitmq_data:
scheduler_data:
empty:
empty-mounted-containers:
services:
########################################
# Enrichment Services + Dapr sidecars
########################################
web-api:
image: ghcr.io/specterops/nemesis/web-api:latest
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8000/healthz"]
interval: 10s
timeout: 5s
retries: 5
environment:
- APP_ID=web-api
- DAPR_GRPC_PORT=50001
- DAPR_HTTP_PORT=3500
- DEFAULT_EXPIRATION_DAYS=${DEFAULT_EXPIRATION_DAYS:-100}
- LOG_LEVEL=${LOG_LEVEL:-INFO}
volumes:
- ${MOUNTED_CONTAINER_PATH:-empty-mounted-containers}:/mounted-containers
logging: &logging-config
driver: "json-file"
options: { max-size: "10m", max-file: "3" }
depends_on:
postgres: { condition: service_started }
placement: { condition: service_started }
rabbitmq: { condition: service_healthy }
labels:
- "traefik.enable=true"
- "traefik.http.routers.web-api.rule=PathPrefix(`/api`)"
- "traefik.http.services.web-api.loadbalancer.server.port=8000"
- "traefik.http.routers.web-api.entrypoints=websecure"
- "traefik.http.routers.web-api.tls=true"
- "traefik.http.routers.web-api.middlewares=auth"
web-api-dapr:
image: "daprio/daprd:1.16.9"
command:
[
"./daprd",
"--app-id",
"web-api",
"--max-body-size",
"1Gi",
"--app-port",
"8000",
"--dapr-http-port",
"3500",
"--dapr-grpc-port",
"50001",
"--placement-host-address",
"placement:50006",
"--scheduler-host-address",
"scheduler:50007",
"--resources-path",
"/dapr/components",
"--log-level",
"${DAPR_LOG_LEVEL:-info}",
"--config",
"/dapr/configuration/config.yaml",
"--enable-metrics",
"--dapr-graceful-shutdown-seconds",
"5",
]
volumes:
- ./infra/dapr/components/pubsub/files.yaml:/dapr/components/files.yaml:ro
- ./infra/dapr/components/pubsub/workflow_monitor.yaml:/dapr/components/workflow_monitor.yaml:ro
- ./infra/dapr/components/secretstore.yaml:/dapr/components/secretstore.yaml:ro
- ./infra/dapr/configuration/general.yaml:/dapr/configuration/config.yaml:ro
environment:
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD:?}
- MINIO_ROOT_USER=${MINIO_ROOT_USER:?}
- RABBITMQ_CONNECTION_STRING=amqp://${RABBITMQ_USER}:${RABBITMQ_PASSWORD}@rabbitmq:5672
- POSTGRES_USER=${POSTGRES_USER:?}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?}
- POSTGRES_HOST=${POSTGRES_HOST:-postgres}
- POSTGRES_PORT=${POSTGRES_PORT:-5432}
- POSTGRES_DB=${POSTGRES_DB:-enrichment}
- POSTGRES_PARAMETERS=${POSTGRES_PARAMETERS:-sslmode=disable}
- RABBITMQ_PASSWORD=${RABBITMQ_PASSWORD:?}
- RABBITMQ_USER=${RABBITMQ_USER:?}
depends_on:
web-api: { condition: service_started }
placement: { condition: service_started }
scheduler: { condition: service_started }
rabbitmq: { condition: service_healthy }
network_mode: "service:web-api"
titus-scanner:
image: ghcr.io/specterops/nemesis/titus-scanner:latest
depends_on:
minio: { condition: service_healthy }
rabbitmq: { condition: service_healthy }
environment:
- DAPR_HTTP_PORT=3513
- MINIO_ACCESS_KEY=${MINIO_ROOT_USER:?}
- MINIO_BUCKET=files
- MINIO_ENDPOINT=http://minio:9000
- MINIO_SECRET_KEY=${MINIO_ROOT_PASSWORD:?}
- LOG_LEVEL=info
- SNIPPET_LENGTH=${SNIPPET_LENGTH:-512} # context length around any Titus matches
- MAX_CONCURRENT_FILES=${TITUS_MAX_CONCURRENT_FILES:-2} # maximum number of concurrent files to scan
- MAX_MATCHES_PER_FILE=${MAX_MATCHES_PER_FILE:-100} # safety cap on matches per file
- MAX_FILE_SIZE_MB=${MAX_FILE_SIZE_MB:-200} # maximum file size to scan, in megabytes
- GOMEMLIMIT=1500MiB # soft memory limit for Go GC to be aggressive before kernel OOM kills us
- EXTRACT_ARCHIVES=${EXTRACT_ARCHIVES:-true} # extract+scan archive contents (zip, jar, war, ear, apk, tar, tar.gz, 7z)
- EXTRACT_MAX_FILE_SIZE_MB=${EXTRACT_MAX_FILE_SIZE_MB:-10} # max per-file size within archives (MB)
- EXTRACT_MAX_TOTAL_SIZE_MB=${EXTRACT_MAX_TOTAL_SIZE_MB:-1000} # total extraction budget per archive (MB)
- EXTRACT_MAX_DEPTH=${EXTRACT_MAX_DEPTH:-2} # max nesting depth for recursive archives
- ENABLE_VALIDATION=${ENABLE_VALIDATION:-false} # whether to enable credential validation
- VALIDATION_WORKERS=${VALIDATION_WORKERS:-4} # concurrent validation workers
- DISABLED_RULES=${DISABLED_RULES:-np.s3.1,np.s3.2,np.linkedin.3,kingfisher.jira.1} # comma-separated rule IDs to disable (np.s3.x have backtracking-prone patterns that timeout on large files, np.linkedin.3 / kingfisher.jira.1 have a lot of false positives)
- PUBSUB_NAME=titus # Name of the Dapr pubsub resource
- OUTPUT_TOPIC=titus_output # Name of the output topic in Dapr
- BULK_MAX_MESSAGES=${BULK_MAX_MESSAGES:-100} # max messages per bulk subscribe batch
- BULK_MAX_AWAIT_DURATION_MS=${BULK_MAX_AWAIT_DURATION_MS:-1000} # max wait before delivering partial batch (ms)
volumes:
- ./projects/titus_scanner/custom_rules/:/opt/titus:ro
deploy:
resources:
limits:
cpus: "1"
memory: 2G
titus-scanner-dapr:
image: "daprio/daprd:1.16.9"
command:
[
"./daprd",
"--max-body-size",
"1Gi",
"--app-id",
"titus-scanner",
"--app-port",
"8080",
"--app-protocol",
"http",
"--dapr-http-port",
"3513",
"--placement-host-address",
"placement:50006",
"--resources-path",
"/dapr/components",
"--log-level",
"${DAPR_LOG_LEVEL:-info}",
"--config",
"/dapr/configuration/config.yaml",
"--app-max-concurrency",
"1",
"--enable-metrics",
"--dapr-graceful-shutdown-seconds",
"5",
]
environment:
- RABBITMQ_CONNECTION_STRING=amqp://${RABBITMQ_USER}:${RABBITMQ_PASSWORD}@rabbitmq:5672
volumes:
- ./infra/dapr/components/pubsub/titus.yaml:/dapr/components/titus.yaml:ro
- ./infra/dapr/components/secretstore.yaml:/dapr/components/secretstore.yaml:ro
- ./infra/dapr/configuration/general.yaml:/dapr/configuration/config.yaml:ro
depends_on: [titus-scanner, placement]
network_mode: "service:titus-scanner"
dotnet-service:
image: ghcr.io/specterops/nemesis/dotnet-service:latest
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
depends_on:
minio: { condition: service_healthy }
rabbitmq: { condition: service_healthy }
environment:
- DAPR_GRPC_PORT=50014
- DAPR_PORT=3514
- MINIO_ACCESS_KEY=${MINIO_ROOT_USER:?}
- MINIO_BUCKET=files
- MINIO_ENDPOINT=http://minio:9000
- MINIO_SECRET_KEY=${MINIO_ROOT_PASSWORD:?}
- ASPNETCORE_URLS=http://0.0.0.0:5000
expose:
- "5000"
dotnet-service-dapr:
image: "daprio/daprd:1.16.9"
command:
[
"./daprd",
"--max-body-size",
"1Gi",
"--app-id",
"dotnet-service",
"--app-port",
"5000",
"--app-protocol",
"http",
"--dapr-http-port",
"3514",
"--dapr-grpc-port",
"50014",
"--placement-host-address",
"placement:50006",
"--resources-path",
"/dapr/components",
"--log-level",
"${DAPR_LOG_LEVEL:-info}",
"--config",
"/dapr/configuration/config.yaml",
"--app-max-concurrency",
"1",
"--enable-metrics",
"--dapr-graceful-shutdown-seconds",
"5",
]
environment:
- RABBITMQ_CONNECTION_STRING=amqp://${RABBITMQ_USER}:${RABBITMQ_PASSWORD}@rabbitmq:5672
volumes:
- ./infra/dapr/components/pubsub/dotnet.yaml:/dapr/components/dotnet.yaml:ro
- ./infra/dapr/components/secretstore.yaml:/dapr/components/secretstore.yaml:ro
- ./infra/dapr/configuration/general.yaml:/dapr/configuration/config.yaml:ro
depends_on: [dotnet-service, placement]
network_mode: "service:dotnet-service"
file-enrichment: &file-enrichment-template
image: ghcr.io/specterops/nemesis/file-enrichment:latest
healthcheck: &file-enrichment-healthcheck
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8001/healthz"]
interval: 5s
timeout: 5s
retries: 5
volumes: []
# Uncomment the following line to use custom YARA rules (*.yar/*.yara files)
# - ./libs/file_enrichment_modules/yara_rules/prod/:/yara_rules/:ro
environment: &file-enrichment-environment
ENABLE_PII_DETECTION: ${ENABLE_PII_DETECTION:-false}
PII_DETECTION_THRESHOLD: ${PII_DETECTION_THRESHOLD:-0.7}
APP_ID: file-enrichment
DAPR_GRPC_PORT: 50003
DAPR_HTTP_PORT: 3503
LOG_LEVEL: ${LOG_LEVEL:-INFO}
MAX_PARALLEL_WORKFLOWS: ${ENRICHMENT_MAX_PARALLEL_WORKFLOWS:-5}
MAX_WORKFLOW_EXECUTION_TIME: ${MAX_WORKFLOW_EXECUTION_TIME:-300}
NEMESIS_MONITORING: ${NEMESIS_MONITORING:-disabled}
NEMESIS_URL: ${NEMESIS_URL:?}
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: http://otel-collector:4317
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT_INSECURE: true
# PYTHONASYNCIOTHREADPOOLSIZE: 100 # Increase thread pool for asyncio.to_thread()
RIGGING_GENERATOR_CREDENTIALS: ${RIGGING_GENERATOR_CREDENTIALS:-}
RIGGING_GENERATOR_SUMMARY: ${RIGGING_GENERATOR_SUMMARY:-}
RIGGING_GENERATOR_TRIAGE: ${RIGGING_GENERATOR_TRIAGE:-}
WORKFLOW_RUNTIME_LOG_LEVEL: ${WORKFLOW_RUNTIME_LOG_LEVEL:-WARNING}
WORKFLOW_CLIENT_LOG_LEVEL: ${WORKFLOW_CLIENT_LOG_LEVEL:-WARNING}
logging: *logging-config
depends_on: &file-enrichment-depends-on
postgres: { condition: service_started }
placement: { condition: service_started }
scheduler: { condition: service_started }
rabbitmq: { condition: service_healthy }
file-enrichment-dapr: &file-enrichment-dapr-template
image: "daprio/daprd:1.16.9"
command:
[
"./daprd",
"--max-body-size",
"1Gi",
"--app-id",
"file-enrichment",
"--app-port",
"8001",
"--dapr-http-port",
"3503",
"--dapr-grpc-port",
"50003",
"--placement-host-address",
"placement:50006",
"--scheduler-host-address",
"scheduler:50007",
"--log-level",
"${DAPR_LOG_LEVEL:-warn}",
"--resources-path",
"/dapr/components",
"--config",
"/dapr/configuration/file_enrichment_monitoring_${NEMESIS_MONITORING:-disabled}.yaml",
"--enable-metrics",
"--dapr-graceful-shutdown-seconds",
"5",
]
volumes: &file-enrichment-dapr-volumes
- ./infra/dapr/components/pubsub/alerting.yaml:/dapr/components/alerting.yaml:ro
- ./infra/dapr/components/pubsub/document_conversion.yaml:/dapr/components/document_conversion.yaml:ro
- ./infra/dapr/components/pubsub/dotnet.yaml:/dapr/components/dotnet.yaml:ro
- ./infra/dapr/components/pubsub/dpapi.yaml:/dapr/components/dpapi.yaml:ro
- ./infra/dapr/components/pubsub/files.yaml:/dapr/components/files.yaml:ro
- ./infra/dapr/components/pubsub/titus.yaml:/dapr/components/titus.yaml:ro
- ./infra/dapr/components/pubsub/workflow_monitor.yaml:/dapr/components/workflow_monitor.yaml:ro
- ./infra/dapr/components/secretstore.yaml:/dapr/components/secretstore.yaml:ro
- ./infra/dapr/components/workflowstate.yaml:/dapr/components/workflowstate.yaml:ro
- ./infra/dapr/configuration/:/dapr/configuration/
environment:
POSTGRES_USER: ${POSTGRES_USER:?}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?}
POSTGRES_HOST: ${POSTGRES_HOST:-postgres}
POSTGRES_PORT: ${POSTGRES_PORT:-5432}
POSTGRES_DB: ${POSTGRES_DB:-enrichment}
POSTGRES_PARAMETERS: ${POSTGRES_PARAMETERS:-sslmode=disable}
RABBITMQ_CONNECTION_STRING: amqp://${RABBITMQ_USER}:${RABBITMQ_PASSWORD}@rabbitmq:5672
MINIO_ROOT_USER: ${MINIO_ROOT_USER:?}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?}
MAX_PARALLEL_WORKFLOWS: ${MAX_PARALLEL_WORKFLOWS:-5}
depends_on: &file-enrichment-dapr-depends-on
file-enrichment: { condition: service_started }
placement: { condition: service_started }
scheduler: { condition: service_started }
rabbitmq: { condition: service_healthy }
network_mode: "service:file-enrichment"
# file-enrichment-1:
# <<: *file-enrichment-template
# environment:
# <<: *file-enrichment-environment
# APP_ID: file-enrichment-1
# file-enrichment-1-dapr:
# <<: *file-enrichment-dapr-template
# depends_on:
# file-enrichment-1: { condition: service_started }
# placement: { condition: service_started }
# scheduler: { condition: service_started }
# rabbitmq: { condition: service_healthy }
# network_mode: "service:file-enrichment-1"
# file-enrichment-2:
# <<: *file-enrichment-template
# environment:
# <<: *file-enrichment-environment
# APP_ID: file-enrichment-2
# file-enrichment-2-dapr:
# <<: *file-enrichment-dapr-template
# depends_on:
# file-enrichment-2: { condition: service_started }
# placement: { condition: service_started }
# scheduler: { condition: service_started }
# rabbitmq: { condition: service_healthy }
# network_mode: "service:file-enrichment-2"
# file-enrichment-3:
# <<: *file-enrichment-template
# environment:
# <<: *file-enrichment-environment
# APP_ID: file-enrichment-3
# file-enrichment-3-dapr:
# <<: *file-enrichment-dapr-template
# depends_on:
# file-enrichment-3: { condition: service_started }
# placement: { condition: service_started }
# scheduler: { condition: service_started }
# rabbitmq: { condition: service_healthy }
# network_mode: "service:file-enrichment-3"
frontend:
image: ghcr.io/specterops/nemesis/frontend:latest
depends_on: [postgres, hasura]
environment:
- HASURA_ADMIN_SECRET=${HASURA_ADMIN_SECRET:-pass456}
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000"]
interval: 10s
timeout: 10s
retries: 3
labels:
- "traefik.enable=true"
- "traefik.http.routers.frontend.rule=PathPrefix(`/`)"
- "traefik.http.services.frontend.loadbalancer.server.port=3000"
- "traefik.http.routers.frontend.entrypoints=websecure"
- "traefik.http.routers.frontend.tls=true"
- "traefik.http.routers.frontend.middlewares=auth"
jupyter:
profiles: ["jupyter"]
image: ghcr.io/specterops/nemesis/jupyter:latest
depends_on: [postgres, hasura]
environment:
- HASURA_ADMIN_SECRET=${HASURA_ADMIN_SECRET:-pass456}
- HASURA_GRAPHQL_URL=http://hasura:8080/v1/graphql
- JUPYTER_PASSWORD=${JUPYTER_PASSWORD:-}
volumes:
- "./projects/jupyter/notebooks:/home/jovyan/work"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8888/jupyter/api"]
interval: 30s
timeout: 10s
retries: 3
labels:
- "traefik.enable=true"
- "traefik.http.routers.jupyter.rule=PathPrefix(`/jupyter`)"
- "traefik.http.services.jupyter.loadbalancer.server.port=8888"
- "traefik.http.routers.jupyter.entrypoints=websecure"
- "traefik.http.routers.jupyter.tls=true"
- "traefik.http.routers.jupyter.middlewares=auth"
alerting:
image: ghcr.io/specterops/nemesis/alerting:latest
environment:
- DAPR_HTTP_PORT=3508
- DAPR_GRPC_PORT=50008
- APPRISE_URLS=${APPRISE_URLS:-}
- NEMESIS_URL=${NEMESIS_URL:?}
- LOG_LEVEL=${LOG_LEVEL:-INFO}
logging: *logging-config
depends_on:
postgres: { condition: service_healthy }
placement: { condition: service_started }
rabbitmq: { condition: service_healthy }
hasura: { condition: service_healthy }
healthcheck: &healthcheck-python-svc
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8000/healthz"]
interval: 5s
timeout: 5s
retries: 5
start_period: 20s
alerting-dapr:
image: "daprio/daprd:1.16.9"
command:
[
"./daprd",
"--max-body-size",
"1Gi",
"--app-id",
"alerting",
"--app-port",
"8000",
"--dapr-http-port",
"3508",
"--dapr-grpc-port",
"50008",
"--placement-host-address",
"placement:50006",
"--scheduler-host-address",
"scheduler:50007",
"--resources-path",
"/dapr/components",
"--log-level",
"${DAPR_LOG_LEVEL:-info}",
"--config",
"/dapr/configuration/config.yaml",
"--enable-metrics",
"--dapr-graceful-shutdown-seconds",
"5",
]
volumes:
- ./infra/dapr/components/pubsub/alerting.yaml:/dapr/components/alerting.yaml:ro
- ./infra/dapr/components/secretstore.yaml:/dapr/components/secretstore.yaml:ro
- ./infra/dapr/configuration/general.yaml:/dapr/configuration/config.yaml:ro
environment:
- RABBITMQ_CONNECTION_STRING=amqp://${RABBITMQ_USER}:${RABBITMQ_PASSWORD}@rabbitmq:5672
- HASURA_ADMIN_SECRET=${HASURA_ADMIN_SECRET:-pass456}
depends_on:
alerting: { condition: service_started }
placement: { condition: service_started }
scheduler: { condition: service_started }
rabbitmq: { condition: service_healthy }
network_mode: "service:alerting"
housekeeping:
image: ghcr.io/specterops/nemesis/housekeeping:latest
environment:
- APP_ID=housekeeping
- CLEANUP_SCHEDULE=*/3 * * * * # Every 3 minutes
- DAPR_GRPC_PORT=50011
- DAPR_HTTP_PORT=3510
- LOG_LEVEL=${LOG_LEVEL:-INFO}
logging: *logging-config
depends_on:
postgres: { condition: service_healthy }
placement: { condition: service_started }
rabbitmq: { condition: service_healthy }
healthcheck: *healthcheck-python-svc
housekeeping-dapr:
image: "daprio/daprd:1.16.9"
command:
[
"./daprd",
"--max-body-size",
"1Gi",
"--app-id",
"housekeeping",
"--app-port",
"8000",
"--dapr-http-port",
"3510",
"--dapr-grpc-port",
"50011",
"--placement-host-address",
"placement:50006",
"--scheduler-host-address",
"scheduler:50007",
"--resources-path",
"/dapr/components",
"--log-level",
"${DAPR_LOG_LEVEL:-info}",
"--config",
"/dapr/configuration/config.yaml",
"--enable-metrics",
"--dapr-graceful-shutdown-seconds",
"5",
]
volumes:
- ./infra/dapr/components/secretstore.yaml:/dapr/components/secretstore.yaml:ro
- ./infra/dapr/components/workflowstate.yaml:/dapr/components/workflowstate.yaml:ro
- ./infra/dapr/configuration/general.yaml:/dapr/configuration/config.yaml:ro
environment:
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD:?}
- MINIO_ROOT_USER=${MINIO_ROOT_USER:?}
- POSTGRES_USER=${POSTGRES_USER:?}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?}
- POSTGRES_HOST=${POSTGRES_HOST:-postgres}
- POSTGRES_PORT=${POSTGRES_PORT:-5432}
- POSTGRES_DB=${POSTGRES_DB:-enrichment}
- POSTGRES_PARAMETERS=${POSTGRES_PARAMETERS:-sslmode=disable}
depends_on:
housekeeping: { condition: service_started }
placement: { condition: service_started }
scheduler: { condition: service_started }
rabbitmq: { condition: service_healthy }
network_mode: "service:housekeeping"
document-conversion:
image: ghcr.io/specterops/nemesis/document-conversion:latest
volumes:
- ./infra/tika/tika-config${TIKA_USE_OCR:+-tesseract}.xml:/tika-config.xml:ro
environment:
- APP_ID=document-conversion
- DAPR_GRPC_PORT=50002
- DAPR_HTTP_PORT=3501
- TIKA_CONFIG=/tika-config.xml
# If you want to have additional language packs supported (see https://github.com/tesseract-ocr/tessdata for a full list):
# $ export TIKA_OCR_LANGUAGES="eng chi_sim chi_tra jpn rus deu spa"
# To change the default:
# - TIKA_OCR_LANGUAGES=${TIKA_OCR_LANGUAGES:-eng chi_sim chi_tra jpn rus deu spa}
# Note: each package installed will increase the image size!
- TIKA_OCR_LANGUAGES=${TIKA_OCR_LANGUAGES:-eng}
- MAX_PARALLEL_WORKFLOWS=${DOCUMENTCONVERSION_WORKERS:-5}
- MAX_WORKFLOW_EXECUTION_TIME=${MAX_WORKFLOW_EXECUTION_TIME:-300}
- LOG_LEVEL=${LOG_LEVEL:-INFO}
- OMP_THREAD_LIMIT=1 # Limit the number of tesseract instances
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8000/healthz"]
interval: 5s
timeout: 5s
retries: 5
logging: *logging-config
depends_on:
postgres: { condition: service_started }
placement: { condition: service_started }
rabbitmq: { condition: service_healthy }
deploy:
resources:
limits:
cpus: "2"
document-conversion-dapr:
image: "daprio/daprd:1.16.9"
command:
[
"./daprd",
"--max-body-size",
"1Gi",
"--app-id",
"document-conversion",
"--app-port",
"8000",
"--dapr-http-port",
"3501",
"--dapr-grpc-port",
"50002",
"--placement-host-address",
"placement:50006",
"--scheduler-host-address",
"scheduler:50007",
"--resources-path",
"/dapr/components",
"--log-level",
"${DAPR_LOG_LEVEL:-info}",
"--config",
"/dapr/configuration/document_conversion_monitoring_${NEMESIS_MONITORING:-disabled}.yaml",
"--enable-metrics",
"--dapr-graceful-shutdown-seconds",
"5",
]
volumes:
- ./infra/dapr/components/pubsub/document_conversion.yaml:/dapr/components/document_conversion.yaml:ro
- ./infra/dapr/components/pubsub/files.yaml:/dapr/components/files.yaml:ro
- ./infra/dapr/components/secretstore.yaml:/dapr/components/secretstore.yaml:ro
- ./infra/dapr/components/workflowstate.yaml:/dapr/components/workflowstate.yaml:ro
- ./infra/dapr/configuration/:/dapr/configuration/
environment:
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD:?}
- MINIO_ROOT_USER=${MINIO_ROOT_USER:?}
- POSTGRES_USER=${POSTGRES_USER:?}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?}
- POSTGRES_HOST=${POSTGRES_HOST:-postgres}
- POSTGRES_PORT=${POSTGRES_PORT:-5432}
- POSTGRES_DB=${POSTGRES_DB:-enrichment}
- POSTGRES_PARAMETERS=${POSTGRES_PARAMETERS:-sslmode=disable}
- RABBITMQ_CONNECTION_STRING=amqp://${RABBITMQ_USER}:${RABBITMQ_PASSWORD}@rabbitmq:5672
depends_on:
document-conversion: { condition: service_started }
placement: { condition: service_started }
scheduler: { condition: service_started }
rabbitmq: { condition: service_healthy }
network_mode: "service:document-conversion"
gotenberg:
#image: gotenberg/gotenberg:8.21.1
image: gotenberg/gotenberg:8.27.0
command:
[
"gotenberg",
"--api-timeout=180s",
"--libreoffice-restart-after=5",
"--libreoffice-auto-start=true",
"--prometheus-collect-interval=10s",
]
environment: { DISABLE_GOOGLE_CHROME: "1" }
healthcheck:
test: ["CMD", "curl", "-sSf", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
gotenberg-dapr:
image: "daprio/daprd:1.16.9"
command:
[
"./daprd",
"--max-body-size",
"1Gi",
"--app-id",
"gotenberg",
"--app-port",
"3000",
"--dapr-http-port",
"3505",
"--dapr-grpc-port",
"50005",
"--placement-host-address",
"placement:50006",
"--scheduler-host-address",
"scheduler:50007",
"--resources-path",
"/dapr/components",
"--log-level",
"${DAPR_LOG_LEVEL:-info}",
"--config",
"/dapr/configuration/config.yaml",
"--enable-metrics",
"--dapr-graceful-shutdown-seconds",
"5",
]
volumes:
- ./infra/dapr/configuration/general.yaml:/dapr/configuration/config.yaml:ro
- empty:/dapr/components:ro
depends_on:
gotenberg: { condition: service_started }
placement: { condition: service_started }
scheduler: { condition: service_started }
rabbitmq: { condition: service_healthy }
network_mode: "service:gotenberg"
############################
# Infrastructure Services
############################
minio:
image: minio/minio:RELEASE.2025-09-07T16-13-09Z
environment:
MINIO_PROMETHEUS_AUTH_TYPE: public
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?}
MINIO_ROOT_USER: ${MINIO_ROOT_USER:?}
volumes:
- minio_data:/data
command: server /data --console-address ":9001" --address ":9000"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 30s
timeout: 20s
retries: 3
minio-init:
image: minio/mc:RELEASE.2025-08-13T08-35-41Z
depends_on: [minio]
environment:
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD}
- MINIO_ROOT_USER=${MINIO_ROOT_USER}
entrypoint:
[
"/bin/sh",
"-c",
"sleep 1; until mc alias set minio http://minio:9000 $${MINIO_ROOT_USER} $${MINIO_ROOT_PASSWORD}; do echo 'Minio is not online. Waiting for it to start...'; sleep 2; done && mc mb minio/loki-data --ignore-existing && mc mb minio/files --ignore-existing",
]
restart: "no"
rabbitmq:
# image: rabbitmq:4.2.0-management
image: rabbitmq:4.2.4-management
hostname: rabbitmq-node # have to do this for persistence reasons
environment:
- RABBITMQ_DEFAULT_PASS=${RABBITMQ_PASSWORD:?}
- RABBITMQ_DEFAULT_USER=${RABBITMQ_USER:?}
- RABBITMQ_SERVER_ADDITIONAL_ERL_ARGS=-rabbitmq_management path_prefix "/rabbitmq"
- RABBITMQ_NODENAME=rabbit@rabbitmq-node # have to do this for persistence reasons
volumes:
- rabbitmq_data:/var/lib/rabbitmq
- ./infra/rabbitmq/enabled_plugins:/etc/rabbitmq/enabled_plugins:ro
- ./infra/rabbitmq/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf:ro
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "check_port_connectivity"]
interval: 10s
timeout: 10s
retries: 5
labels:
- "traefik.enable=true"
- "traefik.http.routers.rabbitmq-ui.entrypoints=websecure"
- "traefik.http.routers.rabbitmq-ui.tls=true"
- "traefik.http.services.rabbitmq-ui.loadbalancer.server.port=15672"
- "traefik.http.routers.rabbitmq-ui.rule=PathPrefix(`/rabbitmq`)"
postgres:
image: postgres:18.1
command:
[
"postgres",
"-c",
"max_connections=200",
"-c",
"shared_buffers=1GB",
"-c",
"shared_preload_libraries=pg_stat_statements",
"-c",
"pg_stat_statements.track=all",
"-c",
"tcp_keepalives_idle=5",
"-c",
"tcp_keepalives_interval=2",
"-c",
"tcp_keepalives_count=3",
]
# ports:
# - "${POSTGRES_EXTERNAL_PORT:-}5432"
environment:
POSTGRES_DB: ${POSTGRES_DB:-enrichment}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?}
POSTGRES_USER: ${POSTGRES_USER:?}
volumes:
- ./infra/postgres:/docker-entrypoint-initdb.d:ro
- postgres_data:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d enrichment"]
interval: 5s
timeout: 5s
retries: 5
postgres-exporter:
profiles: ["monitoring"]
image: prometheuscommunity/postgres-exporter:v0.18.1
environment:
DATA_SOURCE_NAME: "postgresql://${POSTGRES_USER:?}:${POSTGRES_PASSWORD:?}@${POSTGRES_HOST:-postgres}:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-enrichment}?${POSTGRES_PARAMETERS:-sslmode=disable}"
volumes:
- ./infra/postgres-exporter/postgres_exporter.yml:/postgres_exporter.yml:ro
depends_on:
postgres: { condition: service_healthy }
hasura:
#image: hasura/graphql-engine:v2.48.6.cli-migrations-v2
image: hasura/graphql-engine:v2.48.12.cli-migrations-v2
depends_on:
postgres: { condition: service_healthy }
environment:
HASURA_GRAPHQL_ADMIN_SECRET: "${HASURA_ADMIN_SECRET:-pass456}"
HASURA_GRAPHQL_BASE_PATH: "/hasura"
HASURA_GRAPHQL_DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST:-postgres}:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-enrichment}
HASURA_GRAPHQL_ENABLE_CONSOLE: "true"
HASURA_GRAPHQL_ENABLE_METADATA_SYNC: "true"
HASURA_GRAPHQL_ENABLE_TELEMETRY: "false"
HASURA_GRAPHQL_ENABLED_LOG_TYPES: startup, http-log, webhook-log, websocket-log, query-log
HASURA_GRAPHQL_LOG_LEVEL: "warn"
HASURA_GRAPHQL_METADATA_DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST:-postgres}:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-enrichment}
HASURA_GRAPHQL_METADATA_DIR: /hasura-metadata
HASURA_GRAPHQL_UNAUTHORIZED_ROLE: anonymous
volumes:
- ./infra/hasura/metadata:/hasura-metadata:ro
labels:
- "traefik.enable=true"
- "traefik.http.routers.hasura.rule=PathPrefix(`/hasura`)"
- "traefik.http.services.hasura.loadbalancer.server.port=8080"
- "traefik.http.routers.hasura.middlewares=graphql-stripprefix"
- "traefik.http.middlewares.graphql-stripprefix.stripprefix.prefixes=/hasura"
- "traefik.http.routers.hasura.entrypoints=websecure"
- "traefik.http.routers.hasura.tls=true"
traefik:
# image: traefik:v3.6.1
image: traefik:v3.6.9
command:
- "--api.insecure=true"
# - "--log.level=DEBUG"
- "--log.level=WARN"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--providers.file=true"
- "--providers.file.directory=/config"
- "--providers.file.watch=true"
- "--entrypoints.web.address=:80"
- "--entrypoints.web.http.redirections.entryPoint.to=websecure"
- "--entrypoints.web.http.redirections.entryPoint.scheme=https"
- "--entrypoints.websecure.address=:443"
- "--metrics.prometheus=true"
- "--metrics.prometheus.addrouterslabels=true"
labels:
- "traefik.enable=true"
- "traefik.http.middlewares.auth.basicauth.users=${BASIC_AUTH_USERS:-n:$$apr1$$RbnOjtKr$$S706SPJ3KzCKIref.gRFT.}"
- "traefik.http.routers.dashboard.tls=true"
environment:
- NEMESIS_PORT=${NEMESIS_PORT:-7443}
ports: ["${NEMESIS_PORT:-7443}:443"]
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./auth:/auth:ro
- ./infra/traefik/certs:/certs:ro
- ./infra/traefik/config:/config:ro
placement:
image: "daprio/dapr:1.16.9"
command:
[
"./placement",
"-port",
"50006",
"--enable-metrics",
"--log-level",
"${DAPR_LOG_LEVEL:-info}",
]
depends_on:
postgres: { condition: service_healthy }
scheduler-init:
image: busybox:1.37.0
user: "0:0"
command: >
sh -c "
mkdir -p /data/default-dapr-scheduler-server-0 &&
chown -R 65532:65532 /data &&
chmod -R 755 /data
"
volumes:
- scheduler_data:/data
restart: "no"
scheduler:
image: "daprio/scheduler:1.16.9"
command:
[
"./scheduler",
"--port",
"50007",
"--etcd-data-dir=/data",
"--enable-metrics",
"--log-level",
"${DAPR_LOG_LEVEL:-info}",
]
depends_on:
postgres: { condition: service_healthy }
scheduler-init: { condition: service_completed_successfully }
volumes:
- scheduler_data:/data
############################
# Monitoring Services (optional profile)
############################
otel-collector:
profiles: ["monitoring"]
image: otel/opentelemetry-collector:0.139.0
command: ["--config=/etc/otel-collector-config.yaml"]
volumes:
- ./infra/otel-collector/otel-collector-config.yaml:/etc/otel-collector-config.yaml:ro
depends_on: [jaeger]
jaeger:
profiles: ["monitoring"]
# image: jaegertracing/jaeger:2.11.0 # v2.x image
image: jaegertracing/jaeger:2.15.1
user: "0:0"
deploy:
resources:
limits:
memory: 2G
reservations:
memory: 1G
environment:
- QUERY_BASE_PATH=/jaeger
- GOGC=80
volumes:
- ./infra/jaeger/jaeger-config.yaml:/etc/jaeger/config.yaml
- jaeger_data:/badger # Persist trace data
command: ["--config", "/etc/jaeger/config.yaml"]
labels:
- "traefik.enable=true"
- "traefik.http.routers.jaeger.rule=PathPrefix(`/jaeger`)"
- "traefik.http.services.jaeger.loadbalancer.server.port=16686"
- "traefik.http.routers.jaeger.entrypoints=websecure"
- "traefik.http.routers.jaeger.tls=true"
- "traefik.http.routers.jaeger.middlewares=auth"
healthcheck:
test:
[
"CMD-SHELL",
"wget --no-verbose --tries=1 --spider http://localhost:13133/status || exit 1",
]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
loki:
profiles: ["monitoring"]
# image: grafana/loki:3.3.2
image: grafana/loki:3.5.11
command:
["-config.file=/etc/loki/local-config.yaml", "-config.expand-env=true"]
environment:
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD}
- MINIO_ROOT_USER=${MINIO_ROOT_USER}
volumes:
- ./infra/loki:/etc/loki:ro
- loki_data:/loki
depends_on: [minio-init]
healthcheck:
test:
[
"CMD-SHELL",
"wget --no-verbose --tries=1 --spider http://localhost:3100/ready || exit 1",
]
interval: 10s
promtail:
profiles: ["monitoring"]
# image: grafana/promtail:3.3.2
image: grafana/promtail:3.5.11
user: "0:0"
volumes:
- ./infra/promtail:/etc/promtail:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
command:
["-config.file=/etc/promtail/config.yaml", "-config.expand-env=true"]
depends_on: [loki]
grafana:
profiles: ["monitoring"]
# image: grafana/grafana:11.4.0
image: grafana/grafana:12.3.4
environment:
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_ROLE=Editor
# - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD:-Qwerty12345}
# - GF_SECURITY_ADMIN_USER=${GRAFANA_ADMIN_USER:-nemesis}
- GF_SERVER_DOMAIN=${EXTERNAL_HOST:-https://localhost}
- GF_SERVER_ROOT_URL=${EXTERNAL_HOST:-https://localhost}/grafana
- GF_SERVER_SERVE_FROM_SUB_PATH=true
- POSTGRES_CONNECTION_STRING=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST:-postgres}:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-enrichment}?${POSTGRES_PARAMETERS:-sslmode=disable}
volumes:
- ./infra/grafana/provisioning:/etc/grafana/provisioning:ro
- grafana_data:/var/lib/grafana
depends_on:
postgres: { condition: service_healthy }
labels:
- "traefik.enable=true"
- "traefik.http.routers.grafana.rule=PathPrefix(`/grafana`)"
- "traefik.http.services.grafana.loadbalancer.server.port=3000"
- "traefik.http.routers.grafana.entrypoints=websecure"
- "traefik.http.routers.grafana.tls=true"
- "traefik.http.routers.grafana.middlewares=auth"
healthcheck:
test: ["CMD", "curl", "-sSf", "http://localhost:3000/"]
interval: 10s
prometheus:
profiles: ["monitoring"]
# image: prom/prometheus:v3.1.0
image: prom/prometheus:v3.10.0
volumes:
- ./infra/prometheus:/etc/prometheus:ro
- prometheus_data:/prometheus
command:
[
"--config.file=/etc/prometheus/prometheus.yml",
"--storage.tsdb.path=/prometheus",
"--web.external-url=/prometheus/",
]
healthcheck:
test:
[
"CMD",
"wget",
"--spider",
"-q",
"http://localhost:9090/prometheus/-/healthy",
]
interval: 10s
labels:
- "traefik.enable=true"
- "traefik.http.routers.prometheus.rule=PathPrefix(`/prometheus`)"
- "traefik.http.services.prometheus.loadbalancer.server.port=9090"
- "traefik.http.routers.prometheus.entrypoints=websecure"
- "traefik.http.routers.prometheus.tls=true"
- "traefik.http.routers.prometheus.middlewares=auth"
node-exporter:
profiles: ["monitoring"]
image: prom/node-exporter:v1.10.2
restart: unless-stopped
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command:
[
"--path.procfs=/host/proc",
"--path.rootfs=/rootfs",
"--path.sysfs=/host/sys",
"--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)",
]
cadvisor:
profiles: ["monitoring"]
image: ghcr.io/google/cadvisor:v0.53.0
restart: unless-stopped
privileged: true
command:
- "--housekeeping_interval=30s"
- "--storage_duration=2m0s"
- "--docker_only=true"
volumes:
- /:/rootfs:ro
- /dev/disk/:/dev/disk:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
- /var/run:/var/run:ro
devices: ["/dev/kmsg:/dev/kmsg"]
############################
# LLM Services (optional profile)
############################
agents:
profiles: ["llm"]
image: ghcr.io/specterops/nemesis/agents:latest
environment:
- DAPR_HTTP_PORT=3509
- DAPR_GRPC_PORT=50009
- NEMESIS_URL=${NEMESIS_URL:?}
- WORKFLOW_RUNTIME_LOG_LEVEL=${WORKFLOW_RUNTIME_LOG_LEVEL:-WARNING}
- WORKFLOW_CLIENT_LOG_LEVEL=${WORKFLOW_CLIENT_LOG_LEVEL:-WARNING}
- LITELLM_ADMIN_KEY=sk-${LITELLM_ADMIN_PASSWORD:-admin123}
- MAX_BUDGET=${LLM_MAX_BUDGET:-100}
- BUDGET_DURATION=${LLM_BUDGET_DURATION:-30d}
- DOTNET_ANALYSIS_RUN_REQUEST_LIMIT=${DOTNET_ANALYSIS_RUN_REQUEST_LIMIT:-25} # max LLM calls to use per .NET program analysis run
- DOTNET_ANALYSIS_RUN_TOKENS_LIMIT=${DOTNET_ANALYSIS_RUN_TOKENS_LIMIT:-1000000} # max tokens to use per .NET program analysis run
# if we hit this number of the same triage values for the same file, all future findings get that value
- TRIAGE_CONSENSUS_THRESHOLD=${TRIAGE_CONSENSUS_THRESHOLD:-3}
# Phoenix LLM tracing configuration
- PHOENIX_ENABLED=${PHOENIX_ENABLED:-false}
- PHOENIX_ENDPOINT=${PHOENIX_ENDPOINT:-http://phoenix:6006/v1/traces}
- PHOENIX_SQL_DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST:-postgres}:${POSTGRES_PORT:-5432}/phoenix
- NEMESIS_MONITORING=${NEMESIS_MONITORING:-disabled}
- OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://otel-collector:4317
- OTEL_EXPORTER_OTLP_TRACES_ENDPOINT_INSECURE=true
# Chatbot configuration
- CHATBOT_DB_PASSWORD=${CHATBOT_DB_PASSWORD:-chatbot_pass_change_me}
- MCP_MAX_RESULTS=${MCP_MAX_RESULTS:-100}
logging: *logging-config
depends_on:
postgres: { condition: service_healthy }
placement: { condition: service_started }
rabbitmq: { condition: service_healthy }
hasura: { condition: service_healthy }
litellm: { condition: service_healthy }
healthcheck: *healthcheck-python-svc
labels:
- "traefik.enable=true"
- "traefik.http.routers.mcp.rule=PathPrefix(`/mcp`)"
- "traefik.http.services.mcp.loadbalancer.server.port=5000"
- "traefik.http.routers.mcp.entrypoints=websecure"
- "traefik.http.routers.mcp.tls=true"
- "traefik.http.routers.mcp.middlewares=auth"
agents-dapr:
profiles: ["llm"]
image: "daprio/daprd:1.16.9"
command:
[
"./daprd",
"--max-body-size",
"1Gi",
"--app-id",
"agents",
"--app-port",
"8000",
"--dapr-http-port",
"3509",
"--dapr-grpc-port",
"50009",
"--placement-host-address",
"placement:50006",
"--scheduler-host-address",
"scheduler:50007",
"--resources-path",
"/dapr/components",
"--config",
"/dapr/configuration/agents_monitoring_${NEMESIS_MONITORING:-disabled}.yaml",
"--enable-metrics",
"--dapr-graceful-shutdown-seconds",
"5",
]
volumes:
- ./infra/dapr/components/secretstore.yaml:/dapr/components/secretstore.yaml:ro
- ./infra/dapr/components/workflowstate.yaml:/dapr/components/workflowstate.yaml:ro
- ./infra/dapr/configuration/:/dapr/configuration/
environment:
- POSTGRES_USER=${POSTGRES_USER:?}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?}
- POSTGRES_HOST=${POSTGRES_HOST:-postgres}
- POSTGRES_PORT=${POSTGRES_PORT:-5432}
- POSTGRES_DB=${POSTGRES_DB:-enrichment}
- POSTGRES_PARAMETERS=${POSTGRES_PARAMETERS:-sslmode=disable}
- HASURA_ADMIN_SECRET=${HASURA_ADMIN_SECRET:-pass456}
- RABBITMQ_CONNECTION_STRING=amqp://${RABBITMQ_USER}:${RABBITMQ_PASSWORD}@rabbitmq:5672
- MINIO_ROOT_USER=${MINIO_ROOT_USER:?}
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD:?}
depends_on:
agents: { condition: service_started }
placement: { condition: service_started }
scheduler: { condition: service_started }
rabbitmq: { condition: service_healthy }
network_mode: "service:agents"
litellm:
profiles: ["llm"]
# image: ghcr.io/berriai/litellm:v1.74.0-stable
image: ghcr.io/berriai/litellm:v1.81.12-stable.1
environment:
- DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST:-postgres}:${POSTGRES_PORT:-5432}/litellm
- STORE_MODEL_IN_DB=True
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-}
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-}
- AWS_REGION_NAME=${AWS_REGION_NAME:-us-east-1}
- LITELLM_MASTER_KEY=sk-${LITELLM_ADMIN_PASSWORD:-admin123}
- LITELLM_SALT_KEY=sk-${LITELLM_ADMIN_PASSWORD:-admin123}
- SERVER_ROOT_PATH=/llm
- PROXY_BASE_URL=${NEMESIS_URL:?}
- UI_BASE_PATH=/llm/ui
volumes:
- ./infra/litellm/config.yml:/app/config.yml:ro
command: ["--config", "/app/config.yml"]
depends_on:
postgres: { condition: service_healthy }
healthcheck:
test:
[
"CMD-SHELL",
"python3 -c \"import urllib.request,json; r=urllib.request.urlopen(urllib.request.Request('http://localhost:4000/health',headers={'Authorization':'Bearer sk-${LITELLM_ADMIN_PASSWORD:-admin123}'})); d=json.load(r); exit(0 if d.get('healthy_count',0)>0 and d.get('unhealthy_count',0)==0 else 1)\"",
]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
logging: *logging-config
labels:
- "traefik.enable=true"
- "traefik.http.routers.litellm.rule=PathPrefix(`/llm`)"
- "traefik.http.services.litellm.loadbalancer.server.port=4000"
- "traefik.http.routers.litellm.entrypoints=websecure"
- "traefik.http.routers.litellm.tls=true"
phoenix:
profiles: ["llm"]
# image: arizephoenix/phoenix:11.24.1
image: arizephoenix/phoenix:13.3.0
environment:
- PHOENIX_WORKING_DIR=/data
- PHOENIX_HOST_ROOT_PATH=/phoenix
- PHOENIX_SQL_DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST:-postgres}:${POSTGRES_PORT:-5432}/phoenix
- PHOENIX_DEFAULT_RETENTION_POLICY_DAYS=100
volumes:
- phoenix_data:/data
# ports:
# - "6006:6006" # Phoenix UI port, for debugging
labels:
- "traefik.enable=true"
- "traefik.http.services.phoenix.loadbalancer.server.port=6006"
- "traefik.http.routers.phoenix.rule=PathPrefix(`/phoenix`)"
- "traefik.http.routers.phoenix.entrypoints=websecure"
- "traefik.http.routers.phoenix.tls=true"
- "traefik.http.routers.phoenix.middlewares=auth,phoenix-stripprefix"
- "traefik.http.middlewares.phoenix-stripprefix.stripprefix.prefixes=/phoenix"
depends_on:
postgres: { condition: service_healthy }
healthcheck:
test:
[
"CMD",
"python",
"-c",
"import urllib.request; urllib.request.urlopen('http://localhost:6006/healthz').read()",
]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s