Configuration
k8s-borg is configured through the chart's values. Each one is documented in values.yaml, and the sections are summed up under Parameters. This page covers what decides how the backups run; the features have pages of their own.
Two ways to run
Standalone (default)
If BORG_UI_AGENT is unset or not true, k8s-borg runs without Borg UI. The container entrypoint (/run.sh run) validates the environment, mounts the sources (S3 via s3fs for cluster/app jobs), takes the dumps of the configured database engines (cluster jobs) or the datastore snapshot and the SQLite copies (node pods, when enabled), then runs borg-backup against the pod's assigned repository. Scheduling is Kubernetes' own: the DaemonSet's interval loop and the CronJob.
Borg UI managed agent (optional)
Set BORG_UI_AGENT=true (in the chart: node.backupMode=agent, cluster.mode=agent) to enroll the pod at a Borg UI server as a managed agent and register its assigned repository with the server. From Borg UI you then create, schedule and run backup plans, maintenance jobs and restores, next to any other agents and repositories that server manages. Enrollment is idempotent: an existing config.toml skips enrollment as long as the server still recognises the agent; otherwise the pod enrolls again.
Modes
Backup execution is chosen per component, along two independent axes: who runs the backup and whether the pod is enrolled in Borg UI.
- Node (DaemonSet) —
node.backupMode:interval(default): the stable in-pod loop runs/run.sheverynode.backupIntervalSecondsagainstborg.repoBase.agent: each node pod enrolls at Borg UI and registers a per-node backup plan.
- Cluster (CronJob + console/agent pod) — the
clustersection covers the cluster-scope data and the long-lived console pod (deployed whencluster.enabled). Two independent axes:cluster.backupMode— how the backup is scheduled:cronjob(default, a k8s CronJob oncluster.schedule) orplan(a server-side BackupPlan run by the console/agent pod; needscluster.mode=agent; no CronJob).cluster.mode— whether the console pod is enrolled in Borg UI:legacy(default, inspection only — tails the log,kubectl execin) oragent(enroll at Borg UI: register the cluster repository + check-schedule so it is browsable, and run the agent). Incronjobmode the agent just makes the repo visible.cluster.borgUiResync(defaulttrue) — incronjobmode with a Borg UI to talk to (borgUI.enabledorborgUI.agentConnection.server), each run ends by asking Borg UI to resync the cluster repository, so new and pruned archives and the size show up right away instead of on the next periodic reconcile run. Best effort: a failure is logged as a warning and never changes the job's exit code.
Managed-agent modes need a reachable server: set borgUI.agentConnection.server, or deploy one in-cluster with borgUI.enabled=true (which also runs a reconcile Job that mints an admin PAT and reconciles borgUI.oidc).
An example release
The repository's example is a Flux HelmRepository and HelmRelease; its values: block is an ordinary values file, so the same settings work with plain Helm. tests/examples-helm.bats checks every key it sets against the chart and renders it. The Secrets it references are in examples/secrets/, and examples/README.md describes both.
# Flux: install k8s-borg from the public OCI chart repository.
#
# Modelled on a production release, with every site-specific value replaced:
# backups of the nodes and of the cluster's shared data to a Hetzner Storage
# Box, database dumps with the credentials taken from the operators' Secrets,
# a Borg UI server with OIDC login and mail notifications that runs the node
# backups as managed agents, and a repository server for the clients of other
# clusters.
#
# Sensitive values are referenced from Secrets you create separately (SOPS /
# Sealed Secrets / External Secrets) — nothing secret lives in this file; see
# ../secrets for the keys each Secret needs.
#
# tests/examples-helm.bats renders these values against the chart and fails on
# a key chart/values.yaml does not know, so this file cannot drift from the chart.
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: ioanalytica-public
namespace: flux-system
spec:
type: oci
interval: 30m
url: oci://ghcr.io/ioanalytica/charts
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: k8s-borg
namespace: k8s-borg
spec:
interval: 1h
# The reconcile Job waits up to borgUI.reconcile.waitSeconds for the server.
timeout: 15m
chart:
spec:
chart: k8s-borg
version: "1.2.3"
sourceRef:
kind: HelmRepository
name: ioanalytica-public
namespace: flux-system
values:
timezone: "Europe/Berlin"
borg:
version: 1
# The repositories live on a Hetzner Storage Box, whose Borg 1.4 is
# called borg-1.4 (BORG_REPO_BASE: ssh://u123456@u123456.your-storagebox.de:23/./mycluster).
remotePath: borg-1.4
repoBase:
existingSecret: k8s-borg-secret
existingSecretKey: BORG_REPO_BASE
passphrase:
existingSecret: k8s-borg-secret
existingSecretKey: BORG_PASSPHRASE
dbBackupLocation: "/mnt/nfs/db-backup"
# Extra flags for the backup plans Borg UI runs (the node agents).
planCustomFlags: "--filter AME --list --exclude-caches"
retention:
daily: 7
weekly: 4
monthly: 6
# Keyed by pod hostname: the node pods are k8s-borg-<node>, the console
# pod is cluster.nodeName.
checkSchedule:
default: "15 2 * * *" # every repository daily …
perHost:
k8s-borg-mycluster: "15 2 * * 0" # … the large cluster repository weekly
# The node agents' backup plans; without a schedule they only run by hand.
backupSchedule:
default: "45 * * * *" # hourly
# Every node's own file systems, one repository per node, run as a Borg UI
# managed agent. The host's root is mounted read-only at /node.
node:
backupMode: agent
include:
- "P fm"
- "R /node/etc"
- "R /node/home"
- "R /node/root"
exclude:
- "/node/srv"
# The cluster's shared data: a CronJob every 6 hours, and the console pod,
# which enrolls as an agent so that Borg UI shows the repository and runs
# its checks and restores.
cluster:
mode: agent
backupMode: cronjob
nodeName: "k8s-borg-mycluster"
# Mount the shared file system where the CronJob and the console run.
extraVolumes:
- name: nfs
persistentVolumeClaim:
claimName: shared-nfs
extraVolumeMounts:
- name: nfs
mountPath: /mnt/nfs
s3Buckets:
- "gitea-backups"
- "longhorn-backups"
- "media"
include:
- "P fm"
- "R /mnt/s3"
- "R /mnt/nfs/users"
- "R /mnt/nfs/volumes"
- "R /mnt/nfs/db-backup" # the database dumps (borg.dbBackupLocation)
exclude:
- "**/__pycache__/"
- "*.pyc"
- "/mnt/nfs/volumes/*/cache"
s3:
enabled: true
# An S3 server in the cluster, reached over its Service in plain HTTP;
# use an https:// endpoint for one outside the cluster or with TLS.
endpoint: "http://s3.storage.svc:3900"
# S3 servers that check the SigV4 region, such as Garage, need it.
region: "eu-central-1"
accessKey:
existingSecret: k8s-borg-secret
existingSecretKey: AWS_KEY
secretKey:
existingSecret: k8s-borg-secret
existingSecretKey: AWS_SECRET_KEY
# Client key, known_hosts, and the repository server's host key and
# authorized_keys, all in one Secret.
ssh:
existingSecret: k8s-borg-ssh-secret
# The credentials come from the Secrets the database operators created in
# each database's namespace, read at job start; nothing to keep in sync.
databases:
mariadb:
enabled: true
instances:
- name: wordpress
host: wordpress-db.wordpress.svc
user: root
secretRef:
namespace: wordpress
name: mariadb
passwordKey: root-password
postgres:
enabled: true
instances:
# Zalando postgres-operator: <user>.<cluster>.credentials.postgresql.acid.zalan.do
- name: gitea
host: gitea-db.gitea.svc
secretRef:
namespace: gitea
name: postgres.gitea-db.credentials.postgresql.acid.zalan.do
usernameKey: username
passwordKey: password
- name: keycloak
host: keycloak-db.keycloak.svc
secretRef:
namespace: keycloak
name: postgres.keycloak-db.credentials.postgresql.acid.zalan.do
usernameKey: username
passwordKey: password
# Borg 1 repositories for the clients of other clusters, each key bound to
# its own directory (the authorized_keys comment names it).
repoServer:
enabled: true
service:
type: LoadBalancer
port: 22
annotations:
metallb.io/loadBalancerIPs: "192.0.2.10"
persistence:
size: 1Ti
local:
createPV: true
nodeName: storage-node-1
path: /srv/borg/repositories
tolerations:
- key: storage
operator: Exists
effect: NoSchedule
borgUI:
enabled: true
publicBaseUrl: "https://borgui.example.com"
adminUser: "admin"
adminPassword:
existingSecret: k8s-borg-secret
existingSecretKey: BORG_UI_ADMIN_PASS
ingress:
enabled: true
ingressClassName: traefik
host: "borgui.example.com"
tls:
enabled: true
secretName: borgui-tls
# The server's own errors are reported to Borg UI's ingest by default.
errorReporting:
enabled: false
# A paid Borg UI plan: an instance-independent key, or an offline
# entitlement for air-gapped installs (see https://k8s-borg.ioanalytica.com/features/borg-ui-server#licensing).
# licensing:
# licenseKey:
# existingSecret: borgui-license-key
# existingSecretKey: license_key
# Server state in PostgreSQL (Zalando operator) instead of SQLite.
postgres:
enabled: true
host: "borgui-db.k8s-borg.svc"
database: borg
existingSecret: "borg.borgui-db.credentials.postgresql.acid.zalan.do"
# Redis-compatible cache; mode: internal runs one in the release instead.
redis:
mode: external
auth:
existingSecret: k8s-borg-secret
existingSecretKey: REDIS_PASSWORD
cacheTtlMinutes: "720"
external:
host: "redis.k8s-borg.svc"
port: 6379
oidc:
enabled: true
discoveryUrl: "https://id.example.com/.well-known/openid-configuration"
clientId: "<client-id>"
clientSecret:
existingSecret: k8s-borg-secret
existingSecretKey: OIDC_CLIENT_SECRET
providerName: "Example SSO"
tokenAuthMethod: "client_secret_basic"
groupClaim: "groups"
adminGroups: "admin"
newUserMode: "viewer"
defaultRole: "viewer"
defaultAllRepositoriesRole: "viewer"
# Only once OIDC login works: without local login, a lost admin token
# cannot be minted again.
# disableLocalAuth: true
# Machines outside the cluster that Borg UI reaches over SSH, among them
# the Storage Box itself (port 23). Working with remote clients in Borg UI
# needs one of its paid plans.
remoteMachines:
- { host: "u123456.your-storagebox.de", username: "u123456", port: 23 }
- { host: "backup-host.example.com", username: "root", port: 22 }
notifications:
- name: "Ops mail"
titlePrefix: "[mycluster]"
email:
smtp:
host: smtp.example.com
port: 587
mode: starttls
username: borgui@example.com
password:
existingSecret: k8s-borg-secret
existingSecretKey: SMTP_PASSWORD
from: borgui@example.com
fromName: "Borg UI mycluster"
to:
- ops@example.com
# The Borg client cache and Borg UI's state on static NFS volumes. Without
# createPV the chart claims them from a StorageClass (storageClassName), or
# takes an existing claim (existingClaim).
persistence:
cache:
createPV: true
server: "nfs.example.com"
path: "/export/k8s-borg/cache"
ui:
# retain defaults to true — Borg UI /data survives uninstall
createPV: true
server: "nfs.example.com"
path: "/export/k8s-borg/ui"
uiAgent:
createPV: true
server: "nfs.example.com"
path: "/export/k8s-borg/ui-agent"