PostHog hobby self-host: 19 GB of images and a 305-line install script
PostHog publishes a docker-compose.hobby.yml in the public posthog/posthog repo. We thought we'd run it. We did not get further than docker compose pull on the first try — the compose file is intentionally not standalone, it expects environment variables that are set by a 305-line bash installer, and without them you get an error a Docker primer can't help with. Here's exactly where the friction lives, what the real disk and RAM numbers look like, and why we'd reach for managed PostHog Cloud or a different self-host target before this stack on a single VPS.
We're the team behind SimpleReview, a Chrome extension that drafts code-fix PRs on whatever site you click. Not affiliated with PostHog. This page is a scouting note from one real test on one Linux box, dated above. We didn't bring the full stack to a green state — we hit and documented the first three friction points, sized the disk footprint, and stopped. If we got something wrong, open a GitHub issue and we'll fix it.
Friction 1 — the compose file is not standalone
The natural first step on any "docker-compose-based product" is docker compose pull. Skip the install script, see what we're getting into. With docker-compose.hobby.yml from PostHog main:
$ docker compose -f docker-compose.hobby.yml pull --quiet
time="2026-05-07T16:33:31+03:00" level=warning
msg="The \"DOMAIN\" variable is not set. Defaulting to a blank string."
time="2026-05-07T16:33:31+03:00" level=warning
msg="The \"REGISTRY_URL\" variable is not set. Defaulting to a blank string."
time="2026-05-07T16:33:31+03:00" level=warning
msg="The \"POSTHOG_APP_TAG\" variable is not set. Defaulting to a blank string."
time="2026-05-07T16:33:31+03:00" level=warning
msg="The \"ENCRYPTION_SALT_KEYS\" variable is not set. Defaulting to a blank string."
time="2026-05-07T16:33:31+03:00" level=warning
msg="The \"TLS_BLOCK\" variable is not set. Defaulting to a blank string."
unable to get image '-node:latest':
Error response from daemon: invalid reference format
exit 1
The compose file references ${REGISTRY_URL}-node:latest for the posthog-node service. When REGISTRY_URL is empty (the default), Docker tries to pull an image literally named -node:latest, which is not a valid image reference. The pull dies before any of the 23 services has a chance to download.
Setting REGISTRY_URL=posthog/posthog, DOMAIN=<your-domain>, POSTHOG_APP_TAG=latest, ENCRYPTION_SALT_KEYS=<random hex>, and TLS_BLOCK="" lets the pull proceed. None of these defaults appear in the compose file or in a checked-in .env.example; they are inlined into the deploy script.
Friction 2 — the "official" path is a 305-line bash script
The script lives at PostHog/posthog/bin/deploy-hobby. The first thing it does, before any docker activity, is print a warning that's worth quoting verbatim:
echo "Welcome to the single instance PostHog installer 🦔"
echo ""
echo "⚠️ You REALLY need 8GB or more of memory to run this stack ⚠️"
echo ""
The script then prompts interactively for:
- The PostHog version tag (defaults to
latest) - The exact public domain — "Make sure that you have a Host A DNS record pointing to this instance!"
- An admin email for Let's Encrypt
It generates a random POSTHOG_SECRET, a random ENCRYPTION_SALT_KEYS, drops a .env file alongside the compose, fetches Caddy with auto-TLS, and then runs docker compose up -d. The script is not really optional — without it you reproduce the env-var dance by hand and you still need a real domain because the proxy service is wired to do TLS.
Friction 3 — the disk and image footprint
Once the env vars are right, docker compose pull ran for 161 seconds on a 1 Gbit link and added ~19 GB of new images to the host. The biggest single image is posthog/posthog:latest at 10.8 GB uncompressed. Per docker images:
| Image | Tag | Size |
|---|---|---|
posthog/posthog | latest | 10.8 GB |
posthog/posthog-node | latest | 1.75 GB |
clickhouse/clickhouse-server | 26.3.9.8 | 887 MB |
temporalio/admin-tools | 1.26.2 | 857 MB |
cymbal (error-tracking) | master | 476 MB |
feature-flags | master | 449 MB |
capture | master | 439 MB |
auto-setup (temporal) | 1.26.2 | 426 MB |
hypercache-server | master | 392 MB |
temporalio/ui | 2.47.2 | 359 MB |
property-defs-rs | master | 325 MB |
personhog-router | master | 308 MB |
cyclotron-janitor | master | 298 MB |
personhog-replica | master | 277 MB |
zookeeper | 3.7.0 | 277 MB |
chrislusf/seaweedfs | 4.03 | 193 MB |
livestream | master | 171 MB |
caddy | latest | 62 MB |
| Total (PostHog-specific) | ~19 GB |
The posthog/posthog image alone is bigger than most full Linux server installs. It contains the Django app, the Webpack build, the Celery worker, the plugins runtime, and a slate of Python dependencies for ClickHouse, Kafka, Redis, S3, Temporal. It has to — the image is reused across at least seven of the 23 services in the compose file.
What the 23 services actually are
Counted from the compose file at the test commit, after stripping x- blocks:
- Storage:
db(Postgres 15.12),redis7,clickhouse,elasticsearch,seaweedfs,objectstorage - Streaming:
kafka,zookeeper - Workflow:
temporal,temporal-admin-tools,temporal-ui,temporal-django-worker - App:
web,worker,plugins,asyncmigrationscheck,cyclotron-janitor - Ingestion:
ingestion-general,ingestion-sessionreplay,ingestion-error-tracking,ingestion-logs,ingestion-traces,recording-api - Edge:
proxy(Caddy with auto-TLS)
Translation: PostHog is, by 2026, a multi-process distributed system held together by Kafka, Temporal, and a Django monolith. It is not a "small Postgres-backed app" anymore. The hobby compose is the production architecture, just on a single host.
Where the line is
Based on what we measured before stopping:
| You want | Verdict |
|---|---|
| Self-host PostHog on a $20/month VPS | No. The script's 8 GB warning is conservative; in practice you want 16 GB and SSDs. Plan for $40–80/month. |
| Try PostHog locally without a public domain | Hard. The hobby compose has TLS termination wired in. You can edit the compose to drop Caddy and bind the web service to localhost, but you're now off the supported path. |
| Pin a known-good version for compliance | Yes — set POSTHOG_APP_TAG to a specific image tag, but accept that the master-tagged sidecars (capture, cymbal, etc.) move independently and aren't versioned in the compose file. |
| Production analytics for 50+ engineers | Plausible only if you have an ops team. The 23-service stack means migrations, upgrades, and Kafka rebalances are part of your week. |
Things we'd change in the README
- Ship a runnable
.env.examplenext to the compose file, with sane defaults for every variable the file references.${REGISTRY_URL}-node:latestfailing on an empty default is a footgun, not a feature. - Add a "no-TLS local mode" compose that drops Caddy, binds
webto127.0.0.1:8000, and lets curious developers see the UI without owning a domain. - Be explicit about disk. 19 GB of images on a "hobby" deploy needs a callout in the docs, not a discovery in the wild.
- Write down the canonical RAM/CPU floor. The script's "really need 8GB" line is the only quantitative guidance we found. Production-style docs deserve concrete tiers.
What we'd actually do
If the goal is product analytics and we don't have a dedicated ops headcount: pay for PostHog Cloud or evaluate a smaller-footprint alternative (Plausible, Umami) for the use case. The PostHog hobby compose is engineered to mirror their cloud architecture; that buys parity but it's a heavy lift for a single-tenant box. We'd revisit self-host once we cared specifically about session replay or event-level data residency — and even then, we'd budget for a 16 GB VPS, daily ClickHouse backups, and a maintenance window for upgrades.
Where this fits
One short, honest write-up per self-hostable analytics/CMS/forum tool we run on a real Linux box. Adjacent: Cal.com 6.16.1 — migrations don't run on boot, Dify 1.14.0 — the profile flag the README forgets, Open WebUI on Linux Docker — first 90 seconds, measured. SimpleReview is the Chrome extension that turns whatever element you click on a broken admin into a draft code-fix PR — including PostHog's various dashboards once you do bring the stack up.