Skip to content
 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

42 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

BorgServer - Docker image

Debian based container image, running openssh-daemon only accessable by user named "borg" using SSH-Publickey Auth & "borgbackup" as client. Backup-Repositoriees, client's SSH-Keys & SSHd's Hostkeys will be stored in persistent storage. For every ssh-key added, a own borg-repository will be created.

NOTE: I will assume that you know, what a ssh-key is and how to generate & use it. If not, you might want to start here: Arch Wiki

Quick Example

Here is a quick example how to configure & run this image:

Create persistent sshkey storage

 $ mkdir -p borg/sshkeys/clients

Make sure that the permissions are right on the sshkey folder:

 $ chown 1000:1000 borg/sshkeys

(Generate &) Copy every client's ssh publickey into persistent storage

Remember: Filename = Borg-repository name!

 $ cp ~/.ssh/my_machine.pub borg/sshkeys/clients/my_machine

The OpenSSH-Deamon will expose on port 22/tcp - so you will most likely want to redirect it to a different port. Like in this example:

docker run -td \
	-p 2222:22  \
	--volume ./borg/sshkeys:/sshkeys \
	--volume ./borg/backup:/backup \
	ghcr.io/ls1admin/borgserver:latest

Borgserver Configuration

  • Place Borg-Clients SSH-PublicKeys in persistent storage
  • Client backup-directories will be named by the filename found in /sshkeys/clients/

Environment Variables

BORG_SERVE_ARGS

Use this variable if you want to set special options for the "borg serve"-command, which is used internally.

See the the documentation for all available arguments: borgbackup.readthedocs.io

Example
docker run --rm -e BORG_SERVE_ARGS="--progress --debug" (...) ghcr.io/ls1admin/borgserver

BORG_APPEND_ONLY

If you want your client to be only able to append & not prune anything from their repo, set this variable to "yes".

BORG_ADMIN

When BORG_APPEND_ONLY is active, no client is able to prune it's repo. Since you might want to cleanup the repos at some point, you can declare one client to be the borg "admin".

This client will have full access to all repos of any client! So he's able to add/prune/... what ever he wants.

To declare a client as admin, set this variable to the name of the client/sshkey you've added to the /sshkeys/clients directory.

Example
docker run --rm -e BORG_APPEND_ONLY="yes" -e BORG_ADMIN="nolds_notebook" (...) ghcr.io/ls1admin/borgserver

To prune repos from another client, you have to add the path to the repository in the clients directory:

borg prune --keep-last 100 --keep-weekly 1 (...) borgserver:/clientA/clientA

PUID

Used to set the user id of the borg user inside the container. This can be useful when the container has to access resources on the host with a specific user id.

PGID

Used to set the group id of the borg group inside the container. This can be useful when the container has to access resources on the host with a specific group id.

BORG_CHOWN

Controls how ownership of /backup is fixed at startup.

  • auto (default) — recursively chown /backup only when its top-level ownership does not match the borg user, e.g. on first start or after changing PUID/PGID. Newly created client directories are always chowned. This avoids walking a multi-terabyte repository on every start.
  • always — recursively chown /backup on every start (the old behaviour).
  • never — never chown anything; you manage ownership yourself.

Persistent Storages & Client Configuration

We will need two persistent storage directories for our borgserver to be usefull.

/sshkeys

This directory has two subdirectories:

/sshkeys/clients/

Here we will put all SSH public keys from our borg clients, we want to backup. Every key must be it's own file. The name of the file will become the name of the borg repository, we need for our client to connect.

That means every client get's it's own repository. So you might want to use the hostname of the client as the name of the sshkey file.

Filenames must consist only of letters, digits, dots, underscores and dashes ([A-Za-z0-9._-]). The filename becomes a directory name and part of the forced SSH command, so anything else is rejected and the key is skipped with a warning rather than imported.

Migrating an existing /sshkeys/clients directory: if any filename contains a character outside that set — e.g. root@web01 or a + — rename it first. Such a file is now skipped with a warning instead of being imported, and if every file in the directory is rejected the container exits with an error instead of starting.

Each file contributes exactly one key: the first non-comment line, which must be a bare public key. A line carrying its own authorized_keys options is rejected, and any additional keys in the file are ignored — otherwise a second line could grant access without the forced command that confines a client to its own repository.

Hidden files & files inside of hidden directories will be ignored!

e.g. /sshkeys/clients/webserver.mydomain.com

Than your client would have to initiat the borg repository like this:

webserver.mydomain.com ~$ borg init ssh://borg@borgserver-container/backup/webserver.mydomain.com/my_first_repo

!IMPORTANT!: The container wouldn't start the SSH-Deamon until there is at least one ssh-keyfile in this directory!

/sshkeys/host/

This directory will be automaticly created on first start. Also run.sh will copy the SSH-Hostkeys here, so your clients can verify it's borgservers ssh-hostkey.

