From 7f3a08eb00067deda2e200c612e5dac639c94c58 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 14 Jul 2026 13:43:38 -0600 Subject: [PATCH 1/2] feat: add PostHog template Based on the official hobby deployment, trimmed to run on a single server: Django web + Celery worker, Node plugin-server (CDP, ingestion, session replay, recording API), Rust capture/replay-capture/ feature-flags/hypercache, livestream, Caddy path router, Postgres, Redis, Redpanda (Kafka), ZooKeeper, ClickHouse and MinIO. Images are pinned to a tested master commit (PostHog no longer publishes tagged releases); an init container fetches the matching ClickHouse configuration, Kafka table schemas, funnel UDF binaries and the GeoIP database at the same pinned ref. Co-Authored-By: Claude Fable 5 --- blueprints/posthog/docker-compose.yml | 634 ++++++++++++++++++++++++++ blueprints/posthog/instructions.md | 27 ++ blueprints/posthog/meta.json | 19 + blueprints/posthog/posthog.svg | 10 + blueprints/posthog/template.toml | 73 +++ 5 files changed, 763 insertions(+) create mode 100644 blueprints/posthog/docker-compose.yml create mode 100644 blueprints/posthog/instructions.md create mode 100644 blueprints/posthog/meta.json create mode 100644 blueprints/posthog/posthog.svg create mode 100644 blueprints/posthog/template.toml diff --git a/blueprints/posthog/docker-compose.yml b/blueprints/posthog/docker-compose.yml new file mode 100644 index 00000000..b5addc40 --- /dev/null +++ b/blueprints/posthog/docker-compose.yml @@ -0,0 +1,634 @@ +# PostHog self-hosted ("hobby" deployment, trimmed for a single VPS). +# Based on https://github.com/PostHog/posthog/blob/master/docker-compose.hobby.yml +# Omitted vs upstream hobby (heavy/optional): Temporal + Elasticsearch (data +# warehouse / batch exports), browserless (image exports / heatmap screenshots), +# personhog, cymbal + error-tracking/logs/traces ingestion, capture-ai and +# capture-logs. Everything needed for product analytics, feature flags, +# surveys, session replay and the CDP pipeline is included. + +x-logging: &default-logging + driver: json-file + options: + max-size: "50m" + max-file: "3" + +x-django-env: &django-env + # Postgres + DATABASE_URL: postgres://posthog:posthog@db:5432/posthog + PGHOST: db + PGUSER: posthog + PGPASSWORD: posthog + PERSONS_DB_WRITER_URL: postgres://posthog:posthog@db:5432/posthog + PERSONS_DB_READER_URL: postgres://posthog:posthog@db:5432/posthog + # ClickHouse (users match docker/clickhouse/users.xml from the PostHog repo) + CLICKHOUSE_HOST: clickhouse + CLICKHOUSE_DATABASE: posthog + CLICKHOUSE_SECURE: "false" + CLICKHOUSE_VERIFY: "false" + CLICKHOUSE_API_USER: api + CLICKHOUSE_API_PASSWORD: apipass + CLICKHOUSE_APP_USER: app + CLICKHOUSE_APP_PASSWORD: apppass + CLICKHOUSE_LOGS_CLUSTER_HOST: clickhouse + CLICKHOUSE_LOGS_CLUSTER_SECURE: "false" + # Redis / Kafka + REDIS_URL: redis://redis7:6379/ + KAFKA_HOSTS: kafka:9092 + FLAGS_REDIS_ENABLED: "false" + # Object storage (MinIO) + OBJECT_STORAGE_ENABLED: "true" + OBJECT_STORAGE_ENDPOINT: http://objectstorage:19000 + OBJECT_STORAGE_PUBLIC_ENDPOINT: ${POSTHOG_SITE_URL} + OBJECT_STORAGE_ACCESS_KEY_ID: object_storage_root_user + OBJECT_STORAGE_SECRET_ACCESS_KEY: object_storage_root_password + OBJECT_STORAGE_FORCE_PATH_STYLE: "true" + # Session replay storage + SESSION_RECORDING_V2_S3_ENDPOINT: http://objectstorage:19000 + SESSION_RECORDING_V2_S3_ACCESS_KEY_ID: object_storage_root_user + SESSION_RECORDING_V2_S3_SECRET_ACCESS_KEY: object_storage_root_password + RECORDING_API_URL: http://recording-api:6738 + # App + SITE_URL: ${POSTHOG_SITE_URL} + SECRET_KEY: ${POSTHOG_SECRET_KEY} + ENCRYPTION_SALT_KEYS: ${POSTHOG_ENCRYPTION_SALT_KEYS} + DEPLOYMENT: hobby + IS_BEHIND_PROXY: "true" + DISABLE_SECURE_SSL_REDIRECT: "true" + # "false" so login works over plain HTTP; set to "true" once the domain uses HTTPS + SECURE_COOKIES: ${POSTHOG_SECURE_COOKIES} + CDP_API_URL: http://plugins:6738 + FEATURE_FLAGS_SERVICE_URL: http://feature-flags:3001 + LIVESTREAM_HOST: ${POSTHOG_SITE_URL}/livestream + API_QUERIES_PER_TEAM: '{"1": 100}' + OTEL_SDK_DISABLED: "true" + OTEL_EXPORTER_OTLP_ENDPOINT: "" + +services: + # One-shot: fetches the ClickHouse configuration, Kafka table schemas and + # funnel UDFs from the PostHog repo (pinned to the same commit as the app + # image) plus the GeoLite2 GeoIP database. Idempotent across redeploys. + init-assets: + image: alpine:3.21 + restart: on-failure + logging: *default-logging + entrypoint: /bin/sh + command: + - -c + - | + set -e + apk add --no-cache git curl brotli >/dev/null + if [ ! -f /assets/.ok-${POSTHOG_VERSION} ]; then + echo "Fetching PostHog repo assets @ ${POSTHOG_VERSION}" + rm -rf /tmp/ph && mkdir -p /tmp/ph && cd /tmp/ph + git init -q . + git remote add origin https://github.com/PostHog/posthog.git + git sparse-checkout set --no-cone /docker/clickhouse/ /posthog/idl/ /posthog/user_scripts/ + git -c protocol.version=2 fetch -q --depth 1 --filter=blob:none origin ${POSTHOG_VERSION} + git checkout -q FETCH_HEAD + rm -rf /assets/clickhouse /assets/idl /assets/user_scripts /assets/.ok-* + cp -r docker/clickhouse /assets/clickhouse + cp -r posthog/idl /assets/idl + cp -r posthog/user_scripts /assets/user_scripts + touch /assets/.ok-${POSTHOG_VERSION} + echo "Repo assets ready" + fi + if [ ! -s /share/GeoLite2-City.mmdb ]; then + echo "Downloading GeoLite2 database" + curl -sL --http1.1 https://mmdbcdn.posthog.net/ | brotli --decompress --output=/share/GeoLite2-City.mmdb + chmod 644 /share/GeoLite2-City.mmdb + fi + echo "init-assets done" + volumes: + - posthog-assets:/assets + - geoip-data:/share + + db: + image: postgres:15.12-alpine + restart: unless-stopped + logging: *default-logging + environment: + POSTGRES_USER: posthog + POSTGRES_DB: posthog + POSTGRES_PASSWORD: posthog + healthcheck: + test: ["CMD-SHELL", "pg_isready -U posthog"] + interval: 5s + timeout: 10s + retries: 30 + start_period: 10s + volumes: + - postgres-data:/var/lib/postgresql/data + - ../files/db-init:/docker-entrypoint-initdb.d:ro + + redis7: + image: redis:7.2-alpine + restart: unless-stopped + logging: *default-logging + command: redis-server --maxmemory-policy allkeys-lru --maxmemory 200mb + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 10s + retries: 10 + volumes: + - redis-data:/data + + # Required by ClickHouse for distributed DDL (docker/clickhouse/config.xml + # points at zookeeper:2181) + zookeeper: + image: zookeeper:3.7.0 + restart: unless-stopped + logging: *default-logging + environment: + ZOO_AUTOPURGE_PURGEINTERVAL: 1 + ZOO_AUTOPURGE_SNAPRETAINCOUNT: 3 + volumes: + - zookeeper-data:/data + - zookeeper-datalog:/datalog + + # Kafka API provided by Redpanda (as upstream), tuned down for small servers + kafka: + image: docker.io/redpandadata/redpanda:v25.1.9 + restart: unless-stopped + logging: *default-logging + command: + - redpanda + - start + - --kafka-addr internal://0.0.0.0:9092 + - --advertise-kafka-addr internal://kafka:9092 + - --rpc-addr kafka:33145 + - --advertise-rpc-addr kafka:33145 + - --mode dev-container + - --smp 1 + - --memory 1G + - --reserve-memory 0M + - --overprovisioned + - --set redpanda.empty_seed_starts_cluster=false + - --seeds kafka:33145 + - --set redpanda.auto_create_topics_enabled=true + volumes: + - redpanda-data:/var/lib/redpanda/data + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:9644/v1/status/ready || exit 1"] + interval: 5s + timeout: 10s + retries: 30 + start_period: 20s + + # One-shot: pre-creates the Kafka topics that consumers check at startup + kafka-init: + image: docker.io/redpandadata/redpanda:v25.1.9 + restart: on-failure + logging: *default-logging + entrypoint: /bin/sh + depends_on: + kafka: + condition: service_healthy + command: + - -c + - | + set -e + TOPICS="clickhouse_events_json \ + clickhouse_ai_events_json \ + clickhouse_heatmap_events \ + clickhouse_ingestion_warnings \ + events_plugin_ingestion \ + events_plugin_ingestion_dlq \ + events_plugin_ingestion_overflow \ + events_plugin_ingestion_async \ + events_plugin_ingestion_historical \ + ingestion-clientwarnings-main-1 \ + heatmaps_ingestion \ + session_recording_snapshot_item_events \ + session_recording_snapshot_item_overflow \ + clickhouse_groups \ + clickhouse_person \ + clickhouse_person_distinct_id \ + clickhouse_app_metrics2 \ + log_entries \ + clickhouse_tophog" + for topic in $$TOPICS; do + rpk topic create "$$topic" --brokers kafka:9092 -p 1 -r 1 2>/dev/null \ + || rpk topic list --brokers kafka:9092 | grep -q "$$topic" + done + echo "Kafka topics ready" + + clickhouse: + image: clickhouse/clickhouse-server:26.6.1.1193 + restart: unless-stopped + logging: *default-logging + depends_on: + init-assets: + condition: service_completed_successfully + zookeeper: + condition: service_started + kafka: + condition: service_started + entrypoint: /bin/bash + command: + - -c + - | + set -e + cp /assets/clickhouse/config.xml /etc/clickhouse-server/config.xml + cp /assets/clickhouse/config.d/default.xml /etc/clickhouse-server/config.d/default.xml + cp /assets/clickhouse/users.xml /etc/clickhouse-server/users.xml + cp /assets/clickhouse/user_defined_function.xml /etc/clickhouse-server/user_defined_function.xml + mkdir -p /var/lib/clickhouse/format_schemas /var/lib/clickhouse/user_scripts + cp -r /assets/idl/. /var/lib/clickhouse/format_schemas/ + cp -r /assets/user_scripts/. /var/lib/clickhouse/user_scripts/ + chmod -R 755 /var/lib/clickhouse/user_scripts + chown -R clickhouse:clickhouse /var/lib/clickhouse/format_schemas /var/lib/clickhouse/user_scripts || true + # Materialize system log tables (system.crash_log is required by the + # custom_metrics ClickHouse migration), mirroring upstream init-db.sh + ( + i=0 + while [ $$i -lt 90 ]; do + if clickhouse-client --query "SELECT 1" >/dev/null 2>&1; then + clickhouse-client --query "SYSTEM FLUSH LOGS" && echo "system logs flushed" && break + fi + sleep 2 + i=$$((i+1)) + done + ) & + exec /entrypoint.sh + environment: + CLICKHOUSE_SKIP_USER_SETUP: 1 + KAFKA_HOSTS: kafka:9092 + volumes: + - posthog-assets:/assets:ro + - clickhouse-data:/var/lib/clickhouse + healthcheck: + test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:8123/ping || exit 1"] + interval: 5s + timeout: 10s + retries: 30 + start_period: 30s + + # Django app: runs migrations, then serves the API + UI on :8000 + web: + image: posthog/posthog:${POSTHOG_VERSION} + restart: unless-stopped + logging: *default-logging + command: ["sh", "-c", "./bin/migrate && exec ./bin/docker-server"] + depends_on: + db: + condition: service_healthy + redis7: + condition: service_healthy + clickhouse: + condition: service_healthy + kafka: + condition: service_healthy + kafka-init: + condition: service_completed_successfully + objectstorage: + condition: service_started + environment: + <<: *django-env + NGINX_UNIT_APP_PROCESSES: 2 + healthcheck: + test: ["CMD-SHELL", "curl -fs http://localhost:8000/_health || exit 1"] + interval: 30s + timeout: 10s + retries: 15 + start_period: 600s + + # Celery worker + scheduler + worker: + image: posthog/posthog:${POSTHOG_VERSION} + restart: unless-stopped + logging: *default-logging + command: ./bin/docker-worker-celery --with-scheduler + depends_on: + db: + condition: service_healthy + redis7: + condition: service_healthy + clickhouse: + condition: service_healthy + kafka: + condition: service_healthy + web: + condition: service_started + environment: + <<: *django-env + POSTHOG_SKIP_MIGRATION_CHECKS: "1" + WEB_CONCURRENCY: 2 + + # Node plugin server: CDP (destinations, webhooks, transformations) + plugins: + image: posthog/posthog-node:${POSTHOG_NODE_VERSION} + restart: unless-stopped + logging: *default-logging + depends_on: + db: + condition: service_healthy + redis7: + condition: service_healthy + kafka: + condition: service_healthy + clickhouse: + condition: service_started + kafka-init: + condition: service_completed_successfully + init-assets: + condition: service_completed_successfully + environment: + SITE_URL: ${POSTHOG_SITE_URL} + SECRET_KEY: ${POSTHOG_SECRET_KEY} + ENCRYPTION_SALT_KEYS: ${POSTHOG_ENCRYPTION_SALT_KEYS} + 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 + CYCLOTRON_DATABASE_URL: postgres://posthog:posthog@db:5432/posthog + KAFKA_HOSTS: kafka:9092 + REDIS_URL: redis://redis7:6379/ + CDP_REDIS_HOST: redis7 + CDP_REDIS_PORT: 6379 + LOGS_REDIS_HOST: redis7 + TRACES_REDIS_HOST: redis7 + COOKIELESS_REDIS_HOST: redis7 + COOKIELESS_REDIS_PORT: 6379 + CLICKHOUSE_HOST: clickhouse + CLICKHOUSE_DATABASE: posthog + CLICKHOUSE_SECURE: "false" + CLICKHOUSE_VERIFY: "false" + OBJECT_STORAGE_ENABLED: "true" + OBJECT_STORAGE_ENDPOINT: http://objectstorage:19000 + OBJECT_STORAGE_PUBLIC_ENDPOINT: ${POSTHOG_SITE_URL} + OBJECT_STORAGE_ACCESS_KEY_ID: object_storage_root_user + OBJECT_STORAGE_SECRET_ACCESS_KEY: object_storage_root_password + OTEL_EXPORTER_OTLP_ENDPOINT: "" + volumes: + - geoip-data:/share:ro + + # Node plugin server: event ingestion (Kafka -> person processing -> ClickHouse) + ingestion-general: + image: posthog/posthog-node:${POSTHOG_NODE_VERSION} + restart: unless-stopped + logging: *default-logging + depends_on: + db: + condition: service_healthy + redis7: + condition: service_healthy + kafka: + condition: service_healthy + clickhouse: + condition: service_started + kafka-init: + condition: service_completed_successfully + init-assets: + condition: service_completed_successfully + environment: + PLUGIN_SERVER_MODE: ingestion-v2-combined + ENCRYPTION_SALT_KEYS: ${POSTHOG_ENCRYPTION_SALT_KEYS} + 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/ + COOKIELESS_REDIS_HOST: redis7 + COOKIELESS_REDIS_PORT: 6379 + CDP_REDIS_HOST: redis7 + LOGS_REDIS_HOST: redis7 + TRACES_REDIS_HOST: redis7 + CLICKHOUSE_HOST: clickhouse + CLICKHOUSE_DATABASE: posthog + CLICKHOUSE_SECURE: "false" + CLICKHOUSE_VERIFY: "false" + volumes: + - geoip-data:/share:ro + + # Node plugin server: session replay blob ingestion (Kafka -> S3) + ingestion-sessionreplay: + image: posthog/posthog-node:${POSTHOG_NODE_VERSION} + restart: unless-stopped + logging: *default-logging + depends_on: + db: + condition: service_healthy + redis7: + condition: service_healthy + kafka: + condition: service_healthy + kafka-init: + condition: service_completed_successfully + objectstorage: + condition: service_started + environment: + PLUGIN_SERVER_MODE: recordings-blob-ingestion-v2 + ENCRYPTION_SALT_KEYS: ${POSTHOG_ENCRYPTION_SALT_KEYS} + DATABASE_URL: postgres://posthog:posthog@db:5432/posthog + KAFKA_HOSTS: kafka:9092 + REDIS_URL: redis://redis7:6379/ + SESSION_RECORDING_V2_S3_ENDPOINT: http://objectstorage:19000 + SESSION_RECORDING_V2_S3_ACCESS_KEY_ID: object_storage_root_user + SESSION_RECORDING_V2_S3_SECRET_ACCESS_KEY: object_storage_root_password + SESSION_RECORDING_V2_S3_TIMEOUT_MS: 120000 + + # Node plugin server: session replay playback API + recording-api: + image: posthog/posthog-node:${POSTHOG_NODE_VERSION} + restart: unless-stopped + logging: *default-logging + depends_on: + db: + condition: service_healthy + redis7: + condition: service_healthy + clickhouse: + condition: service_started + objectstorage: + condition: service_started + environment: + PLUGIN_SERVER_MODE: recording-api + ENCRYPTION_SALT_KEYS: ${POSTHOG_ENCRYPTION_SALT_KEYS} + DATABASE_URL: postgres://posthog:posthog@db:5432/posthog + REDIS_URL: redis://redis7:6379/ + SESSION_RECORDING_API_REDIS_HOST: redis7 + SESSION_RECORDING_API_REDIS_PORT: 6379 + CLICKHOUSE_HOST: clickhouse + CLICKHOUSE_DATABASE: posthog + CLICKHOUSE_SECURE: "false" + CLICKHOUSE_VERIFY: "false" + SESSION_RECORDING_V2_S3_ENDPOINT: http://objectstorage:19000 + SESSION_RECORDING_V2_S3_ACCESS_KEY_ID: object_storage_root_user + SESSION_RECORDING_V2_S3_SECRET_ACCESS_KEY: object_storage_root_password + + # Rust event capture endpoint (/e, /batch, /capture, /i/v0) + capture: + image: ghcr.io/posthog/posthog/capture:master + restart: unless-stopped + logging: *default-logging + depends_on: + kafka: + condition: service_healthy + redis7: + condition: service_healthy + environment: + ADDRESS: 0.0.0.0:3000 + KAFKA_TOPIC: events_plugin_ingestion + KAFKA_HOSTS: kafka:9092 + REDIS_URL: redis://redis7:6379/ + CAPTURE_MODE: events + RUST_LOG: info,rdkafka=warn + CAPTURE_V1_SINKS: msk + CAPTURE_V1_SINK_MSK_KAFKA_HOSTS: kafka:9092 + CAPTURE_V1_SINK_MSK_KAFKA_TOPIC_MAIN: events_plugin_ingestion + CAPTURE_V1_SINK_MSK_KAFKA_TOPIC_HISTORICAL: events_plugin_ingestion_historical + CAPTURE_V1_SINK_MSK_KAFKA_TOPIC_OVERFLOW: events_plugin_ingestion_overflow + CAPTURE_V1_SINK_MSK_KAFKA_TOPIC_DLQ: events_plugin_ingestion_dlq + CAPTURE_V1_SINK_MSK_KAFKA_TOPIC_EXCEPTION: ingestion-errortracking-main + CAPTURE_V1_SINK_MSK_KAFKA_TOPIC_HEATMAP: heatmaps_ingestion + CAPTURE_V1_SINK_MSK_KAFKA_TOPIC_CLIENT_INGESTION_WARNING: ingestion-clientwarnings-main-1 + + # Rust capture endpoint for session recordings (/s) + replay-capture: + image: ghcr.io/posthog/posthog/capture:master + restart: unless-stopped + logging: *default-logging + depends_on: + kafka: + condition: service_healthy + redis7: + condition: service_healthy + environment: + ADDRESS: 0.0.0.0:3000 + KAFKA_TOPIC: session_recording_snapshot_item_events + KAFKA_HOSTS: kafka:9092 + REDIS_URL: redis://redis7:6379/ + CAPTURE_MODE: recordings + + # Rust feature flag evaluation service (/flags) + feature-flags: + image: ghcr.io/posthog/posthog/feature-flags:master + restart: unless-stopped + logging: *default-logging + depends_on: + db: + condition: service_healthy + redis7: + condition: service_healthy + init-assets: + condition: service_completed_successfully + 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 + REDIS_URL: redis://redis7:6379/ + ADDRESS: 0.0.0.0:3001 + RUST_LOG: info + COOKIELESS_REDIS_HOST: redis7 + COOKIELESS_REDIS_PORT: 6379 + volumes: + - geoip-data:/share:ro + + # Rust hypercache server (/array remote config + /surveys) + hypercache-server: + image: ghcr.io/posthog/posthog/hypercache-server:master + restart: unless-stopped + logging: *default-logging + depends_on: + redis7: + condition: service_healthy + environment: + REDIS_URL: redis://redis7:6379/ + ADDRESS: 0.0.0.0:3002 + RUST_LOG: info + + # Live events stream (/livestream) + livestream: + image: ghcr.io/posthog/posthog/livestream:master + restart: unless-stopped + logging: *default-logging + depends_on: + kafka: + condition: service_healthy + environment: + LIVESTREAM_JWT_SECRET: ${POSTHOG_SECRET_KEY} + volumes: + - ../files/livestream/configs.yml:/configs/configs.yml:ro + + objectstorage: + image: minio/minio:RELEASE.2025-04-22T22-12-26Z + restart: unless-stopped + logging: *default-logging + environment: + MINIO_ROOT_USER: object_storage_root_user + MINIO_ROOT_PASSWORD: object_storage_root_password + entrypoint: sh + command: -c 'mkdir -p /data/posthog && minio server --address ":19000" --console-address ":19001" /data' + volumes: + - objectstorage-data:/data + + # Caddy routes the single public domain to the right internal service, + # exactly like the upstream hobby deployment (TLS is terminated by Traefik) + proxy: + image: caddy:2.10-alpine + restart: unless-stopped + logging: *default-logging + depends_on: + web: + condition: service_started + entrypoint: sh + command: -c 'printf "%s" "$$CADDYFILE" > /etc/caddy/Caddyfile && exec caddy run -c /etc/caddy/Caddyfile' + environment: + CADDYFILE: | + { + auto_https off + } + :80 { + @capture path /e /e/ /e/* /i/v0 /i/v0/ /i/v0/* /i/v1/analytics/events /i/v1/analytics/events/ /batch /batch/ /batch/* /capture /capture/ /capture/* + @replay-capture path /s /s/ /s/* + @flags path /flags /flags/ /flags/* /api/feature_flag/local_evaluation /api/feature_flag/local_evaluation/ /api/feature_flag/local_evaluation/* + @surveys path /surveys /surveys/ /api/surveys /api/surveys/ + @remote-config path /array/* + @webhooks path /public/webhooks /public/webhooks/ /public/webhooks/* /public/m/ /public/m/* + @livestream path /livestream /livestream/ /livestream/* + @objectstorage path /posthog /posthog/ /posthog/* + + handle @capture { + reverse_proxy capture:3000 + } + handle @replay-capture { + reverse_proxy replay-capture:3000 + } + 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 + } + handle @livestream { + uri strip_prefix /livestream + reverse_proxy livestream:8080 { + flush_interval -1 + } + } + handle @objectstorage { + reverse_proxy objectstorage:19000 + } + handle { + reverse_proxy web:8000 + } + } + +volumes: + postgres-data: + redis-data: + zookeeper-data: + zookeeper-datalog: + redpanda-data: + clickhouse-data: + posthog-assets: + geoip-data: + objectstorage-data: diff --git a/blueprints/posthog/instructions.md b/blueprints/posthog/instructions.md new file mode 100644 index 00000000..6db1223e --- /dev/null +++ b/blueprints/posthog/instructions.md @@ -0,0 +1,27 @@ +# PostHog + +Self-hosted PostHog based on the official "hobby" deployment, trimmed to run on a single server. + +## Requirements + +- **RAM: at least 8 GB free** is strongly recommended (the stack runs Django, a Celery worker, two Node plugin-server processes, ClickHouse, Redpanda/Kafka, Postgres, Redis, MinIO and several small Rust services). +- ~15 GB of free disk for the images and data volumes. + +## First boot + +The first deployment takes **5-10 minutes**: an init container fetches the ClickHouse configuration and the GeoIP database, and the `web` service runs all Postgres and ClickHouse migrations before it starts serving. Watch the `web` service logs; once you see the Unit server start, open the domain and create the first account. + +## After enabling HTTPS + +If you serve PostHog over HTTPS (recommended for production), update these environment variables and redeploy: + +- `POSTHOG_SITE_URL` to `https://` (otherwise logins/CSRF and SDK snippets point at the wrong scheme) +- `POSTHOG_SECURE_COOKIES` to `true` (marks session/CSRF cookies as Secure; the default `false` is what allows logging in over the initial plain-HTTP domain) + +## What is not included + +Compared to the full upstream hobby stack, this template omits Temporal + Elasticsearch (data warehouse / batch exports), browserless (dashboard image exports and heatmap screenshots), and the error-tracking/logs/traces ingestion pipelines. Product analytics, feature flags, experiments, surveys, session replay, live events and the CDP destinations pipeline all work. + +## Versioning + +PostHog no longer publishes tagged releases; images are pinned to a tested master commit via the `POSTHOG_VERSION` and `POSTHOG_NODE_VERSION` environment variables. To upgrade, set `POSTHOG_VERSION` to a newer commit sha of `posthog/posthog` (Docker Hub tag) and redeploy. diff --git a/blueprints/posthog/meta.json b/blueprints/posthog/meta.json new file mode 100644 index 00000000..015dec45 --- /dev/null +++ b/blueprints/posthog/meta.json @@ -0,0 +1,19 @@ +{ + "id": "posthog", + "name": "PostHog", + "version": "master-2026-07-14", + "description": "PostHog is an open-source product analytics platform with feature flags, session replay, surveys, A/B testing and a customer data pipeline, all in one tool.", + "logo": "posthog.svg", + "links": { + "github": "https://github.com/PostHog/posthog", + "website": "https://posthog.com/", + "docs": "https://posthog.com/docs/self-host" + }, + "tags": [ + "analytics", + "product-analytics", + "feature-flags", + "session-replay", + "ab-testing" + ] +} diff --git a/blueprints/posthog/posthog.svg b/blueprints/posthog/posthog.svg new file mode 100644 index 00000000..e8cca1e9 --- /dev/null +++ b/blueprints/posthog/posthog.svg @@ -0,0 +1,10 @@ + + + PostHog + + + + + + + diff --git a/blueprints/posthog/template.toml b/blueprints/posthog/template.toml new file mode 100644 index 00000000..9644ee13 --- /dev/null +++ b/blueprints/posthog/template.toml @@ -0,0 +1,73 @@ +[variables] +main_domain = "${domain}" +secret_key = "${password:64}" +salt_keys = "${hash:32}" + +[config] +[[config.domains]] +serviceName = "proxy" +port = 80 +host = "${main_domain}" +path = "/" + +[config.env] +# PostHog stopped publishing tagged releases; images are pinned to a tested +# master commit. POSTHOG_VERSION is both the posthog/posthog image tag and the +# git ref used to fetch the matching ClickHouse configuration. +POSTHOG_VERSION = "1e7b36ebe0da38a738fed250d175809a899ef422" +POSTHOG_NODE_VERSION = "sha-82ea668" +# Set to https:// after enabling HTTPS on the domain +POSTHOG_SITE_URL = "http://${main_domain}" +# Set to "true" once the domain uses HTTPS (marks session/CSRF cookies Secure) +POSTHOG_SECURE_COOKIES = "false" +POSTHOG_SECRET_KEY = "${secret_key}" +POSTHOG_ENCRYPTION_SALT_KEYS = "${salt_keys}" + +[[config.mounts]] +filePath = "/db-init/create-extra-dbs.sh" +content = """ +#!/bin/bash +# Creates the auxiliary databases PostHog services can point at +# (mirrors docker/postgres-init-scripts in the PostHog repo) +set -e +set -u + +for db in cyclotron posthog_persons behavioral_cohorts; do + DB_EXISTS=$(psql -U "$POSTGRES_USER" -tAc "SELECT 1 FROM pg_database WHERE datname='$db'") + if [ -z "$DB_EXISTS" ]; then + echo "Creating database '$db'..." + psql -U "$POSTGRES_USER" -c "CREATE DATABASE $db;" + psql -U "$POSTGRES_USER" -c "GRANT ALL PRIVILEGES ON DATABASE $db TO $POSTGRES_USER;" + fi +done +""" + +[[config.mounts]] +filePath = "/livestream/configs.yml" +content = """ +debug: false +kafka: + brokers: 'kafka:9092' + topic: 'events_plugin_ingestion' + group_id: 'livestream' + security_protocol: 'PLAINTEXT' + session_recording_enabled: true + session_recording_security_protocol: 'PLAINTEXT' +consumers: + event: + enabled: true + brokers: 'kafka:9092' + topic: 'events_plugin_ingestion' + security_protocol: 'PLAINTEXT' + group_id: 'livestream' + session_recording: + enabled: true + brokers: 'kafka:9092' + topic: 'session_recording_snapshot_item_events' + security_protocol: 'PLAINTEXT' + group_id: 'livestream-session-recordings' + notification: + enabled: false +mmdb: + path: 'GeoLite2-City.mmdb' +""" From 3eb1d3b7bd3382b7a8d07e8665fe1d79a291ba6a Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 14 Jul 2026 15:50:16 -0600 Subject: [PATCH 2/2] fix: stop plugins service crash loop (logs-redis TLS default) In production images LOGS_REDIS_TLS/TRACES_REDIS_TLS default to true, so the logs-ingestion consumer (part of the plugin-server default mode) kept attempting a TLS handshake against the plaintext Redis, timed out every ~11s and killed the process after 10 errors (restart loop every ~2.5 min). Constrain the plugins service to the capability groups this template ships (cdp_workflows, realtime_cohorts, feature_flags) and pin the redis TLS flags to false as belt-and-braces. Co-Authored-By: Claude Fable 5 --- blueprints/posthog/docker-compose.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/blueprints/posthog/docker-compose.yml b/blueprints/posthog/docker-compose.yml index b5addc40..eb8f660c 100644 --- a/blueprints/posthog/docker-compose.yml +++ b/blueprints/posthog/docker-compose.yml @@ -334,6 +334,11 @@ services: init-assets: condition: service_completed_successfully environment: + # Run only the pipelines this template ships (CDP destinations/workflows, + # realtime cohorts, feature-flag evaluation). The default mode would also + # start the logs/error-tracking ingestion consumers, which this template + # intentionally omits. + NODEJS_CAPABILITY_GROUPS: cdp_workflows,realtime_cohorts,feature_flags SITE_URL: ${POSTHOG_SITE_URL} SECRET_KEY: ${POSTHOG_SECRET_KEY} ENCRYPTION_SALT_KEYS: ${POSTHOG_ENCRYPTION_SALT_KEYS} @@ -345,8 +350,13 @@ services: REDIS_URL: redis://redis7:6379/ CDP_REDIS_HOST: redis7 CDP_REDIS_PORT: 6379 + # TLS for these defaults to "true" in production images; this stack's + # Redis is plaintext (a TLS handshake against it hangs until timeout and + # crash-loops the process if these consumers ever run). LOGS_REDIS_HOST: redis7 + LOGS_REDIS_TLS: "false" TRACES_REDIS_HOST: redis7 + TRACES_REDIS_TLS: "false" COOKIELESS_REDIS_HOST: redis7 COOKIELESS_REDIS_PORT: 6379 CLICKHOUSE_HOST: clickhouse @@ -392,7 +402,9 @@ services: COOKIELESS_REDIS_PORT: 6379 CDP_REDIS_HOST: redis7 LOGS_REDIS_HOST: redis7 + LOGS_REDIS_TLS: "false" TRACES_REDIS_HOST: redis7 + TRACES_REDIS_TLS: "false" CLICKHOUSE_HOST: clickhouse CLICKHOUSE_DATABASE: posthog CLICKHOUSE_SECURE: "false"