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.
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.
In this build
- One node
replication_factor = 1, SQLite metadata, metadata snapshots every six hours. - Secrets from the environment
GARAGE_RPC_SECRETandGARAGE_ADMIN_TOKENaren't in the config file. - Layout: one node, one zone, version 1The capacity number only weights the layout; it isn't a quota.
- One bucket, one keyRead and write on that bucket only.
- 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. - Runs as my uidThe files on the data disk belong to me and not to root.
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"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:
- Generate
GARAGE_RPC_SECRETandGARAGE_ADMIN_TOKENinto.env(mode 600). - Start Garage and wait for
garage status. - If the node has no role,
layout assign -z devbox -c 4Tandlayout apply --version 1. bucket create yaos,key create yaos-worker, and save the key ID and secret to.env.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.
Seams
Where this meets the rest of the build. Each one leads with what you will actually see.
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
docker compose exec garage /garage status
docker compose exec garage /garage bucket info yaosFrom outside the stack's network, nothing answers on port 3900.
- 4 Nov 2026First written.