Changelog
Unreleased
Dependency updates by Renovate. Renovate opens weekly pull requests for the workflows' actions and the agent image's version pins (
.github/renovate.json); "Dependency updates" inCONTRIBUTING.mdsays what it updates and what stays manual. Every action is now pinned to a commit, with its release in a comment, so a moved tag no longer changes a workflow.tests/renovate-config.batskeeps the pins and the exclusions.A documentation site, https://k8s-borg.ioanalytica.com. VitePress in
site/, published on GitHub Pages by.github/workflows/site.ymlon every push tomainthat changes what it shows; pull requests build it. The guides that filledchart/README.mdmoved there, a page each, and restores have pages of their own (Restore, Disaster recovery).chart/README.mdkeeps the install, the parameters and the security posture, and the README the overview; the site includes those sections, andCONTRIBUTING.md,SECURITY.mdand this changelog whole, instead of copying them. The messages and comments that pointed intochart/README.md(backup-redison Dragonfly, the chart's checks ofrest://andborgUI.extraEnv,values.yaml) now give the page's address. The README starts with a quick start, and no longer counts Borg UI's own features among k8s-borg's. A pull request that changes onlysite/is documentation only.tests/site-includes.batschecks every include and every address of the site in the repository,tests/site-workflow.batsthe workflow, andtests/site-links.batswhere a link of the included files leads: a repository path, also one that starts with a dot (#99), to GitHub or to the page that shows the file. The parameter table ofchart/README.mdwas cut in two by the 1.0.21 upgrade note; the note now follows the table. The quick start names a path to back up (the defaults name none) and the base directory Borg 1 needs on the host; the rebuild of the repository server from its external copy waits for a running copy Job and resumes the copy only afterborg check.A check schedule removed from the values is removed from Borg UI. The agent pods rolled for it since #89, but
set-check-scheduleleft the schedule it had set enabled (#91). It now records the cron it sets on the agent's volume and, once no cron is configured, removes the schedule while Borg UI still has that cron; a check schedule set by hand in Borg UI, or given another cron there, stays, also in releases withoutborg.checkSchedule. An agent that enrols anew (a new or rebuilt Borg UI) drops the record. A schedule set by an older version counts as the chart's after the first start with this one.offinborg.checkScheduleremoves a pod's check schedule whoever set it, and in both schedules keeps a pod offdefault(for a backup plan: no schedule); an unquotedoff, YAML'sfalse, counts too.tests/set-check-schedule.batsandtests/run-agent.batscover the scripts.Lint & test is faster. The Borg end-to-end suite runs Borg 1 and Borg 2 as jobs side by side instead of one after the other (7 and 14 minutes, 22 for the job), and Borg 2 in two parts by file, behind the same required check
e2e, which passes only when all of them do: a run takes about as long as the database suite, some 10 minutes.tests/e2e/run.shtakes the files to run next to the majors, and a new file oftests/e2ein no part of Borg 2 failstests/lint-test-workflow.bats. A pull request that changes documentation alone (Markdown at the top level,LICENSE,examples/;scripts/docs-only.sh) skips the end-to-end jobs, and the unit tests, shellcheck and the chart lint still run; pushes tomainand tag builds always run everything.tests/lint-test-workflow.batsandtests/docs-only.batspin both.A hung end-to-end job ends. Every job of Lint & test has a time limit, the end-to-end ones about twice their longest run; before, a hung job held the run for six hours.
tests/e2e/db/run.shstops a server version afterE2E_DB_TIMEOUTseconds (default 600) and fails it with what its tests were running and had printed: a hungredis:7had left the log empty for 40 minutes (tests/e2e-db-timeout.bats).The database suite says where its time goes. Its summary gives the run's time and each server version's phases (image pull, servers started, tests, servers ready): a version's output comes in one piece when it is done, so its lines in a CI log all carry that time. On the GitHub runner the tests themselves take the time, and the suite is bound by CPU; 3 versions at a time stays the default.
The Helm example matches the chart again.
examples/helm/k8s-borg.yamlpinned chart 1.0.4 and still setconfig.*,app.*andborgUI.agent, which the chart has since moved; it did not render. It is now modelled on a production release with every site-specific value replaced: node and cluster backups to a Hetzner Storage Box, database credentials from the operators' Secrets, Borg UI with OIDC, notifications and node agents, and a repository server for other clusters.tests/examples-helm.batschecks every key it sets againstchart/values.yaml, renders it as is, without Borg UI and with the cluster backup as a Borg UI plan, and fails when it pins another chart version. The example Secrets carry exactly the keys the release references.Plain manifests removed from
examples/. The Kustomize manifests showed a 1.0.4 deployment; with a current image their CronJob would have skipped S3 without a word (noS3_ENABLED). The chart is the maintained way to deploy;helm templaterenders it to plain YAML.Release notes from the changelog. A release now starts with its
CHANGELOG.mdentry, followed by GitHub's list of pull requests (scripts/changelog-section.sh).Project documents.
CONTRIBUTING.md(with a DCO sign-off for contributions),SECURITY.md(private reporting through GitHub),CODE_OF_CONDUCT.md, issue forms and a pull request template. The README explains why to use k8s-borg, how it compares with Velero, K8up and VolSync, what it and the Borg UI server contact, and what costs money.The
borg-uisubmodule is cloned over HTTPS, so a clone with--recurse-submodulesneeds no GitHub SSH key.mainis the default branch.developis renamed tomain, the one long-lived branch, with releases as tags on it, as in Borg UI; the stalemasteris gone. Lint & test runs on pushes tomain, the chart is published from tags only, and the chart's icon is read frommain.Per-host schedules: the right keys, and applied on change. The keys of
borg.checkSchedule.perHostandborg.backupSchedule.perHostare pod hostnames:cluster.nodeNameitself for the console pod (defaultk8s-borg-mycluster),k8s-borg-<node name>for a node pod. values.yaml gaveconsoleas an example key, which no pod has; values.yaml and chart/README.md ("Check and backup schedules") now say which key is whose. The agents apply their schedules only when they start, and a changed schedule did not restart them: the console StatefulSet and the node DaemonSet now carry checksums of the schedule ConfigMaps, so a change rolls the pods that apply it (the console pod for its backup schedule incluster.backupMode=planonly). A check schedule removed from the values stays in Borg UI until it is turned off there. The install notes name the perHost keys that no agent pod applies; ak8s-borg-…key counts as a node pod's, as the chart does not know the node names.Signed images and chart, with SBOMs. A release signs the agent, Borg UI and runtime base images and the chart with Sigstore cosign, keyless and by digest; the certificate names
build.ymlat the release tag. Every platform image now carries an SPDX SBOM next to its SLSA provenance, both covered by the signature of the image index. The GitHub Release is published only once the signatures verified. "Verify the images" in the README has the commands.tests/release-signing.batskeeps the workflows from dropping any of it.Versions with a suffix are prereleases. A tag such as
1.2.4-beta.1or the chart revision1.2.3-1is published as a GitHub prerelease, never as the repository's Latest release, and no longer moves the images'latesttags.
1.2.3
- Borg UI 2.4.6. The
borg-uisubmodule moves to the v2.4.6 release (agent 0.1.22, no database change).
1.2.2
- Borg UI 2.4.5. The
borg-uisubmodule moves to the v2.4.5 release (agent 0.1.22, no database change).
1.2.1
- Borg UI 2.4.4. The
borg-uisubmodule moves to the v2.4.4 release (agent 0.1.22, one database revision, applied at startup).
1.2.0
Borg UI 2.4.1. The
borg-uisubmodule moves to the v2.4.1 release (agent 0.1.22, no database change).Upgrade note: Borg UI 2.4.1 sends the server's error reports to
https://t.borgui.com/eby default. To keep them off, setborgUI.errorReporting.enabled: false(next entry) with this upgrade.Switch for Borg UI's backend error reports (
borgUI.errorReporting.enabled, defaulttruelike the image). Borg UI 2.4.1 and later send the server's errors (request exceptions, 5xx responses, startup and migration failures, failed jobs) tohttps://t.borgui.com/e. The chart always rendersERROR_REPORTING_ENABLED, sofalseturns them off for the instance; older images ignore it. The browser's analytics and error reports stay a per-user preference. NewborgUI.extraEnvappends EnvVar objects to the UI container; a name the chart already sets fails the render.Archive names in UTC. The default archive name template is now
<node>-{utcnow:%Y-%m-%d-%s}(was{now:…}) inregister-backup-planandborg-backup.{now}is local time, which repeats an hour when daylight saving time ends: a backup that runs again in the repeated hour gets the name of the one an hour before and fails with "Archive … already exists". On the Alpine images{now:%s}was also not the Unix time but the local wall time read as UTC. Theborg.archiveNameTemplatecomment recommends{utcnow:…}for custom templates. Existing archives keep their names; after the update the timestamps in new names move back by the UTC offset once.rclone 1.75.2 in the agent image, the current stable release, which includes the 1.75.1 fixes for CVE-2026-88013 to -88018 and CVE-2026-88044 to -88046 (two of them critical: auth-proxy bypasses in
rclone serve s3and the rc server). The image takes the official static build from downloads.rclone.org in a fetch stage instead of Alpine's package: Alpine 3.24 ships 1.74.1 without fixes for these CVEs, and itsv1.74.1-DEVbuild string kept scanners from reporting them.docker/rclone.envpins version and checksums (from the release's PGP-signedSHA256SUMS);tests/rclone-pin.batskeeps the Dockerfile's ARG defaults equal to it. The UI image's rclone comes from borg-ui's runtime base and moves to 1.75.2 with theborg-uipin.Borg UI without
privilegedwhere FUSE allows it (borgUI.fuse). The Borg UI server mounts through FUSE for server-side backups of SSH sources, cloud mirrors of SSH repositories and "Mount archive"; browsing and restoring do not need it. FUSE needs/dev/fuseallowed by the device cgroup,SYS_ADMINand the setuidfusermount3, and containerd lets only privileged containers and devices assigned by a device plugin open/dev/fuse(a hostPath mount of it fails with "Operation not permitted"). So the UI keeps FUSE and stays privileged by default, and two new ways run it unprivileged:borgUI.fuse.deviceResourcenames a FUSE device plugin's resource (e.g.squat.ai/fuse). The UI pod requests one unit and runs withcapabilities.drop: [ALL]plusCHOWN,DAC_OVERRIDE,FOWNER,SETGID,SETUID,SYS_ADMIN, privilege escalation forfusermount3, seccompRuntimeDefaultand AppArmorUnconfined(containerd's default profile deniesmount). The reconcile Job does not request the device.borgUI.fuse.enabled: falsedrops FUSE: the same capabilities withoutSYS_ADMIN, no privilege escalation, seccompRuntimeDefault. The three FUSE features then fail, and so doessudoin the container, which package install commands such assudo apt-getrely on; everything else works.
Both profiles were checked against the published
k8s-borg-ui:2.4.0image: the server starts and logs in with either,borg mountworks with the device profile, and fails withoutSYS_ADMIN, withno-new-privilegesor with/dev/fuseonly bind-mounted.borgUI.containerSecurityContextnow defaults to~; a value you set still replaces the computed one, and an explicit{}still renders no securityContext, so existing releases render unchanged.Redis backups recognise Dragonfly (#76). Dragonfly has no
SYNC, sobackup-redisfailed on it only at the snapshot transfer. An instance that reportsdragonfly_versionnow fails at once with rc 2 and a pointer to the new Dragonfly section inchart/README.md: snapshots to S3, backed up throughcluster.s3Buckets, and how to restore them.Restore scripts (#72, #73, #74, #75):
restore-postgres,restore-mariadb,restore-mysqlandrestore-mongodbtake--from <instance>: the dumps of that instance, restored into the server of the given credentials file. Without it the instance is still the credentials file's name. A path, or an instance without dumps, is refused before anything is dropped.restore-postgres <db> --use-list FILEpasses the list topg_restore -L. A list that matches no entry of the dump, or has a line pg_restore would skip, is refused before the target is dropped.restore-mongodbleaves out mongorestore's "don't know what to do with subdirectory" lines, which mongorestore 100.14 prints for every database of an--allrestore with an oplog. Its other output and exit status are unchanged.chart/README.mddescribes the extract step of a restore, PostgreSQL into another operator, and MongoDB into another server.
1.1.9
- Borg UI 2.4.0. The
borg-uisubmodule moves to the v2.4.0 release. The agent version stays 0.1.21 and the database schema is unchanged. - Agent version between Borg UI releases (#77). When the
borg-uipin lies past a release and changed the agent, both images carry the agent version with the pin's distance from that release appended (Borg UI 2.3.10-64-g… → agent 0.1.21.64), so enrolled agents are offered the new code. - Credentials resolver image:
alpine/kubectl:1.37.1. The init container that resolvessecretRefdatabase credentials defaulted tobitnamilegacy/kubectl:latest, a frozen mirror that no longer gets security fixes and whoselatesttag cannot be tracked. Its script is now POSIX sh and runs in BusyBox's ash, so the default is the small, maintained and version-pinnedalpine/kubectl. Adatabases.credentialsResolver.imageof your own needskubectl,sh,base64,odandawk; Bash is no longer required.
1.1.9-rc.1
- Borg UI 2.3.10-62-ga7219d4d6. The
borg-uisubmodule moves back to upstream main (a7219d4d6). Agent 0.1.21 and the database schema are unchanged.
1.1.9-beta.13
Borg UI unchanged: 2.3.10-52-g2e4031646 (the beta.12 pin), agent 0.1.21, database schema unchanged.
Database dumps for every common engine, the cluster datastore, SQLite on application volumes, and an external copy of the repository server. The agent image gains
etcdctl/etcdutl, MongoDB'smongodump/mongorestore, the MySQL 8.4 client,valkey-cliand the helpers below. Node backups stay on the node's own file systems.License: AGPL-3.0-only throughout (#44). The chart's SPDX headers and
Chart.yamlsaid Apache-2.0 while the repository'sLICENSEwas the AGPL-3.0; all of them now sayAGPL-3.0-only. The README describes what the chart deploys today and lists the licenses of the third-party software in the images.Repository server: an external copy,
repoServer.offsite(issue #54).- A CronJob mirrors every repository below the data directory to an rclone remote — S3-compatible storage, an SFTP target such as a Hetzner Storage Box, or any other — for the case that the server's storage fails. Off by default. A mirror, not a second backup: deletions and compactions reach the copy with the next run.
borg-offsite-copycopies one repository at a time; one that does not exist locally is never touched on the remote, so a server on a new, empty volume does not empty the copy. No lock and no passphrase: Borg 2 seals its locks with the repository key. Each repository is copied in Borg's write order (segments or packs, then what refers to them, then the deletions) and copied again when a client wrote to it or held a lock meanwhile; a run that cannot settle a repository fails, the others are copied all the same.- The job reads the server's volume read-only, as the server's user, on its node, without root or capabilities.
rcloneConfig,sshKeyandknownHostseach from a value or an existing Secret;schedule,timeZone,suspend,bwlimit,transfers,retries,extraArgs. chart/README.mddescribes the restore: the server rebuilt from the copy (with umask 077 — rclone leaves the files readable for everyone), Borg 1 clients that saw a newer state, and reading a copy directly with Borg 1 and Borg 2 for each kind of target.- Tested end to end against Garage and an SFTP target that runs
borg serve, for Borg 1 and Borg 2.
Redis and Valkey: consistent RDB snapshots (issue #52).
- New engine
databases.redis(off by default), for Redis and Valkey alike: instances with a static key=valueconfig(host,port,user,password,tls,cacert,insecure), anexistingSecret, or asecretRefresolved by the credentials resolver; the default user when no user is set. backup-redisstreams the server's own RDB snapshot over the replication protocol:<instance>.rdbof a standalone server, or one<instance>-slots-<range>.rdbper primary of a cluster with<instance>-cluster-nodes.txt. Mode 600, no extra compression, every failure rc 2. A failed dump leaves the previous one in place: a cluster's files are renamed into place one after another once all are complete, so only a failing rename could leave a mixed set.redis-rdb-syncdoes whatvalkey-cli --rdbdoes and tells a Valkey server its version, without which Valkey 9 cannot send a hash field with a TTL of its own. It makes every connection of a dump,INFOandCLUSTER NODESincluded, and with TLS checks the server's name:host, and a cluster's nodes by their announced hostname or their IP address.valkey-clijoins the image, for work from the console pod.- The backup user needs
+info +sync +replconf +cluster|nodes. A restore places the RDB file as the server'sdump.rdband starts it; documented inchart/README.md, "Restoring Redis and Valkey", not scripted. - Tested end to end against Redis 7 and 8 and Valkey 8 and 9, standalone and as a cluster of three primaries.
- New engine
CloudNativePG credentials (issue #48). A
secretRefto CloudNativePG's<cluster>-superuserSecret (usernameKey: username,passwordKey: password,host: <cluster>-rw.<ns>.svc) works as it is; the chart README gains a table of the PostgreSQL operators' Secrets, andvalues.yamlan example. Tested against CloudNativePG 1.30.1 with the superuser, a managed role withpg_read_all_dataand the application owner.Credentials resolver: values passed as they are (issue #61). Every instance value reaches the resolver's script as one single-quoted word: a
$(…), a backtick or a$VARin a host or user no longer runs or expands in the init container. Secret values keep their trailing line breaks: the MariaDB and MySQL files write every value double-quoted with\n/\rescapes, which both clients read back; pgpass and MongoDB's file cannot carry a line break, and the resolver stops with the instance and field named, not the value. pgpass now also escapes:and\in host and port.MySQL and Percona Server 8.4:
databases.mysql(issue #51).- A new engine, configured like
databases.mariadb(staticconfig,secretReforexistingSecret), off by default.backup-mysql,backup-cluster-mysqlandrestore-mysqldump and restore with MySQL's own client 8.4, shipped in/opt/mysqlwith the glibc it runs on (from the officialmysql:8.4.11image, pinned by digest; about 29 MB, 10 MB compressed), in the files, modes and restore modes of MariaDB. The dumps carry no GTIDs. - Measured with the MariaDB client against MySQL 8.4: the dump succeeds without a warning, but every table with a generated column fails its restore (
ERROR 3105), and the full dump copies system tables MySQL refuses and the source's GTIDs.backup-mariadbandrestore-mariadbtherefore fail with rc 2 against a MySQL server, and the MySQL scripts against a MariaDB server, naming the engine to use. tls: falseon a MySQLsecretRefinstance writesssl-mode=DISABLEDandget-server-public-key.- The image's MariaDB client gets its authentication plugins (
mariadb-connector-c): before, it could not log in as acaching_sha2_password(MySQL's default),ed25519orparsecuser. - An error of a MariaDB/MySQL backup or restore is reported once, not again by every subshell it ends.
- Tested end to end against MySQL 8.4 and Percona Server 8.4 with GTIDs on, and against the Percona Operator for MySQL's Secrets.
- A new engine, configured like
Database dumps: the whole PostgreSQL instance, uncompressed dumps,
restore-mariadb, only the configured engines, end-to-end tests (issues #45, #46, #47, #55).- Only the engines a release configures are dumped: those with
databases.<engine>.enabledand at least one instance or anexistingSecret. One chart helper decides it; only these engines get their credentials volume, are published to the cluster agent and attached as pre-backup hooks (plan mode), and run in the cluster CronJob, which gets them asBORG_DB_ENGINESand runs the newbackup-cluster-databasesinstead of every engine's wrapper. backup-postgresdumps the instance aspg_dumpallcovers it:globals.sql(roles, password hashes, memberships, role settings, tablespaces) and one<db>.dump(pg_dump -Fc -Z0) per database,postgresandtemplate1included. The plain<db>.sql.gzis gone:pg_restore -fwrites the SQL of a dump,-l/-Lrestores selected objects. A user without superuser rights gets the roles without their passwords, and the dump ends with rc 1, a warning in Borg UI.- Dumps of both engines are no longer compressed, so that Borg deduplicates them: a second archive of an unchanged instance adds next to nothing. Every dump file is mode 600, and a failing dump leaves the previous one in place. The MariaDB dumps leave out the dump date.
- The per-database MariaDB dumps carry the events again (
-E; the flag was-e, extended inserts). restore-postgresrestores the whole instance (--all), one database (with-j, its properties included), one database under another name (--as), or the roles alone (--globals), also into a PostgreSQL server before 17, whose rejections of the newerpg_restoreit accepts.- New
restore-mariadb: the whole instance from the full dump (--all), one database, or one under another name (--as). databases.mariadb.instances[].tls: false: the credentials resolver writesskip-ssl, for a server without TLS, which the image's MariaDB client refuses by default (a MariaDB before 11.4 without a certificate).- The dumps and restores are tested end to end against PostgreSQL 13–18 and MariaDB 10.11, 11.4 and 11.8 (
tests/e2e/db/), in CI on every push. - Usage, files and privileges:
chart/README.md, "Database dumps".
- Only the engines a release configures are dumped: those with
The node backup snapshots the cluster datastore (#49,
node.datastoreSnapshot.enabled, off by default). A node backup read etcd's data directory or k3s'sstate.dbwhile they were written, which is not a consistent copy. Now each node pod snapshots its node's datastore before the backup: k3s with embedded etcd and kubeadm's etcd withetcdctl snapshot save(at the node's IP, without the host network;hostNetwork: truefor etcd on localhost only), k3s with SQLite with SQLite's online backup, read-only. The k3s server token or kubeadm's PKI goes along, since the restore needs them. Nodes without a datastore skip the step. The snapshot holds every Secret of the cluster in clear text: it is written to a memory-backed volume of the pod, archived undervar/lib/k8s-borg/datastore, and removed after the run. In interval moderun.shtakes it; in agent mode the chart publishesdatastore-snapshotanddatastore-snapshot-cleanto the node agent and attaches them to the node plan as its last pre-backup hook (continuing on error) and its first post-backup hook (run whatever the result). A failed snapshot does not stop the file backup, and fails the run (in Borg UI: a warning). The restore for each datastore is in the chart README.The image carries
etcdctlandetcdutl3.6.15, the official release build, checksummed per architecture.register-backup-plankeeps hooks it does not own. WithBORG_PLAN_OWNED_AGENT_SCRIPTS(set on node agents) only those hooks are replaced or removed; the plan keeps every other hook, such as one chosen in Borg UI.BORG_PLAN_POST_AGENT_SCRIPTSattaches post-backup hooks.Consistent copies of SQLite databases on application volumes (#53,
node.sqlite.enabled, off by default). The node backup read an application's SQLite file and its-walor-journalfile at different moments, which under write load gave a corrupt copy more often than not. Now the databases listed innode.sqlite.databases(namespace, claim, path in the volume) are copied first with SQLite's online backup, by the newsqlite-copy: each node pod resolves the claim to its volume through the API and copies the databases on its node, read-only through/node, with the application's locks honoured. The copies are single files, mode 600, in their database's journal mode, archived undervar/lib/k8s-borg/sqlite/<namespace>/<pvc>/and removed after the run. A database that cannot be copied fails the step with rc 2; the others are still copied, and the file backup runs. The node pods get a ServiceAccount of their own (geton the listed claims and on PersistentVolumes), anemptyDiron the node's disk for the copies, andHostToContainermount propagation on/node, so that CSI volumes mounted after their start are visible. In interval moderun.shmakes the copies; in agent mode the chart attachessqlite-copyandsqlite-copy-cleanto the node plan like the datastore snapshot. No new binary: the image's Python has thesqlite3module. Tested end to end against applications writing under load in WAL and rollback-journal mode, with the SQLite of Alpine, Debian 12 and Debian 11 (tests/e2e/db/sqlite.bats). Usage and restore: chart README, "SQLite databases".Node backups stay on the node's own file systems (#64). Interval-mode node backups (
borg-backup) and the node plansregister-backup-planwrites runborg createwith--one-file-system(appended to the plan's custom flags, once). Mounts below anode.includeroot (kubelet, CSI and Longhorn volumes, NFS shares, btrfs subvolumes) are skipped; every root is still archived, the SQLite copies and the datastore snapshot included. Cluster backups are unchanged. A partition of the node below a root is stored as an empty directory, without a word from Borg: the newnode-skipped-mountsnames each one from the pod's mount table, as a warning before every interval run and at the start of every agent-mode node pod. Tested end to end with a tmpfs below a root and one as a root of its own (tests/e2e/lifecycle.bats).A node plan without sources is disabled (#58). When no root of
node.includeexists on a node,register-backup-plandisables the node's plan in Borg UI and logs why, instead of leaving it with its old sources and hooks. It is not deleted, which would drop its run history; the next pod start with a root enables it again. Cluster plans are left unchanged as before.Credentials resolver Roles per release namespace (#66). The Role and RoleBinding
db-credentials-rbac.yamlrenders in asecretRefnamespace hash the release's namespace with the target namespace, as thenode.sqliteRoles do: releases of the same name in two namespaces no longer collide.MongoDB dumps and restores (#50).
databases.mongodbadds MongoDB to the cluster backup, configured like the other engines: a staticconfig, anexistingSecret, or asecretRefresolved at pod start, either to a whole connection string (uriKey, e.g. the MongoDB Community operator'sconnectionString.standard) or fromhost,port,optionsand user and password keys (Percona operator, Bitnami chart). An instance's credentials file is the YAML filemongodump/mongorestoreread with--config, so the password never shows on a command line; a database in the URI's path (the Community operator's…/admin?…) is handed to the tools asauthSource, as they would otherwise dump or restore that database alone.backup-mongodbdumps an instance into one uncompressed archive,<instance>/<instance>.archive, mode 600, the users and roles included; on a replica set with the oplog of its own run (--oplog). The collections are written one after the other, so that two dumps of unchanged data are the same bytes and Borg deduplicates them. A sharded cluster (mongos) is refused; any failure fails the backup with rc ≥ 2 and keeps the previous archive.backup-cluster-mongodbruns it for every instance, in the CronJob and, published to the cluster agent, as the plan's pre-backup hook.restore-mongodbrestores the whole instance (--all, the oplog replayed;--no-oplogwithout), one database (dropped and created again), or one database under another name (--as); a database the archive does not hold is refused before anything is dropped. With an oplog,--allrestores the users and roles first, so that the replay keeps a session the restored users would otherwise end. The user needs therestorerole, and for--allwith an oplogrootoranyActiononanyResourceas well: the replayed oplog starts with the last entry before the dump, often a command.- The image carries
mongodumpandmongorestorefrom Alpine'smongodb-tools(100.14.1; the package's six other tools are removed) andmongodb-admin, a small pymongo helper for what they cannot do: the server's topology, the config with the URI's database moved, dropping a database collection by collection (therestorerole may not drop a database), and whether an archive holds an oplog. - The credentials resolver reads Secret keys with a
.in their name. tests/e2e/db/mongodb.batsruns dump, Borg archive and extract, and every restore mode against MongoDB 7 and 8 (replica sets and a standalone server), comparing each restore with a fingerprint of the source.
Upgrade notes
- Node archives may shrink: a
node.includeroot that contained mounts of other file systems no longer archives them, a partition of the node itself included (e.g./var/libof its own underR /node/var). Give every partition to keep its ownRroot; the node pod's log names the ones skipped. - The credentials resolver's Roles and RoleBindings (
<fullname>-db-<hash>) are renamed once: Helm deletes the old ones and creates the new ones in the same upgrade. databases.<engine>.enabled: truewithout instances and without anexistingSecretnow fails every render, with a message naming the engine. Before, it failed only where the cluster workloads were rendered (not withcluster.enabled: false). Configure the engine or setenabled: false.- The dump files change their names:
<db>.sql.gzand<db>.dump.gz(PostgreSQL) becomeglobals.sqland<db>.dump;<db>.sql.gzand<instance>-full-dump.sql.gz(MariaDB) become<db>.sqland<instance>-full-dump.sql. The first successful dump removes the old*.gzfiles of its instance directory. To restore from an archive made before,gunzip -kthe file first. borg.dbBackupLocationneeds room for the uncompressed dumps, several times the size of the gzipped ones.- A PostgreSQL backup user without superuser rights turns every cluster backup into a warning (roles without passwords). See "Privileges of the backup user" in
chart/README.md. - A MariaDB server without TLS needs
tls: false(secretRef instances) orskip-sslin its static config. - A MySQL or Percona server under
databases.mariadbnow fails its backup with rc 2 (its dumps did not restore): move the instance todatabases.mysql. MySQL's client knows noskip-ssl: in a static config writessl-mode=DISABLEDandget-server-public-keyinstead; asecretRefinstance withtls: falsegets them from the resolver.
1.1.9-beta.12
- Borg UI 2.3.10-52-g2e4031646, for a 24-hour integration test. The
borg-uisubmodule pins our fork'sio/integrationbranch at 2e4031646 (upstream main with fixes not yet merged there). Agent 0.1.21 (offered as an upgrade to agents installed outside the chart); the database schema changes (Alembic c4e532c20d7b).
Upgrade notes
- Going back to 1.1.9-beta.11 or earlier: run
alembic downgrade -1while still on this image, which deletes only the hooks that name an agent script; otherwise the older image fails at startup with "Can't locate revision".
1.1.9-beta.11
- Borg UI 2.3.10-43-gb45ce0f58. The
borg-uisubmodule moves to upstream main b45ce0f58. Agent 0.1.20 and the database schema are unchanged.
1.1.9-beta.10
- Borg UI unchanged: 2.3.10-36-g5e15222d5, agent 0.1.20, database schema unchanged.
- Plan-mode cluster backups mount the S3 buckets fresh before each run (#39). The console pod mounts its buckets when it starts and keeps the mounts as long as it runs, often for days, and s3fs's caches go wrong on such a mount: archives of plan-mode clusters showed directories with two entries and directory listings that no longer changed (deleted files gone, new ones never shown), while every CronJob archive of the same days was complete, since each CronJob run mounts fresh. The new agent script
s3-remount-bucketsunmounts every listed bucket and mounts it again with the probe and thes3.onMountFailurepolicy of the start; a busy mount is detached and its s3fs ended. Incluster.backupMode=planit is the first pre-backup hook, ahead ofs3-verify-listingands3-check-mounts, and continues on error like them. Ending the s3fs of a busy or failed mount now matches the bucket name literally: a dot in a bucket name no longer also matches another bucket's s3fs (also ins3-mount-bucket). - The console pod reaps orphaned processes. Its main process, the agent, does not collect the exit status of processes left to it, such as an s3fs that ends after its unmount or the watcher of busybox
timeout; they stayed as zombies until the pod restarted, and a remount on every plan run would have added one per bucket per run. The pod now shares its process namespace (shareProcessNamespace: true), so the pause container is PID 1 and reaps them.
Upgrade notes
- The console pod rolls once (
shareProcessNamespace). At its start the cluster plan's pre-backup hooks becomes3-remount-buckets,s3-verify-listing(withs3.verifyListing),s3-check-mounts, the database dumps; as before, hooks changed in Borg UI are replaced. - Each plan run now mounts every bucket fresh: about a second per bucket, and for a bucket that cannot be mounted the probe's
S3_PROBE_TIMEOUT(30 s), once ins3-remount-bucketsand once more ins3-verify-listing. Such a bucket is reported twice in the run, bys3-remount-bucketsand bys3-check-mounts. - The tag
1.1.9-beta.9was built again from the merge of #39, chart and images under the same version. A cluster that had fetched the 1.1.9-beta.9 chart before keeps it, since Flux does not fetch a chart again under the same version: its pods run the new image, which carriess3-remount-buckets, but the old chart neither publishes nor attaches it. Use 1.1.9-beta.10.
1.1.9-beta.9
- Borg UI 2.3.10-36-g5e15222d5. The
borg-uisubmodule moves to upstream main 5e15222d5. Agent 0.1.20 and the database schema are unchanged. - Keyfile modes for
borg.encryption(#20). Borg's key files now go to thepersistence.uiAgentvolume, next to the agent'sconfig.toml: every backup workload setsBORG_KEYS_DIR=/etc/borg-ui-agent/borg-keys, in every mode, so the key of a keyfile repository survives the pod (the cluster CronJob's next run, a rollout of the DaemonSet or the console pod). The chart therefore acceptskeyfile,keyfile-blake2(Borg 1),keyfile-aes-ocbandkeyfile-chacha20-poly1305(Borg 2), which it refused until now. Repositories that exist are not affected: none of them can be a keyfile repository, and the repokey and authenticated modes keep their key in the repository. The new env variable rolls the backup pods once. The chart README's Keyfile modes and where the keys live says how to export a key (do it after the first backup), that an agent reset removesconfig.tomlonly, and that Borg UI never gets the key. - The chart README explains how to apply a changed Secret (#27). The reconcile Job runs once per Helm revision, so a changed Secret referenced through
existingSecret(a notification password, the OIDC client secret, the license) reaches Borg UI only with the next revision. The new section Changing a referenced Secret gives thehelm upgradeandflux reconcile … --forcecommands and notes that the agent pods roll. - S3 sources list every file again. The image's s3fs 1.97 (Alpine's package) showed only two entries of a directory without a directory object of its own, which is how rclone and most S3 writers leave directories, once that directory's stat cache entry had expired (15 minutes by default): the check that recognizes such a directory stored its two-key probe as the directory's listing. Borg then archived those two files. This hit long-lived mounts (the app pod) and any backup walk that reached a directory more than 15 minutes after listing its parent. The image now builds s3fs 1.97 from the release with the upstream fix (s3fs-fuse #2929, not yet in a release) instead of installing the package.
- Cluster backups check the S3 mounts first. Before
borg create, each mounted bucket's files are compared with the bucket's listing through the S3 API (rclone). A bucket whose mount misses objects, or that is no longer mounted, is mounted again and checked once more; if objects are still missing, the backup is written anyway and the run fails, naming the bucket and some of the missing keys.s3.verifyListing: falseswitches the check off. - One S3 bucket that cannot be mounted no longer stops the cluster backup (#26). Until now a bucket that did not answer its first listing stopped the pod's start, so the app pod went into CrashLoopBackOff and no bucket was backed up. s3fs cannot tell a missing bucket from a wrong region, wrong credentials or an unreachable endpoint, so the new
s3-mount-bucketsdecides by count: such a bucket is skipped with a warning that names it, and the start stops only when no listed bucket can be mounted. The mount directory of a skipped bucket is removed, so the archive does not show it as an empty bucket. Beforeborg create,borg-backupchecks that every listed bucket is mounted: a bucket that is not mounted makes the run end with a warning, and no bucket mounted is an error; the other sources are archived either way. Incluster.backupMode=planthe same check runs as the plan's first pre-backup hook,s3-check-mounts, registered withcontinue_on_error: the backup runs whatever the check finds, and Borg UI shows a failed check as a warning.s3.onMountFailure: failkeeps the hard stop at the start. - Plan-mode cluster backups check the S3 listings and mount lost buckets again (#34). In
cluster.backupMode=planthe console pod keeps its S3 mounts for its whole life, and the listing check of the CronJob did not run: the long-lived mount, the one the s3fs listing bug hit, went unchecked, and a bucket skipped at the start or whose s3fs died stayed unmounted until the pod started again. Withs3.verifyListingon (the default) the chart publishess3-verify-listingto the cluster agent and attaches it as the plan's first pre-backup hook, ahead ofs3-check-mounts, so that a bucket it mounts again counts as mounted. It continues on error likes3-check-mounts: a bucket that still misses objects ends the run with a warning in Borg UI, after the backup (the CronJob fails such a run; Borg UI cannot fail a plan run after its backup). s3fs puts itself into a session of its own, so a bucket mounted again from the hook stays mounted after the hook ends or is cancelled. - A failed reconcile Job says why, and the reason stays (#29). Every request of the Job is bounded in time; the health wait gives up on a single attempt after 10 s instead of curl's own 300-s connect timeout, which let one dropped connection use up the whole wait. A failed request names curl's message or the HTTP status with what the server said (a validation error's field and message, never the rejected input; known secrets masked), and the
FATALline carries it. The health wait prints a progress line every 30 s. The reason is kept in three places: the Job and its pod stay for a day instead of 10 minutes, the last log lines land in the pod status (terminationMessagePolicy: FallbackToLogsOnError), and the Job writes theFATALline with its revision to the reconcile-status ConfigMap aslastError, which the gated agent pods print while they wait inwait-for-reconcile. The next successful run removes it.
Upgrade notes
- The console pod and the DaemonSet roll once for the new
BORG_KEYS_DIR; the cluster CronJob picks it up with its next run. - Cluster archives written through a long-lived s3fs mount by an earlier image (above all the console pod in
cluster.backupMode=plan) can miss files of a bucket. The first cluster run with this image archives the bucket's full listing; archives from before the upgrade keep what they hold, so compare them with the bucket before relying on them. - New value
s3.verifyListing(defaulttrue). Each check costs two listings of the bucket through the S3 API and one walk of its mount, once per cluster run; a bucket that is mounted again is checked again, which repeats all three. - A run that fails the check exits with an error after archiving, so the CronJob's pod restarts it (
backoffLimit: 5) with a new mount. - New value
s3.onMountFailure(defaultskip, orfailfor the previous behaviour). A bucket that cannot be mounted costsS3_PROBE_TIMEOUT(30 s) at the start, and withs3.verifyListingonce more in each cluster run, which tries to mount it again. A cluster run with a skipped bucket ends with a warning: the CronJob logs it and exits 0, as for Borg's own warnings. - In plan mode the cluster plan gains the pre-backup hook
s3-check-mountsat the next start of the console pod. A bucket skipped at the start stays unmounted until the pod starts again, and each plan run warns until then. - In plan mode with S3 and no database dump, the console pod now sends the plan's pre-backup hooks on every start, and Borg UI replaces all hooks of the plan with that list. A hook added to the cluster plan in Borg UI is then gone after the next start (with a database dump this was already so). The chart attaches only its own hooks;
cluster.agentScriptspublishes a script but does not attach it. - New value
borgUI.reconcile.ttlSecondsAfterFinished(default86400, until now fixed at 600): how long a finished reconcile Job and its pod are kept. Each revision has its own Job, so a kept Job does not block the next upgrade. borgUI.reconcile.waitSeconds: image pull time plus this wait must stay below the release timeout (HelmReleasespec.timeout,helm --timeout), or raising it only moves the failure from the Job to the release.
1.1.9-beta.8
- Borg UI 2.3.10-34-g49099c8c0. The
borg-uisubmodule moves to upstream main 49099c8c0. Agent 0.1.20 (offered as an upgrade to agents installed outside the chart); the database schema is unchanged. - CI runs on Ubuntu 26.04 with Helm 4.3.0 pinned; the chart and the image contents are not affected.
Upgrade notes
- No value changes; the rendered manifests differ only in the image tags.
- Borg UI's usage analytics are on for every user who has not declined them (banner at first login, or Preferences); they now go to the Borg UI project's own service instead of Umami.
- A
borg.versionother than 1 or 2 now fails the repository registration: Borg UI accepts only these Borg majors.
1.1.9-beta.7
- Borg UI 2.3.10-27-gcdd5712c9. The
borg-uisubmodule moves from our integration branch (54ee5563) to upstream main cdd5712c9. Agent 0.1.19 (offered as an upgrade to agents installed outside the chart); the database schema is unchanged. - The UI image tag names the pinned commit.
borgUI.image.tagisgit describeof the pin (2.3.10-27-gcdd5712c9); a plainX.Y.Zonly when the pin is exactly the release tagvX.Y.Z. borg-ui'sVERSIONfile says 2.3.10 for every main commit since that release, and a tag reused for a new pin overwrote the image an older chart still pointed at.docker/ui-app-version.shcomputes the string for CI anddocker-build-server.sh; the release fails when the chart names another. - New value
borg.encryption, the mode of the repositories the pods create (empty = the major's default:repokey-blake2for Borg 1,repokey-aes-ocbfor Borg 2). A mode the chosenborg.versionlacks fails the render with the valid list. Keyfile modes are refused for now: their key would live in the pod and be lost on restart (#20). An existing repository is never touched. - New value
borgUI.notifications: the reconcile Job creates or updates Borg UI notification channels by name. Anemailentry is assembled from relay, login, sender and recipients with the password from a Secret; any other Apprise service takes its whole URL from a Secret (serviceUrl). Only the settings an entry names are enforced, channels under other names are never touched, nothing is deleted. Failures are warnings; the Job still completes. register-reporecords the modeborg-initcreated. Borg 2 repositories were recorded asrepokey-blake2(a Borg 1 name); they are now recorded under Borg UI's name (repokey-aes-ocbby default,authenticatedforauthenticated-sha256).borg-initandregister-reposhare the mapping inborg-encryption.sh.authenticated-blake3is refused: Borg UI has no name for it.- A pod moves its own record to a changed
BORG_REPO. After a change of the repository base the pod used to find its old record by name and keep working on the old URL. Now it sends the new path to Borg UI and requests a resync, as long as the record belongs to this node's agent, has the same Borg major and the same encryption mode, and the new path isssh://. borg-initblames a repository it cannot reach on the repository. Borg 2's run-time errors (denied key, refused path, host down) were reported as "a bug in borg-init"; only a first lineusage:now counts as a rejected command line.borg-mountno longer leaves a Borg 2 process behind after the unmount. Borg 2 now mounts in the foreground of its own session andborg-mountreturns once the mount is up;BORG_MOUNT_TIMEOUT(seconds, default 300) ends a mount that does not come up.
Upgrade notes
borg.encryptionandborgUI.notificationsare empty by default; with both empty the rendered manifests do not change.- A pod now stops at start with a message naming both sides where it used to carry on silently: a record with its name but another path that belongs to another agent or to none, has the other Borg major, or a local
BORG_REPO;BORG_REPOheld by another record; Borg UI refusing the move; and a move whose record carries another encryption mode. The last one hits every Borg 2 record registered by an earlier release (stored asrepokey-blake2) as soon as the repository base changes: correct the record's mode, or remove the record without deleting data so the pod registers it anew. A pod whose record already has the pathBORG_REPOis not affected. BORG_ENCRYPTION=authenticated-blake3now stopsborg-initandregister-repo.- The UI image tag has the form
2.3.10-27-gcdd5712c9. The UI itself still shows 2.3.10 (it reads borg-ui'sVERSION); the full string is in the image tag and itsorg.opencontainers.image.versionlabel. - Notification URLs with the email options
use_pgp=orpgpkey=, which apprise 2.0.0 removed, now fail.
1.1.9-beta.6
- New value
s3.region, the SigV4 region of the S3 endpoint. It reaches s3fs asendpoint=<region>(envS3_REGION). S3 servers that check the region, such as Garage, refuse every request s3fs signs for its defaultus-east-1. Empty keeps the s3fs options as before. - A bucket that does not answer now stops the pod's start. s3fs reports success and leaves a mount behind even with a wrong region, wrong credentials, a missing bucket or an unreachable endpoint; the first access then blocks until s3fs is killed, while s3fs keeps polling the server. The new
s3-mount-bucketlists every fresh mount withinS3_PROBE_TIMEOUTseconds (default 30); on failure it removes the mount, ends s3fs and stops with the bucket, endpoint and region in the message. - Borg UI unchanged: 2.3.8, agent 0.1.17, database schema unchanged.