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.

Self-hosted S3-compatible storage from Delphi
Contents
  1. Where MinIO stands
  2. Standing up a server
  3. Pointing the SDK at each
  4. One program, three servers
  5. Where they differ
  6. What to run today
  7. What's next

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-1 works everywhere. This is the same quirk Part 1 flagged.
  • Addressing. Path-style, handled automatically when EndpointUrl is set. Not a setting you touch.
  • Administration. MinIO's community admin surface is the mc command 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.