Skip to content

Self-Hosting Guide

This guide covers everything needed to run BetBlocker on your own infrastructure. The self-hosted deployment is identical in functionality to the hosted platform except billing and the automated discovery pipeline are disabled.


ResourceMinimum
CPU1 vCPU
RAM1 GB
Disk5 GB
OSAny Linux, macOS, or Windows with Docker
Section titled “Recommended (small organisation, up to ~50 devices)”
ResourceRecommended
CPU2 vCPU
RAM2 GB
Disk20 GB SSD
OSUbuntu 22.04 LTS / Debian 12
  • Outbound HTTPS to feed.betblocker.org (community blocklist sync)
  • Inbound on your chosen API_PORT (default 8443) reachable by enrolled devices
  • Inbound on WEB_PORT (default 80) for the web dashboard

Terminal window
# Ubuntu / Debian
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
# Log out and back in for group change to take effect

Verify: docker compose version — must be v2.x (docker compose, not docker-compose).

Terminal window
git clone https://github.com/JerrettDavis/BetBlocker.git
cd betblocker/deploy

Or download just the deploy/ directory from a release archive if you prefer not to clone the full repo.

Terminal window
cp .env.example .env

Edit .env:

# REQUIRED: change this
DB_PASSWORD=use-a-long-random-string-here
# REQUIRED: must be reachable by enrolled devices
BETBLOCKER_EXTERNAL_URL=https://betblocker.example.com:8443
# Optional overrides (defaults shown)
# BETBLOCKER_VERSION=latest
# API_PORT=8443
# WEB_PORT=80
# LOG_LEVEL=info
# BETBLOCKER_COMMUNITY_FEED_URL=https://feed.betblocker.org/v1
Terminal window
docker compose up -d
docker compose ps # wait for all services to be healthy
Terminal window
docker compose exec api /betblocker-api setup

This runs migrations, generates keys, and prompts for the initial admin account. Run it once. Running it again on an already-initialised instance is a no-op.

Open http://your-server (or https:// if you have TLS configured — see below). Log in with the admin credentials created in step 5.


Use this path if you cannot use Docker or need to customise the build.

Terminal window
# Rust (version pinned in rust-toolchain.toml)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Node.js 20+
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
# PostgreSQL 16 + TimescaleDB
# See: https://docs.timescale.com/self-hosted/latest/install/
Terminal window
# API and worker
cargo build --release -p bb-api -p bb-worker
# Web dashboard
cd web && npm ci && npm run build

Binaries will be at target/release/bb-api and target/release/bb-worker.

Terminal window
# Set all required environment variables (see Environment Variables section)
export BB_DATABASE_URL="postgres://betblocker:password@localhost:5432/betblocker"
export BB_REDIS_URL="redis://localhost:6379"
export BB_JWT_PRIVATE_KEY_PATH="/etc/betblocker/jwt-signing.pem"
export BB_JWT_PUBLIC_KEY_PATH="/etc/betblocker/jwt-signing-pub.pem"
./target/release/bb-api serve &
./target/release/bb-worker &
# Web dashboard
cd web && npm start

For production, wrap each process in a systemd unit. See deploy/systemd/ for example unit files.


The Docker deployment handles this automatically. If deploying manually:

TimescaleDB is a PostgreSQL extension — it runs as a standard PostgreSQL instance:

Terminal window
# After installing PostgreSQL 16 and the timescaledb package:
sudo -u postgres psql -c "CREATE USER betblocker WITH PASSWORD 'your-password';"
sudo -u postgres psql -c "CREATE DATABASE betblocker OWNER betblocker;"
sudo -u postgres psql -d betblocker -c "CREATE EXTENSION IF NOT EXISTS timescaledb;"
Terminal window
./target/release/bb-api migrate

Migrations are embedded in the binary and run automatically via setup or can be run explicitly with migrate.

The worker creates hypertable partitioning on the events table during first setup. No manual SQL is needed.


All variables use the BB_ prefix (e.g., BB_DATABASE_URL). In docker-compose.yml some variables are named without the prefix for Docker compatibility — the table below shows both forms where they differ.

VariableRequiredDefaultDescription
BB_DATABASE_URLYesPostgreSQL connection string
BB_REDIS_URLNoredis://localhost:6379Redis connection URL
BB_HOSTNo0.0.0.0API bind address
BB_PORTNo3000API bind port (Docker exposes on 8443)
BB_JWT_PRIVATE_KEY_PATHYesPath to Ed25519 private key PEM for JWT signing
BB_JWT_PUBLIC_KEY_PATHYesPath to Ed25519 public key PEM for JWT verification
BB_JWT_ACCESS_TOKEN_TTL_SECSNo3600Access token lifetime in seconds
BB_JWT_REFRESH_TOKEN_TTL_DAYSNo30Refresh token lifetime in days
BB_CORS_ALLOWED_ORIGINSNo*Comma-separated allowed CORS origins
BB_PUBLIC_BASE_URLNoPublic URL used in generated links and QR codes
BB_BILLING_ENABLEDNofalseEnable Stripe billing endpoints (hosted only)
BB_STRIPE_SECRET_KEYConditionalRequired when BILLING_ENABLED=true
BB_STRIPE_WEBHOOK_SECRETConditionalRequired when BILLING_ENABLED=true
BETBLOCKER_DEPLOYMENTNoSet to self-hosted to disable hosted-only features
BETBLOCKER_COMMUNITY_FEED_URLNohttps://feed.betblocker.org/v1Blocklist feed URL
BETBLOCKER_FEDERATED_REPORT_UPSTREAMNoOpt-in: upstream URL for federated report contribution
BETBLOCKER_FEDERATED_REPORT_API_KEYNoAPI key for federated upstream
LOG_LEVELNoinfoLog verbosity: trace, debug, info, warn, error
BETBLOCKER_LOG_FORMATNojsonLog format: json or text
PathContents
/keys/jwt-signing.keyEd25519 JWT signing key
/keys/blocklist-signing.keyBlocklist signing key
/keys/root-ca.keyRoot CA private key
/keys/device-ca.keyDevice certificate CA key

