How to Self-Host Sentry for Error Tracking
There is a specific moment that sends people to this page. An error-tracking invoice arrives, somebody says "we could just host that ourselves," and everyone nods, because in their head self-hosting is a docker run and a volume mount. Then they open the self-hosted repository and find a compose file with well over a dozen services in it.
I'm Ajay, I build PandaStack. The summary up front: Sentry is excellent software, its self-hosted distribution is a genuine distributed system, and for a lot of teams the correct answer is a Sentry-compatible alternative that keeps everything in Postgres. Here is what you are signing up for, and where the lighter path is better.
Three reasons people self-host, and only two of them hold up
The first is the bill: the most common motivation and the weakest, because self-hosting does not delete a cost. It converts a predictable invoice into a machine, a disk, an upgrade cadence and somebody's attention. The honest comparison is not SaaS versus free, it is SaaS versus a VM plus the hours of the person paged when Kafka fills the disk on a Saturday.
The second is data residency or compliance. Stack traces are not sanitised data: they routinely carry local variables, request bodies, headers, query parameters and user identifiers, which makes an error tracker one of the most sensitive data flows you operate. If a regulator or a customer contract says that data does not leave your infrastructure, self-hosting stops being a cost argument and becomes a requirement.
The third is an air-gapped or egress-restricted network, where no hosted tracker can receive your events and the decision has been made for you. Both good reasons are requirements rather than optimisations, which is what licenses you to accept the operational weight. If you are here to save money, check whether something far smaller satisfies you first.
What is actually in the box
Open the compose stack and you find, broadly, these moving parts. Each exists for a reason, and the reason tells you which ones you can never remove.
- Relay — the ingest edge. It terminates the SDK connection, authenticates the DSN, applies rate limits and quotas, scrubs sensitive fields, and hands accepted events onward. It exists so a traffic spike from a broken deploy is absorbed at the door instead of at the database.
- Kafka — the ingest buffer. Events go from Relay onto topics and are consumed asynchronously. This is what lets ingest keep accepting while processing lags, and it is also what turns a slow consumer into a growing disk.
- Workers and consumers — background processes that read from Kafka, do symbolication and grouping, run alert rules and send notifications. Grouping is real work: deciding that these ten thousand stack traces are one issue is the product's core value.
- ClickHouse (via Snuba) — the event store and search layer. Every event search, every issue count, every time series in the UI is an analytical query over a large column store.
- Postgres — the boring relational metadata. Organisations, projects, users, DSN keys, alert rule definitions, issue state. Small, transactional, and the part you actually must not lose.
- Redis — queues, caches, rate-limit counters and buffers. Fast ephemeral state that several of the above depend on.
- The web/API service — the UI you look at and the endpoints your SDKs talk to. The smallest part of the footprint and the only part most people picture.
Now the sentence that explains the architecture: error tracking at scale is a high-cardinality analytics workload wearing a bug tracker's costume. Every event carries a release, an environment, a server, a transaction, a user and arbitrary tags. The questions you ask of it — how many users hit this in the last hour, is this correlated with the deploy at 14:02, which of these forty tags is unique to the failing requests — are OLAP questions over millions of wide rows. A relational schema cannot answer them well at volume, which is why ClickHouse is in the diagram; once analytics is decoupled from ingest you need a durable buffer, which is why Kafka is.
None of this is over-engineering. It is what the problem demands at the scale Sentry runs its own cloud, which is more scale than you have. You are handed the architecture rather than a version of it sized for one team.
The one-VM path, honestly
The supported route is the getsentry/self-hosted repository: a compose stack plus an install script that generates secrets, pulls images, runs migrations and brings the services up. On an adequately sized machine it works, and I do not want to talk anyone out of trying it.
# A single machine, root, Docker + the compose plugin already installed.
# Check the CURRENT minimum RAM/CPU/disk in the docs before you pick a size --
# it is higher than people expect and undersizing is the #1 way this fails.
git clone https://github.com/getsentry/self-hosted.git /opt/sentry
cd /opt/sentry
# Pin a release. Do NOT track the default branch: the upgrade path below
# depends on knowing exactly which version you are on.
git checkout 25.x.x # <- a real tag from the repo's releases page
# The installer generates secrets, pulls images, runs migrations, and
# creates the first user. Budget 20-40 minutes on a cold machine and do
# not run it over a connection that might drop -- use tmux or screen.
./install.sh
docker compose up -d
docker compose ps # every service should be 'running'/'healthy'
# The two things to watch from day one, in this order:
df -h /var/lib/docker # disk is how this dies
docker compose logs --tail=100 -f snuba-errors-consumer # lag is how it warns you
# Kafka consumer lag: if this number grows and never comes back down,
# events are arriving faster than they are being stored. That is your
# early signal, and it arrives long before the UI looks wrong.
docker compose exec kafka kafka-consumer-groups \
--bootstrap-server localhost:9092 --describe --all-groupsThe sizing reality: RAM-bound first, disk-bound second. ClickHouse wants page cache and will use whatever you give it, Kafka wants disk for retained partitions, Postgres and Redis want their working sets resident. Put the data directories on a volume you can grow, and monitor free space before anything else.
Because the most common way a self-hosted Sentry dies is not a subtle distributed-systems failure. It is a fixed-size disk filling up. When it fills, several components fail at once and confusingly: ClickHouse refuses writes, Kafka stops accepting, Postgres may go read-only, and the UI keeps rendering old data long enough for everyone to assume the problem is elsewhere. Alert at seventy percent, not ninety — reclaiming space on a full ClickHouse is much harder than on a nearly full one.
The upgrade tax nobody warns you about
Here is the part that turns a working installation into a bad quarter. Self-hosted Sentry has a documented upgrade path with hard version checkpoints you cannot skip, because the intermediate releases carry migrations — schema changes, ClickHouse table rewrites, Kafka topic changes — that must be applied in order.
So an upgrade you do every month or two is a routine evening. An upgrade after falling six releases behind is not an upgrade, it is a sequential migration project, each step with its own window and its own chance to fail halfway. Teams discover this at the worst moment, usually when a CVE forces their hand.
- Write down the version you installed, somewhere that is not a shell history. The upgrade path is defined relative to it.
- Calendar the upgrades. Monthly is comfortable; quarterly is the outer limit before the checkpoint list looks like a project plan.
- Snapshot the whole machine before each one, not just the databases. Rolling one service back to match a partially migrated ClickHouse is not something to attempt live.
- Read the release notes for every version between yours and the target. The breaking change is in one you skipped.
- Rehearse on a restored copy of production, which turns the real upgrade from an experiment into a repeat.
And note what you own beyond Sentry itself. ClickHouse and Kafka are real systems with their own upgrade semantics and failure modes. You did not choose to become a ClickHouse operator. You became one anyway, as a side effect of wanting to know when your app throws exceptions.
Retention is the entire affordability question
One knob decides whether this is sustainable: event retention. The instinct to keep everything because storage is cheap is precisely how the disk fills, and it is worth being clear why that instinct is wrong here specifically.
Error events are not uniformly distributed. Traffic is bursty and errors are burstier, because errors are correlated: one bad deploy, one dependency outage, one retry loop in a client, and a single issue produces more events in an hour than your whole application produced last month. With a generous retention window that flood is not a spike you ride out — it is permanently resident, competing for space with the incident you have not had yet.
So set retention deliberately and shorter than feels comfortable. Thirty days answers almost every question anyone actually asks an error tracker: is this new, is it getting worse, did the fix work. Then set per-project rate limits and SDK sample rates — Relay enforcing quotas at the edge is good, but the cheapest event is the one the client never sends. Cap ingest before you need to, because the moment you need to is the moment you are least able to think clearly.
The lighter alternatives, and why they are usually right
GlitchTip and Bugsink exist because a lot of people wanted Sentry's developer experience without its operational surface. Both accept events from the standard Sentry SDKs, which is the crucial property: you do not rewrite instrumentation, you change a DSN. Both store their data in Postgres. Neither needs Kafka or ClickHouse.
GlitchTip is an open-source, Sentry-compatible tracker with a familiar issues-and-events UI, project and team structure, and alerting to the usual destinations. Bugsink is similarly SDK-compatible and leans further into being deliberately small and single-machine. Both are simpler than a full Sentry stack by an order of magnitude in component count, and both trade away the deeper feature surface: performance monitoring, tracing, session replay, profiling and the more elaborate integrations. Check current coverage in each project's docs, since both move.
In practice: a compose service and an env file pointing at a managed Postgres rather than a database you also have to operate.
# docker-compose.yml -- a Sentry-compatible tracker, Postgres-backed.
# Two services instead of a fleet. The database is managed elsewhere,
# so nothing here holds state you cannot afford to lose.
services:
web:
image: glitchtip/glitchtip
depends_on: [redis]
ports: ["8000:8000"]
env_file: [.env]
restart: unless-stopped
worker:
image: glitchtip/glitchtip
command: ./bin/run-celery-with-beat.sh
depends_on: [redis]
env_file: [.env]
restart: unless-stopped
redis:
image: redis:7-alpine
restart: unless-stopped
# Queues and cache only. State of record lives in Postgres.
# .env -- note the sslmode: managed Postgres should be TLS-only.
#
# DATABASE_URL=postgres://USER:PASSWORD@abc123.db.pandastack.ai:5432/pandastack?sslmode=require
# SECRET_KEY=<generate a long random value; rotating it logs everyone out>
# PORT=8000
# GLITCHTIP_DOMAIN=https://errors.example.com
# DEFAULT_FROM_EMAIL=errors@example.com
# EMAIL_URL=smtp://user:pass@smtp.example.com:587
# ENABLE_OPEN_USER_REGISTRATION=False
#
# # The retention knob. Set it deliberately, and lower than feels right.
# GLITCHTIP_MAX_EVENT_LIFE_DAYS=30The application side is a DSN change and nothing else. Existing SDK calls, breadcrumbs and release tagging keep working, because the wire protocol is the one Sentry's own SDKs speak.
# app.py -- the same SDK, pointed at your own instance.
import os
import sentry_sdk
sentry_sdk.init(
# The host in the DSN is the only thing that changed. Sentry SaaS,
# self-hosted Sentry, GlitchTip and Bugsink all accept this shape.
dsn=os.environ["SENTRY_DSN"],
environment=os.environ.get("APP_ENV", "production"),
# Tag events with the commit so "did the fix work" is answerable.
release=os.environ.get("GIT_COMMIT", "dev"),
# Sample BEFORE you need to. A retry loop in one client can otherwise
# spend a month of storage in an afternoon.
sample_rate=1.0, # error events: keep all of them, at first
traces_sample_rate=0.0, # tracing: off unless your backend supports it
# Stack traces carry local variables and request data. Decide what
# leaves the process rather than discovering it in the UI later.
send_default_pii=False,
)
def before_send(event, hint):
# Drop the noisy class of error that is not actionable, at the source.
if event.get("logger") == "healthcheck":
return None
return eventThere is a fourth option worth saying out loud, because it is unfashionable and frequently correct: structured logs plus an alert. If what you want is "tell me when it breaks," logging exceptions as structured JSON into a pipeline you already run, with an alert on the error rate, is a complete answer. No grouping, no issue state, no regression detection — but also no upgrade path, no Kafka and no disk to watch. For a small team with one service, the error tracker is sometimes more complex than the thing it monitors.
Side by side
- Sentry SaaS — Model: they run the fleet, you send events over the internet. Ops: none. Best for: teams wanting the full surface (tracing, replay, profiling) whose data may leave their infrastructure. Verify current pricing and quotas on their site.
- Self-hosted Sentry — Model: the full compose stack — Relay, Kafka, ClickHouse, Postgres, Redis, web and workers — on your machine. Ops: substantial and permanent, including ClickHouse and Kafka upgrades and a checkpointed upgrade path. Best for: residency, compliance and air-gapped networks, where it is the only correct answer.
- GlitchTip — Model: open-source, Sentry-SDK-compatible, Postgres-backed, no Kafka or ClickHouse. Ops: modest — a web service, a worker, Redis and a database. Best for: issue grouping and alerting without the analytics tier. Trade: a smaller feature surface.
- Bugsink — Model: Sentry-SDK-compatible, deliberately single-machine, Postgres-backed, built to stay predictable under load. Ops: the lowest of the self-hosted options. Best for: one team and a few services, and a preference for something you can reason about entirely. Trade: same as above, more so.
- Structured logs plus an alert — Model: exceptions as JSON into a pipeline you already run, alerting on error rate. Ops: nearly none. Best for: "tell me when it breaks" and nothing more. Trade: no grouping, no issue state, no regression detection.
Where PandaStack fits, stated plainly
Being direct, because this is my company: we do not offer managed Sentry. We also do not offer managed Kafka, ClickHouse, Redis or a metrics backend. If you want a hosted Sentry stack, we are not it.
What we run is managed PostgreSQL 16 — one dedicated Firecracker microVM per database with a durable volume, TLS, point-in-time restore and clone-to-a-new-database, created in 30 to 90 seconds. That is precisely the dependency GlitchTip and Bugsink have, and the piece you least want to operate yourself, because it holds the issue state that makes the tool useful. We also host the app that reports to it, from a git repo, as a full Ubuntu userspace with root and a real kernel under KVM.
So the honest shape is: run GlitchTip or Bugsink as an app, point its DATABASE_URL at a managed Postgres 16, and have your services report to it. A two-service deployment plus a database somebody else backs up — a reasonable Tuesday.
# The database first -- managed Postgres 16, 30-90s to ready.
pandastack db create --label errors-db --size 4g
pandastack db get errors-db --json | jq -r .connection_url
# postgres://pandastack:...@abc123.db.pandastack.ai:5432/pandastack?sslmode=require
# Then the tracker as a normal app from git.
pandastack apps create errors \
--git-url https://github.com/acme/glitchtip-deploy \
--branch main \
--env DATABASE_URL="postgres://...:5432/pandastack?sslmode=require" \
--env GLITCHTIP_MAX_EVENT_LIFE_DAYS=30 \
--env SECRET_KEY="$(openssl rand -hex 32)"
pandastack apps deploy errors --followOne thing I will not pretend about: a Sentry-class stack wants persistent, always-warm stateful services, and that is not what scale-to-zero is for. Our apps hibernate when idle and wake on a request in about a second — right for a web app, wrong for a Kafka broker or a ClickHouse that must be resident to accept ingest. An error tracker also has an obligation the app it watches does not: it must be up precisely when everything else is falling over. Buy the warm machine deliberately for that tier. Snapshot-restore create is a p50 of 179ms, so rebuilding is cheap, but cheap rebuilds are no substitute for a service meant to always be listening.
The short version
If you are self-hosting for residency, compliance or an air gap, run the real thing: pin a version, calendar the upgrades monthly, alert on free disk at seventy percent, set retention short. If you are self-hosting to escape a bill, try GlitchTip or Bugsink first — same SDK events, Postgres and not much else, and for most teams below serious event volume they are not a compromise but the correct answer. And if you only want to know when things break, structured logs plus one alert may be the whole project.
Whichever you choose, put monitoring on the monitor. The failure mode specific to this category is quietly comic and entirely real: the system whose job is telling you something is wrong is the one system that cannot tell you when it is the wrong thing.
A bug tracker with a Kafka dependency is not over-engineered. It is engineered for somebody else's volume, and you have been handed the architecture instead of a version of it sized for you.
Frequently asked questions
What do you actually need to run self-hosted Sentry?
More than a container. The self-hosted distribution is a compose stack containing Relay for authenticated ingest and rate limiting, Kafka as the ingest buffer, workers and consumers for symbolication, grouping and alerting, ClickHouse via Snuba as the event store and search layer, Postgres for relational metadata such as organisations, projects and issue state, Redis for queues and caching, and the web/API service itself. The docs specify a minimum RAM footprint that surprises most people the first time they read it, so check the current figure before sizing a machine. Undersizing does not produce slowness — it produces OOM-killed consumers and silently backed-up ingest. Budget for the engineering hours too, not just the VM: cost-driven self-hosting usually underestimates them.
Why does Sentry need ClickHouse and Kafka just to track errors?
Because error tracking at scale is a high-cardinality analytics workload wearing a bug tracker's costume. Every event carries a release, environment, server, transaction, user and arbitrary tags, and the questions the UI answers — how many users hit this issue in the last hour, is this correlated with the deploy at 14:02, which tag is unique to failing requests — are OLAP queries over millions of wide rows. A normalised relational schema cannot answer those well at volume, which is why ClickHouse stores events. Once analytics is decoupled from ingest, you need a durable buffer between them so a traffic spike is absorbed rather than dropped, which is what Kafka provides.
What is the upgrade path for self-hosted Sentry, and why does it matter?
Self-hosted Sentry has a documented upgrade path with version checkpoints you cannot skip, because intermediate releases carry mandatory migrations — database schema changes, ClickHouse table rewrites, Kafka topic changes — that must be applied in order. Upgrading monthly is a routine evening. Falling six releases behind turns the upgrade into a sequential migration project, each step with its own window and its own chance to fail halfway, and teams usually discover this when a CVE forces the issue. Record the version you installed somewhere durable, calendar the upgrades, snapshot the whole machine before each one, read the notes for every version you skipped, and rehearse on a restored copy first.
Should I use GlitchTip or Bugsink instead of Sentry?
For a lot of teams, yes. Both accept events from the standard Sentry SDKs, so switching is a DSN change rather than re-instrumenting your application, and both store their data in Postgres with no Kafka and no ClickHouse. That makes them dramatically simpler to operate: a web service, a worker, Redis and a database. What you give up is the deeper feature surface — performance monitoring, tracing, session replay, profiling and the more elaborate integrations — so check each project's current docs for coverage before committing. If your questions are "is this new, is it getting worse, did the fix work," the lighter option answers all three.
Can I run an error tracker on PandaStack?
Yes, with a clear boundary. We do not offer managed Sentry, managed Kafka, managed ClickHouse, managed Redis or a metrics backend, so a full self-hosted Sentry stack is not something we host for you. What we do run is managed PostgreSQL 16 — a dedicated Firecracker microVM per database with a durable volume, TLS, point-in-time restore and clone-to-a-new-database, ready in 30 to 90 seconds — which is exactly what GlitchTip and Bugsink need. Deploy the tracker as an app from git and point its DATABASE_URL at that database. Note that an error tracker should stay warm rather than scale to zero: it must be up precisely when everything else is not.
Keep reading
- How to ship app logs to your own stack — The structured-logs answer, in full.
- How to tail logs and debug a running app — What to reach for before you install anything.
- The best ClickHouse hosting platforms in 2026 — If you self-host Sentry, you now operate one of these.
- The best Kafka hosting platforms in 2026 — The other component you did not choose to own.
- The best managed Postgres providers in 2026 — The one dependency GlitchTip and Bugsink actually have.
- Managed Postgres 16 — Dedicated microVM, durable volume, ready in 30-90s.
49ms p50 cold start. Fork, snapshot, and scale to zero.