# Self-host our MIT error tracker with one Docker Compose file

Telemetry Tracker is our open-source (MIT) error tracker for side projects and small teams: grouped errors, events, sessions and Web Vitals, with a Fastify API, a Next.js dashboard and PostgreSQL. Since v1.22.8, the repo has a `selfhost/` folder that starts all three with one `docker compose up -d`.

This tutorial walks through it end to end: what you need, the three secrets, what actually happens when the stack starts, how to put TLS in front, how to point your apps at it, and what stays your job afterwards. We also include the numbers we measured, with the one caveat that matters: **they were measured on one test box (8 vCPU / 15.6 GiB), not a guarantee.**

## What you need

- Docker with **Compose v2.20+**
- About **5 GB of free disk** for the build (images plus build cache)
- No Node or pnpm on the host. Everything builds inside Docker.

Images are built locally from the repo. We don't publish prebuilt images yet, so the first `up` includes a build.

## Step 1: Clone and enter `selfhost/`

```bash
git clone https://github.com/Telemetry-Tracker/telemetry-tracker.git
cd telemetry-tracker/selfhost
cp .env.example .env
```

## Step 2: Set the three required secrets

No secrets ship with the stack, and Compose refuses to start until these three are set in `selfhost/.env`:

```bash
POSTGRES_PASSWORD=
CHECKIN_ENCRYPTION_KEY=
DASHBOARD_API_SECRET=
```

Generate each one separately:

```bash
openssl rand -hex 32
```

If you forget one, `docker compose up` stops with a message like `set POSTGRES_PASSWORD in selfhost/.env` instead of starting with an empty value.

`CHECKIN_ENCRYPTION_KEY` deserves a note: check-in secrets are encrypted with it, so keep it together with your database backups. A backup without the key can't decrypt them.

The same `.env` file has a few more settings you'll touch later:

```bash
DASHBOARD_URL=http://localhost:3000
API_PUBLIC_URL=http://localhost:3001
BIND_ADDRESS=127.0.0.1
DASHBOARD_PORT=3000
API_PORT=3001
TELEMETRY_ALLOW_REGISTRATION=false
```

## Step 3: Start it

```bash
docker compose up -d
```

Here's what happens on start:

- **Startup is ordered by healthchecks:** Postgres (`postgres:16-alpine`) first, then the API, then the dashboard.
- **Migrations run on every start.** The API applies database migrations before it begins listening, so there's no separate migrate step on first run or after an upgrade.
- **Non-root:** the API and dashboard run as the unprivileged `node` user.
- **Postgres isn't published.** It has no host port and is only reachable on the internal Compose network.
- **Localhost only by default.** The API and dashboard ports are published on `127.0.0.1`, not on all interfaces.

Once the services are healthy, open `http://localhost:3000` and register. The first account can always sign up; after that, signup is invite-only unless you set `TELEMETRY_ALLOW_REGISTRATION=true`. Then create an organization, a project and an API key.

Email is optional. Invites, password resets and notification emails go through Resend if you set `RESEND_API_KEY` and `TELEMETRY_EMAIL_FROM`; you don't need either to ingest errors.

## Step 4: Put a TLS reverse proxy in front

Because both services bind to `127.0.0.1`, nothing is reachable from outside the box yet. That's deliberate. You have two options:

1. **Recommended:** run a reverse proxy you already trust (Caddy, Nginx, Traefik) on the same host and terminate TLS there. For example, forward one hostname to the dashboard on `127.0.0.1:3000` and another to the API on `127.0.0.1:3001`.
2. Set `BIND_ADDRESS=0.0.0.0` to listen on all interfaces. That's **plain HTTP**, so only do it on a network you control.

Then set the public URLs in `.env` to your real HTTPS addresses (no trailing slash): `DASHBOARD_URL` for the dashboard and `API_PUBLIC_URL` for the API your SDKs will send to. The API also uses `DASHBOARD_URL` for its allowed CORS origin.

One gotcha: `DASHBOARD_URL` is **baked into the dashboard image at build time**. After changing it, rebuild:

```bash
docker compose up -d --build
```

## Step 5: Point your SDKs at your API

Self-hosting uses the same npm SDKs as our hosted cloud. You only change `ingestUrl` to your API base URL (no trailing slash) and pass a project API key. For Node, from our docs:

