The catalog in server/containers had drifted from the VPS. Eleven compose
files and twelve units no longer matched what runs, and two running
services, Gbrown_Agent and BillTracker, were missing. server/vps is now a
copy of the live files, and panama-server reads from it.
server/home holds the home server's services in the same shape. They still
run under Docker, so the podman units there are drafts for a later move, and
nothing installs from that tree. PostHog, Sentry and Plausible are checkouts
of upstream repositories. Only their local files are kept, next to an
UPSTREAM file naming the repository and the exact commit.
Every secret in both trees is a ${VAR} reference named in .env.example, or
CHANGE_ME in files that are not compose files. The live servers still hold
some of those values inline, so a live file can differ from its copy on
those lines alone. Existing .env.example files keep their comments, with new
keys appended at the end.
The ignore rules and the secrets contract now cover both trees, plus the
volume/ and volumes/ directories the home server uses.
768 lines
30 KiB
YAML
768 lines
30 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'
|
|
|
|
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: ${SECRET_KEY}
|
|
OBJECT_STORAGE_ACCESS_KEY_ID: ${OBJECT_STORAGE_ACCESS_KEY_ID}
|
|
OBJECT_STORAGE_SECRET_ACCESS_KEY: ${OBJECT_STORAGE_SECRET_ACCESS_KEY}
|
|
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: ${SESSION_RECORDING_V2_S3_ACCESS_KEY_ID}
|
|
SESSION_RECORDING_V2_S3_SECRET_ACCESS_KEY: ${SESSION_RECORDING_V2_S3_SECRET_ACCESS_KEY}
|
|
OBJECT_STORAGE_ENABLED: true
|
|
RECORDING_API_URL: http://recording-api:6738
|
|
INTERNAL_API_SECRET: ${INTERNAL_API_SECRET:-$POSTHOG_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'
|
|
# 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:-}
|
|
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: ${SECRET_KEY}
|
|
OBJECT_STORAGE_ACCESS_KEY_ID: ${OBJECT_STORAGE_ACCESS_KEY_ID}
|
|
OBJECT_STORAGE_SECRET_ACCESS_KEY: ${OBJECT_STORAGE_SECRET_ACCESS_KEY}
|
|
SESSION_RECORDING_V2_S3_ACCESS_KEY_ID: ${SESSION_RECORDING_V2_S3_ACCESS_KEY_ID}
|
|
SESSION_RECORDING_V2_S3_SECRET_ACCESS_KEY: ${SESSION_RECORDING_V2_S3_SECRET_ACCESS_KEY}
|
|
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}
|
|
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'
|
|
# 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:-}
|
|
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: ${SECRET_KEY}
|
|
OBJECT_STORAGE_ACCESS_KEY_ID: ${OBJECT_STORAGE_ACCESS_KEY_ID}
|
|
OBJECT_STORAGE_SECRET_ACCESS_KEY: ${OBJECT_STORAGE_SECRET_ACCESS_KEY}
|
|
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: ${SESSION_RECORDING_V2_S3_ACCESS_KEY_ID}
|
|
SESSION_RECORDING_V2_S3_SECRET_ACCESS_KEY: ${SESSION_RECORDING_V2_S3_SECRET_ACCESS_KEY}
|
|
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: ${SESSION_RECORDING_V2_S3_ACCESS_KEY_ID}
|
|
SESSION_RECORDING_V2_S3_SECRET_ACCESS_KEY: ${SESSION_RECORDING_V2_S3_SECRET_ACCESS_KEY}
|
|
SESSION_RECORDING_V2_S3_ENDPOINT: http://seaweedfs:8333
|
|
ENCRYPTION_SALT_KEYS: ${ENCRYPTION_SALT_KEYS}
|
|
# Must resolve to the same value as the web and worker services, which send it as
|
|
# X-Internal-Api-Secret. The JWT signing secret is deliberately left unset here, so hobby
|
|
# stays on the legacy shared secret until that scheme is rolled out.
|
|
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:
|
|
# nginx-proxy-manager owns host 80/443. Loopback only; NPM reaches
|
|
# this container over nginx-bridge as "posthog".
|
|
- '127.0.0.1:${POSTHOG_HTTP_PORT:-8002}:80'
|
|
networks:
|
|
default:
|
|
nginx-bridge:
|
|
aliases:
|
|
- posthog
|
|
volumes:
|
|
- caddy-data:/data
|
|
- caddy-config:/config
|
|
environment:
|
|
CADDY_TLS_BLOCK: $TLS_BLOCK
|
|
CADDY_HOST: $CADDY_HOST
|
|
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: ${SECRET_KEY}
|
|
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: ${SECRET_KEY}
|
|
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 these two 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}
|
|
# Point Django at the Rust feature-flags container; the setting
|
|
# otherwise defaults to localhost:3001.
|
|
FEATURE_FLAGS_SERVICE_URL: 'http://feature-flags:3001'
|
|
# 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:-}
|
|
depends_on:
|
|
- db
|
|
- redis7
|
|
- clickhouse
|
|
- kafka
|
|
- objectstorage
|
|
- seaweedfs
|
|
- temporal
|
|
- browserless
|
|
|
|
capture:
|
|
build:
|
|
context: ./posthog/rust
|
|
extends:
|
|
file: docker-compose.base.yml
|
|
service: capture
|
|
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
|
|
|
|
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
|
|
depends_on:
|
|
- db
|
|
- redis7
|
|
|
|
hypercache-server:
|
|
build:
|
|
context: ./posthog/rust
|
|
extends:
|
|
file: docker-compose.base.yml
|
|
service: hypercache-server
|
|
logging: *default-logging
|
|
depends_on:
|
|
- redis7
|
|
|
|
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: ${OBJECT_STORAGE_ACCESS_KEY_ID}
|
|
OBJECT_STORAGE_SECRET_ACCESS_KEY: ${OBJECT_STORAGE_SECRET_ACCESS_KEY}
|
|
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: ${INTERNAL_API_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: ${OBJECT_STORAGE_ACCESS_KEY_ID}
|
|
OBJECT_STORAGE_SECRET_ACCESS_KEY: ${OBJECT_STORAGE_SECRET_ACCESS_KEY}
|
|
OBJECT_STORAGE_ENDPOINT: 'http://seaweedfs:8333'
|
|
OBJECT_STORAGE_FORCE_PATH_STYLE: 'true'
|
|
DATABASE_URL: 'postgres://posthog:posthog@db:5432/posthog'
|
|
INTERNAL_API_SECRET: ${INTERNAL_API_SECRET}
|
|
RUST_LOG: 'info'
|
|
depends_on:
|
|
seaweedfs:
|
|
condition: service_healthy
|
|
db:
|
|
condition: service_healthy
|
|
|
|
networks:
|
|
otel_network:
|
|
nginx-bridge:
|
|
external: true
|
|
|
|
volumes:
|
|
zookeeper-data:
|
|
zookeeper-datalog:
|
|
zookeeper-logs:
|
|
objectstorage-data:
|
|
seaweedfs:
|
|
postgres-data:
|
|
clickhouse-data:
|
|
caddy-data:
|
|
caddy-config:
|
|
redis7-data:
|
|
kafka-data:
|
|
redpanda-data:
|