Self-Hosted Error Tracking Without the 16GB Sentry Bill

A 5.75MB Docker image that speaks the Sentry SDK protocol. Swap the DSN, keep your SDK, run error capture next to anything on a cheap VPS.

Self-Hosted Error Tracking Without the 16GB Sentry Bill

Every error report is data you're shipping somewhere

For most teams that somewhere is Sentry, and the SDK story is good: one init call, one DSN, and crash reports flow. The trouble starts when those reports can't live on someone else's cloud. Stack traces carry URLs, query params, environment values, sometimes user identifiers. Audit season makes this concrete fast.

So you look up self-hosting Sentry. The official docs ask for a machine with 4 CPU cores, 16 GB of RAM plus 16 GB of swap, and they warn that the stack is disk I/O heavy because it runs databases, message brokers, and other services on a single box. That is a dedicated server for an error log.

We built TrapFall for the other end of that trade. It is a self-hosted error capture engine, Sentry SDK compatible, that ships as a single Rust binary and a 5.75 MB Docker image. SQLite by default. It runs on the cheapest VPS you can rent, next to whatever else is already there.

Honest status before anything else: TrapFall is young. Current release is v0.2.1, out since end of June, with 8 stars on GitHub. Everything below is what it does today, not a roadmap.

Why self-hosted Sentry got this heavy

Self-hosted Sentry is feature-complete on purpose. The bundle carries the platform Sentry sells: tracing, session replay, profiling, the works. If your day job is debugging a large distributed system, that machinery earns its RAM. If you want an error log for a handful of services, you are paying a platform tax in gigabytes.

GlitchTip sits in the middle. It is MIT licensed, speaks the Sentry SDK protocol, and has a real community. Architecturally it is a Python/Django application that wants Postgres and Redis alongside it, so you end up maintaining a small compose stack with several moving parts.

The gap we kept running into: nothing fills the tier below that. One process, one file, one port. Error capture as infrastructure you forget about, like the syslog daemon you configured once in 2019.

How TrapFall works

Sentry-compatible ingest, DSN swap and nothing else

TrapFall implements the envelope endpoint your Sentry SDK already speaks. Point the DSN at your server on port 9090 and the existing init code keeps working:

import sentry_sdk
sentry_sdk.init(dsn="https://<key>@your-server:9090/<project_id>")

The same pattern holds for the Rust, JavaScript, and Flutter SDKs. No forked SDK, no shim library, no vendor lock you have to untangle later. Projects are isolated per DSN, and you can rotate a DSN from the dashboard when a key leaks, which revokes the old one immediately.

Grouping that cuts the noise

Every incoming event gets a Blake3 fingerprint, so 400 copies of the same NullPointerException land as one issue with a counter instead of 400 rows in your face. The dashboard is SvelteKit 5 with a WebSocket feed: issues appear live, no refresh. Search runs over titles and culprits with trigram substring matching on SQLite, plus filters by status and level.

Alerts that respect your sleep

Alert rules are per project, condition based, and delivered as webhooks with a cooldown. A bug storm pages you once, not once per duplicate. Wire the webhook to whatever you already use for paging.

The deployment footprint

One command, one volume:

docker pull ghcr.io/codecoradev/trapfall:latest
docker run -p 9090:9090 -v trapfall-data:/data ghcr.io/codecoradev/trapfall:latest

Open port 9090, walk through the first-run setup wizard, create your admin account, and a default project with its DSN is waiting. SQLite is the zero-config default. When a busy service outgrows it, Postgres is built in: set TRAPFALL_DATABASE_URL and restart. No flags, no separate migration tooling.

Decisions and trade-offs

5.75 MB or bust

The image is scratch-based with a MUSL static binary and rustls instead of OpenSSL. That is how you get an error tracker smaller than most README screenshots. The cost is real: there is no shell inside the container, no package manager, nothing to poke at. You debug from logs and stack traces, not by exec-ing in. For a service whose job is to be tiny and boring, we think that is the right trade.

SQLite first, Postgres when you earn it

SQLite removes an entire service from your failure domain, which matters more than raw throughput for most self-hosters. At extreme ingest volume it will ceiling out. The escape hatch is one environment variable away, which is the honest way to handle a limitation: visible, boring, documented.

Compatibility scoped to ingest

TrapFall speaks the SDK protocol for error events. It does not reimplement Sentry's platform. No tracing, no session replay, no performance profiles. If your team lives inside those views, Sentry is the correct tool and this post is not an argument against it. For "tell me when it breaks and show me why", protocol compatibility is the entire job, and dropping the platform is what buys back the 16 GB.

An MCP server, because triage is a query problem

TrapFall also runs as an MCP server exposing 12 tools over stdio JSON-RPC. An agent can list projects, search issues, pull details, and act on them from a terminal session. Error tracking is as much a retrieval problem as a storage one, and we would rather expose it as tools than pretend a dashboard is the only interface.

What's next

The last commit landed in mid-August, so things are quiet right now. Near-term attention goes where you would guess: SDK compatibility in the wild, fingerprint tuning as real event shapes show up, and docs. If you point your existing Sentry SDK at it and something arrives mangled, that is precisely the issue worth filing, and the fastest way to make the compatibility claim boring and true.

TrapFall is Apache-2.0 licensed. The repo lives at github.com/codecoradev/trapfall, releases are on GitHub with prebuilt binaries, and OpenAPI docs are served at /api/docs on your own instance. Smallest useful test: run the container, swap one DSN, break something on purpose.