## You don't need a Kubernetes cluster to track production errors

Sentry is a great product. Sentry's self-hosted distribution is also a great product — and a 20-service Docker Compose stack with substantial CPU, RAM, and disk needs. If your team's main interaction with self-hosted observability is a Slack thread that begins the Sentry box is OOMing again, you're not alone.

BugSink takes the opposite approach. One application container, one Postgres database, Sentry-SDK-compatible. You point your existing Sentry SDKs at it by changing the DSN. Per [BugSink's own marketing page](https://www.bugsink.com/), it's more lightweight and less complicated than Sentry — and the install proves it.

This tutorial wires BugSink onto a $5 VPS with Docker, Postgres, HTTPS via Nginx, and Git-based auto-deploys from [DeployHQ](https://www.deployhq.com/). By the end, every change to the stack — env vars, container versions, Nginx config — ships from a `git push`, with [one-click rollback in DeployHQ](https://www.deployhq.com/features/one-click-rollback) if a tweak breaks something.

## What you'll build

- **BugSink** (current 2.x major version) as a single application container
- **Postgres 17** in another container for persistent storage of issues, events, and project config
- **Docker Compose** to glue them together with persistent volumes and restart policies
- **Nginx + Let's Encrypt** for HTTPS termination in front of BugSink
- **DeployHQ** for Git-based auto-deploys so the whole stack lives in version control
- A working **DSN** you can drop into any Sentry SDK to redirect events to your VPS

> **Why BugSink instead of Sentry self-hosted?** Three reasons that come up in real conversations. **Cost predictability** — BugSink is free to self-host with unlimited users and events; you pay for the VPS, full stop. **Resource footprint** — one app + one DB versus Sentry's ~20-service Helm/Compose distribution. **Data residency** — for teams who need stack traces (and the source context Sentry sends with them) to stay on infrastructure they own, BugSink is much easier to keep audit-ready.
> 
> **Why a VPS instead of a hosted plan?** If volume is low (handful of apps, hobby projects) the VPS is genuinely cheaper. If volume is high, you're paying once for a box you control instead of per-event metering forever. Either way, a $5/month VPS at [Hetzner](https://www.deployhq.com/blog/how-to-deploy-django-on-a-budget-with-hetzner-and-deployhq), DigitalOcean, Vultr, or Linode is enough to start.

## Prerequisites

- A VPS running Ubuntu 22.04 or 24.04 with SSH access
- A domain name pointing at the VPS (e.g. `bugsink.example.com`)
- A GitHub account with an empty private repo for the deploy config
- A [DeployHQ](https://www.deployhq.com) account (free trial — sign-up link at the end)
- Docker and Docker Compose installed locally for testing

## Step 1: Prepare the VPS

SSH in as `root` and install Docker:

```
ssh root@your-vps-ip
curl -fsSL https://get.docker.com | sh
```

Create a non-root `deploy` user that [DeployHQ](https://www.deployhq.com) will SSH in as:

```
adduser deploy
usermod -aG docker deploy
mkdir -p /home/deploy/bugsink
chown deploy:deploy /home/deploy/bugsink
```

Copy your SSH key to the new user from your local machine so [DeployHQ](https://www.deployhq.com) can authenticate without a password:

```
ssh-copy-id deploy@your-vps-ip
```

Confirm `ssh deploy@your-vps-ip` works passwordless. If it works for you, [DeployHQ](https://www.deployhq.com) can do the same.

## Step 2: The Docker Compose stack

BugSink ships an official production sample at [github.com/bugsink/bugsink/blob/main/docker-compose-sample.yaml](https://github.com/bugsink/bugsink/blob/main/docker-compose-sample.yaml). Use it as the starting point, then tighten the production knobs.

In your fresh GitHub repo, create `compose.yaml`:

```
services:
  db:
    image: postgres:17-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: bugsinkuser
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: bugsink
    volumes:
      - db-data:/var/lib/postgresql/data
    healthcheck:
      test: pg_isready -h db
      retries: 5
      start_period: 10s
      interval: 5s
      timeout: 5s

  web:
    image: bugsink/bugsink:2
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped
    ports:
      - "127.0.0.1:8000:8000"
    environment:
      SECRET_KEY: ${SECRET_KEY}
      DATABASE_URL: postgresql://bugsinkuser:${POSTGRES_PASSWORD}@db:5432/bugsink
      PORT: 8000
      BEHIND_HTTPS_PROXY: "true"
      USE_X_FORWARDED_HOST: "true"
    healthcheck:
      test: ["CMD-SHELL", "python -c 'import requests; requests.get(\"http://localhost:8000/\").raise_for_status()'"]
      interval: 5s
      timeout: 20s
      retries: 10
    deploy:
      resources:
        limits:
          memory: 1G
          cpus: "1.0"

volumes:
  db-data:
```

Three changes from BugSink's sample worth flagging:

1. **`SECRET_KEY` and `POSTGRES_PASSWORD` are read from environment** , not baked into the file. We'll inject them from [DeployHQ](https://www.deployhq.com) as a config file in the next step. Never commit secrets to the repo.
2. **Port 8000 is bound to `127.0.0.1`** , not `0.0.0.0`. BugSink should not be exposed directly to the public internet — Nginx will handle that. Bind to localhost; firewall the external port at the host level too.
3. **`BEHIND_HTTPS_PROXY` and `USE_X_FORWARDED_HOST` are both `true`** because Nginx will terminate TLS and forward `X-Forwarded-Proto` / `X-Forwarded-Host`. BugSink needs to know to trust those headers when generating absolute URLs (otherwise the DSN it shows you will use `http://`, and the SDK won't be able to connect).

Generate a real `SECRET_KEY` locally:

```
python -c "import secrets; print(secrets.token_urlsafe(50))"
```

And a Postgres password the same way. Save both — you'll paste them into [DeployHQ](https://www.deployhq.com) in the next step.

Also add a `.gitignore`:

```
.env
*.local
```

Commit everything (the compose file, the gitignore, nothing else) and push to GitHub.

## Step 3: Wire it up with DeployHQ

This is where the stack stops being a one-off SSH session and starts being a real deployment.

In DeployHQ:

1. **Create a new project** and point it at your GitHub repo. [DeployHQ](https://www.deployhq.com) supports [deploying directly from a GitHub repo to your server](https://www.deployhq.com/deploy-from-github) without manual webhook setup.
2. **Add a server.** Use the VPS IP, port 22, username `deploy`, and the SSH key you authorised in Step 1. Set the deployment path to `/home/deploy/bugsink`.
3. **Add a config file** at `/home/deploy/bugsink/.env` containing:`
SECRET_KEY=<the value you generated>
POSTGRES_PASSWORD=<the password you generated>
`[DeployHQ](https://www.deployhq.com) writes this file to disk before every deploy, so Compose picks it up — and the secrets never live in your repo or your container image.
4. **Configure the SSH command.** Under the server's SSH commands, add:`bash
cd /home/deploy/bugsink && docker compose up -d
`That's it. [DeployHQ](https://www.deployhq.com) checks out the repo, writes the env file, SSHes in, and runs Compose. New container versions, env tweaks, resource limit changes — all of them ship via the same one-line deploy.
5. **Enable auto-deploy on push to `main`.** Every merge now triggers a build.

Trigger the first deploy manually. Watch the log: clone, write env, run Compose. About 30 seconds later, BugSink and Postgres are both up. Check with `docker compose ps` on the VPS — both services should be `healthy`.

If anything breaks at this stage, DeployHQ's one-click rollback restores the previous deploy in seconds, so you can iterate on config without leaving the box in a half-broken state.

## Step 4: HTTPS with Nginx and Let's Encrypt

BugSink is now running on `127.0.0.1:8000`. To put it on the internet at `bugsink.example.com` with HTTPS:

On the VPS (still as `root`):

```
apt update && apt install -y nginx certbot python3-certbot-nginx
```

Create `/etc/nginx/sites-available/bugsink`:

```
server {
    listen 80;
    server_name bugsink.example.com;

    client_max_body_size 25M;

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host $host;
        proxy_read_timeout 60s;
    }
}
```

Two notes. **`client_max_body_size 25M`** — SDK event payloads can be sizeable (source context, attachments) and the default 1MB will reject them. **`X-Forwarded-Proto` and `X-Forwarded-Host`** are why we set `BEHIND_HTTPS_PROXY` and `USE_X_FORWARDED_HOST` in Compose; the two halves have to agree.

Enable the site and get a cert:

```
ln -s /etc/nginx/sites-available/bugsink /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
certbot --nginx -d bugsink.example.com
```

`certbot` handles cert provisioning, the redirect from HTTP to HTTPS, and auto-renewal via systemd timers. After this, `https://bugsink.example.com/` returns the BugSink login screen.

## Step 5: First login and creating a project

The first time you visit your BugSink instance you'll need a superuser. The cleanest way to create one is a one-off `manage.py` invocation against the running container:

```
docker compose exec web python -m bugsink.manage createsuperuser
```

You'll be prompted for email, username, and password. The official `docker-compose-sample.yaml` also supports a `CREATE_SUPERUSER: email:password` env var for the very first boot, but injecting a password through env is messy; the `createsuperuser` command keeps the secret off your disk history.

Log in. Create a new **project** (one per application is the standard pattern — same as Sentry). BugSink shows you the DSN on the project page; it looks like a normal Sentry DSN, just pointing at your VPS hostname instead of `o<your-org>.ingest.sentry.io`.

## Step 6: Point an application at BugSink

The whole point of Sentry-SDK compatibility: nothing in your application code changes.

In the SDK init for a Python app, that's:

```
import sentry_sdk

sentry_sdk.init(
    dsn="https://<your-bugsink-key>@bugsink.example.com/<project-id>",
    environment="production",
    release="my-app@1.2.3",
)
```

Same for JavaScript, Ruby, PHP, Go, .NET, Rust — every Sentry SDK that takes a DSN takes BugSink's. Per [BugSink's own quickstart docs](https://www.bugsink.com/docs/quickstart/), install an SDK, copy the DSN, trigger an error on purpose, and it appears in BugSink within seconds.

Two things to know:

- **`release` is the same field Sentry uses** , and BugSink's release tracking works the same way — issues get tagged with the release they first appeared in. The `release` string should be a version + commit SHA your build pipeline produces, not a timestamp.
- **`environment`** has to match between your SDK config and any release notifications you send (more on that below). `prod` vs `production` mismatches will silently break attribution.

## Production hardening

A working deploy is the floor, not the ceiling. Five things to add before BugSink starts taking real traffic:

**1. Pin the image by digest, not the floating `:2` tag.** BugSink's `:2` tag is the current major-version label, and the maintainer pushes patch updates to it. For reproducible builds, pin to a specific digest (`bugsink/bugsink@sha256:...`) and bump it explicitly via a commit. Same for `postgres:17-alpine` — pin the minor.

**2. Back up the Postgres volume.** Stack traces are not infinite — BugSink has retention policies — but losing your historical issue grouping mid-incident is painful. A daily `pg_dump` to S3 (or another off-host destination) via a cron container is the minimum bar. The DB volume in the compose above is what survives container restarts; without backups it does not survive a VPS disk failure.

**3. Watch the ingest rate limit.** Per [BugSink's ingestion rate limit docs](https://www.bugsink.com/docs/ingestion-rate-limits-and-retention/), there are project-level limits to keep one noisy app from drowning out the rest. Configure these before an app starts throwing 500 errors at 100/sec.

**4. Resource limits on the web container.** The compose above already includes a 1G memory / 1 CPU cap on the web service. Tune up if you're ingesting heavy traffic — BugSink's event digestion isn't free, and unbounded growth on a $5 VPS will OOM the whole box (including Postgres).

**5. Rate-limit the ingest endpoint at Nginx.** BugSink will rate-limit at the application layer per the link above, but a Nginx `limit_req` zone on `/api/` (or wherever the SDK posts events) acts as a cheap pre-filter against accidental loops in misconfigured clients.

For the deploys themselves, [DeployHQ](https://www.deployhq.com) runs [zero downtime deployments](https://www.deployhq.com/features/zero-downtime-deployments) by default. When you graduate to multiple replicas behind a load balancer or move BugSink to a bigger box, the rollout pattern stays the same.

## Tying BugSink issues to DeployHQ deploys

Because BugSink speaks the Sentry release API, the existing [DeployHQ](https://www.deployhq.com) [Sentry deploy tracking integration](https://www.deployhq.com/blog/sentry-deploy-tracking-with-deployhq) can in principle point at a BugSink installation — same `project:releases` scope, same release-creation endpoint shape. If your team relied on Sentry's release-tagged regressions before, you can keep that workflow with BugSink as the backend. (Test before relying on it in production — Sentry SDK compatibility is BugSink's headline claim, but the management API surface has edges; verify against your specific version.)

If you'd rather not test that route, the same outcome falls out of including a `release` value in every SDK init that matches the commit SHA your build pipeline already exposes. BugSink groups issues by release automatically.

## Where to go from here

The setup above is a working production install for a single low-to-medium-traffic team. A few paths from here, depending on what you actually need next:

**More self-hosted observability on the same VPS pattern.** The same Docker Compose + Nginx + [DeployHQ](https://www.deployhq.com) pattern fits a long list of self-hostable tools:

- [Hermes Agent on a VPS](https://www.deployhq.com/blog/deploy-hermes-agent-vps) — self-improving agent runtime with persistent skills
- [Self-host the Paperclip orchestrator on a VPS](https://www.deployhq.com/blog/self-host-paperclip-vps-docker-deployhq) — agent orchestrator with a Docker-first install
- [Self-host Immich on a VPS](https://www.deployhq.com/blog/self-host-immich-vps-docker-deployhq) — open-source Google Photos alternative
- [Self-host Vaultwarden on a VPS](https://www.deployhq.com/blog/self-host-vaultwarden-vps-docker-deployhq) — open-source Bitwarden server
- [Deploy your first AI agent to a VPS with Docker](https://www.deployhq.com/blog/deploy-first-ai-agent-vps-docker) — minimal FastAPI agent in the same shape

**Or stick with hosted error tracking and add deploy tracking.** If self-hosting is one bridge too far and you'd rather stay on Sentry, Bugsnag, Rollbar, or Honeybadger, the [DeployHQ](https://www.deployhq.com) integration pattern is identical to what you just built — one config block in [DeployHQ](https://www.deployhq.com), every release shows up in the dashboard:

- Sentry deploy tracking with [DeployHQ](https://www.deployhq.com) (see the integration linked above)
- [Bugsnag deploy tracking with DeployHQ](https://www.deployhq.com/blog/track-deployments-in-a-bugsnag-project)
- [Rollbar deploy tracking with DeployHQ](https://www.deployhq.com/blog/rollbar-notifications)
- [Honeybadger deploy tracking with DeployHQ](https://www.deployhq.com/blog/honeybadger-deploy-tracking-with-deployhq)

The point isn't which backend you pick — it's that error tracking, the deploy that introduced each error, and a one-click rollback should all live in the same workflow. BugSink + [DeployHQ](https://www.deployhq.com) gets you there for the price of a VPS.

* * *

Ready to put error tracking on infrastructure you control? [Start a free](https://www.deployhq.com/signup)[DeployHQ](https://www.deployhq.com) trial and have BugSink shipping from `git push` in under thirty minutes.

Need a hand? Email us at **[support@deployhq.com](mailto:support@deployhq.com)** or find us on X at [@deployhq](https://x.com/deployhq).

