Snapshot both servers: the VPS catalog moves to server/vps, the home server joins as server/home

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.
This commit is contained in:
Gabriel Brown committed 2026-10-06 10:52:47 -04:00
1 parent a664cf68f5
commit 913a57f8a6
146 files changed
+5258 -354

No files matched your search

+767
View File
@@ -0,0 +1,767 @@
#
# `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: