RustFS is an S3-compatible object storage server written in Rust and released under the Apache-2.0 license. In this guide you will run RustFS in Docker and Docker Compose, persist data in a named volume, add a healthcheck, open the web console, talk to it with aws-cli, connect a Python app with boto3, and back the buckets up with rclone.
If you used MinIO for this before: the MinIO community edition is no longer maintained. Its GitHub repositories are archived, no community images or binaries have been published since October 2025, and dl.min.io now returns 410 Gone. How to Run MinIO Object Storage in Docker and Docker Compose has the details, and a maintained fork for existing MinIO deployments. For a new setup, RustFS is the simpler choice: one container, a permissive license, and the same S3 API your code already speaks.
Project status (as of November 2026): RustFS reached 1.0.0 on 2026-09-16 after a long beta and release-candidate series. This guide uses
1.0.4, the latest stable release on GitHub and Docker Hub. It is still a young project, so read the release notes before you bump the tag.
Prerequisites
You need Docker with the Compose plugin. See How to Install and Use Docker Engine on Ubuntu 26.04 LTS if you need to install it.
docker compose version
Pin the image tag
Pull a specific release rather than latest:
docker pull rustfs/rustfs:1.0.4
The rustfs/rustfs repository publishes latest, release tags such as 1.0.4, -glibc variants, and -preview.N pre-release builds. latest moves every time a release is cut, so a docker compose pull on a quiet Sunday can upgrade your storage server without you deciding to. Pin the tag and change it on purpose.
Run RustFS with Docker
Pick credentials first. These are placeholders, change both to long random values:
export RUSTFS_ACCESS_KEY="admin"
export RUSTFS_SECRET_KEY="CHANGE_ME_TO_A_LONG_RANDOM_SECRET"
Do not use rustfsadmin. It is the built-in default the server falls back to when no credentials are set, and the container logs a warning if you use it.
Start a single-node server:
docker run -d --restart unless-stopped \
--name rustfs \
-p 127.0.0.1:9000:9000 \
-p 127.0.0.1:9001:9001 \
-v rustfs-data:/data \
-e "RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY}" \
-e "RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY}" \
rustfs/rustfs:1.0.4
What the options do:
-p 127.0.0.1:9000:9000publishes the S3 API on localhost only, and9001is the web console. Drop the127.0.0.1:prefix only when you mean to expose the server on your network.-v rustfs-data:/datastores objects in a named Docker volume, so they survive container restarts and upgrades.RUSTFS_ACCESS_KEYandRUSTFS_SECRET_KEYare the root credentials for the S3 API and the console.
The image already sets RUSTFS_VOLUMES=/data, so no command or data path argument is needed.
Bind mounts and the container user
The container runs as a non-root user:
docker run --rm --entrypoint id rustfs/rustfs:1.0.4
uid=10001(rustfs) gid=10001(rustfs) groups=10001(rustfs)
A named volume handles ownership for you. If you bind-mount a host directory instead, the RustFS Docker docs say to make it owned by UID 10001, otherwise the server fails with permission denied errors:
sudo mkdir -p /srv/rustfs/data
sudo chown -R 10001:10001 /srv/rustfs/data
Then use -v /srv/rustfs/data:/data in place of the named volume.
Ports and endpoints
- S3 API:
http://127.0.0.1:9000 - Web console:
http://127.0.0.1:9001/rustfs/console/
Check that the server is up:
curl -fsS http://127.0.0.1:9000/health
{"service":"rustfs-endpoint","status":"ok","timestamp":"2026-11-03T12:13:51.76735791Z[Etc/Unknown]","version":"1.0.4"}
/health/live and /health/ready also return 200, and so do the MinIO-style /minio/health/live and /minio/health/ready paths, so existing load balancer checks keep working.
Where the logs go
The image sets RUSTFS_OBS_LOG_DIRECTORY=/logs, so docker logs rustfs only shows the entrypoint’s startup lines. The server log is a file inside the container:
docker exec rustfs tail /logs/rustfs.log
To send the logs to stdout, where docker logs and your log shipper can see them, set the variable to an empty value with -e RUSTFS_OBS_LOG_DIRECTORY=.
Run RustFS with Docker Compose
Stop the container from the previous section first, since it holds ports 9000 and 9001:
docker rm -f rustfs
Create a working directory:
mkdir -p ~/rustfs
cd ~/rustfs
Keep the credentials out of the Compose file. Create a .env file next to it, change the values, and do not commit it:
cat > .env <<'EOF'
RUSTFS_ACCESS_KEY=admin
RUSTFS_SECRET_KEY=CHANGE_ME_TO_A_LONG_RANDOM_SECRET
EOF
chmod 600 .env
Create compose.yaml:
services:
rustfs:
image: rustfs/rustfs:1.0.4
restart: unless-stopped
environment:
RUSTFS_ACCESS_KEY: "${RUSTFS_ACCESS_KEY:?set RUSTFS_ACCESS_KEY in .env}"
RUSTFS_SECRET_KEY: "${RUSTFS_SECRET_KEY:?set RUSTFS_SECRET_KEY in .env}"
RUSTFS_ADDRESS: ":9000"
RUSTFS_CONSOLE_ENABLE: "true"
RUSTFS_CONSOLE_ADDRESS: ":9001"
RUSTFS_VOLUMES: /data
RUSTFS_OBS_LOG_DIRECTORY: ""
ports:
- "127.0.0.1:9000:9000"
- "127.0.0.1:9001:9001"
volumes:
- rustfs-data:/data
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:9000/health"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
volumes:
rustfs-data:
A few notes on this file:
- The
:?syntax makes Compose refuse to start if a credential is missing, instead of silently falling back to the default. RUSTFS_ADDRESS,RUSTFS_CONSOLE_ADDRESSandRUSTFS_VOLUMESrepeat the defaults. They are spelled out so the file documents what the container listens on.- The healthcheck uses
curl, which ships in the image. Other services can wait for storage withdepends_on: { rustfs: { condition: service_healthy } }.
Start it and check the status:
docker compose up -d
docker compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
rustfs-rustfs-1 rustfs/rustfs:1.0.4 "/entrypoint.sh rust…" rustfs 15 seconds ago Up 15 seconds (healthy) 127.0.0.1:9000->9000/tcp, 127.0.0.1:9001->9001/tcp
Wait for (healthy) before you point clients at it.
docker compose down -vdeletes therustfs-datavolume and every object in it. Use plaindocker compose downto stop the stack.
Open the web console
Open http://127.0.0.1:9001/rustfs/console/ in your browser and log in with the access key and secret key from .env. The bare http://127.0.0.1:9001/ returns an AccessDenied XML error, so include the /rustfs/console/ path.
The console is on by default in 1.0.4. Port 9001 listens even without RUSTFS_CONSOLE_ENABLE. Set RUSTFS_CONSOLE_ENABLE=false to turn it off on a server that only needs the S3 API.
Use the AWS CLI with RustFS
RustFS speaks the S3 API, so you do not need a vendor-specific client. The official aws-cli image works without installing anything. Load the credentials from .env into your shell, then wrap the container in a shell function:
set -a; . ./.env; set +a
aws() {
docker run --rm --network host \
-v "$PWD:/work" -w /work \
-e AWS_ACCESS_KEY_ID="$RUSTFS_ACCESS_KEY" \
-e AWS_SECRET_ACCESS_KEY="$RUSTFS_SECRET_KEY" \
-e AWS_DEFAULT_REGION=us-east-1 \
amazon/aws-cli --endpoint-url http://127.0.0.1:9000 "$@"
}
--network host lets the CLI container reach the server on the host’s 127.0.0.1:9000. That works on Linux. On Docker Desktop (macOS or Windows), drop --network host and use --endpoint-url http://host.docker.internal:9000 instead. If you already have aws installed, skip the function and pass --endpoint-url http://127.0.0.1:9000 to it directly.
Create a bucket, upload a file and list it:
echo hello > hello.txt
aws s3 mb s3://my-bucket
aws s3 cp hello.txt s3://my-bucket/hello.txt
aws s3 ls s3://my-bucket/
make_bucket: my-bucket
upload: ./hello.txt to s3://my-bucket/hello.txt
2026-11-03 12:14:55 6 hello.txt
For more aws s3 commands, see Using AWS S3 from the terminal with awscli.
Connect an application
Any S3 SDK can talk to RustFS. Three settings matter, and they are the same in every language:
- Endpoint URL: point the client at
http://127.0.0.1:9000(orhttp://rustfs:9000from another container on the same Compose network) instead of AWS. - Path-style addressing: without extra configuration, RustFS serves path-style requests only (
http://host:9000/bucket/key). The server’s own help text says so: virtual-hosted-style requests (bucket.host) needRUSTFS_SERVER_DOMAINSset and matching DNS. Force path style in the client. - A region: RustFS has no regions and accepted every region name I tried, but SigV4 includes a region in every signature, and not every SDK or tool falls back to a default the way boto3 does. Set
us-east-1explicitly.
Keep these in environment variables so the same code runs against RustFS locally and real S3 in production:
S3_ENDPOINT_URL=http://127.0.0.1:9000
S3_BUCKET=app-uploads
S3_ACCESS_KEY_ID=admin
S3_SECRET_ACCESS_KEY=CHANGE_ME_TO_A_LONG_RANDOM_SECRET
S3_REGION=us-east-1
A Python example with boto3. It creates the bucket on first use, uploads an object and reads it back:
import boto3
from botocore.config import Config
from botocore.exceptions import ClientError
s3 = boto3.client(
"s3",
endpoint_url="http://127.0.0.1:9000",
aws_access_key_id="admin",
aws_secret_access_key="CHANGE_ME_TO_A_LONG_RANDOM_SECRET",
region_name="us-east-1",
config=Config(s3={"addressing_style": "path"}),
)
bucket = "app-uploads"
try:
s3.head_bucket(Bucket=bucket)
except ClientError:
s3.create_bucket(Bucket=bucket)
s3.put_object(Bucket=bucket, Key="hello.txt", Body=b"hello from boto3\n")
print(s3.get_object(Bucket=bucket, Key="hello.txt")["Body"].read().decode(), end="")
hello from boto3
In a real app, read those values from the environment instead of hard-coding them. Using AWS S3 Programatically with Python using Boto3 SDK covers the rest of the boto3 S3 API, and all of it works against RustFS.
Back up your buckets with rclone
The rustfs-data volume holds data that no database dump includes, so it needs its own backup. rclone can copy buckets to a local directory or another provider, and you can define the remote entirely in environment variables, with no rclone config and no config file on disk:
export RCLONE_CONFIG_RUSTFS_TYPE=s3
export RCLONE_CONFIG_RUSTFS_PROVIDER=Other
export RCLONE_CONFIG_RUSTFS_ENDPOINT=http://127.0.0.1:9000
export RCLONE_CONFIG_RUSTFS_ACCESS_KEY_ID="$RUSTFS_ACCESS_KEY"
export RCLONE_CONFIG_RUSTFS_SECRET_ACCESS_KEY="$RUSTFS_SECRET_KEY"
export RCLONE_CONFIG_RUSTFS_REGION=us-east-1
export RCLONE_CONFIG_RUSTFS_FORCE_PATH_STYLE=true
The RCLONE_CONFIG_RUSTFS_* prefix defines a remote named rustfs. PROVIDER=Other is rclone’s generic S3 setting, and the dummy region and forced path style are the same settings as in the app section.
List the buckets, then copy one to a local directory:
rclone lsd rustfs:
rclone copy rustfs:my-bucket ./backup/my-bucket
-1 2026-11-03 15:15:12 -1 app-uploads
-1 2026-11-03 15:14:54 -1 my-bucket
Use rclone copy, not rclone sync, for backups. sync makes the destination match the source, deletions included, so an object deleted by mistake in RustFS disappears from the backup on the next run. copy only adds and updates.
Conclusion
You now have RustFS running in Docker and Docker Compose on a pinned tag, with a named volume, a healthcheck and the web console. You also tested it with aws-cli, connected a boto3 app using path-style addressing and a placeholder region, and backed up a bucket with an environment-defined rclone remote. Before you rely on it, change the placeholder credentials, keep ports bound to 127.0.0.1 unless clients on other hosts need them, and schedule the rclone copy.
Related content
- How to Run MinIO Object Storage in Docker and Docker Compose
- How to Install and Use Docker Engine on Ubuntu 26.04 LTS
- Getting Started With Docker Compose: A Guide With Examples
- Using AWS S3 from the terminal with awscli
- Using AWS S3 Programatically with Python using Boto3 SDK
- How to Backup and Restore K3s: Datastore, Token and Cron