```ts
import { init, trackEvent, trackError } from "@telemetry-tracker/node";

init({
  ingestUrl: process.env.TELEMETRY_INGEST_URL || "https://your-api.example.com",
  app: "my-api",
  apiKey: process.env.TELEMETRY_API_KEY,
  platform: "node",
});
```

Set `TELEMETRY_INGEST_URL` to your `API_PUBLIC_URL` (for a local test, `http://localhost:3001`). The Next.js, core and React Native SDKs take the same `ingestUrl` and `apiKey` options; the framework pages in the docs show where `init` goes. Ingest is authenticated with project API keys, so keep it that way in production.

The Node SDK batches events every few seconds. In our measured run, a Node SDK event showed up on the Events page 7.5 s after the SDK started.

## Step 6 (optional): Scheduled alerts, check-ins and retention

**Scheduled** alert rules (heartbeat / no-events) and cron check-ins need the evaluator, which ships in the same Compose file behind a profile:

```bash
docker compose --profile jobs up -d
```

Retention isn't automatic in the Compose stack. Run the retention job nightly from the host's cron:

```bash
docker compose exec api node dist/jobs/run-retention.js
```

## Upgrading

```bash
git pull && docker compose up -d --build
```

Migrations apply on start, so the new API migrates the database before it starts listening. Read the release notes before you pull, especially for security releases.

## Backups

The Compose stack doesn't include a backup job. Your telemetry lives in the Postgres data volume (`pgdata`), so back it up with whatever Postgres tooling you already use, test a restore, and store `CHECKIN_ENCRYPTION_KEY` alongside it.

## What we measured

On 2026-10-19 we measured the `selfhost/` stack (v1.22.7 plus the `selfhost/` change) on **one test box: 8 vCPU / 15.6 GiB, Debian 13, Docker 26.1.5**. The steps were scripted, so time spent reading docs isn't included. **These numbers come from that one setup and are not a guarantee.**

| | Result |
|---|---|
| Cold start (no images, no build cache) | `docker compose up -d` took 221 s to pull, build, start and migrate; all services healthy 232 s after `up`, about 4 minutes from clone |
| Cached (images already built, empty database) | healthy 12 s after `up` |
| Idle memory (`docker stats`, 30 samples, 5 min after last activity) | API 77 MiB, dashboard 119 MiB, Postgres 65 MiB: about 261 MiB total |
| Idle CPU | about 0.4% of one core on average |
| Image sizes | API 575 MB, dashboard 357 MB, `postgres:16-alpine` 294 MB |
| Build cache after a cold build | 3.6 GB (`docker builder prune` reclaims it) |

Most of the cold time is building the two images. An earlier idle sample, with more frequent healthchecks, came to 391 MiB, so your number will depend on your setup. The full method and tables are in DEPLOYMENT.md in the repo.

## What stays your job

Self-hosting means you operate it. You're responsible for:

- **Uptime:** monitoring the box and the containers, and restarting what falls over
- **Backups:** the Postgres volume and the check-in encryption key, plus restore tests
- **TLS:** the reverse proxy and its certificates
- **Upgrades:** pulling releases, especially security fixes, and rebuilding

Other honest limits:

- Images are built locally; there are no prebuilt images yet.
- Hosted plan limits apply only on telemetry-tracker.com, not to your instance.
- It's an error tracker with events, sessions and Web Vitals, not a full observability suite: no tracing and no session replay.

If you'd rather not run any of this, our hosted cloud has a free plan with no credit card, and it uses the same SDKs, so moving between hosted and self-hosted mostly means changing `ingestUrl` and the API key.

## Links

- Self-hosting docs: [telemetry-tracker.com/docs/self-hosting](https://telemetry-tracker.com/docs/self-hosting?utm_source=hashnode&utm_medium=article&utm_campaign=selfhost-launch)
- Release notes for v1.22.8: [github.com/Telemetry-Tracker/telemetry-tracker/releases/tag/v1.22.8](https://github.com/Telemetry-Tracker/telemetry-tracker/releases/tag/v1.22.8)
- The `selfhost/` folder: [github.com/Telemetry-Tracker/telemetry-tracker/tree/v1.22.8/selfhost](https://github.com/Telemetry-Tracker/telemetry-tracker/tree/v1.22.8/selfhost)

If something in these steps doesn't match what you see, open an issue or a discussion on the repo. We'd rather fix the docs than have you guess.
