Ctrl K
ComunidadGuías…

docker-compose.base.yml

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#
# 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:
            # TCP, because the entrypoint runs init scripts on a socket-only temporary server.
            test: ['CMD-SHELL', 'pg_isready -U posthog -h 127.0.0.1']
            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:-ghcr.io/posthog/clickhouse-posthog:26.9.5.2-posthog-aca5bdabb92f}
        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 1
            - --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\ns3.bucket.create -name email-attachments\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 "$$BUCKETS" | grep -q email-attachments \
                                && 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
            # Local Kafka keeps its default 1MB message limit, so cap AI events below it
            # to return `ai_event_too_big` instead of failing at the producer.
            AI_MAX_EVENT_BYTES: '900000'
            KAFKA_PRODUCER_MESSAGE_MAX_BYTES: '1000000'
            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: '<randomly generated secret key>'
            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'
            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'
            ETCD_ENDPOINTS: 'http://etcd:2379'
            ETCD_PREFIX: '/personhog/'
            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 with its visibility store in Postgres. Hobby extends this one, because it runs no
    # elasticsearch, and `extends` copies depends_on, so it cannot drop the dependency below.
    temporal-sql-visibility:
        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=false
        image: temporalio/auto-setup:1.26.2
        labels:
            kompose.volume.type: configMap
        depends_on:
            db:
                condition: service_healthy
    temporal:
        extends:
            service: temporal-sql-visibility
        environment:
            - ENABLE_ES=true
            - ES_SEEDS=elasticsearch
            - ES_VERSION=v7
        depends_on:
            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: