# # docker-compose base file used for local development, hobby deploys, and other compose use cases. # services: proxy: image: caddy entrypoint: sh restart: always command: -c 'set -x && echo "$$CADDYFILE" > /etc/caddy/Caddyfile && echo "$$CADDY_EXTRA_CONFIG" >> /etc/caddy/Caddyfile && exec caddy run -c /etc/caddy/Caddyfile' volumes: - /root/.caddy environment: CADDY_TLS_BLOCK: '' CADDY_HOST: 'http://localhost:8000' CADDY_EXTRA_CONFIG: '' CADDYFILE: | { ${CADDY_TLS_BLOCK:-} } ${CADDY_HOST:-http://localhost:8000} { @replay-capture { path /s path /s/ path /s/* } @capture-ai { path /i/v0/ai path /i/v0/ai/ path /i/v0/ai/* path /i/v1/ai/events path /i/v1/ai/events/ } @capture { path /e path /e/ path /e/* path /i/v0 path /i/v0/ path /i/v0/* path /i/v1/analytics/events path /i/v1/analytics/events/ path /batch path /batch/ path /batch/* path /capture path /capture/ path /capture/* } @capture-logs { path /i/v1/logs path /i/v1/logs/ path /i/v1/logs/* path /i/v1/traces path /i/v1/traces/ path /i/v1/traces/* path /i/v1/metrics path /i/v1/metrics/ path /i/v1/metrics/* } @flags { path /flags path /flags/ path /flags/* # Local evaluation (flag definitions) — mirrors cloud Contour routing to Rust path /api/feature_flag/local_evaluation path /api/feature_flag/local_evaluation/ path /api/feature_flag/local_evaluation/* } @surveys { path /surveys path /surveys/ path /api/surveys path /api/surveys/ } @remote-config { path /array/* } @webhooks { path /public/webhooks path /public/webhooks/ path /public/webhooks/* path /public/m/ path /public/m/* } @livestream { path /livestream path /livestream/ path /livestream/* } handle @livestream { uri strip_prefix /livestream reverse_proxy livestream:8080 { flush_interval -1 } } handle @capture-ai { reverse_proxy capture-ai:3000 } handle @capture { reverse_proxy capture:3000 } handle @replay-capture { reverse_proxy replay-capture:3000 } handle @capture-logs { reverse_proxy capture-logs:4318 } handle @flags { reverse_proxy feature-flags:3001 } handle @surveys { reverse_proxy hypercache-server:3002 } handle @remote-config { reverse_proxy hypercache-server:3002 } handle @webhooks { reverse_proxy plugins:6738 } @objectstorage { path /posthog path /posthog/ path /posthog/* } handle @objectstorage { reverse_proxy objectstorage:19000 } handle { reverse_proxy web:8000 } } db: image: postgres:15.12-alpine restart: on-failure environment: POSTGRES_USER: posthog POSTGRES_DB: posthog POSTGRES_PASSWORD: posthog healthcheck: test: ['CMD-SHELL', 'pg_isready -U posthog'] interval: 5s timeout: 30s retries: 30 start_period: 10s volumes: - ./docker/postgres-init-scripts:/docker-entrypoint-initdb.d - ./products:/products:ro redis7: image: redis:7.2-alpine restart: on-failure command: redis-server --maxmemory-policy allkeys-lru --maxmemory 200mb healthcheck: test: ['CMD', 'redis-cli', 'ping'] interval: 3s timeout: 10s retries: 10 # Shadow store the CDP services dual-write to alongside redis7. Plain mode, unlike the # dev stack's cluster-enabled valkey-cluster: reproducing the CROSSSLOT rejection of a # cluster only earns its complexity where CDP changes get tested. Mirror data is # disposable, so there's no volume and eviction under the cap is fine. valkey: image: valkey/valkey:8.1-alpine restart: on-failure command: valkey-server --maxmemory-policy allkeys-lru --maxmemory 200mb healthcheck: test: ['CMD', 'valkey-cli', 'ping'] interval: 3s timeout: 10s retries: 10 clickhouse: image: ${CLICKHOUSE_SERVER_IMAGE:-clickhouse/clickhouse-server:26.9.13.15} restart: on-failure environment: CLICKHOUSE_SKIP_USER_SETUP: 1 KAFKA_HOSTS: 'kafka:9092' healthcheck: test: ['CMD-SHELL', 'wget --no-verbose --tries=1 --spider http://localhost:8123/ping || exit 1'] # nosemgrep: trailofbits.generic.wget-unencrypted-url.wget-unencrypted-url interval: 3s timeout: 10s retries: 10 zookeeper: image: zookeeper:3.7.0 restart: on-failure kafka: image: docker.io/redpandadata/redpanda:v25.1.9 restart: on-failure command: - redpanda - start - --kafka-addr internal://0.0.0.0:9092,external://0.0.0.0:19092 - --advertise-kafka-addr internal://kafka:9092,external://localhost:19092 - --pandaproxy-addr internal://0.0.0.0:8082,external://0.0.0.0:18082 - --advertise-pandaproxy-addr internal://kafka:8082,external://localhost:18082 - --schema-registry-addr internal://0.0.0.0:8081,external://0.0.0.0:18081 - --rpc-addr kafka:33145 - --advertise-rpc-addr kafka:33145 - --mode dev-container - --smp 2 - --memory 3G - --reserve-memory 500M - --overprovisioned - --set redpanda.empty_seed_starts_cluster=false - --seeds kafka:33145 - --set redpanda.auto_create_topics_enabled=true # Redpanda refuses new partitions past its file descriptor limit divided by # `topic_fds_per_partition` (5). At a 1024 soft limit that is about 200 partitions, fewer # than one cohort-stream-processor integration test creates. ulimits: nofile: soft: 65536 hard: 65536 volumes: - redpanda-data:/var/lib/redpanda/data environment: ALLOW_PLAINTEXT_LISTENER: 'true' healthcheck: test: curl -f http://localhost:9644/v1/status/ready || exit 1 interval: 3s timeout: 10s retries: 10 kafka_ui: image: provectuslabs/kafka-ui:latest restart: on-failure environment: KAFKA_CLUSTERS_0_NAME: local KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:9092 KAFKA_CLUSTERS_0_SCHEMAREGISTRY: http://kafka:8081 DYNAMIC_CONFIG_ENABLED: 'true' objectstorage: # SeaweedFS, S3-compatible object storage on objectstorage:19000. # No static -s3.config (it triggers known SeaweedFS SigV4 signature failures and # presigned-URL regressions; see seaweedfs/seaweedfs #6635 and discussion #3976), # but pure open-access mode doesn't work either: the POST-policy handler always # looks the access key up in the identity store (no open-access bypass, also on # master), so presigned POST uploads — error tracking symbol sets via posthog-cli — # fail with InvalidAccessKeyId when no identities exist. The bootstrap loop below # therefore registers the canonical dev credentials at runtime via `s3.configure` # (which doesn't hit the static-config regressions), plus an `anonymous` identity # so unsigned clients keep working. Signed requests with any other access key are # rejected — everything in this repo uses object_storage_root_user against :19000. # Pin >= 4.05: that's the first release that persists Content-Encoding metadata # (4.03/4.04 silently drop it, breaking gzipped objects that rely on the header # round-tripping). 4.29 is the newest release that's been out at least two weeks. image: chrislusf/seaweedfs:4.29 restart: on-failure entrypoint: - /bin/sh - -c # Background a setup-and-verify loop and exec weed as PID 1 so SIGTERM # reaches it directly. `weed shell` exits 0 as long as the master is # reachable, even if individual commands fail (e.g. while the filer is still # coming up), and setup output can't be grepped for success markers anyway: # the identity is also named `posthog`, so its configure echo would shadow # the `posthog` bucket check. The sentinel is therefore only touched once # read-only `s3.bucket.list` / `s3.configure` sessions report every required # bucket and identity from actual persisted state. - | ( while true; do printf 's3.configure -access_key=object_storage_root_user -secret_key=object_storage_root_password -user=posthog -actions=Admin -apply\ns3.configure -user=anonymous -actions=Admin -apply\ns3.bucket.create -name posthog\ns3.bucket.create -name ducklake-dev\ns3.bucket.create -name ai-blobs\n' \ | /usr/bin/weed shell -master=localhost:19001 >/dev/null 2>&1 || true BUCKETS=$$(echo 's3.bucket.list' | /usr/bin/weed shell -master=localhost:19001 2>/dev/null || true) IDENTITIES=$$(echo 's3.configure' | /usr/bin/weed shell -master=localhost:19001 2>/dev/null || true) if echo "$$BUCKETS" | grep -q posthog \ && echo "$$BUCKETS" | grep -q ducklake-dev \ && echo "$$BUCKETS" | grep -q ai-blobs \ && echo "$$IDENTITIES" | grep -q object_storage_root_user \ && echo "$$IDENTITIES" | grep -q anonymous; then touch /tmp/objectstorage-ready echo "Objectstorage SeaweedFS ready with required buckets and identities" break fi sleep 1 done ) & exec /usr/bin/weed "$$@" - -- # Every S3 bucket is its own SeaweedFS collection that grows volumes in batches. # By default SeaweedFS preallocates 1 GiB per volume, so a workload that creates # many buckets (the test suite uses a unique bucket per test) reserves many GiB # up front and exhausts disk/volumes — writes then fail with HTTP 500 # ("no writable volumes"). volumePreallocate=false makes volumes grow on demand so # capacity tracks actual data, not bucket count; volume.max is raised well above the # default 8 since each collection consumes several volume slots. command: [ 'server', '-master.port=19001', '-s3', '-s3.port=19000', '-dir=/data', '-volume.max=1000', '-master.volumePreallocate=false', ] # SeaweedFS creates buckets after startup via the background loop above, so without # this sentinel bin/wait-for-docker would treat the container as ready the instant # weed execs and race the bucket bootstrap. The loop touches the sentinel once every # required bucket exists. healthcheck: test: ['CMD', 'test', '-f', '/tmp/objectstorage-ready'] interval: 5s timeout: 5s retries: 30 start_period: 10s objectstorage-azure: image: mcr.microsoft.com/azure-storage/azurite:3.35.0 restart: on-failure profiles: ['batch-exports'] command: azurite-blob --blobHost 0.0.0.0 --blobPort 10000 --location /data maildev: image: maildev/maildev:2.0.5 restart: on-failure healthcheck: # nosemgrep: trailofbits.generic.wget-unencrypted-url.wget-unencrypted-url test: wget -qO- http://127.0.0.1:1080/healthz || exit 1 interval: 3s timeout: 1s retries: 3 flower: image: mher/flower:2.0.0 restart: on-failure environment: FLOWER_PORT: 5555 CELERY_BROKER_URL: redis://redis7:6379 worker: &worker command: ./bin/docker-worker-celery --with-scheduler restart: on-failure env_file: .env.services environment: &worker_env OTEL_SDK_DISABLED: 'true' API_QUERIES_PER_TEAM: '{"1": 100}' KAFKA_HOSTS: 'kafka' DEPLOYMENT: hobby CDP_API_URL: 'http://plugins:6738' # Off-ORM persons_db util (posthog/persons_db.py). Hobby keeps persons in # the main posthog DB, so both point there. PERSONS_DB_WRITER_URL: 'postgres://posthog:posthog@db:5432/posthog' PERSONS_DB_READER_URL: 'postgres://posthog:posthog@db:5432/posthog' web: <<: *worker command: ./bin/start-backend & ./bin/start-frontend restart: on-failure capture: image: ghcr.io/posthog/posthog/capture:master build: context: rust/ args: BIN: capture restart: on-failure environment: ADDRESS: '0.0.0.0:3000' KAFKA_TOPIC: 'events_plugin_ingestion' CAPTURE_OUTPUT_ANALYTICS_MAIN_TOPIC: 'events_plugin_ingestion' KAFKA_ERROR_TRACKING_TOPIC: 'ingestion-errortracking-main' CAPTURE_OUTPUT_ERROR_TRACKING_TOPIC: 'ingestion-errortracking-main' KAFKA_HOSTS: 'kafka:9092' KAFKA_INGESTION_PRODUCER_METADATA_BROKER_LIST: 'kafka:9092' REDIS_URL: 'redis://redis7:6379/' CAPTURE_MODE: events RUST_LOG: 'info,rdkafka=warn' replay-capture: image: ghcr.io/posthog/posthog/capture:master build: context: rust/ args: BIN: capture restart: on-failure environment: ADDRESS: '0.0.0.0:3000' KAFKA_TOPIC: 'session_recording_snapshot_item_events' CAPTURE_OUTPUT_SESSION_REPLAY_MAIN_TOPIC: 'session_recording_snapshot_item_events' KAFKA_HOSTS: 'kafka:9092' KAFKA_INGESTION_PRODUCER_METADATA_BROKER_LIST: 'kafka:9092' REDIS_URL: 'redis://redis7:6379/' CAPTURE_MODE: recordings capture-ai: image: ghcr.io/posthog/posthog/capture:master build: context: rust/ args: BIN: capture restart: on-failure environment: ADDRESS: '0.0.0.0:3000' KAFKA_TOPIC: 'events_plugin_ingestion_ai' CAPTURE_OUTPUT_ANALYTICS_MAIN_TOPIC: 'events_plugin_ingestion_ai' KAFKA_HOSTS: 'kafka:9092' KAFKA_INGESTION_PRODUCER_METADATA_BROKER_LIST: 'kafka:9092' REDIS_URL: 'redis://redis7:6379/' # `ai` mode produces to the AI lane as its main topic (no $ai_* divert needed), # matching the production capture-ai deployment. CAPTURE_MODE: ai RUST_LOG: 'info,rdkafka=warn' capture-logs: image: ghcr.io/posthog/posthog/capture-logs:master build: context: rust/ args: BIN: capture-logs restart: on-failure environment: BIND_HOST: '0.0.0.0' BIND_PORT: '4318' RUST_LOG: info,rdkafka=warn RUST_BACKTRACE: '1' KAFKA_HOSTS: kafka:9092 JWT_SECRET: '' KAFKA_TOPIC: logs_ingestion KAFKA_METRICS_TOPIC: metrics_ingestion networks: - otel_network - default property-defs-rs: image: ghcr.io/posthog/posthog/property-defs-rs:master build: context: rust/ args: BIN: property-defs-rs restart: on-failure environment: DATABASE_URL: 'postgres://posthog:posthog@db:5432/posthog' KAFKA_HOSTS: 'kafka:9092' SKIP_WRITES: 'false' SKIP_READS: 'false' FILTER_MODE: 'opt-out' feature-flags: image: ghcr.io/posthog/posthog/feature-flags:master build: context: rust/ args: BIN: feature-flags restart: on-failure healthcheck: test: ['CMD', 'curl', '-f', 'http://localhost:3001/_readiness'] interval: 5s timeout: 5s retries: 12 start_period: 10s volumes: - ./share:/share environment: WRITE_DATABASE_URL: 'postgres://posthog:posthog@db:5432/posthog' READ_DATABASE_URL: 'postgres://posthog:posthog@db:5432/posthog' PERSONS_WRITE_DATABASE_URL: 'postgres://posthog:posthog@db:5432/posthog' PERSONS_READ_DATABASE_URL: 'postgres://posthog:posthog@db:5432/posthog' MAXMIND_DB_PATH: '/share/GeoLite2-City.mmdb' # Shared Redis for non-critical path (analytics, billing, cookieless) REDIS_URL: 'redis://redis7:6379/' # Optional: Use separate Redis URL for read replicas # REDIS_READER_URL: 'redis://redis-replica:6379/' # Optional: Increase Redis timeout (default is 100ms) # REDIS_TIMEOUT_MS: 200 # Dedicated Redis database for critical path (team cache + flags cache) # Hobby deployments start in Mode 1 (shared-only). Developers override in docker-compose.dev.yml for Mode 2. # FLAGS_REDIS_URL: 'redis://redis7:6379/1' # Optional: Use separate Flags Redis URL for read replicas # FLAGS_REDIS_READER_URL: 'redis://redis-replica:6379/1' ADDRESS: '0.0.0.0:3001' RUST_LOG: 'info' COOKIELESS_REDIS_HOST: redis7 COOKIELESS_REDIS_PORT: 6379 usage-ingestion: image: ghcr.io/posthog/posthog/usage-ingestion:master build: context: rust/ args: BIN: usage-ingestion restart: on-failure healthcheck: test: ['CMD', 'curl', '-f', 'http://localhost:7144/_readiness'] interval: 5s timeout: 5s retries: 12 start_period: 10s environment: USAGE_INGESTION_DATABASE_URL: 'postgres://posthog:posthog@db:5432/posthog' KAFKA_HOSTS: 'kafka:9092' RUST_LOG: 'info' personhog-replica: image: ghcr.io/posthog/posthog/personhog-replica:master build: context: rust/ args: BIN: personhog-replica restart: on-failure environment: GRPC_ADDRESS: '0.0.0.0:50051' PRIMARY_DATABASE_URL: 'postgres://posthog:posthog@db:5432/posthog_persons' RUST_LOG: 'info' METRICS_PORT: '9100' personhog-router: image: ghcr.io/posthog/posthog/personhog-router:master build: context: rust/ args: BIN: personhog-router restart: on-failure environment: GRPC_ADDRESS: '0.0.0.0:50052' REPLICA_URL: 'http://personhog-replica:50051' BACKEND_TIMEOUT_MS: '5000' RUST_LOG: 'info' METRICS_PORT: '9101' personhog-router-leader: # The same router binary in leader mode: person writes and strong # reads go to leader pods via etcd partition routing; the rest goes # to the replica. image: ghcr.io/posthog/posthog/personhog-router:master build: context: rust/ args: BIN: personhog-router restart: on-failure environment: GRPC_ADDRESS: '0.0.0.0:50054' REPLICA_URL: 'http://personhog-replica:50051' ROUTER_MODE: 'leader' ETCD_ENDPOINTS: 'http://etcd:2379' ETCD_PREFIX: '/personhog/' BACKEND_TIMEOUT_MS: '5000' RUST_LOG: 'info' METRICS_PORT: '9104' personhog-leader: image: ghcr.io/posthog/posthog/personhog-leader:master build: context: rust/ args: BIN: personhog-leader restart: on-failure # The leader binds the wildcard, so it must advertise the container's # own IP in etcd for routers to dial; compose cannot inject it as # K8s's downward API does, so the entrypoint resolves it at start. entrypoint: ['/bin/sh', '-c', 'POD_IP=$$(hostname -i) exec /usr/local/bin/personhog-leader'] environment: GRPC_ADDRESS: '0.0.0.0:50053' CACHE_MEMORY_CAPACITY: '100000' ETCD_ENDPOINTS: 'http://etcd:2379' ETCD_PREFIX: '/personhog/' POD_NAME: 'personhog-leader' KAFKA_HOSTS: 'kafka:9092' KAFKA_PERSON_STATE_TOPIC: 'personhog_updates' FALLBACK_DATABASE_URL: 'postgres://posthog:posthog@db:5432/posthog_persons' # Must name the same table the identity service and the writer # target (their defaults), or leader misses read the wrong table. FALLBACK_TABLE: 'personhog_person_tmp' RUST_LOG: 'info' METRICS_PORT: '9102' personhog-writer: image: ghcr.io/posthog/posthog/personhog-writer:master build: context: rust/ args: BIN: personhog-writer restart: on-failure environment: DATABASE_URL: 'postgres://posthog:posthog@db:5432/posthog_persons' KAFKA_HOSTS: 'kafka:9092' KAFKA_TOPIC: 'personhog_updates' KAFKA_CONSUMER_GROUP: 'personhog-writer' FLUSH_INTERVAL_MS: '5000' FLUSH_BUFFER_SIZE: '1000' BUFFER_CAPACITY: '50000' RUST_LOG: 'info' METRICS_PORT: '9103' personhog-identity: image: ghcr.io/posthog/posthog/personhog-identity:master build: context: rust/ args: BIN: personhog-identity restart: on-failure environment: GRPC_ADDRESS: '0.0.0.0:50055' PRIMARY_DATABASE_URL: 'postgres://posthog:posthog@db:5432/posthog_persons' ROUTER_URL: 'http://personhog-router-leader:50054' ETCD_ENDPOINTS: 'http://etcd:2379' ETCD_PREFIX: '/personhog/' RUST_LOG: 'info' METRICS_PORT: '9108' hypercache-server: image: ghcr.io/posthog/posthog/hypercache-server:master build: context: rust/ args: BIN: hypercache-server restart: on-failure healthcheck: test: ['CMD', 'curl', '-f', 'http://localhost:3002/_readiness'] interval: 5s timeout: 5s retries: 12 start_period: 10s environment: REDIS_URL: 'redis://redis7:6379/' ADDRESS: '0.0.0.0:3002' RUST_LOG: 'info' plugins: command: node nodejs/dist/index.js restart: on-failure environment: DATABASE_URL: 'postgres://posthog:posthog@db:5432/posthog' PERSONS_DATABASE_URL: 'postgres://posthog:posthog@db:5432/posthog' BEHAVIORAL_COHORTS_DATABASE_URL: 'postgres://posthog:posthog@db:5432/posthog' KAFKA_HOSTS: 'kafka:9092' REDIS_URL: 'redis://redis7:6379/' CLICKHOUSE_HOST: 'clickhouse' CLICKHOUSE_DATABASE: 'posthog' CLICKHOUSE_SECURE: 'false' CLICKHOUSE_VERIFY: 'false' ingestion-general: command: node nodejs/dist/index.js restart: on-failure environment: PLUGIN_SERVER_MODE: 'ingestion-v2-combined' DATABASE_URL: 'postgres://posthog:posthog@db:5432/posthog' PERSONS_DATABASE_URL: 'postgres://posthog:posthog@db:5432/posthog' BEHAVIORAL_COHORTS_DATABASE_URL: 'postgres://posthog:posthog@db:5432/posthog' KAFKA_HOSTS: 'kafka:9092' REDIS_URL: 'redis://redis7:6379/' CLICKHOUSE_HOST: 'clickhouse' CLICKHOUSE_DATABASE: 'posthog' CLICKHOUSE_SECURE: 'false' CLICKHOUSE_VERIFY: 'false' COOKIELESS_REDIS_HOST: redis7 COOKIELESS_REDIS_PORT: 6379 CDP_REDIS_HOST: redis7 ENCRYPTION_SALT_KEYS: '00beef0000beef0000beef0000beef00' # Combined mode consumes the events_plugin_ingestion_ai lane, where capture diverts # $ai_* events, so it owns AI blob offload. Without a bucket, buildAiBlobStore returns # null and multimodal payloads are stored inline instead of as phaiblob:// pointers. AI_BLOB_S3_BUCKET: ai-blobs AI_BLOB_S3_PREFIX: 'aio/' AI_BLOB_S3_ENDPOINT: 'http://objectstorage:19000' AI_BLOB_S3_REGION: us-east-1 AI_BLOB_S3_ACCESS_KEY_ID: object_storage_root_user AI_BLOB_S3_SECRET_ACCESS_KEY: object_storage_root_password AI_BLOB_OFFLOAD_TEAMS: '*' ingestion-sessionreplay: command: node nodejs/dist/index.js restart: on-failure environment: PLUGIN_SERVER_MODE: 'recordings-blob-ingestion-v2' DATABASE_URL: 'postgres://posthog:posthog@db:5432/posthog' KAFKA_HOSTS: 'kafka:9092' REDIS_URL: 'redis://redis7:6379/' recording-api: command: node nodejs/dist/index.js restart: on-failure environment: PLUGIN_SERVER_MODE: 'recording-api' DATABASE_URL: 'postgres://posthog:posthog@db:5432/posthog' REDIS_URL: 'redis://redis7:6379/' CLICKHOUSE_HOST: 'clickhouse' CLICKHOUSE_DATABASE: 'posthog' CLICKHOUSE_SECURE: 'false' CLICKHOUSE_VERIFY: 'false' SESSION_RECORDING_API_REDIS_HOST: redis7 SESSION_RECORDING_API_REDIS_PORT: 6379 ingestion-error-tracking: command: node nodejs/dist/index.js restart: on-failure environment: PLUGIN_SERVER_MODE: 'ingestion-errortracking' DATABASE_URL: 'postgres://posthog:posthog@db:5432/posthog' KAFKA_HOSTS: 'kafka:9092' REDIS_URL: 'redis://redis7:6379/' CDP_REDIS_HOST: redis7 ENCRYPTION_SALT_KEYS: '00beef0000beef0000beef0000beef00' ingestion-logs: command: node nodejs/dist/index.js restart: on-failure environment: PLUGIN_SERVER_MODE: 'ingestion-logs' DATABASE_URL: 'postgres://posthog:posthog@db:5432/posthog' KAFKA_HOSTS: 'kafka:9092' REDIS_URL: 'redis://redis7:6379/' LOGS_REDIS_HOST: redis7 # Hog log transformations: function health state lives on CDP redis, and # encrypted function inputs need the salt keys (same as error tracking above) CDP_REDIS_HOST: redis7 ENCRYPTION_SALT_KEYS: '00beef0000beef0000beef0000beef00' ingestion-traces: command: node nodejs/dist/index.js restart: on-failure environment: PLUGIN_SERVER_MODE: 'ingestion-traces' DATABASE_URL: 'postgres://posthog:posthog@db:5432/posthog' KAFKA_HOSTS: 'kafka:9092' REDIS_URL: 'redis://redis7:6379/' TRACES_REDIS_HOST: redis7 livestream: image: 'ghcr.io/posthog/posthog/livestream:master' restart: on-failure depends_on: kafka: condition: service_started migrate: <<: *worker command: sh -c " python manage.py migrate && python manage.py migrate_clickhouse && python manage.py run_async_migrations " restart: 'no' deploy: replicas: 0 asyncmigrationscheck: <<: *worker command: python manage.py run_async_migrations --check restart: 'no' deploy: replicas: 0 environment: <<: *worker_env SKIP_ASYNC_MIGRATIONS_SETUP: 0 # Temporal containers elasticsearch: environment: - cluster.routing.allocation.disk.threshold_enabled=true - cluster.routing.allocation.disk.watermark.low=512mb - cluster.routing.allocation.disk.watermark.high=256mb - cluster.routing.allocation.disk.watermark.flood_stage=128mb - discovery.type=single-node - ES_JAVA_OPTS=-Xms256m -Xmx256m - xpack.security.enabled=false image: elasticsearch:7.17.28 healthcheck: test: [ 'CMD-SHELL', 'curl -s http://localhost:9200/_cluster/health | grep -qE "\"status\":\"(green|yellow)\""', ] interval: 10s timeout: 10s retries: 30 start_period: 60s expose: - 9200 volumes: - /var/lib/elasticsearch/data temporal: restart: on-failure healthcheck: test: ['CMD-SHELL', 'nc -z $(hostname) 7233'] interval: 5s timeout: 5s retries: 30 start_period: 300s environment: - DB=postgres12 - DB_PORT=5432 - POSTGRES_USER=posthog - POSTGRES_PWD=posthog - POSTGRES_SEEDS=db - DYNAMIC_CONFIG_FILE_PATH=config/dynamicconfig/development-sql.yaml - ENABLE_ES=true - ES_SEEDS=elasticsearch - ES_VERSION=v7 image: temporalio/auto-setup:1.26.2 labels: kompose.volume.type: configMap depends_on: db: condition: service_healthy elasticsearch: condition: service_healthy temporal-admin-tools: environment: - TEMPORAL_CLI_ADDRESS=temporal:7233 image: temporalio/admin-tools:1.26.2 stdin_open: true tty: true temporal-ui: depends_on: - temporal environment: - TEMPORAL_ADDRESS=temporal:7233 - TEMPORAL_CORS_ORIGINS=http://localhost:3000 - TEMPORAL_CSRF_COOKIE_INSECURE=true - TEMPORAL_CODEC_ENDPOINT=http://localhost:8000 image: temporalio/ui:2.47.2 temporal-django-worker: <<: *worker command: ./bin/temporal-django-worker restart: on-failure environment: <<: *worker_env TEMPORAL_HOST: temporal # OpenSearch (LLM trace reverse index) opensearch: image: opensearchproject/opensearch:2.13.0 environment: - cluster.name=opensearch-cluster - node.name=opensearch-node-1 - discovery.type=single-node - DISABLE_SECURITY_PLUGIN=true - DISABLE_INSTALL_DEMO_CONFIG=true - OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m ulimits: memlock: soft: -1 hard: -1 nofile: soft: 65536 hard: 65536 healthcheck: test: [ 'CMD-SHELL', 'curl -s http://localhost:9200/_cluster/health | grep -qE "\"status\":\"(green|yellow)\""', ] interval: 10s timeout: 10s retries: 30 start_period: 60s expose: - 9200 volumes: - /usr/share/opensearch/data opensearch-dashboards: image: opensearchproject/opensearch-dashboards:2.13.0 environment: - OPENSEARCH_HOSTS=http://opensearch:9200 - DISABLE_SECURITY_DASHBOARDS_PLUGIN=true expose: - 5601 depends_on: opensearch: condition: service_healthy otel-collector: image: otel/opentelemetry-collector-contrib:0.142.0 command: [--config=/etc/otel-collector-config.yaml] user: '0:0' # have to run as root to read the docker logs volumes: # mount docker containers dir so we can ship the docker logs to local posthog - /var/lib/docker/containers:/var/lib/docker/containers:ro - /var/run/docker.sock:/var/run/docker.sock:ro depends_on: - jaeger networks: - otel_network # Cap the footprint: the collector parses every container's logs, so one # log-flooding container could otherwise peg many cores and balloon RAM. cpus: 2 mem_limit: 2g environment: # Keep Go's GC under mem_limit so pressure triggers collection, not an OOM kill. GOMEMLIMIT: 1750MiB logging: driver: json-file options: max-size: '50m' max-file: '3' jaeger: image: jaegertracing/all-in-one:1.76.0 networks: - otel_network dynamodb: # Session recording encryption keys live in DynamoDB on Cloud (see getKeyStore in # nodejs/src/ingestion/pipelines/sessionreplay/shared/keystore), so the keystore # integration test needs a real DynamoDB API to exercise its composite key and # conditional writes. -inMemory holds the tables in the JVM rather than in a file inside # the container, so a restart cannot resurrect a table from an interrupted run. -sharedDb # puts every credential and region pair in one table namespace; without it a client using # different credentials than the one that created a table sees an empty table list, which # -inMemory alone does not prevent. -disableTelemetry stops it reporting usage to AWS in # the background, which it does by default. image: amazon/dynamodb-local:3.3.1 command: ['-jar', 'DynamoDBLocal.jar', '-inMemory', '-sharedDb', '-disableTelemetry'] healthcheck: # Opening the port from bash rather than curling it, because DynamoDB answers GET / # with a 400 and `curl -f` treats that as failure. Without a healthcheck, # `docker compose up --wait` returns as soon as the JVM starts, before it listens. test: ['CMD-SHELL', 'exec 3<>/dev/tcp/localhost/8000'] interval: 5s timeout: 3s retries: 12 seaweedfs: # Default open-access mode; see the `objectstorage` service above for rationale # (including the >= 4.05 Content-Encoding pin). image: chrislusf/seaweedfs:4.29 entrypoint: - /bin/sh - -c - | /usr/bin/weed "$$@" & WEED_PID=$$! echo "Waiting for SeaweedFS to initialize..." while true; do sleep 5 echo "Checking if posthog bucket exists..." if echo "s3.bucket.list" | /usr/bin/weed shell -master=localhost:9333 2>&1 | grep -q "posthog"; then echo "Bucket posthog exists!" break fi echo "Bucket not found, attempting to create..." echo "s3.bucket.create -name posthog" | /usr/bin/weed shell -master=localhost:9333 2>&1 || true echo "Retrying in 5s..." done echo "SeaweedFS ready with posthog bucket" wait $$WEED_PID - -- # volume.max mirrors the objectstorage service above: SeaweedFS defaults to 8 # volume slots, which sustained recording ingestion exhausts long before the disk # is full — writes then fail ("no writable volumes") and the sessionreplay blob # flusher crash-loops on flush timeouts. command: ['server', '-s3', '-s3.port=8333', '-dir=/data', '-volume.max=1000'] duckgres: # DuckLake 0.4 + DuckDB 1.5.1, multi-arch (posthog/duckgres@5cfca88) image: ghcr.io/posthog/duckgres:5cfca88a52f366765fcdb122056e7ad3f038ec4f restart: on-failure volumes: - ./devenv/duckgres.yaml:/app/config.yaml:ro - duckgres-data:/app/data depends_on: db: condition: service_healthy objectstorage: # Wait for the bucket-bootstrap sentinel, not just the process: DuckLake # targets s3://ducklake-dev/, which SeaweedFS creates asynchronously after # startup. service_started would race the bootstrap loop (see the # objectstorage healthcheck rationale above). condition: service_healthy command: ['--config', '/app/config.yaml'] networks: otel_network: driver: bridge volumes: duckgres-data: redpanda-data: