docker-compose.base.yml
Rama master · réplica de lectura del repositorio original.
#
# 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: