Rebuild Runbook
How to stand BackupBloc Control back up on a fresh server — from bare Debian to a fully-licensed, TLS-terminated, admin-UI-serving host at license.backupbloc.com.
Read this first. These steps assume you are rebuilding from scratch on a new server. If you just want to push a code change to the existing server, jump to Redeploy code.
account_treeOverview
BackupBloc Control is a single Flask app + SQLite DB + static admin UI, fronted by nginx with Let's Encrypt TLS. It runs as one systemd service (backupbloc-control.service) on port 5100, and nginx terminates HTTPS on 443.
The server has exactly one job: it is the license & update authority that every customer panel phones home to. Customer panels identify themselves by their public outbound IP — no tokens, no keys. If the control server is offline, panels keep working for 12 hours on the last cached validation, then lock down.
What you need before you start
- A fresh Debian 12 (or Ubuntu 22.04/24.04) box with root SSH access.
- The server's public IP (for the DNS A record).
- Control of the
license.backupbloc.comDNS record (or whatever hostname you're using). - A local clone of the
backupbloc-controlrepo on your dev machine. - If restoring: the
licenses.db,users.json,sessions.json, andreleases/directory backups from the old server.
buildRebuild steps
Provision a fresh Debian box
Any hosting provider will do — we've used standard VPS boxes. Minimum: 1 vCPU, 1 GB RAM, 20 GB disk. The app is tiny; most of the disk goes to uploaded release tarballs.
Log in as root and confirm the base system is up-to-date:
apt-get update -q && apt-get dist-upgrade -yq
Point DNS at the new server
Create an A record for license.backupbloc.com → your new server's public IP. Wait for it to resolve globally before the next step — certbot will fail otherwise.
Verify from your laptop:
dig +short license.backupbloc.com
# should return the new server IP
Let's Encrypt issues the cert by making an HTTP-01 callback to http://license.backupbloc.com/. If DNS isn't propagated yet, the installer's certbot step fails and you'll have to re-run it manually.
Upload the code to the new server
From your dev machine, either clone from git or SCP the repo over. The fastest way is to use the deployment helpers already in the repo, but for a fresh box you need the whole repo not just the app files. Simplest:
# On the new server, as root:
apt-get install -yq git
git clone <your-repo-url> /root/backupbloc-control
cd /root/backupbloc-control
Or, if you don't keep a remote git mirror, from your laptop:
scp -r ./backupbloc-control root@<new-ip>:/root/
Run the installer
One command does everything — installs packages, copies the app to /opt/backupbloc-control/, writes /etc/backupbloc-control/config.env, registers the systemd unit, configures nginx, and requests a TLS certificate:
cd /root/backupbloc-control
bash install-server.sh --hostname license.backupbloc.com
The installer is idempotent — re-running it on a working box is safe.
The installer already patches the known Debian 12 certbot crash (pyOpenSSL 23.0.0 breaks against modern cryptography). It upgrades pyopenssl to a matching version automatically.
Useful flags
| Flag | Purpose |
|---|---|
--hostname | Public FQDN. Certbot issues a cert for this name. Omit for a private/LAN install. |
--api-port | Override the internal Flask port (default 5100). |
--skip-cert | Skip Let's Encrypt — useful when DNS isn't ready yet. Re-run certbot later. |
Grab the initial admin password
The installer prints it at the end. If you missed it:
journalctl -u backupbloc-control.service | grep -i "initial admin password"
Log in at https://license.backupbloc.com/ (user admin), then go to Settings → Security and change the password immediately.
Smoke test
# From anywhere:
curl -s https://license.backupbloc.com/api/health
# → {"ok":true,"service":"backupbloc-control"}
# Unlicensed IPs always get 403:
curl -s -X POST https://license.backupbloc.com/api/validate \
-H "Content-Type: application/json" -d "{}"
# → {"valid":false,"reason":"No license for this IP","ip":"..."}
Issue yourself a license immediately. Go to Licenses → New and create one for the customer panel's public IP (e.g. your own panel at 119.148.82.102). If you don't, the panel locks itself out once Phase B is deployed.
restoreRestoring from a backup
If you're rebuilding because the old server died, run steps 1–4 on the new box (this creates a fresh empty DB), then stop the service, overwrite the data files, and start it again:
systemctl stop backupbloc-control.service
# Restore from wherever you keep the backup (rsync, S3, restic, etc.)
cp /path/to/backup/licenses.db /etc/backupbloc-control/licenses.db
cp /path/to/backup/users.json /etc/backupbloc-control/users.json
cp /path/to/backup/sessions.json /etc/backupbloc-control/sessions.json
rsync -a /path/to/backup/releases/ /var/lib/backupbloc-control/releases/
chown -R root:root /etc/backupbloc-control /var/lib/backupbloc-control
chmod 700 /etc/backupbloc-control
chmod 600 /etc/backupbloc-control/*.json /etc/backupbloc-control/licenses.db /etc/backupbloc-control/config.env
systemctl start backupbloc-control.service
systemctl status backupbloc-control.service
All admin sessions in sessions.json will still work — no need to re-login on your laptop unless sessions have expired by time.
settingsDay-2 operations
Redeploying code changes
From your dev machine, with BBC_PASS set:
export BBC_HOST=license.backupbloc.com BBC_PASS=<root password>
python _deploy_api.py # pushes license-api.py + schema.sql, restarts service
python _deploy_html.py # pushes admin/index.html
Both scripts use paramiko over SSH. They restart the systemd service automatically when needed.
Logs and debugging
# Live tail
journalctl -u backupbloc-control.service -f
# Last 200 lines
journalctl -u backupbloc-control.service -n 200 --no-pager
# Nginx access/error
tail -f /var/log/nginx/access.log /var/log/nginx/error.log
# Service state
systemctl status backupbloc-control.service
What to back up on this server
The only stateful paths you need to protect:
| Path | Why |
|---|---|
/etc/backupbloc-control/licenses.db | All issued licenses, customers, check-in history, audit trail. |
/etc/backupbloc-control/users.json | Admin accounts (hashed passwords). |
/etc/backupbloc-control/sessions.json | Active admin sessions. Safe to lose — you'll just re-login. |
/etc/backupbloc-control/config.env | Port, trust-proxy, grace-hours config. |
/var/lib/backupbloc-control/releases/ | Uploaded signed panel release tarballs. |
Everything else — the app code, systemd unit, nginx config, certbot state — is reproduced by re-running the installer.
TLS certificate renewal
Certbot installs a systemd timer that auto-renews. Check it:
systemctl list-timers | grep certbot
certbot certificates
menu_bookReference
File locations (on the control server)
| Path | Contents |
|---|---|
/opt/backupbloc-control/api/ | Flask app (license-api.py, schema.sql) |
/opt/backupbloc-control/admin/ | Admin UI (nginx-served static files) |
/etc/backupbloc-control/config.env | Runtime config |
/etc/backupbloc-control/licenses.db | SQLite database |
/etc/backupbloc-control/users.json | Admin accounts |
/etc/backupbloc-control/sessions.json | Active admin bearer-token sessions |
/var/lib/backupbloc-control/releases/ | Uploaded release tarballs |
/etc/systemd/system/backupbloc-control.service | systemd unit |
/etc/nginx/sites-available/backupbloc-control | nginx site config |
/etc/letsencrypt/live/license.backupbloc.com/ | TLS cert + key |
config.env settings
| Key | Default | Purpose |
|---|---|---|
API_PORT | 5100 | Flask listens here; nginx proxies to it |
TRUST_PROXY | 1 | Honour X-Forwarded-For (needed behind nginx) |
GRACE_HOURS | 12 | How long a customer panel keeps working if we're unreachable |
CHECKIN_KEEP_DAYS | 90 | Telemetry retention window |
Restart the service after editing: systemctl restart backupbloc-control.service.
Public API endpoints (customer panels call these)
| Endpoint | Purpose | Frequency |
|---|---|---|
POST /api/validate | License check (IP-gated) | 24h |
GET /api/updates/manifest | Check for new version | 6h |
GET /api/updates/bundle/<version> | Download signed tarball | On apply |
POST /api/checkin | Telemetry (version, agent count) | 1h |
POST /api/updates/event | Report update outcome | Per update |
GET /api/health | Liveness probe (no auth) | — |
All public endpoints except /api/health are IP-gated: unlicensed IPs receive 403 {valid:false, reason, ip}.
Release signing keys
Release bundles are signed with an ed25519 keypair. The private key stays on your dev laptop and is never uploaded to the control server. The public key ships in the customer panel installer.
# One-time, on your dev laptop:
bash tools/keygen.sh
# private: ~/.backupbloc/update-signing-key.pem (keep offline)
# public : ~/.backupbloc/update-pubkey.pem
# Each release:
bash tools/sign-bundle.sh dist/backupbloc-1.8.0.tar.gz
# prints the base64 signature — paste into Releases → Upload Release
If you lose the private key, you can never sign another release that existing panels will trust — they'll all reject new bundles. Back it up (and the passphrase if you added one) somewhere offline and redundant.
troubleshootTroubleshooting
Service won't start
journalctl -u backupbloc-control.service -n 50 --no-pager
Common causes: syntax error in config.env, missing Python dep (re-run pip3 install --break-system-packages -r /opt/backupbloc-control/api/requirements.txt), permission issue on /etc/backupbloc-control/ (chmod 700 that dir).
Certbot fails with AttributeError: module 'lib' has no attribute 'X509_V_FLAG_NOTIFY_POLICY'
The Debian 12 / pyOpenSSL / cryptography mismatch. The current installer fixes it automatically; if you hit it on an old install:
pip3 install --break-system-packages --upgrade pyopenssl
certbot --nginx -d license.backupbloc.com --redirect --non-interactive --agree-tos --register-unsafely-without-email
Customer panel says "no license for this IP" but a license exists
- Check their current public IP (ask them to visit
https://ifconfig.me). ISPs change CGNAT IPs. - Confirm
TRUST_PROXY=1is inconfig.env— otherwise Flask sees nginx's 127.0.0.1 instead of the real client IP. - Look at the last check-in for that customer in the admin UI; it records the IP they came from.
Admin login rejected but you're sure the password is right
Sessions live in /etc/backupbloc-control/sessions.json — if that file is corrupted, delete it (you'll just have to log in fresh). User accounts live in users.json. To reset the admin user entirely: stop the service, delete users.json, start the service — it will seed a new random password and log it.
Locked out of the admin UI entirely
SSH to the server and run:
systemctl stop backupbloc-control.service
rm /etc/backupbloc-control/users.json /etc/backupbloc-control/sessions.json
systemctl start backupbloc-control.service
journalctl -u backupbloc-control.service -n 20 | grep -i "initial admin password"