Local S3 Storage Without MinIO: SeaweedFS and Garage in Docker Compose

SitePoint TeamPublished inProgramming·DevOps·Cloud·
September 16, 2026
The AI briefing for Developers
Stay up to date with AI tools, model releases, and developer workflows that matter.
Weekly. Free. One click to leave.
SitePoint Premium
Stay Relevant and Grow Your Career in Tech
- Premium Results
- Publish articles on SitePoint
- Daily curated jobs
- Learning Paths
- Discounts to dev tools
7 Day Free Trial. Cancel Anytime.
S3-compatible local storage has become a baseline requirement for cloud-native development. This tutorial walks through building a single docker-compose.yml that runs both SeaweedFS and Garage simultaneously, with pre-seeded buckets, verified by a shared Node.js test script using the AWS SDK v3.
How to Set Up Local S3 Storage with SeaweedFS and Garage
- Create the project directory structure with
docker-compose.yml,garage.toml,init-garage.sh, and atest/folder. - Configure SeaweedFS in Docker Compose using the
servercommand with S3 gateway flags and a healthchecked init sidecar for bucket creation. - Write the
garage.tomlfile withreplication_factor = 1, SQLite metadata engine, and an RPC secret generated viaopenssl rand -hex 32. - Build the Garage init script to assign the cluster layout, create an API key, provision a test bucket, and write credentials to a shared volume.
- Define the complete
docker-compose.ymlwith both backends, init sidecars, named volumes, and a shared bridge network. - Install AWS SDK v3 dependencies (
@aws-sdk/client-s3and@aws-sdk/s3-request-presigner) in the test directory. - Run
docker compose up -d --wait, copy Garage credentials, and execute the integration test script against both endpoints.
Table of Contents
Why Move Beyond MinIO for Local S3?
S3-compatible local storage has become a baseline requirement for cloud-native development. Every team building against AWS S3 needs a local emulation layer for integration tests, CI pipelines, and offline development. MinIO has long been the default choice, but its 2021 relicensing to AGPL-3.0 introduced legal complexity that many engineering and legal teams prefer to avoid, particularly in commercial products where AGPL’s copyleft provisions create ambiguity around network interaction. Beyond licensing, MinIO’s resource footprint has grown alongside its enterprise feature set. For simple dev and test workloads, dedicating 300-400 MB of RAM to a storage backend that only needs to handle a few PutObject and GetObject calls is wasteful.
Two lesser-known alternatives address these concerns. SeaweedFS addresses the licensing concern directly with its Apache-2.0 license; Garage addresses the resource footprint concern with a 30-60 MB RAM footprint, though it shares MinIO’s AGPL-3.0 license. SeaweedFS provides a distributed architecture that scales from a single container to large clusters. Garage, while also licensed under AGPL-3.0 (and therefore subject to the same licensing considerations as MinIO), was purpose-built for edge and self-hosted deployments, making it a strong fit for resource-constrained CI runners and development laptops.
For simple dev and test workloads, dedicating 300-400 MB of RAM to a storage backend that only needs to handle a few PutObject and GetObject calls is wasteful.
This tutorial walks through building a single docker-compose.yml that runs both SeaweedFS and Garage simultaneously, with pre-seeded buckets, verified by a shared Node.js test script using the AWS SDK v3. The test harness exercises PutObject, GetObject, multipart upload initiation, and pre-signed URL generation against both backends.
Prerequisites: Docker Desktop 4.2.0+ or Docker Engine with Compose v2.1.0+ (docker compose version to verify), Node.js 18 or later, ports 8333, 9333, 3900, and 3903 free on the host, and basic familiarity with S3 concepts (buckets, keys, pre-signed URLs).
SeaweedFS vs Garage vs MinIO: Quick Comparison
Licensing, Architecture, and Re
| Feature | MinIO | SeaweedFS | Garage |
|---|---|---|---|
| License | AGPL-3.0 | Apache-2.0 | AGPL-3.0 |
| Language | Go | Go | Rust |
| Idle RAM | ~300-400 MB (idle, default config, no data) | ~60-100 MB (idle, default config, no data) | ~30-60 MB (idle, default config, no data) |
| Single-binary deployment | Yes | Yes (via server command) | Yes |
| S3 API coverage | Broad (PutObject, GetObject, multipart, versioning, object lock, lifecycle rules) | Moderate (PutObject, GetObject, multipart, basic ACLs; no object lock or bucket versioning) | Moderate (PutObject, GetObject, multipart, pre-signed URLs; no object lock or bucket versioning) |
| Multi-tenancy | Yes | Limited | Yes (API key scoping) |
| Erasure coding | Yes | Yes | No (replication-based) |
MinIO remains the best fit when teams need the broadest S3 API compatibility and are comfortable with AGPL-3.0. SeaweedFS is the strongest choice for CI pipelines and commercial integrations where Apache-2.0 licensing is non-negotiable. Garage excels in edge deployments, low-rematters more than API breadth, but note that Garage’s AGPL-3.0 license carries the same obligations as MinIO’s
Project Structure
The project contains five files across two directories:
docker-compose.yml defines both storage backends, their init sidecars, volumes, and networking. garage.toml provides Garage’s required configuration specifying storage paths, S3 bind address, and replication factor. init-garage.sh handles post-start setup: assigning Garage’s cluster layout, creating an API key, and provisioning a test bucket. Under test/, package.json declares AWS SDK v3 dependencies and s3.test.mjs validates S3 operations against both backends.
local-s3/├── docker-compose.yml├── garage.toml├── init-garage.sh└── test/├── package.json└── s3.test.mjsConfiguring SeaweedFS in Docker Compose
Master + Volume + Filer + S3 Gateway in One Container
SeaweedFS decomposes storage into four components. The master server manages topology and volume assignment. The volume server stores actual data chunks, while the filer layers a file-system abstraction with directories on top. Finally, the S3 gateway translates S3 API calls into filer operations and requires the filer component to function. In production, these typically run as separate processes across nodes. For local development, the SeaweedFS image provides a server command that bundles all four components into a single process.
Key command flags control the S3 gateway behavior: -s3 enables the gateway, -s3.port=8333 sets its listen port, -master.volumeSizeLimitMB=100 caps individual volume file sizes (keeping disk usage predictable in dev), and -volume.max=10 limits the number of volume files the server will create.
Pre-seeding a Test Bucket
SeaweedFS can create buckets programmaticallyr Compose pattern uses an init sidecar service that waits for the S3 gateway health endpoint, then creates the test bucket using the weed shell CLI
seaweedfs:image: seaweedfs/seaweedfs:3.68container_name: seaweedfscommand:>server-s3-s3.port=8333-master.volumeSizeLimitMB=100-volume.max=10-dir=/dataports:-"8333:8333"-"9333:9333"volumes:- seaweedfs_data:/datahealthcheck:test:["CMD","curl","-f","http://localhost:9333/cluster/status"]interval: 5stimeout: 3sretries:10networks:- s3netseaweedfs-init:image: seaweedfs/seaweedfs:3.68container_name: seaweedfs-initdepends_on:seaweedfs:condition: service_healthyentrypoint: /bin/shcommand:["-c","echo 's3.bucket.create -name test-bucket' | weed shell -master=seaweedfs:9333 && echo 'SeaweedFS bucket created'"]networks:- s3netNote: The SeaweedFS Docker image was previously published under the chrislusf/seaweedfs namespace. The canonical image is now seaweedfs/seaweedfs. Verify the current stable tag at Docker Hub. The healthcheck targets the master server’s cluster status endpoint on port 9333, which reliably returns HTTP 200 when the server is ready.
With weed shell, the init sidecar connects to the master server on port 9333 and issues a bucket creation command. It runs once, creates the bucket, and exits. The depends_on with condition: service_healthy ensures the gateway is ready before the init service attempts any operations.
Configuring Garage in Docker Compose
Understanding Garage’s Layout and Key System
Garage introduces concepts not found in simpler S3-compatible stores. Each Garage node has a unique node ID generated on first startup. Before the cluster can accept data, you must apply a cluster layout that assigns each node a zone identifier and a storage capacity. Garage also requires explicit API key creation: keys have an access ID and a secret, and you must grant bucket access per key. This design supports multi-tenancy by default but adds setup steps that a local dev environment must automate.
The garage.toml Configuration File
metadata_dir="/var/lib/garage/meta"data_dir="/var/lib/garage/data"db_engine="sqlite"replication_factor=1[s3_api]s3_region="us-east-1"api_bind_addr="[::]:3900"root_domain=".s3.garage.localhost"[s3_web]bind_addr="[::]:3902"root_domain=".web.garage.localhost"[admin]api_bind_addr="[::]:3903"[rpc_encryption]rpc_secret="0000000000000000000000000000000000000000000000000000000000000000"The replication_factor = 1 setting is critical for single-node local development. It tells Garage that a single copy of each block is sufficient, so a lone node can accept writes without waiting for replicas. The db_engine = "sqlite" option keeps metadata storage simple and avoids the need for an external database. The rpc_secret must be a 64-character hex string; generate one with openssl rand -hex 32 before first use. The zero-value shown above is provided only as a local-dev fallback — replace it for any shared or networked environment.
Initialization Script for Layout and Bucket
#!/bin/bashset-euntilcurl-sf http://garage:3903/health >/dev/null 2>&1;doecho"Waiting for Garage..."sleep2doneNODE_ID=$(garage -c /etc/garage.toml nodeid2>/dev/null |head-1|tr-d'[:space:]')if[[-z"$NODE_ID"]];thenecho"ERROR: Failed to retrieve Garage node ID. Is the node registered?">&2exit1fiecho"Node ID: $NODE_ID"garage -c /etc/garage.toml layout assign -z dc1 -c1000"$NODE_ID"CURRENT_VERSION=$(garage -c /etc/garage.toml layout show 2>/dev/null |grep-i"^version"|awk'{print $NF}')CURRENT_VERSION=${CURRENT_VERSION:-0}NEXT_VERSION=$(( CURRENT_VERSION +1))garage -c /etc/garage.toml layout apply --version"$NEXT_VERSION"if garage -c /etc/garage.toml key list 2>/dev/null |grep-q"local-dev-key";thenecho"Key 'local-dev-key' already exists, retrieving info..."KEY_OUTPUT=$(garage -c /etc/garage.toml key info local-dev-key)elseKEY_OUTPUT=$(garage -c /etc/garage.toml key create local-dev-key)fiACCESS_KEY=$(echo"$KEY_OUTPUT"|grep"Key ID"|awk'{print $NF}')SECRET_KEY=$(echo"$KEY_OUTPUT"|grep"Secret key"|awk'{print $NF}')if[[-z"$ACCESS_KEY"||-z"$SECRET_KEY"]];thenecho"ERROR: Failed to parse access key or secret key from output:">&2echo"$KEY_OUTPUT">&2exit1figarage -c /etc/garage.toml bucket create test-bucket 2>/dev/null ||truegarage -c /etc/garage.toml bucket allow --read--write--owner test-bucket --key local-dev-keyCREDS_PATH="${CREDS_PATH:-/shared/garage-credentials.json}"TMP_PATH="${CREDS_PATH}.tmp"printf'{"accessKey":"%s","secretKey":"%s"}'"$ACCESS_KEY""$SECRET_KEY">"$TMP_PATH"mv"$TMP_PATH""$CREDS_PATH"echo"Credentials written to $CREDS_PATH"echo"Garage initialization complete"Security note: The script writes credentials only to a file on the shared volume. It never echoes credentials to stdout, preventing leakage through docker logs or CI log aggregation.
After waiting for Garage’s admin API health endpoint, the script retrieves the node ID using the garage CLI, assigns it to a layout with a zone and capacity (specified as an integer in MB), and applies the layout with a dynamically computed version number to ensure idempotent re-runs. It then creates a named API key (or retrieves existing credentials if the key already exists), provisions the test bucket, and grants the key read/write/owner permissions. Credentials land atomically on a shared volume so the test harness can read them at runtime.
Docker Compose Service Block
garage:image: dxflrs/garage:v1.0.1container_name: garagevolumes:- ./garage.toml:/etc/garage.toml:ro- garage_meta:/var/lib/garage/meta- garage_data:/var/lib/garage/dataports:-"3900:3900"-"3903:3903"healthcheck:test:["CMD","curl","-sf","http://localhost:3903/health"]interval: 5stimeout: 3sretries:10networks:- s3netgarage-init:image: dxflrs/garage:v1.0.1container_name: garage-initdepends_on:garage:condition: service_healthyvolumes:- ./garage.toml:/etc/garage.toml:ro- ./init-garage.sh:/init-garage.sh:ro- shared_data:/sharedentrypoint:["/bin/bash"]command:["/init-garage.sh"]networks:- s3netGarage mounts the garage.toml as read-only and uses named volumes for metadata and data persistence across container restarts. Its init sidecar shares the same config file and mounts the initialization script. The shared_data volume provides a channel for passing Garage’s dynamically generated credentials to the test harness.
Setting entrypoint to ["/bin/bash"] with command: ["/init-garage.sh"] makes bash execute the init script directly. This avoids failures caused by the script lacking the execute bit. Files bind-mounted from the host often do not preserve Unix permissions, especially on Windows or after a fresh git clone.
The Complete docker-compose.yml
services:seaweedfs:image: seaweedfs/seaweedfs:3.68container_name: seaweedfscommand:>server-s3-s3.port=8333-master.volumeSizeLimitMB=100-volume.max=10-dir=/dataports:-"8333:8333"-"9333:9333"volumes:- seaweedfs_data:/datahealthcheck:test:["CMD","curl","-f","http://localhost:9333/cluster/status"]interval: 5stimeout: 3sretries:10networks:- s3netseaweedfs-init:image: seaweedfs/seaweedfs:3.68container_name: seaweedfs-initdepends_on:seaweedfs:condition: service_healthyentrypoint: /bin/shcommand:["-c","echo 's3.bucket.create -name test-bucket' | weed shell -master=seaweedfs:9333 && echo 'SeaweedFS bucket created'"]networks:- s3netgarage:image: dxflrs/garage:v1.0.1container_name: garagevolumes:- ./garage.toml:/etc/garage.toml:ro- garage_meta:/var/lib/garage/meta- garage_data:/var/lib/garage/dataports:-"3900:3900"-"3903:3903"healthcheck:test:["CMD","curl","-sf","http://localhost:3903/health"]interval: 5stimeout: 3sretries:10networks:- s3netgarage-init:image: dxflrs/garage:v1.0.1container_name: garage-initdepends_on:garage:condition: service_healthyvolumes:- ./garage.toml:/etc/garage.toml:ro- ./init-garage.sh:/init-garage.sh:ro- shared_data:/sharedentrypoint:["/bin/bash"]command:["/init-garage.sh"]networks:- s3netvolumes:seaweedfs_data:garage_meta:garage_data:shared_data:networks:s3net:driver: bridgeSeaweedFS listens on localhost:8333 for S3 traffic, while Garage uses localhost:3900. This separation avoids port conflicts and allows the test harness to target each backend independently. The shared s3net bridge network lets the init sidecars communicate with their respective storage services by container name.
Writing AWS SDK v3 Integration Tests
Project Setup and Dependencies
{"name":"local-s3-test","version":"1.0.0","type":"module","scripts":{"test":"node s3.test.mjs"},"dependencies":{"@aws-sdk/client-s3":"^3.600.0","@aws-sdk/s3-request-presigner":"^3.600.0"}}Run npm install inside the test/ directory. The "type": "module" field enables ESM imports, which the test script uses throughout. Both SDK packages are required: @aws-sdk/client-s3 for core S3 operations and @aws-sdk/s3-request-presigner for generating pre-signed URLs.
Test Script Structure
The test script defines an array of endpoint configurations, one per backend. Each entry specifies the endpoint URL, credentials, and a human-readable label. The script iterates over this array, running the same operation suite against each backend, and logs pass/fail results. SeaweedFS accepts any non-empty access key and secret by default, when no S3 authentication configuration file is provided to the server, so static dummy credentials work. Garage requires the real credentials generated by the init script.
Note: The code blocks below are excerpts from a single s3.test.mjs file. Combine them in order to produce the complete test script, or adapt them into your own test structure.
PutObject and GetObject
After running docker compose up -d, copy Garage credentials to the test directory so the script can read them on the host:
dockercp garage-init:/shared/garage-credentials.json ./test/garage-credentials.jsonif[!-s ./test/garage-credentials.json ];thenecho"ERROR: garage-credentials.json is missing or empty">&2exit1fiimport{S3Client,PutObjectCommand,GetObjectCommand,CreateMultipartUploadCommand,AbortMultipartUploadCommand}from"@aws-sdk/client-s3";import{ getSignedUrl }from"@aws-sdk/s3-request-presigner";import{ readFile }from"fs/promises";conststreamToString=async(stream)=>{if(typeof stream.getReader==="function"){const reader = stream.getReader();const chunks =[];let done =false;while(!done){const{ value,done: d }=await reader.read();if(value) chunks.push(value);done = d;}returnBuffer.concat(chunks).toString("utf-8");}returnnewPromise((resolve, reject)=>{const chunks =[];stream.on("data",(chunk)=> chunks.push(chunk));stream.on("end",()=>resolve(Buffer.concat(chunks).toString("utf-8")));stream.on("error", reject);});};let garageCredentials ={accessKey:"dummy",secretKey:"dummy"};try{const raw =awaitreadFile(newURL("./garage-credentials.json",import.meta.url),"utf-8");garageCredentials =JSON.parse(raw);}catch{console.warn("Garage credentials not found at ./garage-credentials.json, using defaults — Garage tests will fail");}const endpoints =[{label:"SeaweedFS",endpoint:"http://localhost:8333",credentials:{accessKeyId:"any",secretAccessKey:"any"},},{label:"Garage",endpoint:"http://localhost:3900",credentials:{accessKeyId: garageCredentials.accessKey,secretAccessKey: garageCredentials.secretKey,},},];let anyFailed =false;for(const ep of endpoints){console.log(`--- Testing${ep.label}---`);const client =newS3Client({endpoint: ep.endpoint,region:"us-east-1",credentials: ep.credentials,forcePathStyle:true,});try{const putResult =await client.send(newPutObjectCommand({Bucket:"test-bucket",Key:"hello.txt",Body:"Hello from local S3!",ContentType:"text/plain",}));const putStatus = putResult.$metadata.httpStatusCode;const putPass = putStatus >=200&& putStatus <300;console.log(`PutObject:${putPass ?"PASS":"FAIL"}`);if(!putPass) anyFailed =true;const getResult =await client.send(newGetObjectCommand({Bucket:"test-bucket",Key:"hello.txt"}));const body =awaitstreamToString(getResult.Body);const getPass = body ==="Hello from local S3!";console.log(`GetObject:${getPass ?"PASS":"FAIL"}(body: "${body}")`);if(!getPass) anyFailed =true;}catch(e){console.log(`PutObject/GetObject: FAIL (${e.message})`);anyFailed =true;}The forcePathStyle: true setting is essential. Both SeaweedFS and Garage expect path-style requests (http://host:port/bucket/key) rather than virtual-hosted-style (http://bucket.host:port/key). Omitting this flag causes DNS resolution to fail in virtual-hosted mode. Separately, credential mismatches (wrong access key or secret) cause SignatureDoesNotMatch errors.
The
forcePathStyle: truesetting is essential. Both SeaweedFS and Garage expect path-style requests rather than virtual-hosted-style. Omitting this flag causes DNS resolution to fail in virtual-hosted mode.
CreateMultipartUpload
try{const multipartResult =await client.send(newCreateMultipartUploadCommand({Bucket:"test-bucket",Key:"large-file.bin",}));const uploadId = multipartResult.UploadId;console.log(`CreateMultipartUpload:${uploadId ?"PASS":"FAIL"}(UploadId:${uploadId})`);if(!uploadId) anyFailed =true;if(uploadId){await client.send(newAbortMultipartUploadCommand({Bucket:"test-bucket",Key:"large-file.bin",UploadId: uploadId,}));console.log(`AbortMultipartUpload: PASS`);}else{console.log(`AbortMultipartUpload: SKIP (no uploadId)`);}}catch(e){console.log(`CreateMultipartUpload: FAIL (${e.message})`);anyFailed =true;}Initiating a multipart upload without completing it validates that the backend supports the multipart API surface. A full multipart flow would require UploadPartCommand and CompleteMultipartUploadCommand, but for verifying backend compatibility, initiation and abort are sufficient.
Generating Pre-signed URLs with getSignedUrl
try{const presignedUrl =awaitgetSignedUrl(client,newGetObjectCommand({Bucket:"test-bucket",Key:"hello.txt"}),{expiresIn:3600});const urlPass = presignedUrl.startsWith("http");console.log(`Pre-signed URL:${urlPass ?"PASS":"FAIL"}`);console.log(`URL:${presignedUrl}`);if(!urlPass) anyFailed =true;const response =awaitfetch(presignedUrl);if(!response.ok){const errBody =await response.text();console.log(`Pre-signed fetch: FAIL (HTTP${response.status}:${errBody.slice(0,120)})`);anyFailed =true;}else{const urlBody =await response.text();const fetchPass = urlBody ==="Hello from local S3!";console.log(`Pre-signed fetch:${fetchPass ?"PASS":"FAIL"}`);if(!fetchPass) anyFailed =true;}}catch(e){console.log(`Pre-signed URL: FAIL (${e.message})`);anyFailed =true;}}if(anyFailed){process.exitCode=1;}The pre-signed URL test confirms that each backend correctly validates SigV4 query-string signatures. The fetch call against the generated URL verifies end-to-end functionality, not just URL generation. This test depends on the PutObject call above having succeeded first.
Running the Full Stack
From the local-s3/ project root:
- Generate a Garage RPC secret:
openssl rand -hex 32and paste it intogarage.tomlas therpc_secretvalue. - Start both backends:
docker compose up -d - Wait for health checks to pass:
docker compose ps(all services should show “healthy” or “exited (0)” for init sidecars) - Copy Garage credentials to the test directory and run tests:
dockercp garage-init:/shared/garage-credentials.json ./test/garage-credentials.jsoncdtest&&npminstall&&node s3.test.mjs--- Testing SeaweedFS ---PutObject: PASSGetObject: PASS (body: "Hello from local S3!")CreateMultipartUpload: PASS (UploadId: ...)AbortMultipartUpload: PASSPre-signed URL: PASSPre-signed fetch: PASS--- Testing Garage ---PutObject: PASSGetObject: PASS (body: "Hello from local S3!")CreateMultipartUpload: PASS (UploadId: ...)AbortMultipartUpload: PASSPre-signed URL: PASSPre-signed fetch: PASSCommon issues: Port 8333 or 3900 already in use will cause bind failures; check with lsof -i :8333 (on Windows, use netstat -ano | findstr :8333). If Garage returns SignatureDoesNotMatch, the init script likely has not completed; verify the credentials JSON exists in the shared volume. If the Garage layout was not applied, garage-init logs will show the error; rerun with docker compose run garage-init.
Teardown
To stop all services and remove volumes (including persisted Garage credentials and metadata):
docker compose down -vThis ensures a clean state if you need to re-run the init scripts. The garage-init container is designed for idempotent re-runs — it checks for existing keys and tolerates pre-existing buckets, so repeated executions produce non-fatal output.
Tips for CI and Team Workflows
Embedding in GitHub Actions / GitLab CI
Both backends work well as services in CI pipelines. Ensure test/package-lock.json is committed to version control before running in CI — npm ci requires this file and will fail without it.
A minimal GitHub Actions step:
-name: Start S3 backendsrun: docker compose up -d --wait-name: Copy Garage credentialsrun:|# garage-init exits after completion; use docker cp (works on exited containers).docker cp garage-init:/shared/garage-credentials.json ./test/garage-credentials.jsonif [ ! -s ./test/garage-credentials.json ]; thenecho "ERROR: garage-credentials.json is missing or empty" >&2exit 1fi-name: Run S3 integration testsrun: cd test && npm ci && node s3.test.mjsThe --wait flag blocks until all healthchecks pass, eliminating the need for manual sleep statements. This flag requires Docker Compose v2.1.0 or later. Verify with docker compose version. On older versions, replace with a polling loop or sleep command.
Switching Backends
Parameterizing the endpoint URL allows the same test suite to target SeaweedFS, Garage, or real AWS S3. Setting S3_ENDPOINT=http://localhost:8333 in the environment and reading it with process.env.S3_ENDPOINT in the test script removes the need for code changes when switching backends. For AWS, omit the endpoint property entirely and let the SDK resolve the default regional endpoint.
Parameterizing the endpoint URL allows the same test suite to target SeaweedFS, Garage, or real AWS S3.
What Comes Next
This setup delivers a reproducible, license-aware local S3 development environment with two backends running in parallel and a single test harness validating both. The Docker Compose file is portable across development machines and CI runners, and the init sidecar pattern provisions buckets deterministically.
Add TLS termination with Caddy or Traefik in front of the S3 endpoints, explore Garage’s multi-node replication mode for staging environments that more closely mirror production, or integrate with Terraform’s S3 backend to store local state files against either backend.
SeaweedFS documentation is available at github.com/seaweedfs/seaweedfs. Garage documentation is maintained at garagehq.deuxfleurs.fr.
Sharing our passion for building incredible internet things.


