Self-host BugSink on a VPS — the open-source Sentry alternative

Devops & Infrastructure, Docker, Tutorials, and VPS

Self-host BugSink on a VPS — the open-source Sentry alternative

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, 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. 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 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, 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 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 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 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 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. 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 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 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 supports deploying directly from a GitHub repo to your server 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 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 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, 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, 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 runs 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 Sentry deploy tracking integration 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 pattern fits a long list of self-hostable tools:

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 integration pattern is identical to what you just built — one config block in DeployHQ, every release shows up in the dashboard:

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 gets you there for the price of a VPS.


Ready to put error tracking on infrastructure you control? Start a free DeployHQ trial and have BugSink shipping from git push in under thirty minutes.

Need a hand? Email us at support@deployhq.com or find us on X at @deployhq.