Self-hosted, S3-compatible object storage for local development and personal projects, with a management dashboard.
It implements the subset of the Amazon S3 API that applications actually use — buckets, objects, listings, copies, presigned URLs — and verifies AWS Signature Version 4, so the AWS CLI and every AWS SDK talk to it unchanged.
git clone https://github.com/SebasRaul2503/local-s3
cd local-s3
docker compose up| Service | URL | Purpose |
|---|---|---|
| S3 API | http://localhost:9000 | Point your application here |
| Dashboard | http://localhost:3000 | Browse buckets, objects, keys and logs |
| Health | http://localhost:9000/_health | Liveness for orchestrators |
| Metrics | http://localhost:9000/_metrics | Prometheus exposition format |
The dashboard signs in with admin / admin, and the first start creates the
credentials local-s3-root / local-s3-secret plus an uploads bucket.
Change all of them in docker-compose.yml before using this anywhere but your
own machine.
Enable path-style addressing; local-s3 does not do virtual-host addressing.
S3_ENDPOINT=http://localhost:9000
S3_REGION=us-east-1
S3_ACCESS_KEY=local-s3-root
S3_SECRET_KEY=local-s3-secret
S3_FORCE_PATH_STYLE=trueThe dashboard generates this block for any access key on its Secrets page.
AWS CLI
export AWS_ACCESS_KEY_ID=local-s3-root
export AWS_SECRET_ACCESS_KEY=local-s3-secret
export AWS_DEFAULT_REGION=us-east-1
aws --endpoint-url http://localhost:9000 s3 mb s3://avatars
aws --endpoint-url http://localhost:9000 s3 cp photo.png s3://avatars/users/15/avatar.png
aws --endpoint-url http://localhost:9000 s3 ls s3://avatars --recursive
aws --endpoint-url http://localhost:9000 s3 presign s3://avatars/users/15/avatar.png --expires-in 300JavaScript (AWS SDK v3)
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
const s3 = new S3Client({
endpoint: process.env.S3_ENDPOINT,
region: process.env.S3_REGION,
forcePathStyle: true,
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY!,
secretAccessKey: process.env.S3_SECRET_KEY!,
},
});
await s3.send(
new PutObjectCommand({ Bucket: 'avatars', Key: 'hello.txt', Body: 'hello' }),
);Python (boto3)
import boto3
s3 = boto3.client(
"s3",
endpoint_url="http://localhost:9000",
aws_access_key_id="local-s3-root",
aws_secret_access_key="local-s3-secret",
region_name="us-east-1",
)
s3.upload_file("photo.png", "avatars", "users/15/avatar.png")Go (AWS SDK v2)
cfg, err := config.LoadDefaultConfig(ctx,
config.WithRegion("us-east-1"),
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(
"local-s3-root", "local-s3-secret", "")),
)
client := s3.NewFromConfig(cfg, func(options *s3.Options) {
options.BaseEndpoint = aws.String("http://localhost:9000")
options.UsePathStyle = true
})Buckets — create, delete, list, simulated regions, public read access, storage quotas.
Objects — PUT, GET, HEAD, DELETE, COPY, move and rename, virtual
folders through prefixes, ranged reads, conditional requests, multi-object
delete, ListObjects and ListObjectsV2 with pagination.
Metadata — content type, cache control, content disposition, encoding and
language, ETag, SHA-256, timestamps and arbitrary x-amz-meta-* entries, all
stored in a sidecar file next to the payload.
Authentication — AWS Signature Version 4 for header-signed requests and
presigned URLs, including aws-chunked streaming uploads with per-chunk
signature verification. Multiple access keys, each with permissions
(read, write, delete, list, admin) and an optional bucket allow-list.
Operations — per-key, per-bucket and per-address rate limiting, request history with filtering, metrics, health and readiness endpoints.
Dashboard — overview, bucket and object management with drag and drop upload, image/PDF/Markdown/JSON/text previews, access keys, copy-ready credentials, a signed URL generator, live logs, metrics, system status and global search. English and Spanish, light and dark.
Versioning, lifecycle rules, replication, object locking, server-side
encryption, ACLs beyond public-read, bucket policies, tagging, multipart
uploads and event notifications. Requests for those return
NotImplemented rather than failing in a confusing way.
Everything lives under one directory (a Docker volume by default), so your data is inspectable with ordinary tools:
storage/
├── buckets/
│ └── avatars/
│ └── users/15/
│ ├── avatar.png
│ └── avatar.png.meta.json
├── system/
│ ├── buckets.json
│ ├── access-keys.json
│ ├── signed-urls.json
│ └── session-secret
├── logs/requests.jsonl
└── tmp/
Files copied into storage/buckets/<bucket>/ by hand are adopted on first
read: their metadata sidecar is rebuilt from the payload.
Secret keys are stored in clear text because verifying a signature requires them. Keep this service on your own machine.
- Using it in your apps (en español) — connecting a real application, recipes per stack, and the traps worth knowing
- Architecture — how the code is organised and why
- Development guide — running and testing without Docker
- Environment variables — every setting and its default
- API reference — the S3 and dashboard endpoints
- Contributing — workflow and standards
Estantería is a team file library built against this service: a NestJS and Angular monorepo that puts storage behind one interface with two adapters, and implements both upload routes — through the API, and direct with a presigned URL. It is the closest thing here to a worked example of connecting a real application.
MIT — see LICENSE.


