Minimal HTTP API daemon for managing Borg backup repositories on a self-hosted server. Designed to be driven by a GUI frontend so the end user never has to SSH into the server to create repos, register keys, or see stats.
On your Linux server (the one that will host the repos):
curl -fsSL https://github.com/cprieto/borgbox/releases/latest/download/install.sh | sudo bashThe installer:
- Detects OS/arch and downloads the correct binary from the latest release.
- Verifies the SHA256 against
SHA256SUMS. - Creates a system
borguser (or reuses the existing one). - Creates
~borg/repos/and~borg/.ssh/authorized_keyswith the right permissions. - Writes
/etc/borgbox/{token,borgbox.env}(token is random, 40 chars). - Installs and enables
borgbox.serviceunder systemd. - Prints the bearer token on stdout — save it for your client app.
Options:
sudo bash install.sh \
--addr :9999 \
--home /var/lib/borg \
--version v0.1.0Pin a specific version:
curl -fsSL https://github.com/cprieto/borgbox/releases/download/v0.1.0/install.sh | sudo bashInstall from a local dist/ during development:
sudo ./install.sh --dist ./distBase URL: http://<host>:9999/api/v1
Auth: Authorization: Bearer <token> (all endpoints except /health).
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/v1/health |
no | Liveness check. |
GET |
/api/v1/info |
yes | Version, free disk, uptime, borg version. |
GET |
/api/v1/system/stats |
yes | Hostname, host uptime, load avg, storage. |
GET |
/api/v1/sessions |
yes | Active borg serve processes (from /proc). |
GET |
/api/v1/repos |
yes | List all repos with size and init state. |
POST |
/api/v1/repos |
yes | Create repo directory and register pubkey. |
GET |
/api/v1/repos/{name} |
yes | Detailed stats for one repo. |
DELETE |
/api/v1/repos/{name} |
yes | Remove directory and authorized_keys entry. |
GET |
/api/v1/repos/{name}/archives |
yes | List archives via borg list --json. |
POST |
/api/v1/repos/{name}/break-lock |
yes | Run borg break-lock on a stuck repo (sync). |
POST |
/api/v1/repos/{name}/check |
yes | Async borg check. Returns job_id. |
POST |
/api/v1/repos/{name}/prune |
yes | Async borg prune with retention policy. |
POST |
/api/v1/repos/{name}/compact |
yes | Async borg compact. |
GET |
/api/v1/jobs |
yes | List recent jobs (in-memory, max 200). |
GET |
/api/v1/jobs/{id} |
yes | Status, exit code and last 200 log lines. |
POST /api/v1/repos
Content-Type: application/json
Authorization: Bearer <token>
{
"name": "mac",
"pubkey": "ssh-ed25519 AAAAC3... user@host"
}201 Created on success. 409 Conflict if a repo with that name already has
a key registered. 400 for invalid inputs.
After a successful create, the client app can run:
borg init --encryption=repokey-blake2 ssh://borg@<host>/./repos/macThe daemon never calls borg init itself — it only prepares the server side
so the client's first borg init over SSH succeeds.
GET /api/v1/repos/mac/archives
Authorization: Bearer <token>
X-Borg-Passphrase: <passphrase> # only for encrypted reposReturns the array of archives reported by borg list --json. The header is
discarded after the call — borgbox never persists passphrases.
check, prune and compact return 202 Accepted with a job_id. Poll
/api/v1/jobs/{id} to follow progress.
POST /api/v1/repos/mac/check
Content-Type: application/json
Authorization: Bearer <token>
{ "repair": false, "verify_data": false, "passphrase": "..." }POST /api/v1/repos/mac/prune
Content-Type: application/json
Authorization: Bearer <token>
{
"keep_daily": 7,
"keep_weekly": 4,
"keep_monthly": 12,
"keep_yearly": 2,
"dry_run": false,
"passphrase": "..."
}POST /api/v1/repos/mac/compact
Content-Type: application/json
Authorization: Bearer <token>
{ "passphrase": "..." }Job record:
{
"id": "j_8f2a4c",
"repo": "mac",
"kind": "check",
"status": "running",
"started_at": "2026-04-13T10:02:17Z",
"finished_at": "",
"exit_code": -1,
"dry_run": false,
"log_tail": ["Starting repository check", "..."]
}status is one of queued, running, done, error. exit_code is -1
while the job is still queued or running. The job registry is in-memory only,
capped at 200 jobs (oldest finished are evicted first), and is wiped on
borgbox restart.
- Not a borg server.
borg serveis invoked by sshd via a forced command inauthorized_keys, using the system's/usr/bin/borgbinary. borgbox only manages the filesystem layout and the key file. - No TLS out of the box. Put it behind Tailscale, Caddy, or nginx if it needs to leave the LAN.
- No multi-user, quotas, or multi-key-per-repo yet.
- Runs as the unprivileged
borguser, not root. - systemd unit enforces
ProtectSystem=strict,NoNewPrivileges,ReadWritePathslimited to the repo root and SSH dir. - Each key line in
authorized_keyshas acommand="borg serve --restrict-to-repository ..."prefix plusrestrict(no PTY, no forwarding, no X11). - Repo names are validated against
^[a-z0-9][a-z0-9-]{0,62}$. - SSH public keys are validated by type prefix before being written.
- Delete removes only the line tagged
# borgbox:<name>, so manually-added keys are safe.
make build # native binary
make dist # cross-compile for linux/darwin × amd64/arm64Cross-compile outputs land in dist/ with a SHA256SUMS file, mirroring what
goreleaser produces in CI.
Tag the commit and push:
git tag v0.2.0
git push --tagsGitHub Actions runs goreleaser, which compiles all 4 targets, creates the
GitHub release, uploads the binaries, writes SHA256SUMS, and attaches
install.sh so the curl | sudo bash URL keeps working for the new version.
The daemon reads these env vars (also accepted as CLI flags):
| Variable | Default | Description |
|---|---|---|
BORGBOX_ADDR |
:9999 |
Listen address. |
BORGBOX_REPO_ROOT |
~borg/repos |
Where repo directories live. |
BORGBOX_AUTH_KEYS |
~borg/.ssh/authorized_keys |
File to append SSH keys to. |
BORGBOX_TOKEN |
(required) | Bearer token. Prefix with @ to read a file. |
The installer writes these to /etc/borgbox/borgbox.env.