/backup

In this directory will borg write all the client data to. It's best to start with an empty directory.

Example Setup

docker-compose.yml

Here is a quick example, how to run borgserver using docker-compose: docker-compose.yml

~/.ssh/config for clients

With this configuration (on your borg client) you can easily connect to your borgserver.

Host backup
	Hostname my.docker.host
	Port 2222
	User borg

Now initiate a borg-repository like this:

 $ borg init backup:my_first_borg_repo

And create your first backup!

 $ borg create backup:my_first_borg_repo::documents-2017-11-01 /home/user/MyImportentDocs

Automatic nightly updates

Because the image is rebuilt daily and only republished when something actually changed (see Rebuild policy), a nightly pull is cheap: most nights the digest is identical, nothing is downloaded and the container keeps running. On the nights a security fix does land, you get it without touching anything.

Two ways to set that up:

a) watchtower, as part of the compose stack. The docker-compose.yml ships a commented-out watchtower service — uncomment it to enable. It checks at 04:00 nightly, and pulls and restarts only if the digest changed. --label-enable scopes it to containers carrying com.centurylinklabs.watchtower.enable=true, which is already set on the borgserver service, so it will not touch anything else on the host.

Note that this mounts /var/run/docker.sock, which gives the watchtower container root-equivalent control of the host. That is a real trade-off on a machine whose job is holding your backups.

b) A host cron job or systemd timer. No extra container and no exposed docker socket:

0 4 * * * cd /srv/borgserver && docker compose pull -q && docker compose up -d

docker compose up -d is a no-op when the image digest has not changed, so this only restarts the container on a real update.

Pick the window carefully. Either approach restarts the container, which drops any backup that is in flight — the client's SSH connection dies mid-transfer. Borg is designed to survive this (the next run resumes; no archive is left half-committed), but the repository can be left holding a stale lock that the client clears with borg break-lock. Schedule the update so it cannot overlap your clients' backup runs.

This only works while you track a moving tag such as latest, 1.4 or 1. If you pin to a digest, updates are deliberately your call and neither approach will do anything.

Docker Releases

Images are published to the GitHub Container Registry only:

Tag Contents
latest, 1.4, 1, trixie Debian trixie + borgbackup 1.4.x — use this one
2.0, 2 Debian trixie + borgbackup2 2.0.0bNN — upstream beta, see the warning below
ghcr.io/ls1admin/borgserver:latest

The borg major version is pinned by the Debian suite: trixie carries the 1.4 line for the life of the release, so :1.4 cannot silently become a different major version. The build fails rather than publishing an image whose tag disagrees with the borg version inside it.

Platform: images are built for linux/amd64 only. The previous Woodpecker CI also published linux/386, linux/arm/v7 and linux/arm64/v8; this workflow does not, so pulling on an arm64 host (e.g. a Raspberry Pi) will fail with a "no matching manifest" error rather than silently pulling amd64.

Rebuild policy

The image is rebuilt daily and republished only when something actually changed — a new base image, a new version of any package in the dependency closure, or a change to this repository. Debian backports security fixes into trixie's packages, so a daily rebuild puts an openssh or borg CVE fix in the published image within 24 hours. If nothing changed, no new image is pushed and the digest stays stable, so docker pull is a no-op.

Every published image is scanned with Trivy, ships an SBOM and provenance attestation, and is signed with cosign. To verify:

cosign verify ghcr.io/ls1admin/borgserver:latest \
  --certificate-identity-regexp '^https://github\.com/ls1admin/borgserver/' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

About the 2.x images

Borg 2.x is upstream beta software and its repository format is incompatible with 1.x. A 2.x server only serves 2.x clients, and 1.x repositories cannot be used with it without an explicit borg transfer. It is published for testing; latest will never point at it while upstream calls it beta.

SSH URL path semantics in borg 2.x

Borg 2.x interprets SSH URLs differently from borg 1.x when using relative vs. absolute paths:

  • borg 2.x: A URL with a double slash (ssh://user@host//abs/path) is absolute; a single slash (ssh://user@host/rel/path) is relative to the server's working directory.
  • borg 1.x: A single slash always means absolute.

Because this server's forced command changes the working directory (cd /backup/<client>), a 2.x client must use the double-slash form for absolute paths. For example, to create a repository at /backup/myclient/myrepo, use:

borg2 repo-create --repo ssh://borg@backupserver//backup/myclient/myrepo

Borg 1.x clients continue to use the single-slash form and are unaffected by this difference.

Client version compatibility

A 1.4 server serves older borg 1.x clients — including the 1.2.8 shipped by Ubuntu 24.04 — because borg does nearly all work client-side and keeps borg serve compatible across the 1.x series. Use :latest regardless of your clients' 1.x version.

About

Dockerimage that runs a Borg backup server, accessible by ssh using publickey authentication

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages