How to Run Your Own S3 Storage with MinIO (Buckets for Backups, Apps and Static Files)

Half the software world now assumes an "S3 bucket" is where files go: backup tools, Immich and Nextcloud's external storage, static-site hosts, CI systems, your own apps. MinIO is an S3-compatible server in one container. Point those tools at https://s3.example.com with a key pair you made, and they behave exactly as they would with Amazon — on your disk, with no bill. It's also a good place to learn the S3 model before you ever pay for it.
Quick reference
| Task | What |
|---|---|
| Folder | ~/docker/minio/compose.yaml |
| API | port 9000 (s3.example.com); console port 9001 |
| First login | MINIO_ROOT_USER / MINIO_ROOT_PASSWORD |
| Bucket + key | Console → Buckets → Create; Access Keys → Create |
| Command line | mc alias set home https://s3.example.com KEY SECRET |
| Clients | restic s3:, rclone s3 provider, Nextcloud external storage, Immich S3 |
1. The compose file
mkdir -p ~/docker/minio && cd ~/docker/minio
nano compose.yaml
services:
minio:
image: minio/minio:latest
container_name: minio
command: server /data --console-address ":9001"
ports:
- "127.0.0.1:9000:9000"
- "127.0.0.1:9001:9001"
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
MINIO_BROWSER_REDIRECT_URL: https://minio.example.com
volumes:
- /mnt/data/minio:/data
restart: unless-stopped
.env with a root user and a long password (not minioadmin). Data on the big drive.
docker compose up -d
2. Reverse proxy
Two names (tutorial 28): s3.example.com → 127.0.0.1:9000 (the API, with client_max_body_size 0 and proxy_request_buffering off) and minio.example.com → 127.0.0.1:9001 (the console, with WebSocket lines). HTTPS on both. Internal-only is fine over the wildcard certificate (147).
3. Buckets and keys
Open https://minio.example.com, log in as root. Buckets → Create Bucket backups, then photos, site. Access Keys → Create access key: it gives an access key and secret — one pair per client, with a policy restricting it to its bucket:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["s3:*"],
"Resource": ["arn:aws:s3:::backups", "arn:aws:s3:::backups/*"] } ] }
Least privilege (120): restic's key can't see the photos bucket.
4. The mc client
curl -LO https://dl.min.io/client/mc/release/linux-amd64/mc && chmod +x mc && sudo mv mc /usr/local/bin/
mc alias set home https://s3.example.com ACCESSKEY SECRETKEY
mc ls home
mc cp /mnt/backup/etc-2026-11-05.tar.gz home/backups/
mc mirror /var/www/mysite home/site
mc is cp, ls, mirror, rm for buckets, plus admin commands.
5. Point things at it
- restic (55): RESTIC_REPOSITORY=s3:https://s3.example.com/backups/laptop with AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY. The server as a backup target, S3-style (an alternative to 116).
- rclone (54): rclone config → provider Minio → endpoint, keys. Then rclone sync to or from it; or a crypt remote on top.
- Nextcloud: External storage → Amazon S3 → hostname s3.example.com, port 443, SSL, bucket, keys. Or as the primary object store.
- Immich: supports S3 for uploads via its storage settings (check the current docs).
- A static site: mc anonymous set download home/site makes the bucket public-read; put it behind the proxy as www.
- Your own app (102): any S3 SDK with the endpoint set.
6. Lifecycle and versioning
Bucket → Lifecycle → Add rule: expire objects older than 90 days in backups, or move to a slower tier. Versioning on site keeps every overwritten version — undo for uploads. Object locking (set at bucket creation) makes a bucket WORM: append-only backups (116) in S3 form.
When plain folders are better
A Samba share or a bind mount is simpler for media the household browses, Docker volumes, and anything that doesn't speak S3. MinIO is for tools that do — backups, app storage, static hosting, S3 practice — and for giving each client its own key and bucket rather than a filesystem path.
Troubleshooting
"SignatureDoesNotMatch". Clock skew (timedatectl), or the proxy altered the Host header — proxy_set_header Host $host; must be present.
Uploads over 5 GB fail. The proxy's body size limit; multipart uploads need it unlimited.
Console works, API doesn't. s3.example.com not proxied to 9000, or the client is using path-style vs virtual-host style — MinIO accepts path-style by default (https://s3.example.com/bucket).
MinIO warns about a single drive. Correct — no erasure coding on one disk. It's a store, not redundancy; the drive underneath still needs the mirror/parity tutorials (97–99) and backups.
Wrapping up
One container, two proxied names, a bucket and a scoped key per client, mc for the command line. restic, rclone, Nextcloud, your site and your own code all talk to "S3" that lives on your disk — with lifecycle rules, versioning and object locking when you want them.
