S3-compatible object storage in a single static binary and an embeddable Go library. It began as a lightweight server for development and testing.
Status: experimental. The single-node server is mature and heavily conformance-tested. A Garage-style cluster (zone/rack-aware replication) is planned, see #279. Pin a version and read COMPATIBILITY.md before trusting production data to it.
- Bucket operations (create, delete, list) and object operations (put, get,
delete, list, copy, tagging, metadata,
x-amz-meta-*). - Multipart uploads, presigned URLs (≤7-day expiry) and streaming (chunked) uploads.
- AWS Signature V4 auth by default: multiple credentials, per-bucket grants
(
read/write/admin), public-read buckets and canned ACLs. - Hot-reloadable TLS; credential and certificate reload on
SIGHUPwith no restart. - Crash-atomic writes,
fsyncpolicy control, and content-addressed blocks verified on every read. - Compatible with the AWS CLI, MinIO client (
mc),s3cmd,rcloneand the AWS SDKs; liveness/readiness endpoints and OpenTelemetry metrics/traces.
- Admin API on a separate bearer-token listener (runtime access-key CRUD, config reload).
systemdunit generation (fs systemd), commented config generation, and a library core with no forced observability stack.
Quick Start:
# Install
go install github.com/go-faster/fs/cmd/fs@latest
# Start the server
fs s3
# Or with custom configuration
fs s3 --addr :9000 --root /data/s3See COMPATIBILITY.md for the full compatibility statement
(what's implemented, what returns NotImplemented, what's planned, and the
durability & failure model). Compatibility is measured against the upstream
ceph/s3-tests suite and real S3 clients —
the machine-generated breakdown is in the
S3 conformance report.
Example Usage:
# Using AWS CLI
export AWS_ENDPOINT_URL=http://localhost:8080
aws s3 mb s3://mybucket --endpoint-url $AWS_ENDPOINT_URL
aws s3 cp file.txt s3://mybucket/ --endpoint-url $AWS_ENDPOINT_URL
# Using cURL
curl -X PUT http://localhost:8080/mybucket
curl -X PUT -d "Hello!" http://localhost:8080/mybucket/hello.txt
curl http://localhost:8080/mybucket/hello.txtThe binary authenticates requests with AWS Signature V4 by default. Provide a root credential and (optionally) TLS:
export FS_ROOT_ACCESS_KEY=AKIAEXAMPLE
export FS_ROOT_SECRET_KEY=exampleSecretKey
fs s3 --tls-cert cert.pem --tls-key key.pemAdditional keys, per-bucket grants (read/write/admin), and public-read
buckets are configured under auth: in the config file. To run without any
authentication (development only), pass --insecure-no-auth.
SigV4 header auth, presigned URLs (≤7-day expiry) and streaming uploads are all
verified; TLS certificates hot-reload without dropping connections. As a
library, enable it with server.WithAuth(store) / server.WithCORS(cfg) — the
bare handler stays anonymous unless you opt in.
Multiple access-key/secret credentials can be managed at runtime — without a
restart — through a separate admin listener. Config-defined keys stay read-only and keys created through the admin
API are persisted (<root>/.access-keys.json, mode 0600) and survive restarts
and SIGHUP reloads.
admin:
enabled: true
addr: "localhost:8090" # keep bound to localhost or behind a proxy
token: "change-me" # or set FS_ADMIN_TOKENIt is a JSON API under /api/v1 (bearer-token protected), generated from
_oas/admin.yml with ogen:
list credentials and their grants, create keys (generating the access key and
secret, shown once), delete runtime keys, and reload the config:
# List credentials
curl -H "Authorization: Bearer $FS_ADMIN_TOKEN" localhost:8090/api/v1/access-keys
# Create a credential scoped to buckets matching "uploads-*"
curl -H "Authorization: Bearer $FS_ADMIN_TOKEN" -H "Content-Type: application/json" \
-d '{"grants":[{"bucket":"uploads-*","permission":"write"}]}' \
localhost:8090/api/v1/access-keysWith cluster.node_id set, nodes form a cluster: every object's metadata and
data are kept on three nodes, spread across zones then racks, written and read
at quorum, and repaired in the background (Garage-style; see
#279). Nodes agree on a
layout — which nodes hold which partitions — and any node serves any key.
The same engine runs a single server: leave cluster: out.
storage:
type: engine
cluster:
node_id: "node-1"
advertise_addr: "10.0.0.1:7080"
peers: ["10.0.0.2:7080"]
secret: "change-me-0123456789" # or FS_CLUSTER_SECRETAssign roles through any node's admin API; gossip carries the result to the rest:
cat > roles.yaml <<'YAML'
members:
- {id: node-1, zone: dc1, capacity: 4TB}
- {id: node-2, zone: dc2, capacity: 4TB}
- {id: node-3, zone: dc3, capacity: 4TB}
YAML
fs layout apply -f roles.yaml --dry-run # what would move
fs layout apply -f roles.yaml
fs layout show
fs layout nodesGenerate a unit for fs s3 — a per-user service by default, or a hardened
system service with --user=false:
# Install and enable a per-user service
fs systemd --install --config ~/fs.yaml
systemctl --user daemon-reload
systemctl --user enable --now fs
loginctl enable-linger "$USER" # keep it running after logout
# Or emit a system unit
fs systemd --user=false --config /etc/fs/config.yaml | sudo tee /etc/systemd/system/fs.serviceThe unit wires ExecReload to SIGHUP, so systemctl --user reload fs performs
the hot credential/TLS reload.
- Durability — data lives in the engine under
<storage.root>/.engine(bbolt metadata + SHA-256-named blocks).storage.fsyncisfile(default: data and metadata are fsynced before a write is acknowledged) ornone(dev/CI only); writes are always crash-atomic (no torn object). Every block is verified on read, and in a cluster anti-entropy repairs replicas. - Upgrading from a filesystem-backend release — the filesystem backend is gone and its data directory is not converted: the server refuses to start on one. Copy the objects out with the previous release into a new storage root (see docs/DEPLOYMENT.md).
- Health & readiness —
/health(liveness: the process is up) and/ready(readiness: storage is reachable, 503 otherwise). Prometheus/metricsand pprof are served on a separate listener (defaultlocalhost:9464,METRICS_ADDRto change). - Hot reload — send
SIGHUPto reload credentials and the TLS certificate from disk without a restart.
go install github.com/go-faster/fs/cmd/fs@latestOr build from source:
git clone https://github.com/go-faster/fs
cd fs
go build -o bin/fs ./cmd/fs# Start S3 server with defaults
fs s3
# Show help
fs s3 --helpThe server supports both YAML configuration files and command-line flags:
# Using YAML configuration
fs s3 --config config.yaml
# Using command-line flags
fs s3 --addr :9000 --root /var/lib/s3data
# Mix both (flags override config file)
fs s3 --config config.yaml --addr :9000
# Generate example configuration
fs s3 --generate-config > my-config.yamlRun fs s3 --generate-config to produce a fully commented configuration template, and fs s3 --help for the list of flags.
server:
addr: ":8080"
read_timeout: 30s
write_timeout: 30s
idle_timeout: 120s
health_path: "/health"
storage:
root: ".s3data"
observability:
service_name: "go-faster/fs"
enable_request_logging: true
enable_metrics: true
enable_tracing: trueThe S3 server is embeddable. Install the module and pick a storage backend —
engine for durable storage (open it with engine.Open and release
it with Close), storagemem for in-memory, or your own implementation of the fs.Storage
interface:
go get github.com/go-faster/fsThe library core pulls in no observability stack — wrap the handler yourself
(e.g. with otelhttp) via server.NewHandler or Config.WrapHandler.
Custom backends can verify themselves against the storage contract with the
storagetest conformance suite:
func TestStorage(t *testing.T) {
storagetest.Run(t, func(t testing.TB) fs.Storage {
return mybackend.New(t.TempDir())
})
}Use server.NewHandler when you already run an http.Server or mux and just
want to expose the S3 API (optionally under a path prefix):
package main
import (
"net/http"
"github.com/go-faster/fs/engine"
"github.com/go-faster/fs/server"
)
func main() {
store, err := engine.Open("/data", engine.Options{})
if err != nil {
panic(err)
}
defer store.Close()
mux := http.NewServeMux()
mux.Handle("/s3/", http.StripPrefix("/s3", server.NewHandler(store)))
http.ListenAndServe(":8080", mux)
}Use server.New for a managed server with a health endpoint, request timeouts,
optional bucket pre-creation and graceful shutdown driven by a context:
package main
import (
"context"
"os/signal"
"syscall"
"github.com/go-faster/fs/server"
"github.com/go-faster/fs/storagemem"
)
func main() {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
srv, err := server.New(server.Config{
Storage: storagemem.New(),
Addr: ":9000",
Buckets: []string{"uploads"}, // pre-created if absent
})
if err != nil {
panic(err)
}
// Serves until ctx is canceled, then drains in-flight requests.
if err := srv.ListenAndServe(ctx); err != nil {
panic(err)
}
}| Field | Default | Description |
|---|---|---|
Storage |
— (required) | Backend serving S3 operations (fs.Storage). |
Addr |
:8080 |
TCP address to listen on. |
ReadTimeout / WriteTimeout / IdleTimeout |
30s / 30s / 120s |
Underlying http.Server timeouts. |
HealthPath |
/health |
Plaintext liveness endpoint; "-" disables it. |
ReadyPath / Ready |
/ready / — |
Readiness endpoint and its probe; a non-nil probe error returns 503. |
Buckets |
— | Buckets created (idempotently) before serving. |
Auth / CORS / TLS |
— | SigV4 auth store, per-bucket CORS, and hot-reloadable TLS. |
WrapHandler |
— | Wrap the handler with middleware/observability (e.g. otelhttp.NewHandler). |
See the server package reference
for the full API and runnable examples.
Delivered so far: full SDK wire compatibility, exact S3 semantics and metadata, SigV4 auth/authorization/TLS, canned ACLs, and durability & integrity operations.
Planned and in progress (see findings/ROADMAP.md for the authoritative, detailed list):
- Lifecycle noncurrent-version cleanup and the rest of lifecycle.
- Cluster mode — a Garage-style cluster with zone/rack-aware replication (#279).
- Virtual-host–style addressing, ACME / automatic TLS, and static website hosting.
- Geo-replication — async bucket-level replication between deployments.
- Bucket-policy subset — demand-gated, when canned ACLs are not enough.
# Run tests
go test ./...
# Build
go build ./cmd/fs
# Run with coverage
make coverageApache 2.0