Self-hosted S3-compatible storage from Delphi
Run your own S3 endpoint and point the AWS SDK for Delphi at it: MinIO, RustFS, and SeaweedFS, one program against all three, and where MinIO's archival leaves things.
Contents
The hosted providers in this series each run the storage for you. Sometimes you want to run it yourself: data that has to stay on your own hardware, an air-gapped network, or a dev environment that shouldn't reach out to anyone's cloud. For that you run an S3-compatible server, and the SDK points at it the same way it points at anything else, using the endpoint mechanism from Part 1.
For years the default answer here was MinIO, and it still runs, though its open-source footing has shifted. We'll stand up three servers, MinIO, RustFS, and SeaweedFS, and point the same Delphi program at each. S3 is a protocol, the SDK speaks it, and moving between servers is a configuration change rather than a rewrite.
Where MinIO stands
MinIO relicensed its server from Apache 2.0 to AGPLv3 in 2021. In 2025 the administrative features were removed from the Community Edition's web console and moved to the commercial product. In late 2025 the open-source repository went into maintenance mode, and in April 2026 it was archived: read-only, no new community releases, security fixes considered case by case.
In practice, existing MinIO deployments keep working, and the S3 API a Delphi app
talks to is unchanged. What changed is around it. There are no new community builds,
and you administer a community server from the command line with mc rather than
the web console. None of that alters how the SDK talks to it, though it does change the calculus if
you're choosing a self-hosted store today.
Standing up a server
Each server is a container that serves the S3 API on a port. Locally you'd run one at a time; in production each lives on its own host, behind TLS at a real hostname, which is the endpoint your application points at. The commands below get each one serving the S3 API so you have something to aim the SDK at.
MinIO serves the S3 API on port 9000:
docker run -p 9000:9000 -p 9001:9001 \
-e MINIO_ROOT_USER=minioadmin \
-e MINIO_ROOT_PASSWORD=minioadmin \
minio/minio server /data --console-address ":9001"
RustFS is a newer server written in Rust and licensed Apache 2.0. It also serves the S3 API on port 9000, with its own console on 9001:
docker run -p 9000:9000 -p 9001:9001 \
-e RUSTFS_ACCESS_KEY=rustfsadmin \
-e RUSTFS_SECRET_KEY=rustfsadmin \
-v rustfs-data:/data \
rustfs/rustfs:latest /data
The default rustfsadmin keys are public knowledge; set your own before the ports
go anywhere near a network. RustFS is worth knowing about, but it's pre-1.0 at the
time of writing and its own maintainers advise against production use until 1.0 is
stable. Treat it as one to evaluate, not yet one to depend on.
SeaweedFS is a mature, Apache-2.0 project whose S3 gateway serves the API on port 8333:
docker run -p 8333:8333 -p 9333:9333 \
-v seaweedfs-data:/data \
chrislusf/seaweedfs server -dir=/data -s3
Started this way the S3 gateway runs in an allow-all mode that suits a first look;
for anything real you give it a credentials file with -s3.config and it enforces
access keys like the others.
None of these needs its own admin tool to get a bucket for what follows. MinIO's community console has lost its admin features, RustFS ships a console and SeaweedFS has none, but you can skip all of that: the SDK creates the bucket itself, the same call against every one of them, as the worked example does below.
Pointing the SDK at each
Nothing here is new. Each server gets a profile in the shared config file with an
endpoint_url, exactly the shape Part 1 covered. Locally that's a localhost URL
on the server's port; in production it's the https hostname the server sits behind.
# ~/.aws/config
[profile minio]
region = us-east-1
s3 =
endpoint_url = http://localhost:9000
[profile rustfs]
region = us-east-1
s3 =
endpoint_url = http://localhost:9000
[profile seaweedfs]
region = us-east-1
s3 =
endpoint_url = http://localhost:8333
# ~/.aws/credentials
[minio]
aws_access_key_id = minioadmin
aws_secret_access_key = minioadmin
[rustfs]
aws_access_key_id = rustfsadmin
aws_secret_access_key = rustfsadmin
[seaweedfs]
aws_access_key_id = <your-access-key>
aws_secret_access_key = <your-secret-key>
Every one of these servers wants a region set even though none of them use it the way
AWS does. us-east-1 is a safe value; the SDK sends it in the signature and the
server accepts it. Addressing is path-style, which the SDK selects automatically once
EndpointUrl is set, so there's nothing to configure there.
One program, three servers
Here's the point of the whole exercise. This is the same upload-and-download program from the earlier posts. The only line that decides which server it talks to is the profile:
uses
AWS.S3;
var
Options: IS3Options;
Client: IS3Client;
Bucket: IS3Bucket;
Obj: IS3Object;
begin
Options := TS3Options.Create;
Options.Profile := 'minio';
Client := TS3Client.Create(Options);
Bucket := TS3Bucket.Create('reports', Client);
if not Bucket.Exists then
Client.CreateBucket('reports');
Obj := TS3Object.Create('reports', 'quarterly.pdf', Client);
Obj.UploadFile('C:\Reports\quarterly.pdf');
Obj.DownloadFile('C:\Downloads\quarterly.pdf');
end;
Bucket.Exists checks for the bucket before creating it, so the program is safe to
run more than once. It's a HeadBucket request under the hood, so
Client.HeadBucket('reports').IsSuccessful is the same check written out; catching
ES3BucketAlreadyExists from CreateBucket is the other way round. The upload and
download are the same two calls as ever.
Change 'minio' to 'rustfs' or 'seaweedfs' and the same program runs against a
different server. Not a line of the storage code changes, because there's nothing
server-specific in it: it creates a client, makes a call, and reads the response, the
shape every post in this series has used. The endpoint, region, and credentials all
come from the profile.
That's what "S3-compatible" buys you in practice. The server is an operational choice you can revisit later without touching the application.
Where they differ
The differences sit at the edges, not in the object operations:
- Region. All three ignore it but require it to be set.
us-east-1works everywhere. This is the same quirk Part 1 flagged. - Addressing. Path-style, handled automatically when
EndpointUrlis set. Not a setting you touch. - Administration. MinIO's community admin surface is the
mccommand line now, not the web console. RustFS ships a console. SeaweedFS is configured through its own flags and files. None of this touches the S3 API the SDK uses. - Feature coverage. The object basics (put, get, list, copy, delete, multipart) are solid across all three. Past that, versioning, lifecycle rules, tagging and object lock are implemented to differing degrees, and some servers implement only older versions of otherwise-common operations. Check the specific feature against the specific server before you lean on it.
- Maturity. SeaweedFS and MinIO are production-proven; RustFS is not there yet.
What to run today
If you're choosing a self-hosted S3 server now rather than inheriting one, a few are worth knowing. This is a starting map, not a survey, and the right answer depends on the workload you have:
- SeaweedFS. Mature, Apache-2.0, production-proven. More moving parts than a single binary (master, volume, and filer roles), but a well-worn S3 gateway sits in front of them.
- RustFS. Apache-2.0, written in Rust, fast, and permissively licensed with no AGPL question to answer. Pre-1.0 at the time of writing, so one to evaluate rather than deploy.
- Garage. Apache-2.0 and deliberately small, aimed at geo-distributed self-hosting across modest nodes. A good fit when simplicity and multi-site replication matter more than a large feature surface.
- Ceph. The heavyweight. Its RADOS Gateway speaks S3 on top of a full object/block/file storage system. Reach for it when you're already running serious storage infrastructure, not for a single bucket.
Whichever you pick, the AWS SDK for Delphi and your code stay the same. The endpoint mechanism from Part 1 points at all of them, and the program above runs against any of them by name. The decision is an operational one about who runs your storage and under what licence, not a question your application code has to answer.
What's next
Next in the series: Backblaze B2 from Delphi. Back to a hosted provider, with its S3-compatible endpoint, its application keys, and the couple of things worth watching for. The same shape as always: set the endpoint, point the client, and the storage code you already have keeps working.
More posts
Build ReadWhatYouSee: Textract, Translate, and Polly together
The series finale: a Delphi app that reads text from an image with Textract, translates it with Translate, and speaks it aloud with Polly.
Read more →
Cloudflare R2 from Delphi
Point the AWS SDK for Delphi at Cloudflare R2: bucket setup, access keys, region and endpoint config, and a worked example end to end.
Read more →
Sending email with SES from Delphi
A small Delphi console program that sends transactional email through Amazon SES v2, covering verified identities and the move out of the SES sandbox.
Read more →