Backing Up Data with Kopia

Encrypted, deduplicated, self-scheduling backups from a workstation to a TrueNAS box

Contents

We all know the 3-2-1 backup rule: three copies of your data, stored on two different types of media, with one copy kept off-site. I've covered a few backup solutions in previous posts, including TrueNAS, Synology, VPS setups, and UrBackup. I've also been using Kopia for some time now, and it has become one of my favourite tools because of how simple it is to set up and manage.

You may already be familiar with alternatives like Restic and Borg. Both are excellent backup solutions, but Kopia arguably provides a more complete experience by combining strong features with a straightforward workflow.

This guide covers setting up Kopia in Docker, backing up a workstation to a TrueNAS system over the local network, configuring retention policies, enabling automatic scheduling, and completing a restore test to verify everything is working as expected.

What is Kopia?

Kopia is an open-source, cross-platform backup tool written in Go. One binary that runs on Linux, macOS and Windows, and the same binary is the CLI, the desktop app (KopiaUI), and the server. The headline features:

  • Snapshot-based: Each backup is a point-in-time tree of a directory that you can browse like a filesystem and restore from selectively.
  • Client-side encryption: AES-256-GCM by default. Encryption happens on the machine being backed up, so the destination only ever stores ciphertext. The server holding the backups does not need to be trusted.
  • Content-addressable deduplication: Identical data is stored once across every snapshot and every machine pointed at the same repository.
  • Compression: zstd, s2 or gzip, configurable per-policy.
  • Built-in scheduling and retention: These live in the repository as policies, not as external cron or systemd timers.
  • Three interfaces: CLI for automation, KopiaUI for the desktop, and server mode with a web UI and a central repository that multiple clients can back up to with per-user access control.

Repository backends include local/NFS filesystem, SFTP, S3, Backblaze B2, Azure, GCS, WebDAV and rclone.

The alternatives

Every tool in this table is good and trusted, but I find Kopia to be more suitable for both business and personal use.

Kopia Restic Borg Duplicati
Client-side encryption Yes Yes Yes Yes
Deduplication Yes Yes Yes Block-level
Compression Yes (zstd) Yes (0.14+) Yes Yes
Built-in scheduling Yes (policies) No (cron/systemd) No (cron/borgmatic) Yes
GUI Yes (KopiaUI / web) No (third-party) No (Vorta) Yes (web)
Multi-client server Yes Limited (REST server) ssh-based No
Native cloud backends Yes Yes No (via rclone/ssh) Yes
Track record Good Excellent Excellent Mixed

Borg is the OG. If your destination is an ssh target you control and you never need cloud, Borg with borgmatic is very solid, but it has no native object-storage support. Restic has the largest community and the best scripting ergonomics. Great if you want maximum third-party tooling and don't mind writing your own scheduling and pruning. Duplicati has the best GUI and is simple to use on Windows, but it has a history of database-corruption headaches that makes me hesitant to trust it as the only copy.

Why Kopia over them?

  1. Policies replace cron: Retention, schedule, compression and ignore rules are stored in the repository, attached per source, and inherited hierarchically, overridable per host, per user, per directory. With Restic or Borg you assemble this yourself out of cron, and a wrapper script. With Kopia you run one policy set and the server enforces it.
  2. Server mode is multi-machine: One repository, many clients, per-user ACLs, one web UI. The nearest Restic equivalent is a REST server with none of the management features.
  3. Integrity: Optional Reed-Solomon error correction, kopia snapshot verify, and scheduled maintenance that compresses and garbage-collects.
  4. Restores are flexible: Restore via the CLI, download a single file from the web UI, or mount any snapshot as a read-only filesystem and copy out what you need.

How does it work?

The data flow, from a file on disk to an encrypted blob on the NAS (in order):

  • Repository: the encrypted blob store on your NAS or in the cloud. Clients are essentially stateless apart from a local cache and a small config file.
  • Source: a directory being backed up, identified as user@host:/path.
  • Snapshot: a point-in-time tree of a source. Incremental, only changed content is uploaded after the first run.
  • Content-addressable storage: files are split into variable-sized chunks by a rolling-hash splitter, each chunk is hashed, and the hash is used as the identity. The same chunk appearing anywhere is stored only once.
  • Blobs (pack files): chunks are bundled into larger pack files so the repository isn't millions of tiny objects, which helps a lot with NFS and SFTP performance.
  • Manifests: snapshot metadata, directory trees and policies, also stored encrypted in the repository.
  • Maintenance: periodic compaction and garbage collection that reclaims space from chunks no longer referenced by any live snapshot.

