Se você já usa MinIO pra guardar upload de usuário, sabe o motivo: rodar seu próprio S3 tira a dependência de nuvem, corta custo de egress e te deixa livre pra trocar de provedor sem reescrever nada. O Garage HQ ataca o mesmo problema, mas com uma proposta diferente — feito em Rust pela Deuxfleurs, pensado desde o início pra rodar geo-distribuído, em máquinas modestas, até atrás de um link residencial.
O que é o Garage e pra que ele serve
Garage é um object store compatível com a API S3, assim como o MinIO. A diferença de proposta aparece no design: onde o MinIO mira clusters robustos em datacenter, o Garage foi desenhado pra replicar dados entre nós heterogêneos e geograficamente espalhados — um nó na sua casa, outro numa VPS, outro no servidor de um amigo — sem exigir hardware idêntico ou rede rápida entre eles.
Na prática, com um único binário (ou container) em Rust, dá pra guardar e servir objetos via API S3 padrão, o que significa que qualquer SDK, CLI ou biblioteca feita pra AWS S3 funciona sem alteração — só troca o endpoint. Dá também pra expor buckets como sites estáticos direto, sem servidor web extra, replicar dados entre múltiplos nós e zonas com fator de replicação configurável, gerenciar chaves de acesso isoladas por bucket, e usar como storage de apps que já falam S3 nativamente, como Nextcloud, PeerTube ou backups via restic.
Não existe um recurso “mágico” que o MinIO não tenha. O que muda é o peso: o binário é pequeno, o consumo de memória é baixo, e o modelo de configuração inteiro vive num arquivo .toml e numa CLI — sem um console administrativo pesado rodando junto.
Subindo com Docker
Antes de mais nada, é preciso um arquivo de configuração. Gere um garage.toml com segredos únicos:
cat > garage.toml <<EOF
metadata_dir = "/data/meta"
data_dir = "/data/data"
db_engine = "sqlite"
replication_factor = 1
rpc_bind_addr = "[::]:3901"
rpc_public_addr = "127.0.0.1:3901"
rpc_secret = "$(openssl rand -hex 32)"
[s3_api]
s3_region = "garage"
api_bind_addr = "[::]:3900"
root_domain = ".s3.localhost"
[s3_web]
bind_addr = "[::]:3902"
root_domain = ".web.localhost"
index = "index.html"
[admin]
api_bind_addr = "[::]:3903"
admin_token = "$(openssl rand -base64 32)"
metrics_token = "$(openssl rand -base64 32)"
EOF
replication_factor = 1 serve só pra teste local — sem redundância, se o nó cai, os dados vão junto. Num cluster real, sobe pra 3.
Com o arquivo pronto, defina as credenciais iniciais e suba o container:
export GARAGE_DEFAULT_ACCESS_KEY="GK$(openssl rand -hex 16)"
export GARAGE_DEFAULT_SECRET_KEY="$(openssl rand -hex 32)"
export GARAGE_DEFAULT_BUCKET="default-bucket"
docker run -d \
--name garage \
-p 3900:3900 -p 3901:3901 -p 3902:3902 -p 3903:3903 \
-v $(pwd)/garage.toml:/etc/garage.toml \
-v $(pwd)/data:/data \
-e GARAGE_DEFAULT_ACCESS_KEY \
-e GARAGE_DEFAULT_SECRET_KEY \
-e GARAGE_DEFAULT_BUCKET \
dxflrs/garage:v2.3.0 \
/garage server --single-node --default-bucket
As flags --single-node --default-bucket fazem o Garage se autoconfigurar como cluster de um nó só e já criar uma chave de acesso e um bucket padrão — recurso disponível desde a versão 2.3.0. Sem elas, é preciso rodar os passos manuais de layout, bucket e chave, que aparecem na próxima seção.
Confirme que subiu certo:
docker exec garage /garage status
Gerenciando o cluster: WebUI, CLI e Admin API
O binário do Garage não vem com painel embutido — só CLI e uma Admin API HTTP por trás. A própria equipe da Deuxfleurs ainda trabalha numa interface oficial, mas dá pra ter um painel completo rodando hoje com o garage-webui, projeto comunitário que conversa com essa Admin API e cobre o dia a dia sem exigir nenhum comando de terminal.
Subindo o painel
O jeito mais direto é adicionar o serviço no mesmo docker-compose.yml do Garage:
services:
garage:
image: dxflrs/garage:v2.3.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
webui:
image: khairul169/garage-webui:latest
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"
Ele lê o admin_token direto do garage.toml montado, então não precisa configurar mais nada além disso. Suba com docker compose up -d e acesse http://seu-ip:3909. Em produção, coloque atrás do mesmo reverse proxy com TLS usado pro resto (seção seguinte) e ative autenticação com a variável AUTH_USER_PASS (usuário + hash bcrypt gerado via htpasswd -nbBC 10 usuario senha) — sem isso, qualquer um que chegue na porta 3909 tem controle total do cluster.
O que dá pra fazer no painel
A tela inicial (Dashboard) mostra a saúde do cluster de cara: status geral (Healthy), quantos nós estão ativos e conectados, quantas partições existem e quantas estão com quorum — é o mesmo dado que o garage status daria pela CLI, só que visual e atualizado sozinho.
O menu lateral tem quatro seções, que cobrem praticamente toda a operação do dia a dia:
Cluster mostra os nós que compõem o cluster, zona e capacidade de cada um, e é onde se atribui layout pra novos nós — o equivalente visual do garage layout assign e garage layout apply.
Buckets lista todos os buckets com uso de armazenamento e número de objetos em cada card, e tem o botão Create Bucket pra criar um novo sem tocar em terminal. Clicando num bucket (como no riffbook-uploads do exemplo), a tela se divide em três abas: Overview mostra uso de storage, aliases do bucket, o toggle de Website Access (o mesmo que o garage bucket website --allow faz pela CLI) e quotas opcionais de tamanho/número de objetos; Permissions é onde se liga quais chaves têm acesso de leitura, escrita ou posse sobre aquele bucket; Browse funciona como um explorador de arquivos, pra ver e baixar objetos direto do navegador sem precisar de aws s3 ls.
Keys lista as chaves de acesso existentes e permite criar novas — a versão visual do garage key create — mostrando Key ID e Secret Key na criação, exatamente como a CLI mostra no terminal.
Pra quem prefere terminal ou quer automatizar (CI/CD, scripts de provisionamento), os mesmos comandos continuam valendo. Criar um bucket:
docker exec garage /garage bucket create posts-images
Criar uma chave de acesso e liberar ela no bucket:
docker exec garage /garage key create posts-images-key
docker exec garage /garage bucket allow \
--read --write --owner \
posts-images \
--key posts-images-key
O comando key info mostra a Key ID e a Secret key geradas — são elas que vão pro .env da aplicação. Pra checar o estado do cluster a qualquer momento:
docker exec garage /garage status
docker exec garage /garage bucket info posts-images
E se quiser automatizar isso fora da CLI ou do painel — por exemplo, criar um bucket por tenant dentro da própria aplicação — a Admin API (porta 3903) expõe as mesmas operações via HTTP, com um SDK oficial em JavaScript ainda em estágio inicial, mas funcional pra nodes, layout, key e bucket. É a mesma API que tanto o garage-webui quanto a CLI usam por trás — os três caminhos (painel, CLI, API) chegam no mesmo lugar.
Colocando em produção numa VPS
Rodar em /tmp com um nó só é ótimo pra testar, ruim pra produção. Três ajustes resolvem isso.
Primeiro, volumes persistentes: troque metadata_dir e data_dir do garage.toml por caminhos reais no disco da VPS (por exemplo /opt/garage/meta e /opt/garage/data), e garanta que o volume do Docker aponte pra lá — é o que já fizemos no docker run acima com -v $(pwd)/data:/data.
Segundo, TLS na frente. O Garage não fala HTTPS nativamente — isso é papel do reverse proxy. Caddy é o caminho mais curto porque emite certificado automático via Let’s Encrypt:
s3.suahost.com, *.s3.suahost.com {
reverse_proxy localhost:3900
}
*.web.suahost.com {
reverse_proxy localhost:3902
}
Se preferir Nginx ou Traefik, a lógica é a mesma: dois blocos de proxy, um pra porta 3900 (API S3) e outro pra 3902 (endpoint web), sem tocar na porta 3901 (RPC entre nós) nem na 3903 (Admin API) — essas duas ficam só na rede interna, nunca expostas pra internet.
Terceiro, DNS. Pra usar acesso “virtual-hosted style” (bucket.s3.suahost.com), é preciso um registro wildcard *.s3.suahost.com apontando pra VPS, além de certificado wildcard — o bloco do Caddy acima já cobre isso automaticamente. Se não quiser mexer com wildcard, o Garage também aceita acesso “path style” (s3.suahost.com/bucket), mais simples de configurar e o que usamos no exemplo a seguir.
Com isso no ar, replication_factor continua em 1 se for um nó só. Redundância real exige pelo menos três nós em zonas diferentes — fora do escopo deste post, mas documentado no cookbook de cluster multi-nó oficial.
Exemplo prático: upload de imagem num projeto Next.js
Com bucket e chave já criados na seção anterior, a integração no Next.js segue o mesmo padrão de presigned URL que vale pra qualquer S3: o backend nunca vê o arquivo, só autoriza o upload.
Instale o SDK da AWS — o mesmo client funciona com Garage, só muda o endpoint:
npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
Configure o client apontando pra sua VPS:
// lib/garage.ts
import { S3Client } from "@aws-sdk/client-s3";
export const garage = new S3Client({
endpoint: process.env.GARAGE_ENDPOINT, // https://s3.suahost.com
region: "garage",
credentials: {
accessKeyId: process.env.GARAGE_ACCESS_KEY_ID!,
secretAccessKey: process.env.GARAGE_SECRET_ACCESS_KEY!,
},
forcePathStyle: true, // acesso via s3.suahost.com/bucket
});
Um route handler que gera a URL de upload:
// app/api/upload-url/route.ts
import { NextRequest, NextResponse } from "next/server";
import { PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { garage } from "@/lib/garage";
import { randomUUID } from "crypto";
export async function GET(req: NextRequest) {
const contentType = req.nextUrl.searchParams.get("type") ?? "image/jpeg";
const key = `avatares/${randomUUID()}.jpg`;
const command = new PutObjectCommand({
Bucket: "posts-images",
Key: key,
ContentType: contentType,
});
const uploadUrl = await getSignedUrl(garage, command, { expiresIn: 300 });
return NextResponse.json({ uploadUrl, key });
}
E o componente que faz o upload direto do browser pro Garage:
// components/AvatarUpload.tsx
"use client";
import { useState } from "react";
export function AvatarUpload() {
const [imageUrl, setImageUrl] = useState<string | null>(null);
async function handleUpload(file: File) {
const res = await fetch(`/api/upload-url?type=${file.type}`);
const { uploadUrl, key } = await res.json();
await fetch(uploadUrl, {
method: "PUT",
headers: { "Content-Type": file.type },
body: file,
});
setImageUrl(`${process.env.NEXT_PUBLIC_GARAGE_ENDPOINT}/posts-images/${key}`);
}
return (
<div>
<input
type="file"
accept="image/*"
onChange={(e) => e.target.files?.[0] && handleUpload(e.target.files[0])}
/>
{imageUrl && <img src={imageUrl} alt="preview" width={200} />}
</div>
);
}
Pra a URL final funcionar sem autenticação, o bucket precisa aceitar leitura pública. O Garage não tem ACL por objeto como o S3 clássico — a forma de liberar leitura é expor o bucket como site:
docker exec garage /garage bucket website --allow posts-images
A partir daí, os arquivos em posts-images ficam acessíveis via o endpoint web (porta 3902, ou o domínio configurado no reverse proxy) sem precisar de URL assinada pra leitura — só o upload continua exigindo a URL temporária gerada pelo backend.
Garage ou MinIO?
Se você já tem MinIO rodando numa VPS só e funciona, trocar não traz ganho nenhum — o modelo de presigned URL, o SDK, o forcePathStyle, tudo é idêntico. O Garage compensa quando o objetivo é justamente distribuir dados entre máquinas diferentes, sem depender de um único ponto de falha caro, ou quando o console administrativo do MinIO é peso que você não precisa. Pra um bucket só, numa VPS só, servindo upload de app Next.js, a escolha é mais sobre preferência de operação do que sobre recurso técnico.