Joel Always one syllable

Self-hosting Garage for object storage

26 Oct 2025

Photo of a garage courtesy Todd Kent @ Unsplash
Photo of a full, busy, garage. Courtesy Todd Kent @ Unsplash

Due to the work on Shubox block storage had become an interesting little corner that I’ve enjoyed researching. I had previously played with minio and found it to be fine. Recently, the maintainers of Minio had decided to change directions in how “open” they wanted to make their “open source” product. That’s fine. With the velocity and severity of these changes, though, it made sense to take a look at something that doesn’t risk having an even more severe rug-pull moment. Hence - garage.

Up and running with Garage

Docker compose is my preferred method for spinning up services in-house. For this experiment I decided to try both Garage and garage-webui together. Keep in mind that this is a single node installation, no replication. Just one machine, one instance.

That said, here’s a simple config in a compose.yml file to get things going:

services:
  garage:
    image: dxflrs/garage:v2.1.0
    container_name: garage
    volumes:
      - ./garage.toml:/etc/garage.toml
      - ./meta:/var/lib/garage/meta
      - ./data:/var/lib/garage/data
    restart: unless-stopped
    ports:
      - 3900:3900
      - 3901:3901
      - 3902:3902
      - 3903:3903
  garage-webui:
    image: khairul169/garage-webui:1.1.0
    container_name: garage-webui
    restart: unless-stopped
    volumes:
      - ./garage.toml:/etc/garage.toml:ro
    ports:
      - 3909:3909
    environment:
      API_BASE_URL: "http://garage:3903"
      S3_ENDPOINT_URL: "http://garage:3900"
    depends_on:
      - garage
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:3909 || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 20s

Where the contents of ./garage.toml are:

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

replication_factor = 1

rpc_bind_addr = "[::]:3901"
rpc_public_addr = "[::]:3901"
rpc_secret = "..." # generate secret with `openssl rand -hex 32`

bootstrap_peers = []

[s3_api]
s3_region = "bully"
api_bind_addr = "[::]:3900"
root_domain = ".s3.your-domain.com"

[s3_web]
bind_addr = "[::]:3902"
root_domain = ".web.your-domain.com"
index = "index.html"

[admin]
api_bind_addr = "[::]:3903"
admin_token = "..."   # generate secret with `openssl rand -hex 32`
metrics_token = "..." # generate secret with `openssl rand -hex 32`

A quick docker compose up -d will get that up and running.

Create a cluster layout

While it may be up and running, you will still need to allocate the disk space (layout) for Garage. Here’s how:

# get the node id
NODE_ID=`xxd -p ./meta/node_key.pub | tr -d '\n'`

# shorthand for the garage cli
alias garage='docker exec garage /garage -c /etc/garage.toml --rpc-host $NODE_ID@127.0.0.1:3901'

# check status
garage status

# assign node and cluster layout with zone name (dc1) and disk space (10G)
garage layout assign $NODE_ID -z dc1 -c 10G

# apply the layout
garage layout apply --version 1

Check your work

At this point you should be able to visit http://[host-ip]:3909, see Garage-webui, create buckets & keys, etc. Beyond that, you should install an s3 client like awscli, mc, or s5cmd to test your buckets with whatever key and secret you’ve created via garage-webui.

Enjoy! 🛠️️