mirror of
https://github.com/PostHog/posthog.git
synced 2026-10-26 09:51:43 +08:00
831 lines
32 KiB
YAML
831 lines
32 KiB
YAML
#
|
|
# `docker-compose` file used ONLY for hobby deployments.
|
|
#
|
|
# Please take a look at https://posthog.com/docs/self-host/deploy/hobby
|
|
# for more info.
|
|
#
|
|
# PostHog has sunset support for self-hosted K8s deployments.
|
|
# See: https://posthog.com/blog/sunsetting-helm-support-posthog
|
|
#
|
|
|
|
x-logging: &default-logging
|
|
driver: json-file
|
|
options:
|
|
max-size: '50m'
|
|
max-file: '3'
|
|
|
|
# HyperCache's S3 tier for the Rust readers. With no endpoint the AWS SDK dials real S3 on
|
|
# every Redis miss and waits out its retries (~4s) before falling back to Postgres.
|
|
# Services using this also wait for objectstorage to be healthy: it registers these
|
|
# credentials after start and rejects them until then.
|
|
x-hypercache-s3: &hypercache-s3
|
|
OBJECT_STORAGE_ENDPOINT: http://objectstorage:19000
|
|
AWS_ACCESS_KEY_ID: 'object_storage_root_user'
|
|
AWS_SECRET_ACCESS_KEY: 'object_storage_root_password'
|
|
|
|
services:
|
|
db:
|
|
extends:
|
|
file: docker-compose.base.yml
|
|
service: db
|
|
image: ${DOCKER_REGISTRY_PREFIX:-}postgres:15.12-alpine
|
|
logging: *default-logging
|
|
volumes:
|
|
- postgres-data:/var/lib/postgresql/data
|
|
|
|
redis7:
|
|
extends:
|
|
file: docker-compose.base.yml
|
|
service: redis7
|
|
logging: *default-logging
|
|
volumes:
|
|
- redis7-data:/data
|
|
|
|
valkey:
|
|
extends:
|
|
file: docker-compose.base.yml
|
|
service: valkey
|
|
logging: *default-logging
|
|
|
|
clickhouse:
|
|
extends:
|
|
file: docker-compose.base.yml
|
|
service: clickhouse
|
|
restart: on-failure
|
|
logging: *default-logging
|
|
depends_on:
|
|
- kafka
|
|
- zookeeper
|
|
volumes:
|
|
- ./posthog/posthog/idl:/idl
|
|
- ./posthog/docker/clickhouse/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
|
|
- ./posthog/docker/clickhouse/config.xml:/etc/clickhouse-server/config.xml
|
|
- ./posthog/docker/clickhouse/config.d/default.xml:/etc/clickhouse-server/config.d/default.xml
|
|
- ./posthog/docker/clickhouse/users.xml:/etc/clickhouse-server/users.xml
|
|
- ./posthog/docker/clickhouse/user_defined_function.xml:/etc/clickhouse-server/user_defined_function.xml
|
|
- ./posthog/posthog/user_scripts:/var/lib/clickhouse/user_scripts
|
|
- clickhouse-data:/var/lib/clickhouse
|
|
|
|
zookeeper:
|
|
extends:
|
|
file: docker-compose.base.yml
|
|
service: zookeeper
|
|
logging: *default-logging
|
|
environment:
|
|
ZOO_AUTOPURGE_PURGEINTERVAL: 1
|
|
ZOO_AUTOPURGE_SNAPRETAINCOUNT: 3
|
|
volumes:
|
|
- zookeeper-datalog:/datalog
|
|
- zookeeper-data:/data
|
|
- zookeeper-logs:/logs
|
|
kafka:
|
|
extends:
|
|
file: docker-compose.base.yml
|
|
service: kafka
|
|
logging: *default-logging
|
|
depends_on:
|
|
- zookeeper
|
|
environment:
|
|
KAFKA_LOG_RETENTION_MS: 3600000
|
|
KAFKA_LOG_RETENTION_CHECK_INTERVAL_MS: 300000
|
|
KAFKA_LOG_RETENTION_HOURS: 1
|
|
volumes:
|
|
- kafka-data:/bitnami/kafka
|
|
|
|
worker:
|
|
extends:
|
|
file: docker-compose.base.yml
|
|
service: worker
|
|
logging: *default-logging
|
|
environment:
|
|
SITE_URL: https://$DOMAIN
|
|
SECRET_KEY: $POSTHOG_SECRET
|
|
OBJECT_STORAGE_ACCESS_KEY_ID: 'object_storage_root_user'
|
|
OBJECT_STORAGE_SECRET_ACCESS_KEY: 'object_storage_root_password'
|
|
OBJECT_STORAGE_ENDPOINT: http://objectstorage:19000
|
|
OBJECT_STORAGE_PUBLIC_ENDPOINT: https://$DOMAIN
|
|
SESSION_RECORDING_V2_S3_ENDPOINT: http://seaweedfs:8333
|
|
SESSION_RECORDING_V2_S3_ACCESS_KEY_ID: 'any'
|
|
SESSION_RECORDING_V2_S3_SECRET_ACCESS_KEY: 'any'
|
|
OBJECT_STORAGE_ENABLED: true
|
|
RECORDING_API_URL: http://recording-api:6738
|
|
INTERNAL_API_SECRET: ${INTERNAL_API_SECRET:-$POSTHOG_SECRET}
|
|
RECORDING_API_JWT_SECRET: ${RECORDING_API_JWT_SECRET:-}
|
|
ENCRYPTION_SALT_KEYS: $ENCRYPTION_SALT_KEYS
|
|
POSTHOG_SKIP_MIGRATION_CHECKS: '1'
|
|
PERSONHOG_ADDR: 'personhog-router:50052'
|
|
PERSONHOG_ENABLED: 'true'
|
|
BROWSERLESS_CDP_URL: 'ws://browserless:3000'
|
|
BROWSERLESS_TOKEN: ${BROWSERLESS_SECRET:-$POSTHOG_SECRET}
|
|
HEATMAP_BROWSERLESS_URL: 'http://browserless:3000'
|
|
HEATMAP_BROWSERLESS_TOKEN: ${BROWSERLESS_SECRET:-$POSTHOG_SECRET}
|
|
HEATMAP_BROWSERLESS_SCREENSHOT_COOKIES_ENABLED: ${HEATMAP_BROWSERLESS_SCREENSHOT_COOKIES_ENABLED:-false}
|
|
# Point Django at the Rust feature-flags container; the setting
|
|
# otherwise defaults to localhost:3001.
|
|
FEATURE_FLAGS_SERVICE_URL: 'http://feature-flags:3001'
|
|
# Must match INTERNAL_REQUEST_TOKEN on the feature-flags service; flag test
|
|
# evaluation otherwise fails with "Internal request token not configured".
|
|
INTERNAL_REQUEST_TOKEN: ${INTERNAL_REQUEST_TOKEN:-}
|
|
# The logs product uses a dedicated ClickHouse cluster config (separate
|
|
# from CLICKHOUSE_HOST) whose host defaults to localhost and whose SECURE
|
|
# defaults to true outside DEBUG. Point it at the stack's ClickHouse over
|
|
# plaintext; the other logs-cluster settings already default correctly
|
|
# (native port 9000, user "default", empty password).
|
|
CLICKHOUSE_LOGS_CLUSTER_HOST: 'clickhouse'
|
|
CLICKHOUSE_LOGS_CLUSTER_SECURE: 'false'
|
|
# AI provider keys for PostHog's AI features. Blank unless set in .env.
|
|
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
|
|
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
|
|
OPENAI_BASE_URL: ${OPENAI_BASE_URL:-https://api.openai.com/v1}
|
|
OPENAI_MODEL: ${OPENAI_MODEL:-gpt-4.1-mini}
|
|
image: $REGISTRY_URL:$POSTHOG_APP_TAG
|
|
depends_on:
|
|
db:
|
|
condition: service_healthy
|
|
redis7:
|
|
condition: service_healthy
|
|
clickhouse:
|
|
condition: service_started
|
|
kafka:
|
|
condition: service_healthy
|
|
web:
|
|
condition: service_started
|
|
personhog-router:
|
|
condition: service_started
|
|
browserless:
|
|
condition: service_started
|
|
web:
|
|
extends:
|
|
file: docker-compose.base.yml
|
|
service: web
|
|
command: /compose/start
|
|
logging: *default-logging
|
|
volumes:
|
|
- ./compose:/compose
|
|
image: $REGISTRY_URL:$POSTHOG_APP_TAG
|
|
environment:
|
|
SITE_URL: https://$DOMAIN
|
|
LIVESTREAM_HOST: 'https://${DOMAIN}/livestream'
|
|
SECRET_KEY: $POSTHOG_SECRET
|
|
OBJECT_STORAGE_ACCESS_KEY_ID: 'object_storage_root_user'
|
|
OBJECT_STORAGE_SECRET_ACCESS_KEY: 'object_storage_root_password'
|
|
SESSION_RECORDING_V2_S3_ACCESS_KEY_ID: 'any'
|
|
SESSION_RECORDING_V2_S3_SECRET_ACCESS_KEY: 'any'
|
|
OBJECT_STORAGE_ENDPOINT: http://objectstorage:19000
|
|
OBJECT_STORAGE_PUBLIC_ENDPOINT: https://$DOMAIN
|
|
SESSION_RECORDING_V2_S3_ENDPOINT: http://seaweedfs:8333
|
|
OBJECT_STORAGE_ENABLED: true
|
|
RECORDING_API_URL: http://recording-api:6738
|
|
INTERNAL_API_SECRET: ${INTERNAL_API_SECRET:-$POSTHOG_SECRET}
|
|
RECORDING_API_JWT_SECRET: ${RECORDING_API_JWT_SECRET:-}
|
|
ENCRYPTION_SALT_KEYS: $ENCRYPTION_SALT_KEYS
|
|
OTEL_SERVICE_NAME: 'posthog'
|
|
OTEL_EXPORTER_OTLP_ENDPOINT: ''
|
|
OTEL_SDK_DISABLED: 'true'
|
|
OPT_OUT_CAPTURE: ${OPT_OUT_CAPTURE:-false}
|
|
# Blank sends no CSP reports. Point it at this instance's own /report/ endpoint to
|
|
# collect them yourself.
|
|
CSP_REPORT_ENDPOINT: ${CSP_REPORT_ENDPOINT:-}
|
|
DEPLOYMENT: 'hobby'
|
|
PERSONHOG_ADDR: 'personhog-router:50052'
|
|
PERSONHOG_ENABLED: 'true'
|
|
# Point Django at the Rust feature-flags container; the setting
|
|
# otherwise defaults to localhost:3001.
|
|
FEATURE_FLAGS_SERVICE_URL: 'http://feature-flags:3001'
|
|
# Must match INTERNAL_REQUEST_TOKEN on the feature-flags service; flag test
|
|
# evaluation otherwise fails with "Internal request token not configured".
|
|
INTERNAL_REQUEST_TOKEN: ${INTERNAL_REQUEST_TOKEN:-}
|
|
# The logs product uses a dedicated ClickHouse cluster config (separate
|
|
# from CLICKHOUSE_HOST) whose host defaults to localhost and whose SECURE
|
|
# defaults to true outside DEBUG. Point it at the stack's ClickHouse over
|
|
# plaintext; the other logs-cluster settings already default correctly
|
|
# (native port 9000, user "default", empty password).
|
|
CLICKHOUSE_LOGS_CLUSTER_HOST: 'clickhouse'
|
|
CLICKHOUSE_LOGS_CLUSTER_SECURE: 'false'
|
|
# AI provider keys for PostHog's AI features. Blank unless set in .env.
|
|
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
|
|
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
|
|
OPENAI_BASE_URL: ${OPENAI_BASE_URL:-https://api.openai.com/v1}
|
|
OPENAI_MODEL: ${OPENAI_MODEL:-gpt-4.1-mini}
|
|
depends_on:
|
|
- db
|
|
- redis7
|
|
- clickhouse
|
|
- kafka
|
|
- objectstorage
|
|
- seaweedfs
|
|
- personhog-router
|
|
|
|
plugins:
|
|
extends:
|
|
file: docker-compose.base.yml
|
|
service: plugins
|
|
image: ${REGISTRY_URL}-node:${POSTHOG_NODE_TAG:-latest}
|
|
logging: *default-logging
|
|
environment:
|
|
SITE_URL: https://$DOMAIN
|
|
SECRET_KEY: $POSTHOG_SECRET
|
|
OBJECT_STORAGE_ACCESS_KEY_ID: 'object_storage_root_user'
|
|
OBJECT_STORAGE_SECRET_ACCESS_KEY: 'object_storage_root_password'
|
|
OBJECT_STORAGE_ENDPOINT: http://objectstorage:19000
|
|
OBJECT_STORAGE_PUBLIC_ENDPOINT: https://$DOMAIN
|
|
OBJECT_STORAGE_ENABLED: true
|
|
CDP_REDIS_HOST: redis7
|
|
CDP_REDIS_PORT: 6379
|
|
LOGS_REDIS_HOST: redis7
|
|
LOGS_REDIS_PORT: 6379
|
|
LOGS_REDIS_TLS: 'false'
|
|
TRACES_REDIS_HOST: redis7
|
|
TRACES_REDIS_PORT: 6379
|
|
TRACES_REDIS_TLS: 'false'
|
|
# Every CDP process dual-writes to Valkey and refuses to start without a host.
|
|
CDP_VALKEY_HOST: valkey
|
|
CDP_VALKEY_PORT: 6379
|
|
ENCRYPTION_SALT_KEYS: $ENCRYPTION_SALT_KEYS
|
|
PERSONS_DATABASE_URL: 'postgres://posthog:posthog@db:5432/posthog'
|
|
BEHAVIORAL_COHORTS_DATABASE_URL: 'postgres://posthog:posthog@db:5432/posthog'
|
|
OTEL_EXPORTER_OTLP_ENDPOINT: ''
|
|
volumes:
|
|
- ./share:/share
|
|
depends_on:
|
|
- db
|
|
- redis7
|
|
- valkey
|
|
- clickhouse
|
|
- kafka
|
|
- objectstorage
|
|
- seaweedfs
|
|
|
|
ingestion-general:
|
|
extends:
|
|
file: docker-compose.base.yml
|
|
service: ingestion-general
|
|
image: ${REGISTRY_URL}-node:${POSTHOG_NODE_TAG:-latest}
|
|
logging: *default-logging
|
|
environment:
|
|
PERSONS_DATABASE_URL: 'postgres://posthog:posthog@db:5432/posthog'
|
|
BEHAVIORAL_COHORTS_DATABASE_URL: 'postgres://posthog:posthog@db:5432/posthog'
|
|
ENCRYPTION_SALT_KEYS: $ENCRYPTION_SALT_KEYS
|
|
# The combined-mode AI consumer reads persons through PersonHog
|
|
PERSONHOG_ADDR: 'personhog-router:50052'
|
|
PERSONHOG_ENABLED: 'true'
|
|
depends_on:
|
|
- db
|
|
- redis7
|
|
- clickhouse
|
|
- kafka
|
|
# The combined AI consumer's blob store healthchecks the ai-blobs bucket at startup
|
|
- objectstorage
|
|
- personhog-router
|
|
|
|
ingestion-sessionreplay:
|
|
extends:
|
|
file: docker-compose.base.yml
|
|
service: ingestion-sessionreplay
|
|
image: ${REGISTRY_URL}-node:${POSTHOG_NODE_TAG:-latest}
|
|
logging: *default-logging
|
|
environment:
|
|
SESSION_RECORDING_V2_S3_ACCESS_KEY_ID: 'any'
|
|
SESSION_RECORDING_V2_S3_SECRET_ACCESS_KEY: 'any'
|
|
SESSION_RECORDING_V2_S3_TIMEOUT_MS: 120000
|
|
SESSION_RECORDING_V2_S3_ENDPOINT: http://seaweedfs:8333
|
|
ENCRYPTION_SALT_KEYS: $ENCRYPTION_SALT_KEYS
|
|
depends_on:
|
|
- db
|
|
- redis7
|
|
- kafka
|
|
- seaweedfs
|
|
|
|
recording-api:
|
|
extends:
|
|
file: docker-compose.base.yml
|
|
service: recording-api
|
|
image: ${REGISTRY_URL}-node:${POSTHOG_NODE_TAG:-latest}
|
|
logging: *default-logging
|
|
environment:
|
|
# Read side of the session recording V2 blob store. Must point at the same
|
|
# SeaweedFS the ingestion-sessionreplay writer uses — without these the
|
|
# service falls back to its localhost:8333 default, so every playback
|
|
# request 500s with ECONNREFUSED ("We're unable to play this recording").
|
|
SESSION_RECORDING_V2_S3_ACCESS_KEY_ID: 'any'
|
|
SESSION_RECORDING_V2_S3_SECRET_ACCESS_KEY: 'any'
|
|
SESSION_RECORDING_V2_S3_ENDPOINT: http://seaweedfs:8333
|
|
ENCRYPTION_SALT_KEYS: $ENCRYPTION_SALT_KEYS
|
|
# Both must resolve to the same values as on the web, worker and temporal-django-worker
|
|
# services. Those services sign a team-scoped token with RECORDING_API_JWT_SECRET, and
|
|
# this service verifies it. They also send INTERNAL_API_SECRET as X-Internal-Api-Secret,
|
|
# which this service still accepts in place of a token.
|
|
RECORDING_API_JWT_SECRET: ${RECORDING_API_JWT_SECRET:-}
|
|
INTERNAL_API_SECRET: ${INTERNAL_API_SECRET:-$POSTHOG_SECRET}
|
|
depends_on:
|
|
- db
|
|
- redis7
|
|
- clickhouse
|
|
- seaweedfs
|
|
|
|
ingestion-error-tracking:
|
|
extends:
|
|
file: docker-compose.base.yml
|
|
service: ingestion-error-tracking
|
|
image: ${REGISTRY_URL}-node:${POSTHOG_NODE_TAG:-latest}
|
|
logging: *default-logging
|
|
environment:
|
|
ENCRYPTION_SALT_KEYS: $ENCRYPTION_SALT_KEYS
|
|
ERROR_TRACKING_CYMBAL_BASE_URL: 'http://cymbal:3302'
|
|
depends_on:
|
|
- db
|
|
- redis7
|
|
- kafka
|
|
|
|
ingestion-logs:
|
|
extends:
|
|
file: docker-compose.base.yml
|
|
service: ingestion-logs
|
|
image: ${REGISTRY_URL}-node:${POSTHOG_NODE_TAG:-latest}
|
|
logging: *default-logging
|
|
environment:
|
|
LOGS_REDIS_TLS: 'false'
|
|
depends_on:
|
|
- db
|
|
- redis7
|
|
- kafka
|
|
|
|
ingestion-traces:
|
|
extends:
|
|
file: docker-compose.base.yml
|
|
service: ingestion-traces
|
|
image: ${REGISTRY_URL}-node:${POSTHOG_NODE_TAG:-latest}
|
|
logging: *default-logging
|
|
environment:
|
|
TRACES_REDIS_TLS: 'false'
|
|
depends_on:
|
|
- db
|
|
- redis7
|
|
- kafka
|
|
|
|
proxy:
|
|
extends:
|
|
file: docker-compose.base.yml
|
|
service: proxy
|
|
logging: *default-logging
|
|
ports:
|
|
- '80:80'
|
|
- '443:443'
|
|
volumes:
|
|
- caddy-data:/data
|
|
- caddy-config:/config
|
|
environment:
|
|
CADDY_TLS_BLOCK: $TLS_BLOCK
|
|
CADDY_HOST: '$DOMAIN, http://, https://'
|
|
depends_on:
|
|
- web
|
|
- livestream
|
|
objectstorage:
|
|
# SeaweedFS on the same objectstorage:19000 endpoint and credentials this service
|
|
# served when it ran MinIO, so no other service in this file changes.
|
|
extends:
|
|
file: docker-compose.base.yml
|
|
service: objectstorage
|
|
restart: on-failure
|
|
logging: *default-logging
|
|
# The dev/CI bootstrap this replaces also registers an `anonymous` identity with
|
|
# Admin rights. Hobby must not, because Caddy proxies /posthog/* straight to this
|
|
# service: an anonymous identity opens every object to the internet. With only the
|
|
# posthog identity, SeaweedFS rejects unsigned requests and the presigned URLs
|
|
# Django hands out keep working. There is no duckgres here, so there is no
|
|
# ducklake-dev bucket either. The loop holds no shell variables on purpose:
|
|
# bin/upgrade-hobby pipes this file through envsubst, which collapses a
|
|
# doubled-dollar variable reference to a bare dollar sign.
|
|
entrypoint:
|
|
- /bin/sh
|
|
- -c
|
|
- |
|
|
(
|
|
while true; do
|
|
printf 's3.configure -access_key=object_storage_root_user -secret_key=object_storage_root_password -user=posthog -actions=Admin -apply\ns3.bucket.create -name posthog\ns3.bucket.create -name ai-blobs\n' \
|
|
| /usr/bin/weed shell -master=localhost:19001 >/dev/null 2>&1 || true
|
|
if echo 's3.bucket.list' | /usr/bin/weed shell -master=localhost:19001 2>/dev/null | grep -q ai-blobs \
|
|
&& echo 's3.configure' | /usr/bin/weed shell -master=localhost:19001 2>/dev/null | grep -q object_storage_root_user; then
|
|
touch /tmp/objectstorage-ready
|
|
echo "Objectstorage SeaweedFS ready with required buckets and identities"
|
|
break
|
|
fi
|
|
sleep 1
|
|
done
|
|
) &
|
|
exec /usr/bin/weed "$$@"
|
|
- --
|
|
# A new volume. SeaweedFS cannot read the MinIO layout, so the old objects stay on
|
|
# the `objectstorage` volume until the operator copies them across with
|
|
# bin/migrate-storage-hobby or deletes the volume.
|
|
volumes:
|
|
- objectstorage-data:/data
|
|
ports:
|
|
# Localhost only. MinIO published this port on every interface.
|
|
- '127.0.0.1:19000:19000'
|
|
|
|
seaweedfs:
|
|
extends:
|
|
file: docker-compose.base.yml
|
|
service: seaweedfs
|
|
container_name: '${SEAWEEDFS_DOCKER_NAME:-seaweedfs-main}'
|
|
restart: on-failure
|
|
logging: *default-logging
|
|
ports:
|
|
- '127.0.0.1:8333:8333' # S3 API
|
|
- '127.0.0.1:9333:9333' # Master server
|
|
volumes:
|
|
- seaweedfs:/data
|
|
healthcheck:
|
|
test:
|
|
[
|
|
'CMD',
|
|
'sh',
|
|
'-c',
|
|
"echo 's3.bucket.list' | /usr/bin/weed shell -master=localhost:9333 2>&1 | grep -q posthog",
|
|
]
|
|
interval: 5s
|
|
timeout: 10s
|
|
retries: 30
|
|
start_period: 10s
|
|
|
|
browserless:
|
|
# Renders image exports and heatmap screenshots (no browser ships in the PostHog image).
|
|
image: ghcr.io/browserless/chromium:v2.51.2
|
|
restart: on-failure
|
|
logging: *default-logging
|
|
environment:
|
|
TOKEN: ${BROWSERLESS_SECRET:-$POSTHOG_SECRET}
|
|
LOG_LEVEL: warn
|
|
DEBUG: '-*'
|
|
|
|
asyncmigrationscheck:
|
|
extends:
|
|
file: docker-compose.base.yml
|
|
service: asyncmigrationscheck
|
|
image: $REGISTRY_URL:$POSTHOG_APP_TAG
|
|
logging: *default-logging
|
|
environment:
|
|
SITE_URL: https://$DOMAIN
|
|
SECRET_KEY: $POSTHOG_SECRET
|
|
SKIP_ASYNC_MIGRATIONS_SETUP: 0
|
|
|
|
# Temporal containers
|
|
temporal:
|
|
extends:
|
|
file: docker-compose.base.yml
|
|
service: temporal
|
|
logging: *default-logging
|
|
environment:
|
|
- ENABLE_ES=false
|
|
ports:
|
|
- '127.0.0.1:7233:7233'
|
|
volumes:
|
|
- ./posthog/docker/temporal/dynamicconfig:/etc/temporal/config/dynamicconfig
|
|
elasticsearch:
|
|
extends:
|
|
file: docker-compose.base.yml
|
|
service: elasticsearch
|
|
logging: *default-logging
|
|
temporal-admin-tools:
|
|
extends:
|
|
file: docker-compose.base.yml
|
|
service: temporal-admin-tools
|
|
logging: *default-logging
|
|
depends_on:
|
|
- temporal
|
|
temporal-ui:
|
|
extends:
|
|
file: docker-compose.base.yml
|
|
service: temporal-ui
|
|
logging: *default-logging
|
|
ports:
|
|
- '127.0.0.1:8081:8080'
|
|
depends_on:
|
|
temporal:
|
|
condition: service_started
|
|
db:
|
|
condition: service_healthy
|
|
temporal-django-worker:
|
|
command: /compose/temporal-django-worker
|
|
extends:
|
|
file: docker-compose.base.yml
|
|
service: temporal-django-worker
|
|
logging: *default-logging
|
|
volumes:
|
|
- ./compose:/compose
|
|
image: $REGISTRY_URL:$POSTHOG_APP_TAG
|
|
environment:
|
|
SITE_URL: https://$DOMAIN
|
|
SECRET_KEY: $POSTHOG_SECRET
|
|
BROWSERLESS_CDP_URL: 'ws://browserless:3000'
|
|
BROWSERLESS_TOKEN: ${BROWSERLESS_SECRET:-$POSTHOG_SECRET}
|
|
HEATMAP_BROWSERLESS_URL: 'http://browserless:3000'
|
|
HEATMAP_BROWSERLESS_TOKEN: ${BROWSERLESS_SECRET:-$POSTHOG_SECRET}
|
|
HEATMAP_BROWSERLESS_SCREENSHOT_COOKIES_ENABLED: ${HEATMAP_BROWSERLESS_SCREENSHOT_COOKIES_ENABLED:-false}
|
|
# This worker runs the recording deletion Temporal activities, which call
|
|
# recording-api. Without the URL and a credential the calls go out unauthenticated
|
|
# and recording-api rejects them with a 401.
|
|
RECORDING_API_URL: http://recording-api:6738
|
|
INTERNAL_API_SECRET: ${INTERNAL_API_SECRET:-$POSTHOG_SECRET}
|
|
RECORDING_API_JWT_SECRET: ${RECORDING_API_JWT_SECRET:-}
|
|
PERSONHOG_ADDR: 'personhog-router:50052'
|
|
PERSONHOG_ENABLED: 'true'
|
|
# Point Django at the Rust feature-flags container; the setting
|
|
# otherwise defaults to localhost:3001.
|
|
FEATURE_FLAGS_SERVICE_URL: 'http://feature-flags:3001'
|
|
# Must match INTERNAL_REQUEST_TOKEN on the feature-flags service; flag test
|
|
# evaluation otherwise fails with "Internal request token not configured".
|
|
INTERNAL_REQUEST_TOKEN: ${INTERNAL_REQUEST_TOKEN:-}
|
|
# The logs product uses a dedicated ClickHouse cluster config (separate
|
|
# from CLICKHOUSE_HOST) whose host defaults to localhost and whose SECURE
|
|
# defaults to true outside DEBUG. Point it at the stack's ClickHouse over
|
|
# plaintext; the other logs-cluster settings already default correctly
|
|
# (native port 9000, user "default", empty password). This worker runs
|
|
# the logs alert-check Temporal activities, which query ClickHouse under
|
|
# Workload.LOGS.
|
|
CLICKHOUSE_LOGS_CLUSTER_HOST: 'clickhouse'
|
|
CLICKHOUSE_LOGS_CLUSTER_SECURE: 'false'
|
|
# AI provider keys for PostHog's AI features. Blank unless set in .env.
|
|
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
|
|
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
|
|
OPENAI_BASE_URL: ${OPENAI_BASE_URL:-https://api.openai.com/v1}
|
|
OPENAI_MODEL: ${OPENAI_MODEL:-gpt-4.1-mini}
|
|
depends_on:
|
|
- db
|
|
- redis7
|
|
- clickhouse
|
|
- kafka
|
|
- objectstorage
|
|
- seaweedfs
|
|
- temporal
|
|
- browserless
|
|
- personhog-router
|
|
|
|
capture:
|
|
build:
|
|
context: ./posthog/rust
|
|
extends:
|
|
file: docker-compose.base.yml
|
|
service: capture
|
|
logging: *default-logging
|
|
|
|
capture-ai:
|
|
build:
|
|
context: ./posthog/rust
|
|
extends:
|
|
file: docker-compose.base.yml
|
|
service: capture-ai
|
|
logging: *default-logging
|
|
|
|
capture-logs:
|
|
build:
|
|
context: ./posthog/rust
|
|
extends:
|
|
file: docker-compose.base.yml
|
|
service: capture-logs
|
|
logging: *default-logging
|
|
|
|
replay-capture:
|
|
build:
|
|
context: ./posthog/rust
|
|
extends:
|
|
file: docker-compose.base.yml
|
|
service: replay-capture
|
|
logging: *default-logging
|
|
|
|
property-defs-rs:
|
|
build:
|
|
context: ./posthog/rust
|
|
extends:
|
|
file: docker-compose.base.yml
|
|
service: property-defs-rs
|
|
logging: *default-logging
|
|
depends_on:
|
|
kafka-init:
|
|
condition: service_completed_successfully
|
|
db:
|
|
condition: service_healthy
|
|
|
|
property-defs-ai:
|
|
build:
|
|
context: ./posthog/rust
|
|
extends:
|
|
file: docker-compose.base.yml
|
|
service: property-defs-rs
|
|
logging: *default-logging
|
|
environment:
|
|
KAFKA_CONSUMER_TOPIC: clickhouse_ai_events_json
|
|
KAFKA_CONSUMER_GROUP: property-defs-ai
|
|
depends_on:
|
|
kafka-init:
|
|
condition: service_completed_successfully
|
|
db:
|
|
condition: service_healthy
|
|
|
|
livestream:
|
|
extends:
|
|
file: docker-compose.base.yml
|
|
service: livestream
|
|
logging: *default-logging
|
|
environment:
|
|
- LIVESTREAM_JWT_SECRET=${POSTHOG_SECRET}
|
|
volumes:
|
|
- ./posthog/docker/livestream/configs-hobby.yml:/configs/configs.yml
|
|
|
|
personhog-replica:
|
|
build:
|
|
context: ./posthog/rust
|
|
extends:
|
|
file: docker-compose.base.yml
|
|
service: personhog-replica
|
|
logging: *default-logging
|
|
environment:
|
|
PRIMARY_DATABASE_URL: 'postgres://posthog:posthog@db:5432/posthog'
|
|
depends_on:
|
|
db:
|
|
condition: service_healthy
|
|
|
|
personhog-router:
|
|
build:
|
|
context: ./posthog/rust
|
|
extends:
|
|
file: docker-compose.base.yml
|
|
service: personhog-router
|
|
logging: *default-logging
|
|
depends_on:
|
|
- personhog-replica
|
|
|
|
feature-flags:
|
|
build:
|
|
context: ./posthog/rust
|
|
extends:
|
|
file: docker-compose.base.yml
|
|
service: feature-flags
|
|
logging: *default-logging
|
|
environment:
|
|
<<: *hypercache-s3
|
|
# Shared secret for authenticated Django → flags calls (flag test evaluation);
|
|
# must match INTERNAL_REQUEST_TOKEN on web and worker. The installers generate it.
|
|
# Never fall back to POSTHOG_SECRET: this container faces the internet, and that
|
|
# value is Django's SECRET_KEY.
|
|
INTERNAL_REQUEST_TOKEN: ${INTERNAL_REQUEST_TOKEN:-}
|
|
depends_on:
|
|
db:
|
|
condition: service_started
|
|
redis7:
|
|
condition: service_started
|
|
objectstorage:
|
|
condition: service_healthy
|
|
|
|
hypercache-server:
|
|
build:
|
|
context: ./posthog/rust
|
|
extends:
|
|
file: docker-compose.base.yml
|
|
service: hypercache-server
|
|
logging: *default-logging
|
|
environment: *hypercache-s3
|
|
depends_on:
|
|
redis7:
|
|
condition: service_started
|
|
objectstorage:
|
|
condition: service_healthy
|
|
|
|
kafka-init:
|
|
image: docker.io/redpandadata/redpanda:v25.1.9
|
|
entrypoint: /bin/sh
|
|
logging: *default-logging
|
|
command:
|
|
- -c
|
|
- |
|
|
set -x
|
|
echo "Waiting for Kafka broker to accept connections..."
|
|
TIMEOUT=60
|
|
ELAPSED=0
|
|
# Use rpk topic list as the readiness check - it's a simple operation
|
|
# that confirms the broker is accepting Kafka protocol requests
|
|
until rpk topic list --brokers kafka:9092 2>/dev/null; do
|
|
echo "Kafka broker not ready yet (elapsed: $${ELAPSED}s)..."
|
|
sleep 2
|
|
ELAPSED=$$((ELAPSED + 2))
|
|
if [ $$ELAPSED -ge $$TIMEOUT ]; then
|
|
echo "Timeout waiting for Kafka broker after $${TIMEOUT}s"
|
|
echo "Final attempt to list topics:"
|
|
rpk topic list --brokers kafka:9092 || true
|
|
exit 1
|
|
fi
|
|
done
|
|
echo "Kafka broker is accepting requests, creating topics..."
|
|
# The topic list is docker/kafka/topics.txt, mounted below, shared with
|
|
# docker-compose.dev.yml so the two stacks cannot drift apart. That header
|
|
# explains what needs pre-creating.
|
|
# Note: $$ escapes $ for docker-compose variable substitution
|
|
TOPICS=$$(grep -vE '^[[:space:]]*#|^[[:space:]]*$$' /topics.txt)
|
|
for topic in $$TOPICS; do
|
|
if rpk topic create "$$topic" --brokers kafka:9092 -p 1 -r 1 2>&1; then
|
|
echo "Topic $$topic created successfully"
|
|
else
|
|
if rpk topic list --brokers kafka:9092 | awk '{print $$1}' | grep -qx "$$topic"; then
|
|
echo "Topic $$topic already exists, continuing"
|
|
else
|
|
echo "Failed to create topic $$topic"
|
|
exit 1
|
|
fi
|
|
fi
|
|
done
|
|
echo "Final topic list:"
|
|
rpk topic list --brokers kafka:9092
|
|
echo "Topics ready"
|
|
volumes:
|
|
- ./posthog/docker/kafka/topics.txt:/topics.txt:ro
|
|
depends_on:
|
|
kafka:
|
|
condition: service_healthy
|
|
|
|
cymbal:
|
|
image: ghcr.io/posthog/posthog/cymbal:master
|
|
build:
|
|
context: ./posthog/rust
|
|
args:
|
|
BIN: cymbal
|
|
restart: on-failure
|
|
logging: *default-logging
|
|
volumes:
|
|
- ./share:/share
|
|
environment:
|
|
KAFKA_HOSTS: 'kafka:9092'
|
|
OBJECT_STORAGE_BUCKET: posthog
|
|
OBJECT_STORAGE_ACCESS_KEY_ID: 'any'
|
|
OBJECT_STORAGE_SECRET_ACCESS_KEY: 'any'
|
|
OBJECT_STORAGE_ENDPOINT: 'http://seaweedfs:8333'
|
|
OBJECT_STORAGE_FORCE_PATH_STYLE: 'true'
|
|
BIND_HOST: '0.0.0.0'
|
|
BIND_PORT: '3302'
|
|
DATABASE_URL: 'postgres://posthog:posthog@db:5432/posthog'
|
|
PERSONS_URL: 'postgres://posthog:posthog@db:5432/posthog'
|
|
MAXMIND_DB_PATH: '/share/GeoLite2-City.mmdb'
|
|
REDIS_URL: 'redis://redis7:6379/'
|
|
ISSUE_BUCKETS_REDIS_URL: 'redis://redis7:6379/'
|
|
RUST_LOG: 'info'
|
|
# Processing offloads all symbol resolution to cymbal-resolution over gRPC. Both are
|
|
# mandatory: startup aborts when either is empty, so omitting them crash-loops cymbal.
|
|
CYMBAL_REMOTE_RESOLUTION_HOST: 'cymbal-resolution'
|
|
INTERNAL_API_SECRET: $POSTHOG_SECRET
|
|
depends_on:
|
|
kafka-init:
|
|
condition: service_completed_successfully
|
|
seaweedfs:
|
|
condition: service_healthy
|
|
db:
|
|
condition: service_healthy
|
|
redis7:
|
|
condition: service_healthy
|
|
cymbal-resolution:
|
|
condition: service_started
|
|
|
|
# The cymbal binary in resolution mode: the gRPC symbol-resolution service that cymbal's
|
|
# processing mode calls. Needs only Postgres and object storage — no Kafka or Redis — hence
|
|
# the narrower env than cymbal above.
|
|
cymbal-resolution:
|
|
image: ghcr.io/posthog/posthog/cymbal:master
|
|
build:
|
|
context: ./posthog/rust
|
|
args:
|
|
BIN: cymbal
|
|
restart: on-failure
|
|
logging: *default-logging
|
|
environment:
|
|
CYMBAL_MODE: resolution
|
|
GRPC_ADDRESS: '0.0.0.0:50061'
|
|
OBJECT_STORAGE_BUCKET: posthog
|
|
OBJECT_STORAGE_ACCESS_KEY_ID: 'any'
|
|
OBJECT_STORAGE_SECRET_ACCESS_KEY: 'any'
|
|
OBJECT_STORAGE_ENDPOINT: 'http://seaweedfs:8333'
|
|
OBJECT_STORAGE_FORCE_PATH_STYLE: 'true'
|
|
DATABASE_URL: 'postgres://posthog:posthog@db:5432/posthog'
|
|
INTERNAL_API_SECRET: $POSTHOG_SECRET
|
|
RUST_LOG: 'info'
|
|
depends_on:
|
|
seaweedfs:
|
|
condition: service_healthy
|
|
db:
|
|
condition: service_healthy
|
|
|
|
networks:
|
|
otel_network:
|
|
|
|
volumes:
|
|
zookeeper-data:
|
|
zookeeper-datalog:
|
|
zookeeper-logs:
|
|
objectstorage-data:
|
|
seaweedfs:
|
|
postgres-data:
|
|
clickhouse-data:
|
|
caddy-data:
|
|
caddy-config:
|
|
redis7-data:
|
|
kafka-data:
|
|
redpanda-data:
|