Managed Agents
Managed Agents let one Borg UI server coordinate backups on client machines. The client runs borg-ui-agent, connects outbound to Borg UI, runs Borg locally, and streams progress and logs back to the server.
Open Managed Agents from the Infrastructure navigation group.
Add an Agent
In Managed Agents, choose Add Agent. The wizard asks for:
- platform: Linux or macOS
- agent name
- enrollment token expiry: 1 hour, 24 hours, 7 days, 30 days, or Never
- service user (Linux only): Installing user, dedicated
borg-ui-agentuser, or Root - server URL reachable by the client machine
The final step shows a one-line installer command:
curl -fsSL http://borg-ui-host:8083/agent/install.sh | sudo bash -s -- \
--server http://borg-ui-host:8083 \
--token borgui_enroll_example \
--name laptop \
--borg-repo "<BORG_REPO_URL>" \
--no-promptReplace <BORG_REPO_URL> with the repository the machine backs up to (see Repository defaults), or remove --borg-repo when there is none yet; the installer refuses the placeholder left in place.
Run it on the machine that owns the files you want Borg to back up. The installer requires root or sudo, installs system dependencies, registers /etc/borg-ui-agent/config.toml, runs service-check, and enables the systemd service with systemctl enable --now borg-ui-agent.
By default the installer fetches the exact Borg versions the server runs, as the static Linux binaries borgbackup publishes for x86_64 and aarch64. Those binaries need a minimum glibc; when the machine's glibc is too old, the installer says which version it needs and stops. Borg 1 can then be taken from the distribution instead (--borg-source distro). No distribution ships Borg 2 yet: install the server's version yourself and expose it as borg2 on PATH, then re-run the installer with --skip-borg-install. The installer's message prints the commands for that, pinned to the server's version — a virtualenv with borgbackup and borgstore, and a borg2 symlink in /usr/local/bin (a plain pip install provides only borg, which the agent does not use for Borg 2). That install builds Borg from source, so the machine needs a C toolchain, Borg's build dependencies and, from 2.0.0b24 on, OpenSSL 3.2 or newer.
By default, the service runs as the user who invoked sudo. That means the agent can read and write the same paths that user can access, matching the permission model used by SSH remote machines. Repository paths must be writable by that service user.
Advanced service-user modes are available:
--service-user currentuses the sudo-invoking user. This is the default.--service-user borg-ui-agentuses a dedicated low-privilege system user and creates it if needed.--service-user rootruns root-level Borg operations. Use it only when the agent must back up root-owned paths.--service-user USERNAMEruns as another existing local user.
macOS
For a macOS endpoint the dialog shows the same command without sudo:
curl -fsSL http://borg-ui-host:8083/agent/install.sh | bash -s -- \
--server http://borg-ui-host:8083 \
--token borgui_enroll_example \
--name laptop \
--borg-repo "<BORG_REPO_URL>" \
--no-promptRun it as the user whose data is backed up; the installer refuses root. It installs nothing system-wide and needs no package manager: the Borg versions the server runs and a Python runtime come from the server's manifests as checksummed release builds, unpacked under ~/Library/Application Support/borg-ui-agent/, and the agent is loaded as a launchd user agent (com.borg-ui.agent) that starts at login. Locations macOS protects (~/Library/Mail, ~/Library/Containers, and so on) stay unreadable to the job until its interpreter is granted Full Disk Access by hand; until then Borg reports each such path as a warning. The service-user options and --borg-source distro are Linux-only. Details, including what a grant binds to, are in the macOS section of agent/README.md.
The agent runs in the user's login session at the Mac, at its screen or through Screen Sharing; an ssh session alone has none. The installer stops before installing anything when there is no such session, and after a restart the agent, remote upgrade included, is offline until the user logs in. With FileVault that happens anyway, since unlocking the disk at boot logs the user in; without it, automatic login does the same for a Mac nobody sits at.
The job reads what its user reads. macOS keeps each user's private folders (Desktop, Documents, Downloads, Library, Movies, Music, Pictures) readable by that user alone, so an agent installed by one user backs up that user's data and what is world-readable on the machine (/Applications, most of /Library, /usr/local, /etc), and never another user's private folders, /var/root or root-only system state. Membership in the admin group changes none of that, because the job runs as the user, not as root; it matters only for granting Full Disk Access, which System Settings requires an administrator for. A user without admin rights can install the agent and back up the home directory except what TCC protects. To back up several users' data, each user installs an agent in their own session: files, launchd jobs and config are per user, each agent is its own endpoint (give them distinct names; they share the hostname), and each runs while its user is logged in. A whole-machine backup as root is not what this install does: the job executes code the user can write, so it is never loaded as a system daemon.
Repository defaults and unattended installs
The agent reports the repository it backs up to, BORG_REPO (and BORG_REMOTE_PATH, the Borg executable on a host that offers several), from its environment, and Borg UI pre-fills the repository form with them. The command the Add Agent dialog shows passes them as flags, with a <BORG_REPO_URL> placeholder to replace, and --no-prompt, so it asks nothing on the terminal and also runs over ssh -t.
A Linux install never asks for the values: it is scripted over ssh -t or by configuration management as often as it is typed, and a question would hang it. A first-time macOS install run from a terminal without the flags asks for both, and for an ssh:// or rest:// repository offers to open one SSH connection as the user, so the host key and the login are confirmed while someone is there to answer; a service cannot do that later. Empty answers skip them, and --no-prompt skips the questions. That check covers the one repository given there. For the command the dialog shows (which passes --no-prompt), for a Linux install and for every SSH repository added later, accept the host key once as the user the agent runs as before the first job, for example ssh -p 23 user@host exit (on Linux through sudo -u <service user>).
curl -fsSL http://borg-ui-host:8083/agent/install.sh | sudo bash -s -- \
--server http://borg-ui-host:8083 \
--token borgui_enroll_example \
--name laptop \
--borg-repo ssh://u123456@u123456.your-storagebox.de:23/./borg-repository \
--borg-remote-path borg-1.4The values are recorded in agent.env next to the agent config on both platforms (/etc/borg-ui-agent/agent.env on Linux, which the systemd unit reads; ~/Library/Application Support/borg-ui-agent/agent.env on macOS, rendered into the launchd job's environment) and survive a reinstall. On a reinstall a flag replaces that one value, and an empty value (--borg-repo "") clears it. The repository passphrase is never asked for or stored on the machine: Borg UI keeps it and sends it with each job.
The machine appears in Managed Agents after registration and its first live session. The wizard waits for that connection while the command is displayed.
Plain HTTP and self-signed certificates
A server URL on plain http works as it is. The installer names the server as a trusted host for pip, since pip otherwise ignores a cleartext package source, and the agent talks to it in the clear. Remote upgrade is the one feature that needs https (see below).
A server behind a self-signed or private-CA certificate works once that certificate is trusted by the machine. Install it into the system trust store before running the installer:
sudo cp borg-ui-ca.crt /usr/local/share/ca-certificates/
sudo update-ca-certificatesEvery step then verifies against that store: curl fetching the installer, pip fetching the agent package, and the agent itself, which checks TLS against the machine's trust store rather than only the bundle it ships with. There is no flag to skip certificate verification.
Reinstall or Update an Existing Agent
Use the Reinstall agent action on an existing agent card when you want to update the installed borg-ui-agent package on a machine that is already enrolled. Borg UI shows a tokenless command:
curl -fsSL http://borg-ui-host:8083/agent/install.sh | sudo bash -s -- --reinstallRun it on that enrolled machine. Reinstall mode requires the existing /etc/borg-ui-agent/config.toml, preserves the stored agent credential and the recorded repository values, skips the registration step, refreshes the installed package and systemd unit, and restarts borg-ui-agent. You do not need a new enrollment token unless you are enrolling a different machine or recreating a missing local agent config. On macOS the command runs without sudo, as the agent's user, and reloads the launchd job.
Remote Upgrade and What It Grants
New installs place four root-owned files on the endpoint so a future Borg UI release can reinstall the agent from the server instead of you visiting the machine. The agent asks for an upgrade by creating one empty file, which is the entire privilege it is given. It passes no arguments and runs no privileged command itself, so nothing here needs sudo, which the agent's own unit would refuse anyway under NoNewPrivileges=true.
| File | Purpose |
|---|---|
/etc/borg-ui-agent-upgrade.conf | The reinstall parameters. Root-owned, and outside the agent-owned config directory so the agent cannot replace it. |
/opt/borg-ui-agent/bin/borg-ui-agent-upgrade | The helper. Takes no arguments and reads only upgrade.conf. |
/etc/systemd/system/borg-ui-agent-upgrade.service | A oneshot unit that runs the helper. Never enabled. |
/etc/systemd/system/borg-ui-agent-upgrade.path | Watches for /etc/borg-ui-agent/upgrade-requested and starts that one unit when it appears. |
On macOS the agent runs as its user, so there is no privilege to bound and the same three pieces live in that user's directories:
| File | Purpose |
|---|---|
~/Library/Application Support/borg-ui-agent/upgrade.conf | The reinstall parameters. |
~/Library/Application Support/borg-ui-agent/bin/borg-ui-agent-upgrade | The helper, unchanged: no arguments, reads only the conf. |
~/Library/LaunchAgents/com.borg-ui.agent-upgrade.plist | One launchd job that runs the helper and, through KeepAlive → PathState, starts it while upgrade-requested exists. |
Be clear about the trade. Before this, a compromised Borg UI server could already run code as the agent's service user on every endpoint and read any file on it, and it already decided which agent code the endpoint runs. With the helper it can additionally obtain root on that endpoint: write access and persistence. That is a real escalation, not a repackaging of existing trust. It is bounded to the server that already controls the endpoint's agent code, and it is what makes upgrades possible on the installer's default service user mode rather than only on root installs.
Remote upgrade needs an https server URL, because the helper runs what it downloads as root and will not fetch it over cleartext. An endpoint enrolled against an http server reports no remote upgrade support and stays on the manual path.
To decline it on a sensitive host:
curl -fsSL https://borg-ui-host:8083/agent/install.sh | sudo bash -s -- \
--server https://borg-ui-host:8083 --token TOKEN --name NAME \
--no-remote-upgradeThat endpoint keeps the manual reinstall path and reports no remote upgrade support. A later reinstall remembers the choice; pass --remote-upgrade to undo it.
Endpoints enrolled before this release have none of these files and are shown as manual only. One reinstall gives them remote upgrade:
curl -fsSL https://borg-ui-host:8083/agent/install.sh | sudo bash -s -- \
--server https://borg-ui-host:8083 --reinstallKnowing Which Agents Are Out of Date
Every agent reports the version it runs each time it checks in. Borg UI compares that against the agent package the server itself ships, and shows the result as a chip on the agent card:
| Chip | Meaning |
|---|---|
| No chip | The agent runs the version this server serves. Nothing to do. |
| Update available | The agent is older than the version this server serves. Use the Upgrade action on the row, or reinstall it with the plain --reinstall command above. |
| Ahead of server | The agent is newer than the version this server serves, which happens after a server rollback. Upgrade the server rather than downgrading the agent. |
| Pinned | The agent is held at a specific version and will not follow the server. |
| Version unknown | The agent has not reported a version yet, or the version cannot be compared. A freshly enrolled agent shows this until its first check-in. |
A banner above the fleet counts how many endpoints are running an older agent and offers to upgrade the ones this server can move.
Upgrading an Endpoint from the UI
An endpoint that carries the helper described above shows an Upgrade action on its row when it is out of date. Confirming it asks that endpoint to reinstall itself from this server.
What to expect:
- The endpoint disconnects for a short period while it reinstalls, and reconnects on its own.
- An endpoint that is running a backup refuses the upgrade, because the restart would orphan that backup. Try again once it is idle.
- The row shows Upgrading with the target version until the endpoint comes back. Nothing to do while it does.
- When the endpoint reconnects on the target version, the row clears itself.
- If it does not come back within 10 minutes, the row shows Upgrade failed. That endpoint needs the manual reinstall command above; nothing is retried automatically.
Upgrading a Whole Fleet
Two ways to move more than one endpoint at once:
- Tick the checkbox on each out-of-date card and use Upgrade N endpoints in the bar above the list.
- Use Upgrade all in the out-of-date banner. Its count is the number of endpoints that will actually move, so endpoints that cannot upgrade themselves are excluded from it and called out separately in the banner.
Either way one confirmation lists every endpoint before anything is requested.
Endpoints are upgraded at most five at a time. Every upgrading endpoint is briefly offline, and taking a whole fleet down together turns routine maintenance into an outage. Endpoints beyond that limit show Waiting to upgrade and start on their own as slots free, with nothing further for you to do. An endpoint that does not come back in time is marked Upgrade failed, frees its slot, and is skipped by later waves until you act on it.
The limit counts upgrades in flight, not endpoints offline. A timed-out endpoint frees its slot while it may still be mid-reinstall, so briefly more than five can be down at once. The alternative, holding a slot until an endpoint reconnects, lets one machine that never comes back stall the rest of the fleet indefinitely.
Endpoints shown as manual only, and endpoints enrolled before the helper existed, keep the manual reinstall path and are never included in a fleet upgrade.
Pinning an Agent Version
An endpoint can be held at a specific agent version so it stops tracking the server. The pin action on the agent row opens the version pin, with the agent version (default "Track server") and the Borg major version. You can only pin to a version this server can actually serve, because the installer installs from this server and nowhere else. A pinned endpoint upgrades to its pin rather than to the version the server serves.
The Borg choice takes effect at that endpoint's next upgrade. Pinning Borg 2 on an endpoint running Borg 1 does not change anything by itself: press Upgrade on the row, and the reinstall installs the pinned major version. The row shows "Borg 2 pending" until the endpoint reports it.
A Borg pin outranks how the endpoint was installed. An endpoint installed with --skip-borg-install still gets the pinned version, and an endpoint that took Borg 1 from distribution packages gets a pinned Borg 2 from this server's static binaries, because no distribution ships Borg 2.
Because the upgrade is only complete once the endpoint reports the pinned major version, a Borg pin that cannot be installed shows up as a failed upgrade once the timeout elapses, rather than as a success that changed nothing.
The same pin is available through the API:
curl -X PUT "$BASE_URL/api/managed-machines/agents/<id>/desired-version" \
-H "X-Borg-Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"desired_agent_version": "0.1.3", "desired_borg_version": null}'This is an admin endpoint. $TOKEN is an API token for an admin account; see the API guide for how to obtain one. Send null for desired_agent_version to clear the pin and track the server again. You can only pin to a version this server can actually serve, because the installer installs from this server and nowhere else.
desired_borg_version takes "1", "2", or null to leave whatever is installed alone. It is applied by the next upgrade of that endpoint, not by this call.
Server URL and Localhost
The --server value must be reachable from the client machine. If Borg UI and the agent run on the same machine, localhost is valid. If the agent runs on another machine, localhost points at that client machine, so use the Borg UI server host name, IP address, reverse-proxy URL, or HTTPS URL.
Borg UI proposes a server URL in the Add Agent wizard. You can edit it before generating the command.
Moving an endpoint to a new server address
If the Borg UI server moves, for example because its IP address changed or it went behind a reverse proxy, every enrolled agent still holds the old address and goes offline. Reinstalling does not fix this: --reinstall deliberately preserves /etc/borg-ui-agent/config.toml, which is where the old address lives.
To see what address an endpoint currently holds, run this on that machine:
borg-ui-agent statusTo move it, open Managed Agents, click Change server URL on that endpoint's card, enter the new address, and run the command it gives you on that machine. The endpoint keeps its identity, its credential and its history, so you do not need a new enrollment token and you do not get a second card in the fleet list.
The command Borg UI shows depends on the agent version that endpoint last reported. From agent 0.1.5 onward it uses the subcommand:
sudo borg-ui-agent set-server "https://borg.example.com" && sudo systemctl restart borg-ui-agentOlder agents do not have that subcommand, so Borg UI shows an equivalent edit of the config file instead. Either way it is one command, and you do not have to choose between them.
Removing an endpoint
borg-ui-agent unregister tells the server the endpoint is gone and deletes its config, but it leaves the service, the virtualenv and the rest on the machine. To remove everything, open Managed Agents, click Uninstall on that endpoint's card, and run the command it gives you:
curl -fsSL https://borg-ui.example.com/agent/uninstall.sh | sudo bashIf this Borg UI server is reachable only over plain HTTP, the dialog says so. The command downloads a script and runs it as root, so anyone on the network path between the endpoint and the server can replace what it downloads. That is worth fixing with HTTPS or a tunnel before you run it across a network you do not control. It is a warning rather than a block, because a private LAN server on plain HTTP is a normal Borg UI setup and you are the one who can judge your own network.
The script unregisters with the server first, so the card shows the endpoint as revoked without you clicking Delete. If the server cannot be reached, which is likely if you are removing an endpoint that has been stranded, it says so and removes everything locally anyway.
It removes the service and its upgrade helper, the virtualenv at /opt/borg-ui-agent, the configuration at /etc/borg-ui-agent, and the dedicated borg-ui-agent service user if the install created one.
Two things it never removes, with or without flags:
- A Borg your distribution installed. Only binaries this installer placed under
/opt/borg-ui-agentare removed, along with the/usr/local/binsymlinks pointing at them. A Borg at/usr/bin/borgis left alone, because removing it would break Borg for everything else on that machine. - A service user that is not the dedicated account. If the agent was installed with
--service-user current, it runs as your own login account, and that account is never deleted.
Your backup repositories are never touched.
Flags, if you want to keep something:
--keep-borgleaves the Borg binaries this installer placed, and their symlinks, in place--keep-userleaves the dedicated service user and/var/lib/borg-ui-agentin place--keep-configleaves/etc/borg-ui-agent/config.tomlin place, for a reinstall against the same registration
Running the script twice, or on a machine that was never fully installed, is safe: every removal tolerates a missing target.
Enrollment Tokens and Agent Credentials
Enrollment tokens are temporary setup credentials. They can expire after 1 hour, 24 hours, 7 days, 30 days, or never expire. The default UI choice is 7 days.
After enrollment, the agent receives and stores its own credential. Token expiry does not limit the enrolled agent lifetime. An enrolled agent keeps working until you revoke access, delete it from the fleet list, or unregister it on the client.
Revoke and Delete
- Run diagnostics opens a focused check for the selected agent. A session-only run verifies that Borg UI can reach the agent over its current connection and shows troubleshooting details such as online state, last seen time, agent version, Borg versions, capabilities, and last error.
- To check whether the agent host can reach another service, open Advanced: test another service and enter the service host, port, and timeout in seconds before running diagnostics. The timeout controls how long the agent waits for that TCP connection before reporting a timeout. The agent attempts the connection from the agent machine and reports success or failure, elapsed time, timeout, and normalized error text. Borg UI validates the target input before asking the agent to run the check.
- Revoke access blocks the agent credential but keeps the machine visible for history and troubleshooting.
- Delete agent removes the machine from active fleet lists. Existing job and log records remain readable. The local systemd service may still run on the client until you stop, remove, or unregister it there.
- View agent logs opens recent session-level logs for that machine, including connection, dispatch, and live command messages kept by the Borg UI process.
Advanced Manual Setup
The one-command installer is the default Linux path. Manual setup is useful for development or troubleshooting.
Run this on the machine that owns the files you want Borg to back up:
git clone https://github.com/karanhudia/borg-ui.git
cd borg-ui
python3.11 -m venv .venv
. .venv/bin/activate
pip install .Verify the CLI:
borg-ui-agent statusRegister manually:
borg-ui-agent register \
--server http://borg-ui-host:8083 \
--token borgui_enroll_example \
--name laptopRun one manual agent check:
borg-ui-agent onceRun continuously:
borg-ui-agent runLinux systemd Manual Service
The installer creates and enables the systemd service automatically. For manual service setup, the default Linux unit expects a system user and group named borg-ui-agent:
sudo useradd --system --user-group --home-dir /var/lib/borg-ui-agent \
--create-home --shell /usr/sbin/nologin borg-ui-agent
sudo install -d -o borg-ui-agent -g borg-ui-agent -m 0750 /etc/borg-ui-agentInstall the agent into the path used by agent/install/systemd/borg-ui-agent.service, then register the service config:
sudo install -d -m 0755 /opt/borg-ui-agent
sudo python3.11 -m venv /opt/borg-ui-agent/.venv
sudo /opt/borg-ui-agent/.venv/bin/pip install .
sudo -u borg-ui-agent /opt/borg-ui-agent/.venv/bin/borg-ui-agent \
--config /etc/borg-ui-agent/config.toml \
register \
--server http://borg-ui-host:7879 \
--token borgui_enroll_example \
--name laptopValidate the service setup before enabling it. This catches a missing or invalid service user/group before systemd reaches status=217/USER:
sudo /opt/borg-ui-agent/.venv/bin/borg-ui-agent service-check \
--user borg-ui-agent \
--group borg-ui-agent \
--exec /opt/borg-ui-agent/.venv/bin/borg-ui-agent \
--config /etc/borg-ui-agent/config.tomlThen install and start the unit:
sudo cp agent/install/systemd/borg-ui-agent.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now borg-ui-agentIf you choose a different service user, group, binary path, or config path, edit the systemd unit and pass the same values to service-check.
If systemctl status borg-ui-agent shows status=217/USER or Failed at step USER, systemd could not use the configured User= or Group=. Run:
getent passwd borg-ui-agent
getent group borg-ui-agent
sudo /opt/borg-ui-agent/.venv/bin/borg-ui-agent service-check \
--user borg-ui-agent \
--group borg-ui-agent \
--exec /opt/borg-ui-agent/.venv/bin/borg-ui-agent \
--config /etc/borg-ui-agent/config.tomlOn macOS the install command from the Add Agent dialog does all of this as the user whose data is backed up, without sudo, and loads the agent as a launchd user agent (see agent/README.md). A manual install uses the installer's layout, because agent/install/launchd/com.borg-ui.agent.plist is the job the installer renders: the virtualenv at ~/Library/Application Support/borg-ui-agent/.venv, the config at the agent's default path in that directory, logs under ~/Library/Logs/borg-ui-agent/. From a checkout of this repository, as the user whose data is backed up and never as root, since the job executes code the user can write:
AGENT_ROOT="$HOME/Library/Application Support/borg-ui-agent"
mkdir -p "$AGENT_ROOT" ~/Library/Logs/borg-ui-agent ~/Library/LaunchAgents
python3.11 -m venv "$AGENT_ROOT/.venv"
"$AGENT_ROOT/.venv/bin/pip" install .
"$AGENT_ROOT/.venv/bin/borg-ui-agent" register \
--server http://borg-ui-host:8083 \
--token borgui_enroll_example \
--name laptop
sed "s#/Users/alex/#${HOME}/#g" agent/install/launchd/com.borg-ui.agent.plist \
> ~/Library/LaunchAgents/com.borg-ui.agent.plist
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.borg-ui.agent.plistThe sed puts the real home directory in place of /Users/alex, since launchd expands no ~. Borg has to be on the PATH the template sets (Homebrew, MacPorts and /usr/local/bin are on it; the bin/ forwarders exist only after the installer ran).
This loads the agent job only. The remote-upgrade job (com.borg-ui.agent-upgrade) and its upgrade.conf are rendered by the installer and have no template, so an agent set up by hand takes no remote upgrades until the install command has been run once.
Keep the agent config file readable only by the service user or local admin. It contains the agent credential used to authenticate with Borg UI.
A Windows installer is not available yet.