Compression and encryption happen on the client, before writing anything. The repository target sees only ciphertext, hence backing up to an untrusted box, like a friend's NAS, or a cheap VPS is fine.


The lab

What I'm working with here:

  • Workstation: the machine being backed up, running Docker. This is where the Kopia client/server container lives, and its local directories are the backup sources.
  • TrueNAS box: holds the repository. A dataset at /backups/kopia, exported over NFS to the workstation's IP only.

There are three ways to point Kopia at a TrueNAS box:

  • NFS export, mounted on the host, used as a filesystem repository: what I'm using in this guide. Simplest, fastest on a LAN, easy to inspect.
  • SFTP straight to the NAS: no host mount needed, slightly slower, but a good approach if the repository is across a WAN.
  • S3 via MinIO: if you're using object storage.

Prepare TrueNAS

On the NAS, create the dataset and export it. In the UI this is Datasets > Add Dataset (/backups/kopia), then Shares > NFS > Add, with the export path set to the new dataset and the Networks / Hosts field restricted to the workstation's IP. If you prefer the shell:

# /etc/exports style
/mnt/tank/backups/kopia  192.168.1.50(rw,sync,no_subtree_check)

Installing Kopia with Docker

Mount the NFS share on the host

Mount the export on the workstation so the container can treat it as a local path. Add it to /etc/fstab so it remains persistent:

192.168.1.10:/mnt/backups/kopia  /mnt/nas/kopia-repo  nfs  defaults,hard,_netdev,noatime  0  0
sudo mkdir -p /mnt/nas/kopia-repo
sudo mount -a
touch /mnt/nas/kopia-repo/.write-test && rm /mnt/nas/kopia-repo/.write-test && echo "writable"

The compose file

Server mode hosts the web UI and the policy scheduler in one container. Create ~/kopia/docker-compose.yml:

services:
  kopia:
    image: kopia/kopia:latest
    container_name: kopia
    hostname: workstation          # part of the snapshot identity (user@host)
    restart: unless-stopped
    ports:
      - "51515:51515"
    environment:
      KOPIA_PASSWORD: ${KOPIA_REPO_PASSWORD}     # repository encryption password
      TZ: Europe/London
    command:
      - server
      - start
      - --insecure                 # TLS terminated upstream, LAN only
      - --address=0.0.0.0:51515
      - --server-username=${KOPIA_UI_USER}
      - --server-password=${KOPIA_UI_PASS}
      - --server-control-username=${KOPIA_UI_USER}
      - --server-control-password=${KOPIA_UI_PASS}
    volumes:
      - ./config:/app/config
      - ./cache:/app/cache         # preferably an SSD drive, used for dedup performance
      - ./logs:/app/logs
      - /mnt/nas/kopia-repo:/repository
      - /home:/data/home:ro        # backup sources, mounted read-only
      - /etc:/data/etc:ro
    # Uncomment to enable `kopia mount` (FUSE) for browsable restores from inside the container. `kopia snapshot restore` does not need these, but mounting does.
    # devices:
    #   - /dev/fuse
    # cap_add:
    #   - SYS_ADMIN
    # security_opt:
    #   - apparmor:unconfined

And a .env next to it:

# ~/kopia/.env
KOPIA_REPO_PASSWORD=use-a-long-random-passphrase-you-have-backed-up
KOPIA_UI_USER=admin
KOPIA_UI_PASS=another-strong-password

A few things to note:

  • hostname: workstation: a snapshot's identity is user@host:/path. Inside the container the user is root and the host is whatever you set here, so your source becomes root@workstation:/data/home. If you change the hostname later, Kopia treats it as a brand new source.
  • Sources are mounted :ro. A backup tool should never be able to write to the thing it's backing up.
  • The cache directory needs a disk. Kopia keeps content indexes here and starving it (or worse, putting it on the NFS share itself) plummets performance.
  • --insecure serves the web UI over plain HTTP. That's fine only because this is on a trusted LAN, or behind a reverse proxy that terminates TLS. If you're exposing it, remove --insecure and let Kopia generate a cert with --tls-generate-cert --tls-cert-file=/app/config/cert.pem --tls-key-file=/app/config/key.pem, or place your existing proxy in front of it.

Create the directories so the bind mounts don't come up owned by root:

cd ~/kopia
mkdir -p config cache logs

Creating the repository and first snapshot

The server can only start once a repository exists and the config knows how to reach it. Create it with a one-off compose run, which reuses the volume definitions from the compose file:

