How to run self-hosted S3 object storage with Garage
Run S3-compatible object storage on your own Linux server with Garage in Docker, behind Caddy HTTPS, with buckets, access keys, rclone tests and backups.
- Intermediate
- 45 min read
- Updated
Tested on: Ubuntu 24.04 LTS, Ubuntu 26.04 LTS, Debian 12, Debian 13
On this page
- Prerequisites
- Step 1 — Create the folders
- Step 2 — Write the configuration
- Step 3 — Start Garage with Docker Compose
- Step 4 — Assign the storage layout
- Step 5 — Create a bucket and an access key
- Step 6 — Publish the S3 API over HTTPS with Caddy
- Step 7 — Test with rclone
- Step 8 — Plan a three-node cluster (optional)
- Back up and restore
- Update Garage
- Troubleshooting
- garage status shows NO ROLE ASSIGNED
- AuthorizationHeaderMalformed or a region error
- NoSuchBucket or DNS errors for addresses like backups.s3.example.com
- SignatureDoesNotMatch or RequestTimeTooSkewed
- The disk is full although there is free space
- Cluster nodes do not see each other
- Next steps
S3-compatible object storage lets backup tools, media servers, Nextcloud, Mastodon, Matrix and many other applications store files through the Amazon S3 API on a server you control. This guide sets up such a store on a Linux server with Garage.
MinIO used to be the usual choice for self-hosted S3. Its GitHub repository now states that the repository is no longer maintained and that MinIO is distributed there as source code only, without pre-compiled binaries; GitHub shows the repository as archived since April 2026. Existing MinIO installations keep running, but they no longer receive updates from that repository.
We chose Garage for this guide for these reasons: it ships as a single dependency-free binary and an official Docker image with versioned tags; it is configured with one TOML file; its documentation publishes minimum requirements (1 GB of RAM, 16 GB of disk) and describes it as made for small to medium self-hosted deployments; it runs on one server and grows into a replicated multi-zone cluster; and it is actively released (version 2.4.1 came out in September 2026). We also looked at SeaweedFS, which is more flexible but runs several components (master, volume servers, filer and S3 gateway) that you deploy and operate separately.
Know Garage's limits before you start: it does not support object versioning, Object Lock, bucket policies or ACLs, and lifecycle rules only cover expiration and aborting incomplete multipart uploads. If a tool needs immutable (Object Lock) backups, Garage is not the right store for it. This guide runs Garage with Docker Compose, publishes the S3 API over HTTPS with Caddy, creates a bucket and an access key, tests them with rclone, and covers firewall rules, backups, updates and the path to a three-node cluster.
Prerequisites
- A server running Ubuntu 24.04 LTS, Ubuntu 26.04 LTS, Debian 12 or Debian 13. The steps work on a HyperDC Linux VPS, VDS or dedicated server with root access.
- A non-root user with
sudorights who can run Docker: see Secure a new Linux server, Set up SSH keys and Install Docker on Ubuntu or on Debian. - A domain name such as
s3.example.comwith an A (and optionally AAAA) record pointing at the server, and Caddy installed as in Caddy as a reverse proxy.
| Resource | Minimum (official) | Suggested starting point |
|---|---|---|
| CPU | Any x86_64 CPU from the last 10 years, or ARMv7/ARMv8 | 2 vCPU |
| RAM | 1 GB | 2 GB |
| Disk | 16 GB | The data you plan to store, plus 20% headroom |
| Network (clusters) | 200 ms latency or less, 50 Mbps or more between nodes | Not needed for a single server |
The minimums come from Garage's website; the right-hand column is a conservative starting point, not a benchmark. Garage's deployment guide recommends XFS for the data directory and notes that ext4 has stricter limits on the number of inodes; on a small server with an ext4 root disk, keep an eye on df -i. It also suggests keeping the metadata directory on an SSD.
Step 1 — Create the folders
Keep the configuration in /opt/garage and the data in the paths Garage's own deployment guide uses:
sudo mkdir -p /opt/garage /var/lib/garage/meta /var/lib/garage/data
sudo chown $USER:$USER /opt/garage
cd /opt/garage/var/lib/garage/meta holds the metadata database, the node key and the cluster layout; /var/lib/garage/data holds the stored data blocks.
Step 2 — Write the configuration
Create garage.toml. The shell fills in a fresh RPC secret and admin tokens with openssl while writing the file, as in Garage's quick start. Replace 203.0.113.10 with your server's public IP address and s3.example.com with your domain:
cat > garage.toml <<EOF
metadata_dir = "/var/lib/garage/meta"
data_dir = "/var/lib/garage/data"
db_engine = "sqlite"
metadata_auto_snapshot_interval = "6h"
replication_factor = 1
rpc_bind_addr = "[::]:3901"
rpc_public_addr = "203.0.113.10:3901"
rpc_secret = "$(openssl rand -hex 32)"
[s3_api]
s3_region = "garage"
api_bind_addr = "127.0.0.1:3900"
root_domain = ".s3.example.com"
[admin]
api_bind_addr = "127.0.0.1:3903"
admin_token = "$(openssl rand -base64 32)"
metrics_token = "$(openssl rand -base64 32)"
EOF
chmod 600 garage.toml
cat garage.tomlWhat the settings mean:
replication_factor = 1stores one copy, which is all a single server can do. It must be the same on every node and cannot be changed casually later: the documentation calls changing it a dangerous operation that is not officially supported.db_engine = "sqlite": the default engine, LMDB, is faster and recommended for clusters with a replication factor of 2 or more, but it can be corrupted by an unclean shutdown. On a single node there is no other copy to resync from, so SQLite is the safer choice.metadata_auto_snapshot_intervalkeeps the two most recent metadata snapshots for recovery.api_bind_addr = "127.0.0.1:3900"keeps the S3 API on loopback; Caddy adds TLS in front of it. The admin API on port 3903 also stays on loopback.rpc_bind_addrandrpc_public_addrare only used for traffic between Garage nodes. The firewall keeps port 3901 closed on a single server.root_domainenables virtual-host-style bucket addresses such asbackups.s3.example.com; path-style access always works (Step 6).
The secrets in this file protect your cluster and admin API, which is why it gets mode 600.
Step 3 — Start Garage with Docker Compose
Create compose.yaml. It follows the Docker Compose example in Garage's deployment guide, which uses host networking so that Garage nodes can reach each other directly. With host networking, Docker publishes no ports and ufw applies normally:
services:
garage:
image: dxflrs/garage:v2.4.1
container_name: garage
restart: unless-stopped
network_mode: host
volumes:
- ./garage.toml:/etc/garage.toml:ro
- /var/lib/garage/meta:/var/lib/garage/meta
- /var/lib/garage/data:/var/lib/garage/dataGarage publishes images tagged with exact versions; v2.4.1 was current when this guide was written. Check the download page for the latest release. Start the container and create a shortcut for the garage command-line tool, which runs inside the container and reads the same configuration file:
docker compose up -d
docker compose logs --tail 20
echo "alias garage='docker exec garage /garage'" >> ~/.bashrc
source ~/.bashrc
garage statusgarage status lists one healthy node with NO ROLE ASSIGNED. Copy the first few characters of its ID for the next step.
Step 4 — Assign the storage layout
A Garage cluster only stores data after you assign each node a zone and a capacity and apply that layout. The capacity tells Garage how much space it may use on this node; pick a value below the free space of /var/lib/garage/data. Replace 563e with the start of your node ID:
garage layout assign -z dc1 -c 100G 563e
garage layout show
garage layout apply --version 1
garage status
curl -s http://127.0.0.1:3903/healthgarage status now shows the node with zone dc1 and its capacity, and the health endpoint of the admin API reports that Garage is operational.
Step 5 — Create a bucket and an access key
Create a bucket, create a key for the application that will use it, and allow that key to read and write the bucket:
garage bucket create backups
garage key create backups-key
garage bucket allow --read --write --owner backups --key backups-key
garage bucket info backupsgarage key create prints a Key ID that starts with GK and a Secret key. Store both in your password manager; applications use them as the access key and secret key. garage bucket info lists the key with its permissions.
Give every application its own key and only the rights it needs. For a tool that only reads, grant --read alone. Keys and buckets are independent, so one key can be allowed on several buckets.
Step 6 — Publish the S3 API over HTTPS with Caddy
Add a site block to /etc/caddy/Caddyfile. Caddy keeps the original Host header when it proxies, which S3 request signatures depend on:
s3.example.com {
reverse_proxy 127.0.0.1:3900
}Reload Caddy and allow only SSH and web traffic. Ports 3900 to 3903 stay closed:
sudo systemctl reload caddy
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
curl -sI https://s3.example.comcurl connects over a valid certificate and Garage answers with an S3 error status such as 403, because the request is not signed. That is expected and proves the proxy works.
Path-style or virtual-host style? With this setup clients use path-style addresses, where the bucket is part of the path: https://s3.example.com/backups/file.txt. Virtual-host style puts the bucket in the host name: https://backups.s3.example.com/file.txt. That needs a wildcard DNS record for *.s3.example.com, the site address s3.example.com, *.s3.example.com in Caddy, and a wildcard certificate, which Caddy can only obtain through the DNS challenge with a plugin for your DNS provider. Most tools (rclone, restic, the AWS CLI, Nextcloud) can be set to path-style, so start there.
Step 7 — Test with rclone
rclone is a command-line tool for cloud storage and is packaged by Ubuntu and Debian. Garage's documentation provides the configuration for it:
sudo apt install rclone
mkdir -p ~/.config/rclone
nano ~/.config/rclone/rclone.conf[garage]
type = s3
provider = Other
env_auth = false
access_key_id = GK_REPLACE_WITH_KEY_ID
secret_access_key = REPLACE_WITH_SECRET_KEY
region = garage
endpoint = https://s3.example.com
force_path_style = true
acl = private
bucket_acl = privateProtect the file, then list buckets and copy a test file up and back:
chmod 600 ~/.config/rclone/rclone.conf
rclone lsd garage:
echo "hello from garage" > hello.txt
rclone copy hello.txt garage:backups
rclone ls garage:backups
rclone cat garage:backups/hello.txtYou should see the backups bucket, the file with its size, and its content. Other clients work the same way: the AWS CLI needs region=garage and endpoint_url=https://s3.example.com in ~/.aws/config (or --endpoint-url on older versions), and backup tools such as restic take the endpoint, region, key ID and secret.
Step 8 — Plan a three-node cluster (optional)
For data that must survive the loss of a server, Garage's documentation recommends at least three nodes with replication_factor = 3; Garage then keeps three copies in three different zones. Because the replication factor should not be changed later, decide before you store data. The setup differs from the single server in a few points:
- Install Garage the same way on every node, with the same
rpc_secretandreplication_factor = 3, and each node's own public IP inrpc_public_addr. The documentation recommends the LMDB engine (db_engine = "lmdb") for clusters. - Open port 3901/tcp only between the nodes, for example
sudo ufw allow from 203.0.113.11 to any port 3901 proto tcpfor each peer. - Connect the nodes, then give each one its own zone (for example one per location) and apply the layout once:
garage node id
garage node connect 563e1ac825ee3323aa441e72c26d1030d6d4414aeb3dd25287c531e7fc2bc95d@203.0.113.11:3901
garage layout assign -z zone-a -c 1T 563e
garage layout assign -z zone-b -c 1T 8f2a
garage layout assign -z zone-c -c 1T c41d
garage layout apply --version 1Run garage node id on each node to get its full identifier, and garage node connect from one node to the others; nodes discover the rest of the cluster on their own. Caddy can list several upstreams in one reverse_proxy line, so a single HTTPS endpoint can spread requests across the nodes.
Back up and restore
State lives in three places: /opt/garage (the configuration with your RPC secret, and the Compose file), /var/lib/garage/meta (the metadata database, node key and layout) and /var/lib/garage/data (the data blocks, which are useless without the metadata). On a single server, back up two things.
1. Configuration and metadata. Take a consistent metadata snapshot, then archive it with the configuration:
sudo mkdir -p /opt/backups
garage meta snapshot --all
sudo tar -czf /opt/backups/garage-meta-$(date +%F).tar.gz -C / opt/garage var/lib/garage/metaThe snapshot lands in /var/lib/garage/meta/snapshots, named after the time it was taken. The archive also contains the live database file, which may be inconsistent; restores use the snapshot.
2. The objects themselves. Copy each bucket to storage on another machine, for example another Garage server, an S3 provider or an SFTP server configured as a second rclone remote named offsite:
rclone sync garage:backups offsite:garage-backupsRestore metadata from a snapshot (for example after metadata corruption), following Garage's recovery guide. Stop Garage, keep the damaged database aside, copy the newest snapshot in its place, start again and repair the tables:
cd /opt/garage
docker compose stop
sudo ls /var/lib/garage/meta/snapshots
sudo mv /var/lib/garage/meta/db.sqlite /var/lib/garage/meta/db.sqlite.bak
sudo cp /var/lib/garage/meta/snapshots/2026-10-09T03:00:00Z /var/lib/garage/meta/db.sqlite
docker compose start
garage repair -a --yes tablesOn a single node, changes made after the snapshot are lost. To rebuild on a new server, set Garage up as in Steps 1 to 7, create the bucket and key again, and copy the objects back with rclone copy offsite:garage-backups garage:backups.
Update Garage
Read the release notes linked from the download page first. Garage's upgrade guide treats updates within the same major version as minor upgrades that can be rolled out node by node, and asks you to check cluster health first; take a metadata snapshot as well:
cd /opt/garage
garage repair --all-nodes --yes tables
garage worker list
garage meta snapshot --all
nano compose.yaml
docker compose pull
docker compose up -d
garage statusIn compose.yaml, change only the image tag to the new version. garage worker list should show the repair workers as done before you continue, and garage status shows the new version afterwards. In a cluster, upgrade one node at a time. Major upgrades (for example from 2.x to a future 3.x) can require downtime and a garage migrate step; follow the version-specific migration guide in Garage's documentation.
Troubleshooting
garage status shows NO ROLE ASSIGNED
The layout was not applied, so the node stores nothing and S3 requests fail. Run garage layout show, then garage layout apply with the version number it suggests.
AuthorizationHeaderMalformed or a region error
The client signs requests for a different region, often us-east-1. Set the region to garage (the s3_region value) in the client configuration.
NoSuchBucket or DNS errors for addresses like backups.s3.example.com
The client uses virtual-host style but there is no wildcard DNS record or certificate. Switch the client to path-style (force_path_style = true in rclone, addressing_style = path in the AWS CLI configuration) or set up wildcard DNS as described in Step 6.
SignatureDoesNotMatch or RequestTimeTooSkewed
Check the key ID and secret first. S3 signatures also include a timestamp, so a client or server clock that is several minutes off breaks every request; check with timedatectl and enable time synchronisation. Make sure no proxy in front of Garage rewrites the Host header.
The disk is full although there is free space
On ext4, Garage's many small block files can exhaust the inodes before the space. Compare df -h with df -i. Lower the layout capacity, add a dedicated XFS volume for /var/lib/garage/data, or move to a larger disk.
Cluster nodes do not see each other
Port 3901/tcp must be open between the nodes, rpc_secret must be identical on all of them, and rpc_public_addr must contain each node's reachable IP address. garage status lists only the nodes that are connected.
Next steps
- Send your app backups to this bucket, for example from Vaultwarden.
- Use the same Caddy setup for more services with Caddy as a reverse proxy.
- Learn more about Compose files in Docker Compose basics.
- Compare servers for object storage on the S3 storage hosting page.
- Read the Garage documentation for the admin API, static website hosting and monitoring.
Frequently asked questions
Why does this guide not use MinIO?
MinIO's GitHub repository states that it is no longer maintained and that MinIO is distributed there as source code only, without pre-compiled binaries. GitHub shows the repository as archived since April 2026, so new installations would receive no updates from it.
Why does this guide use Garage instead of SeaweedFS?
Garage is one binary with one configuration file, publishes versioned Docker images and documented minimum requirements, and is designed for small to medium self-hosted deployments. SeaweedFS is a capable alternative but runs several components: master, volume servers, filer and S3 gateway.
Is a single Garage server safe for important data?
It has no redundancy. Garage's documentation says a replication factor of 1 should only be used for test deployments. On one server, keep off-site copies of every bucket, or run three nodes in different locations with a replication factor of 3.
Does Garage support versioning or Object Lock?
No. Garage's S3 compatibility page lists object versioning, Object Lock, bucket policies and ACLs as unsupported, and lifecycle rules only cover expiration and aborting multipart uploads. Tools that require these features need a different storage server.
Which region and URL style should S3 clients use?
Use the region set in s3_region, which is garage in this guide, and path-style addressing such as https://s3.example.com/bucket. Virtual-host style also works if you add wildcard DNS and a wildcard certificate.
Sources
- github.com/minio/minio
- garagehq.deuxfleurs.fr
- garagehq.deuxfleurs.fr/documentation/quick-start
- garagehq.deuxfleurs.fr/documentation/cookbook/real-world
- garagehq.deuxfleurs.fr/documentation/reference-manual/configuration
- garagehq.deuxfleurs.fr/documentation/reference-manual/s3-compatibility
- garagehq.deuxfleurs.fr/documentation/cookbook/reverse-proxy
- garagehq.deuxfleurs.fr/documentation/connect/cli
- garagehq.deuxfleurs.fr/documentation/operations/upgrading
- garagehq.deuxfleurs.fr/documentation/operations/recovering
- garagehq.deuxfleurs.fr/documentation/design/goals
- garagehq.deuxfleurs.fr/_releases.html