What Garage is, and what it is not
Garage is an S3-compatible object store designed for small self-hosted deployments, optionally distributed across multiple sites. It runs in a container, needs no adjacent database server, and the project targets modest hardware: its official requirements stop at 1 GB of RAM.
What it is not, let us be upfront: Garage does not bundle a web console yet. Administration goes through the garage command-line tool and an HTTP admin API. Deuxfleurs is testing a browser interface, Garage Web Admin, published separately as a pre-release and meant to ship inside the binary. Community projects also provide a browser interface on top of the admin API if you need one.
If you are here because MinIO removed its console from the community edition in 2025 — and then stopped publishing community Docker images and archived its repository — the honest answer is that Garage does not, today, give you an equivalent built-in console. It does give you a project whose AGPLv3 licence reserves nothing for a paid tier.
What Garage does well on a VPS
- Standard S3 API: compatible with
awsCLI,rclone,mc,restic, Python/Go/Rust SDKs and virtually all S3 clients on the market. - Modest footprint: the project targets modest hardware — it coexists without friction with a web server, database or reverse proxy on the same VPS.
- No external dependency: no PostgreSQL, no Redis, no etcd. The metadata engine is embedded (LMDB or SQLite).
- Optional replication: in single-node mode a single disk is enough; in multi-node mode Garage replicates objects between machines, even across different sites.
- Unencumbered AGPLv3 licence: no feature gated behind a paid edition, no volume or bucket count limits.
- Virtual subdomains: Garage routes
<bucket>.s3.your-domain.com, which some S3 clients and SDKs require.
Prerequisites: sizing your VPS for a storage bucket
Garage itself is frugal — it is the data volume you store that determines disk size, not the service. An entry-level VPS is enough to run the daemon. For typical workloads (application backups, site assets):
- CPU: 1 vCPU is sufficient; Garage is light on compute.
- RAM: 1 GB, the figure in the project's official requirements; the LMDB engine keeps its index in memory-mapped files.
- Disk: NVMe SSD sized for your data, plus 20 % headroom for metadata. A single node stores one copy of your objects — plan accordingly.
- OS: Debian 12 or Ubuntu 22.04 LTS, Docker and the Compose plugin installed.
To install Docker in one command:
curl -fsSL https://get.docker.com | shMinimal storage bucket configuration
Garage reads a garage.toml file. For a single node, the essentials fit in a few lines — note replication_factor = 1, which explicitly states there is no replication:
metadata_dir = "/var/lib/garage/meta"
data_dir = "/var/lib/garage/data"
db_engine = "sqlite"
replication_factor = 1
rpc_bind_addr = "[::]:3901"
rpc_public_addr = "127.0.0.1:3901"
rpc_secret = "<64 hexadecimal characters>"
[s3_api]
s3_region = "garage"
api_bind_addr = "[::]:3900"
root_domain = ".s3.your-domain.com"
[admin]
api_bind_addr = "[::]:3903"
admin_token = "<admin token>"The rpc_secret must be 32 bytes, i.e. 64 hexadecimal characters: openssl rand -hex 32. The root_domain sets the subdomain routing scheme for bucket-based access — replace it with your actual domain.
Deploy Garage and create your first S3 bucket
Create the directory structure
Create a working directory and configuration file:
mkdir -p /opt/garage/config openssl rand -hex 32 # → paste as rpc_secret openssl rand -base64 32 | tr -d '=' | head -c 40 # → admin_token # S3 credentials for the default bucket, in a root-only .env file cd /opt/garage && umask 077 printf 'GARAGE_DEFAULT_ACCESS_KEY=GK%s\nGARAGE_DEFAULT_SECRET_KEY=%s\n' "$(openssl rand -hex 12)" "$(openssl rand -hex 32)" > .envWrite
garage.tomlinto/opt/garage/config/, replacing the two generated tokens.Write the Compose file and start
Since version 2.3, Garage can bootstrap a single-node cluster and create its first bucket automatically from environment variables. This avoids the manual
layout assign/layout apply/bucket create/key createsequence:services: garage: image: dxflrs/garage:v2.5.0 restart: unless-stopped command: ["/garage", "-c", "/etc/garage/garage.toml", "server", "--single-node", "--default-bucket"] environment: GARAGE_DEFAULT_BUCKET: my-bucket GARAGE_DEFAULT_ACCESS_KEY: ${GARAGE_DEFAULT_ACCESS_KEY} GARAGE_DEFAULT_SECRET_KEY: ${GARAGE_DEFAULT_SECRET_KEY} ports: - "127.0.0.1:3900:3900" - "127.0.0.1:3903:3903" volumes: - ./config:/etc/garage - garage-meta:/var/lib/garage/meta - garage-data:/var/lib/garage/data volumes: garage-meta: garage-data:The image is distroless: no shell, empty entrypoint. The command must name the binary by absolute path (
/garage) and pass the config file (-c). Without both, the container loops onNo such file or directory.Both credentials come from the
.envfile created in the previous step, which Compose reads on its own: no secret is written in clear in the file.GKfollowed by 24 hexadecimal characters, with a 64-character secret, is the shape of the keys Garage generates; since 2.3, an imported key only needs at least 8 characters, and its secret at least 16.docker compose up -dVerify the API is responding
An anonymous call to the S3 API should return 403 — the correct sign that the service is alive and rejecting unsigned requests:
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3900/ # 403 → API is upThe admin API returns 200 with the token:
curl -H "Authorization: Bearer $ADMIN_TOKEN" http://127.0.0.1:3903/v2/ListBucketsIf you get
connection refused, the container has not started — checkdocker compose logs garage.Expose the API behind nginx and TLS
Never publish port 3900 directly on the public interface. Bind it to localhost and let nginx handle the proxy and TLS certificate. A minimal block for
s3.your-domain.com:server { listen 443 ssl; server_name s3.your-domain.com *.s3.your-domain.com; ssl_certificate /etc/letsencrypt/live/s3.your-domain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/s3.your-domain.com/privkey.pem; client_max_body_size 0; # otherwise nginx rejects any upload > 1 MB (413) location / { proxy_pass http://127.0.0.1:3900; proxy_set_header Host $host; proxy_max_temp_file_size 0; } }The
*.s3.your-domain.comwildcard certificate covers per-bucket subdomains. Use certbot in DNS-01 mode to obtain it.Test with an S3 client
With the environment variables set, the
awsCLI works without modification:export AWS_ACCESS_KEY_ID=GK... export AWS_SECRET_ACCESS_KEY=... export AWS_DEFAULT_REGION=garage aws --endpoint-url https://s3.your-domain.com s3 ls aws --endpoint-url https://s3.your-domain.com s3 cp myfile.txt s3://my-bucket/For restic, set
RESTIC_REPOSITORY=s3:https://s3.your-domain.com/my-bucketand runrestic init.
Common use cases for a self-hosted S3 storage bucket
A Garage storage bucket on a VPS covers several common needs without depending on a third-party cloud.
What people deploy behind Garage
- restic backups: set
RESTIC_REPOSITORY=s3:https://s3.your-domain.com/backups. S3 compatibility in restic is native and requires no plugin; Borg, in its stable 1.4 branch, only supports local or SSH repositories. - Static site origin: store compiled frontend assets and serve them through nginx or Cloudflare in proxy mode. No signed S3 requests reach the end user.
- Application file uploads: Laravel, Django or Rails apps write to Garage through their usual S3 SDK — only the endpoint changes.
- CI/CD artefacts: GitHub Actions or GitLab CI push build artefacts to Garage with
aws s3 cp, exactly as with AWS S3. - Log and metric archiving: Loki, Vector or Fluentd can write to an S3-compatible store for long-term archiving.
- File sharing between services: two containers on the same or different VPS nodes access the same bucket via the S3 API, with no NFS mount.
Garage vs self-hosted S3 alternatives
Scroll the table
| Criterion | Garage | MinIO (community) |
|---|---|---|
| Licence | AGPLv3 — no usage restriction | AGPLv3, but console removed from free edition in 2025 |
| Docker images | Actively published on Docker Hub (`dxflrs/garage`) | Community images discontinued in 2025; GitHub repository now archived |
| Web console | Not bundled yet (CLI + admin API; Web Admin in pre-release) | Console removed from community edition |
| Multi-site | Natively designed for geographic zones | Possible but requires a dedicated cluster |
| Memory footprint | Modest hardware (1 GB of RAM in the requirements) | Aimed at high performance and intensive workloads |
| S3 compatibility | Good on the common surface | Very broad, including AWS-specific extensions |
Buckets, access keys and permissions
Garage manages access at the key level: each S3 key can be authorised on one or more buckets, read-only or read-write. For automated backups, create a dedicated key with access to the backup bucket only — not the entire server.
The command garage key create <name> generates a key, and garage bucket allow --key <id> --read --write <bucket> grants it rights. You can also use the HTTP admin API to automate this in a script.
Things to keep in mind
A single node is not redundancy. Garage can replicate across multiple machines, even across different sites — that is its design goal. But as long as you have one node, your objects exist in a single copy on a single disk. Maintain the backup discipline you would apply regardless — backing up your backup bucket to a second location is not optional.
Expose the API behind a domain and TLS, not on a bare IP. It is both a security requirement and a practical necessity: several S3 clients refuse to operate without TLS, and per-bucket subdomain routing requires a wildcard certificate.
S3 compatibility covers the common surface, not every AWS-specific extension. Standard clients, libraries and backup tools work without modification; if you depend on an unusual call (Object Lambda, S3 Select, cross-account Replication…), check the project documentation before migrating.
Watch your disk space. A backup bucket that grows without a retention policy fills the disk silently. Set a lifecycle rule through the S3 API (aws s3api put-bucket-lifecycle-configuration, expiration only; since 2.5.0, restricted to a key with owner permission on the bucket) or a cron job that purges old snapshots (restic forget --prune).