Fall 2026brianmars.comBuilt at EGBT Technologies
ReferenceStorage

Single-node Garage for one app

Garage as a private S3 store for one service, with nothing published to the host.

Names and addresses are placeholders. The list says what each one stands for.

What it does here

Garage holds yaos's attachments and snapshots. It runs as one node in the yaos stack, and only the yaos container can reach its S3 port. Sync state doesn't go through it.

From the manual

Canonical setup

Garage is a single Rust binary, with its metadata in SQLite. A node needs a config file, then a cluster layout, a bucket and a key before it stores anything. Upstream: Garage quick start.

Edited for this build

In this build

  1. One nodereplication_factor = 1, SQLite metadata, metadata snapshots every six hours.
  2. Secrets from the environmentGARAGE_RPC_SECRET and GARAGE_ADMIN_TOKEN aren't in the config file.
  3. Layout: one node, one zone, version 1The capacity number only weights the layout; it isn't a quota.
  4. One bucket, one keyRead and write on that bucket only.
  5. No published portsThe S3 port is reached by service name on the stack's network. The only published port in the stack is yaos on 127.0.0.1:8787, for Caddy.
  6. Runs as my uidThe files on the data disk belong to me and not to root.
garage/garage.toml
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 = "127.0.0.1:3901"

[s3_api]
s3_region = "garage"
api_bind_addr = "[::]:3900"

[admin]
api_bind_addr = "[::]:3903"
/srv/stacks/yaos/docker-compose.yml
name: yaos
services:
  garage:
    image: dxflrs/garage:v2.5.0
    restart: unless-stopped
    user: "1000:1000"
    environment:
      GARAGE_RPC_SECRET: ${GARAGE_RPC_SECRET:?run scripts/setup.sh}
      GARAGE_ADMIN_TOKEN: ${GARAGE_ADMIN_TOKEN:?run scripts/setup.sh}
    volumes:
      - ./garage/garage.toml:/etc/garage.toml:ro
      - ${YAOS_DATA_DIR}/garage/meta:/var/lib/garage/meta
      - ${YAOS_DATA_DIR}/garage/data:/var/lib/garage/data

  yaos:
    build: ./worker
    restart: unless-stopped
    user: "1000:1000"
    depends_on: [garage]
    environment:
      S3_BUCKET: ${S3_BUCKET:-yaos}
      S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID:-}
      S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY:-}
      SYNC_TOKEN: ${SYNC_TOKEN:-}
    volumes:
      - ${YAOS_DATA_DIR}/workerd/do:/data/do
    ports:
      - "127.0.0.1:8787:8787"

scripts/setup.sh does the first-run steps in order, and each step checks first, so running it again is harmless:

  1. Generate GARAGE_RPC_SECRET and GARAGE_ADMIN_TOKEN into .env (mode 600).
  2. Start Garage and wait for garage status.
  3. If the node has no role, layout assign -z devbox -c 4T and layout apply --version 1.
  4. bucket create yaos, key create yaos-worker, and save the key ID and secret to .env.
  5. bucket allow --read --write --owner, then build and start yaos.

Everything lives under /srv/data/yaos, so that path goes in the restic job. For a consistent copy, stop the stack first.

Where it breaks

Seams

Where this meets the rest of the build. Each one leads with what you will actually see.

A fresh Garage meets its first write

SymptomGarage accepts the key but writes nothing

A Garage node with no role in the cluster layout has no storage assigned to it, even when it's the only node. Writes fail until a layout exists. Assign it a zone and capacity, then apply the layout. The setup script does this; it's the step people skip when they set Garage up by hand.

Verify

Checks
docker compose exec garage /garage status
docker compose exec garage /garage bucket info yaos

From outside the stack's network, nothing answers on port 3900.

Corrections and updates
  1. 4 Nov 2026First written.