Install (3-node example)

# On each of 3 nodes (or just one for testing)
GARAGE_VER=v1.0.1
wget https://garagehq.deuxfleurs.fr/_releases/${GARAGE_VER}/x86_64-unknown-linux-musl/garage
sudo install garage /usr/local/bin/

# Each node has its own config (with a unique RPC secret)
sudo mkdir -p /etc/garage /var/lib/garage/{meta,data}

/etc/garage.toml:

metadata_dir = "/var/lib/garage/meta"
data_dir = "/var/lib/garage/data"

db_engine = "lmdb"

replication_factor = 3

rpc_bind_addr = "[::]:3901"
rpc_public_addr = "10.0.1.10:3901"
rpc_secret = "<random 64-char hex from openssl rand -hex 32>"

[s3_api]
s3_region = "garage"
api_bind_addr = "[::]:3900"
root_domain = ".s3.example.com"

[s3_web]
bind_addr = "[::]:3902"
root_domain = ".web.example.com"
index = "index.html"

[k2v_api]
api_bind_addr = "[::]:3904"

[admin]
api_bind_addr = "[::]:3903"
admin_token = "<random>"
metrics_token = "<random>"

Same rpc_secret on all 3 nodes. rpc_public_addr changes per node.

Start + connect the nodes

# systemd unit
sudo tee /etc/systemd/system/garage.service <<'EOF'
[Unit]
Description=Garage
After=network-online.target

[Service]
ExecStart=/usr/local/bin/garage -c /etc/garage.toml server
Restart=always
User=garage

[Install]
WantedBy=multi-user.target
EOF

sudo useradd -r garage
sudo chown -R garage:garage /var/lib/garage
sudo systemctl enable --now garage

# Verify the node
garage node id
# Output: a node ID + the rpc_public_addr

# On node 1, connect the other nodes (paste each node's ID@addr from "garage node id")
garage node connect <node-2-id>@10.0.1.11:3901
garage node connect <node-3-id>@10.0.1.12:3901

garage status
# Shows all 3 nodes as Connected

Define the cluster layout

Garage uses a Russian-doll layout: each node belongs to a zone (typically a datacenter or city); replication ensures copies in different zones.

# Tag each node with a zone + capacity (in bytes-style human units)
garage layout assign <node-1-id> -z home -c 500G -t homelab
garage layout assign <node-2-id> -z cloud -c 200G -t hetzner
garage layout assign <node-3-id> -z office -c 300G -t office

# Show pending changes
garage layout show

# Apply
garage layout apply --version 1

Now writes are placed so each object has one copy in each zone (with replication_factor=3). Lose a zone, the others still serve reads + writes. Critical for the geo-distributed use case.

Create an S3 bucket + access key

# Create a key
garage key create my-app-key
# Output: AKEY + SKEY

# Create a bucket
garage bucket create my-app-data

# Grant the key access to the bucket
garage bucket allow --read --write --owner my-app-data --key my-app-key

Use with any S3 client

# aws-cli
aws s3 --endpoint-url http://10.0.1.10:3900 ls

# rclone
rclone config create garage s3 \
    provider Other \
    access_key_id AKEY \
    secret_access_key SKEY \
    endpoint http://garage.example.com:3900 \
    acl private \
    region garage

rclone copy /local/data garage:my-app-data

# restic (see /tutorials/restic-s3-encrypted-backups.html)
export AWS_ACCESS_KEY_ID=AKEY
export AWS_SECRET_ACCESS_KEY=SKEY
restic -r s3:http://garage.example.com:3900/my-app-data backup ~/photos

Web (static site hosting)

Garage's [s3_web] section turns any bucket into a static-site host. Upload an index.html + assets; Garage serves them via path-style URLs at the configured root domain.

# Upload a site
aws s3 cp ./public/ s3://my-site/ --recursive --endpoint http://garage:3900

# Browse to http://my-site.web.example.com/

The web subsystem honors index + standard 404 page; useful for self-hosted Hugo / Astro / Eleventy / SilverBullet builds.

Reverse proxy

# Caddy
s3.example.com {
    reverse_proxy 127.0.0.1:3900
}
*.s3.example.com {
    reverse_proxy 127.0.0.1:3900
}

web.example.com {
    reverse_proxy 127.0.0.1:3902
}
*.web.example.com {
    reverse_proxy 127.0.0.1:3902
}

Wildcards needed because S3 uses subdomain bucket-naming (<bucket>.s3.example.com). With root_domain set to .s3.example.com, Garage routes requests to the right bucket based on the subdomain.

WAN-friendly design

What makes Garage geo-friendly:

  • No strict consistency required across nodes. Writes commit when a majority of zones have it; readers see eventually-consistent results within a few seconds.
  • Compact RPC. Inter-node traffic is minimal; you can run replicas on cheap VPSes with consumer bandwidth.
  • No bandwidth-amplifying erasure coding by default. Garage uses plain replication; predictable WAN cost.

MinIO's design assumes LAN-speed inter-node communication; running it across the WAN works but doesn't scale as gracefully. Garage explicitly targets the "3 nodes in 3 different cities" case.

Garage vs alternatives

  • MinIO — high-throughput, LAN-first, K8s-native, erasure-coded. Heavier on bandwidth across the WAN. Pick for "I want fast S3 on a single cluster."
  • Ceph RGW (see that tutorial) — production-grade distributed storage; way more setup; tightly LAN-coupled.
  • SeaweedFS — small Go S3 store; LAN-focused; less geo-distributed-aware.
  • Backblaze B2 / Cloudflare R2 — commercial; cheap; no self-hosting.

When Garage is the right pick

  • Multi-location homelab / small business with home + office + cloud VPS storage to combine.
  • Geo-redundant backups (restic to Garage; same data lives in 3 cities).
  • "I want S3 that survives one of my homes burning down without paying per-GB cloud rates."

When it isn't

  • For "single-DC fast S3 for K8s," MinIO is faster + more K8s-native.
  • For "store petabytes," Ceph is more battle-tested at that scale.
  • For "I only have one place to store this and one disk," plain disk is simpler.