Skip to content

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.sh every node.backupIntervalSeconds against borg.repoBase.
    • agent: each node pod enrolls at Borg UI and registers a per-node backup plan.
  • Cluster (CronJob + console/agent pod) — the cluster section covers the cluster-scope data and the long-lived console pod (deployed when cluster.enabled). Two independent axes:
    • cluster.backupMode — how the backup is scheduled: cronjob (default, a k8s CronJob on cluster.schedule) or plan (a server-side BackupPlan run by the console/agent pod; needs cluster.mode=agent; no CronJob).
    • cluster.mode — whether the console pod is enrolled in Borg UI: legacy (default, inspection only — tails the log, kubectl exec in) or agent (enroll at Borg UI: register the cluster repository + check-schedule so it is browsable, and run the agent). In cronjob mode the agent just makes the repo visible.
    • cluster.borgUiResync (default true) — in cronjob mode with a Borg UI to talk to (borgUI.enabled or borgUI.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.

yaml
# 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"

Distributed under the AGPL-3.0-only license.