# one-time to create the repository on the NFS-mounted path
docker compose run --rm kopia repository create filesystem --path=/repository

This writes an encrypted repository to the NAS and saves a repository.config into ./config. If you'd g~~one the SFTP rout~~e instead, the only change is:

docker compose run --rm kopia repository create sftp \
  --host=192.168.1.10 --username=kopia --path=/mnt/tank/backups/kopia \
  --keyfile=/app/config/id_ed25519

Now bring up the server:

docker compose up -d
docker compose logs -f kopia

Browse to http://<workstation-ip>:51515, log in with the UI credentials from .env, and you'll get an empty Snapshots view. Take the first snapshot by either clicking New Snapshot in the UI and point it at /data/home, or from the shell:

docker exec kopia kopia snapshot create /data/home

The first run reads everything, subsequent runs only move changed data. When it finishes, check what dedup and compression created:

docker exec kopia kopia snapshot list
docker exec kopia kopia content stats

Policies: retention, schedule, compression

Once the first run is complete, automate it by setting retention, turning on compression, and setting up a schedule:

docker exec kopia kopia policy set /data/home \
  --keep-latest 10 \
  --keep-hourly 24 \
  --keep-daily 14 \
  --keep-weekly 8 \
  --keep-monthly 12 \
  --keep-annual 2 \
  --compression zstd \
  --snapshot-interval 4h

Because the policy has a schedule (--snapshot-interval 4h), the running server takes over and snapshots every four hours on its own. Policies are hierarchical, so set safe global defaults once and override them only where needed:

# global defaults inherited by every source
docker exec kopia kopia policy set --global --compression zstd --keep-daily 7

# exclude backing up certain extensions from a specific source
docker exec kopia kopia policy set /data/home \
  --add-ignore 'node_modules' \
  --add-ignore '*.tmp' \
  --add-ignore '.cache'

# inspect what is in place for a path (including inherited rules)
docker exec kopia kopia policy show /data/home

The restore test

A backup you haven't restored is not a backup. There are three restore paths: Full or selective restore (without FUSE). kopia snapshot restore copies files out directly and works in the container with no extra capabilities:

# grab a snapshot ID
docker exec kopia kopia snapshot list /data/home

# restore that snapshot's tree
docker exec kopia kopia snapshot restore <snapshotID> /data/restore-test

Browse a snapshot as a filesystem (needs FUSE). Mounting is the nicest way to pull out a single file, but it needs the FUSE device and capabilities commented in the compose file above. With those enabled:

docker exec kopia kopia mount all /tmp/kopia-mnt
# every snapshot now appears as a browsable directory tree under /tmp/kopia-mnt

The web UI also offers a per-file download from any snapshot, which is the simplest to use.

Then verify integrity:

# Read back 10% of file content
docker exec kopia kopia snapshot verify --verify-files-percent=10

Maintenance

Kopia runs maintenance automatically in two ways: Quick maintenance compacts indexes frequently while full maintenance garbage-collects unreferenced content after snapshots expire and requires more resources:

docker exec kopia kopia maintenance info
docker exec kopia kopia maintenance run --full
  • The maintenance owner is the user@host that created the repository. In a multi-client setup, only one client owns maintenance and the others just snapshot. If you rebuild the owner machine, reassign it with kopia maintenance set --owner=<user@host>.
  • Always use hard mounts, NAS reboot during a snapshot can surface as I/O errors instead of blocking.
  • Don't back up the cache, and don't put the cache on the NAS. Exclude ./cache, keep it on local disk.
  • Permissions on restore. The container runs as root by default, so restored files will be owned by root. If that matters for your sources, run the container with a user: mapping.
  • The repository password is unrecoverable, if you lose it, that data is gone. Store it somewhere safe.

3-2-1

Everything so far is one copy on one server. To finish the rule, sync the repository to off-site object storage. Kopia can replicate a repository to a second backend without re-encrypting because the blobs are already ciphertext:

docker exec kopia kopia repository sync-to b2 \
  --bucket=my-offsite-kopia --parallel=8

Schedule that sync, and you've got three copies across two media with one off-site.

Wrapping up

Kopia is one of the best backup tools I’ve used, and also one of the most straightforward. You get encrypted, deduplicated, self-scheduling backups with a web UI using something like forty lines of YAML.

The next logical step is making sure those 4-hourly snapshots are actually running without having to check manually. By scraping Kopia's metrics and putting them into a dashboard, we can set up failed backup alerts. That's probably a post of its own, and a good opportunity to explain why I'm migrating away from Prometheus and Loki.