Keys are generated by bb-api setup and stored in the betblocker-keys Docker volume. Back this volume up — losing it requires re-enrolling all devices.


Run nginx in front of the API if you want standard HTTPS on port 443 and to avoid exposing port 8443.

/etc/nginx/sites-available/betblocker
# Redirect HTTP to HTTPS
server {
listen 80;
server_name betblocker.example.com;
return 301 https://$host$request_uri;
}
# API
server {
listen 443 ssl http2;
server_name betblocker.example.com;
ssl_certificate /etc/letsencrypt/live/betblocker.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/betblocker.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# Web dashboard
location / {
proxy_pass http://127.0.0.1:80;
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;
}
# API
location /v1/ {
proxy_pass http://127.0.0.1:8443;
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_read_timeout 30s;
client_max_body_size 2M;
}
location /health {
proxy_pass http://127.0.0.1:8443;
}
}
Terminal window
sudo ln -s /etc/nginx/sites-available/betblocker /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

After setting up the proxy, update .env:

BETBLOCKER_EXTERNAL_URL=https://betblocker.example.com
API_PORT=8443 # still needed for container binding

Section titled “Let’s Encrypt (recommended for internet-facing instances)”
Terminal window
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d betblocker.example.com

Certbot will auto-renew via a systemd timer. Verify: sudo certbot renew --dry-run

Self-signed certificate (LAN / internal use)

Section titled “Self-signed certificate (LAN / internal use)”

If your instance is not internet-facing:

Terminal window
openssl req -x509 -newkey ed25519 \
-keyout betblocker-tls.key \
-out betblocker-tls.crt \
-days 3650 \
-nodes \
-subj "/CN=betblocker.local"

Enrolled devices must trust your self-signed CA. The agent allows configuring a custom CA certificate at enrollment time.


DataLocationPriority
Databasebetblocker-db Docker volumeCritical
Keysbetblocker-keys Docker volumeCritical — losing keys requires full re-enrollment
Databetblocker-data Docker volumeImportant
.env filedeploy/.envImportant
Terminal window
# Dump
docker compose exec db pg_dump -U betblocker betblocker | gzip > backup-$(date +%Y%m%d).sql.gz
# Restore
gunzip -c backup-20260315.sql.gz | docker compose exec -T db psql -U betblocker betblocker
Terminal window
# Backup keys volume (CRITICAL)
docker run --rm \
-v betblocker_betblocker-keys:/keys:ro \
-v $(pwd):/backup \
alpine tar czf /backup/keys-backup-$(date +%Y%m%d).tar.gz -C /keys .
  1. Stop services: docker compose down
  2. Restore volumes from archives
  3. Restore database from dump
  4. Start services: docker compose up -d

Terminal window
cd deploy
# Pull new images
docker compose pull
# Restart with new images (zero-downtime if you have multiple API replicas)
docker compose up -d
# Migrations run automatically on API startup
# Check logs to confirm
docker compose logs api | grep -E "migration|error"

To pin a specific version, set BETBLOCKER_VERSION=1.2.3 in .env.


Terminal window
curl -s https://your-server/health | jq .
# {"status": "ok", "version": "1.2.3", "db": "ok", "cache": "ok"}

All containers expose Docker health checks. Integrate with your monitoring tool:

Terminal window
# Quick status
docker compose ps
# JSON output for scripting
docker compose ps --format json

Logs are emitted as JSON to stdout (configurable via BETBLOCKER_LOG_FORMAT). Pipe to any log aggregator:

Terminal window
# Example: ship to a syslog target
docker compose logs -f api | logger -t betblocker-api
# Example: forward with Filebeat / Fluentd
# Point your agent at the Docker daemon's log driver output
MetricAlert condition
Container restarts> 2 in 5 minutes
API health endpointNon-ok response
Database disk> 80% full
Worker last runNo successful run in 1 hour
Heartbeat failuresSpike in missed device heartbeats

The database grows as events accumulate. TimescaleDB’s compression and retention policies reduce this significantly. Configure retention from Admin > Analytics Settings in the dashboard.


Self-hosted instances can contribute unknown domain reports back to the central BetBlocker blocklist. This strengthens the community feed for everyone.

To opt in, add to .env:

BETBLOCKER_FEDERATED_REPORT_UPSTREAM=https://api.betblocker.org/v1/reports
BETBLOCKER_FEDERATED_REPORT_API_KEY=your-key-from-betblocker.com

Privacy guarantee: only blocked/flagged domain metadata is sent — never full browsing history, never user-identifying information, never device identifiers. Source IP addresses are stripped before federated report processing.