Migrating Longhorn Backup from MinIO to Garage

#storage#docker

How to replace MinIO with Garage as the S3 backend for Longhorn backups — setup, bucket config, Kubernetes secret, and HelmRelease update.

MinIO stopped making sense for homelab use: Docker images dropped, source-only distribution, licensing drift. What that leaves behind is a lot of self-hosted installs frozen on an old version with known CVEs and no clean upgrade path. Mine was one of them, holding every Longhorn backup I have.

I replaced it with Garage — lightweight, actively maintained, S3-compatible, and with none of that baggage. Longhorn only ever sees an S3 endpoint, so the swap is a bucket, a secret and one Helm value.

You’ll need: an Ubuntu host with Docker and Compose, and a cluster with Longhorn already installed. Check the Garage releases for the current version before you pin one.

Setting up Garage

Two secrets, generated up front — the RPC secret authenticates nodes to each other, the admin token guards the admin API the WebUI talks to:

Terminal window
sudo mkdir -p /srv/docker/garage/{meta,data}
cd /srv/docker/garage
# Generate RPC secret
openssl rand -hex 32
# Generate admin token
openssl rand -hex 32
garage.toml
metadata_dir = "/var/lib/garage/meta"
data_dir = "/var/lib/garage/data"
db_engine = "lmdb"
replication_factor = 1
rpc_bind_addr = "0.0.0.0:3901"
rpc_public_addr = "127.0.0.1:3901"
rpc_secret = "YOUR_RPC_SECRET_HERE"
[s3_api]
s3_region = "us-east-1"
api_bind_addr = "0.0.0.0:3900"
root_domain = ".s3.garage"
[admin]
api_bind_addr = "0.0.0.0:3903"
admin_token = "YOUR_ADMIN_TOKEN_HERE"

replication_factor = 1 is a single copy on a single node. That’s correct here — this bucket is one leg of a backup chain, not the backup itself. If Garage is the only place your data exists, use 3 and more than one node.

Compose

The WebUI is optional, but it’s the fastest way to see whether a backup actually landed:

docker-compose.yml
version: "3"
services:
garage:
image: dxflrs/garage:v2.1.0
container_name: garage
network_mode: "host"
restart: unless-stopped
volumes:
- ./garage.toml:/etc/garage.toml
- ./meta:/var/lib/garage/meta
- ./data:/var/lib/garage/data
webui:
image: khairul169/garage-webui:latest
container_name: garage-webui
restart: unless-stopped
volumes:
- ./garage.toml:/etc/garage.toml:ro
environment:
API_BASE_URL: "http://127.0.0.1:3903"
S3_ENDPOINT_URL: "http://127.0.0.1:3900"
network_mode: "host"
Terminal window
docker-compose up -d

Layout, bucket, key

A fresh Garage node holds no data until you assign it capacity and apply a layout. Skip layout apply and every write fails with an error that doesn’t mention layouts:

Terminal window
alias garage="docker exec -ti garage /garage"
garage node id
# Assign storage capacity
garage layout assign <node-id> -z default -c 100G
garage layout show
garage layout apply --version 1
# Create bucket and key
garage bucket create longhorn
garage key create longhorn-key
garage bucket allow longhorn --read --write --owner --key longhorn-key
# Note the Key ID and Secret key
garage key info longhorn-key --show-secret

--show-secret prints the secret once, at creation. Copy it now.


Pointing Longhorn at it

The secret keeps the name minio-secret. Renaming it means editing the HelmRelease too, and there’s nothing to gain from making both changes at the same time:

minio-secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: minio-secret
namespace: longhorn-system
type: Opaque
stringData:
AWS_ACCESS_KEY_ID: <Key-ID-from-garage>
AWS_SECRET_ACCESS_KEY: <Secret-Key-from-garage>
AWS_ENDPOINTS: http://<GARAGE_SERVER_IP>:3900
AWS_REGION: us-east-1
Terminal window
kubectl apply -f minio-secret.yaml

Then the backup target itself:

---
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: longhorn
namespace: longhorn-system
spec:
interval: 1h
url: https://charts.longhorn.io
---
19 collapsed lines
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: longhorn
spec:
interval: 1h
chart:
spec:
chart: longhorn
version: 1.10.0
sourceRef:
kind: HelmRepository
name: longhorn
namespace: longhorn-system
values:
defaultSettings:
backupTarget: "s3://longhorn@us-east-1/"
backupTargetCredentialSecret: "minio-secret"
backupstorePollInterval: "300"
Warning

On Longhorn 1.10.0 and later these live under defaultSettings, not in a separate defaultBackupStore section. Put them in the old place and Helm accepts the values, Longhorn ignores them, and the UI shows no backup target at all.


Verify

Take a real backup — Longhorn UI → Volume → Create Backup — and go looking for it on the Garage side. A backup target that reports healthy while the bucket stays empty is the failure mode worth catching now rather than during a restore:

Terminal window
garage bucket list
garage bucket info longhorn

The WebUI at http://<SERVER_IP>:3909 shows the same thing with object counts.

Restoring works exactly as it did before — the procedure is in Restoring from Longhorn Backups, and nothing in it depends on which S3 implementation is underneath.


Troubleshooting

WebUI returns “Unknown Error”. The [admin] section is missing from garage.toml, or the container started before you added it:

Terminal window
docker-compose restart
docker logs garage

Port 3903 connection refused. The admin API isn’t bound:

Terminal window
netstat -tlnp | grep 3903

Longhorn can’t reach Garage. Test from inside the cluster, not from your laptop — the endpoint has to resolve and route from the pods:

Terminal window
kubectl run -it --rm debug --image=amazon/aws-cli --restart=Never -- \
s3 ls s3://longhorn --endpoint-url http://<GARAGE_IP>:3900

References