A minimal S3-compatible object storage server written in Go that persists files to the local filesystem. Designed for development and self-hosted deployments where a full S3 service is overkill.
- S3-compatible API (AWS Signature V4 authentication)
- Local filesystem storage
- Public file access (optional prefix-based)
- Download mode with
?download=1query parameter - Configurable cache headers for public files
- Single binary, no dependencies
- Multi-platform Docker images (amd64, arm64)
selfhost_s3 implements the minimum S3 API required by the Notifuse file manager:
| Operation | Description |
|---|---|
GetObject |
Download/serve files (used for file URLs) |
ListObjectsV2 |
List all objects in the bucket |
PutObject |
Upload files and create folders |
DeleteObject |
Delete files and folders |
HeadObject |
Check if file exists (optional, but recommended) |
Pull and run directly from Docker Hub:
docker run -d \
--name selfhost_s3 \
-p 9000:9000 \
-v $(pwd)/data:/data \
-e S3_BUCKET=my-bucket \
-e S3_ACCESS_KEY=myaccesskey \
-e S3_SECRET_KEY=mysecretkey \
notifuse/selfhost_s3:latestCreate a compose.yaml:
services:
selfhost_s3:
image: notifuse/selfhost_s3:latest
container_name: selfhost_s3
restart: unless-stopped
ports:
- "9000:9000"
volumes:
- ./s3-data:/data
environment:
S3_BUCKET: my-bucket
S3_ACCESS_KEY: myaccesskey
S3_SECRET_KEY: mysecretkeyThen run:
docker compose up -d# Clone the repository
git clone https://github.com/Notifuse/selfhost_s3.git
cd selfhost_s3
# Build
go build -o selfhost_s3 ./cmd/selfhost_s3
# Run
export S3_BUCKET=my-bucket
export S3_ACCESS_KEY=myaccesskey
export S3_SECRET_KEY=mysecretkey
./selfhost_s3selfhost_s3 is configured via environment variables:
| Variable | Required | Default | Description |
|---|---|---|---|
S3_BUCKET |
Yes | - | S3 bucket name |
S3_ACCESS_KEY |
Yes | - | Access key for authentication |
S3_SECRET_KEY |
Yes | - | Secret key for authentication |
S3_PORT |
No | 9000 |
Port to listen on |
S3_STORAGE_PATH |
No | ./data |
Local directory for file storage |
S3_REGION |
No | us-east-1 |
AWS region (for signature validation) |
S3_CORS_ORIGINS |
No | * |
Allowed CORS origins (comma-separated) |
S3_MAX_FILE_SIZE |
No | 100MB |
Maximum upload file size |
S3_PUBLIC_PREFIX |
No | public/ |
Prefix for public files (empty string disables) |
S3_PUBLIC_CACHE_MAX_AGE |
No | 31536000 |
Cache-Control max-age for public files (seconds) |
Official images are available at hub.docker.com/r/notifuse/selfhost_s3
Available tags:
latest- Latest stable releasevX.Y- Specific version (e.g.,v1.0)
Supported platforms:
linux/amd64linux/arm64
In your workspace settings, configure the File Manager with:
- Endpoint:
http://localhost:9000(or your selfhost_s3 URL) - Bucket: Your
S3_BUCKETvalue - Access Key: Your
S3_ACCESS_KEYvalue - Secret Key: Your
S3_SECRET_KEYvalue - Region:
us-east-1(default) - Path-style: Enabled (required)
Files are stored on the local filesystem mirroring the S3 key structure:
data/
└── my-bucket/
├── documents/
│ └── report.pdf
└── images/
└── logo.png
- Files: Stored at
{storage_path}/{bucket}/{key} - Folders: Represented as empty files with keys ending in
/
selfhost_s3 supports serving files publicly without authentication. By default, files under the public/ prefix are accessible via GET and HEAD requests without AWS Signature V4 authentication.
- The
public/directory is automatically created on server startup - GET and HEAD requests to paths starting with the public prefix skip authentication
- PUT and DELETE operations still require authentication (only uploads/deletes are protected)
- Public files include
Cache-Controlheaders for CDN/browser caching
Files in the public prefix can be accessed directly via browser:
http://localhost:9000/my-bucket/public/images/logo.png
http://localhost:9000/my-bucket/public/documents/report.pdf
Add ?download=1 to force the browser to download the file instead of displaying it:
http://localhost:9000/my-bucket/public/report.pdf?download=1
This sets the Content-Disposition: attachment header with the filename.
Custom public prefix:
S3_PUBLIC_PREFIX=assets/ # Files under assets/ are publicDisable public access entirely:
S3_PUBLIC_PREFIX= # Empty string disables public accessCustom cache duration (1 hour):
S3_PUBLIC_CACHE_MAX_AGE=3600Disable caching:
S3_PUBLIC_CACHE_MAX_AGE=0When exposing files publicly:
- Use a reverse proxy (nginx, Caddy, Traefik) in front of selfhost_s3 for SSL/TLS
- Restrict CORS origins in production:
S3_CORS_ORIGINS=https://yourdomain.com - Consider a CDN for frequently accessed public files
- Validate uploads in your application before storing files in the public prefix
Since browser clients connect directly to selfhost_s3, CORS headers are automatically included:
Access-Control-Allow-Origin: <from S3_CORS_ORIGINS>
Access-Control-Allow-Methods: GET, HEAD, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, x-amz-*
Access-Control-Expose-Headers: ETag, Content-Length, Content-Type
For production, set S3_CORS_ORIGINS to your specific domain(s):
S3_CORS_ORIGINS=https://app.example.com,https://admin.example.comselfhost_s3 exposes a health endpoint for container orchestration:
GET /health
Returns 200 OK with {"status": "ok"} when the server is running.
# Via AWS CLI
aws s3 cp s3://my-bucket/path/myfile.txt ./myfile.txt \
--endpoint-url http://localhost:9000
# Direct URL (for browser/images)
curl http://localhost:9000/my-bucket/path/image.pngaws s3api list-objects-v2 \
--endpoint-url http://localhost:9000 \
--bucket my-bucketaws s3 cp myfile.txt s3://my-bucket/path/myfile.txt \
--endpoint-url http://localhost:9000aws s3 rm s3://my-bucket/path/myfile.txt \
--endpoint-url http://localhost:9000- No multipart uploads: Files are uploaded in a single request
- No presigned URLs: Direct authentication required
- No versioning: Files are overwritten in place
- No bucket operations: Bucket must be pre-configured via env var
- Single bucket: One selfhost_s3 instance = one bucket
# Run tests
go test ./...
# Run with hot reload
go run ./cmd/selfhost_s3- Standard library only -
net/httpis sufficient, no web framework needed - AWS Signature V4 - Validates signatures with proper URI encoding for special characters
- File locking - Uses
sync.RWMutexfor concurrent read/write safety - Content-Type - Guessed from file extension using Go's
mimepackage - ETag - Generated from file modification time and size
- Path traversal - Keys are sanitized to prevent
../attacks
MIT