If you've ever hit the point where an app needs an S3 bucket — file uploads, backups, an ML
dataset dump, whatever — and you didn't want to open a whole AWS account for it, MinIO is
probably the answer you've already heard of. It's open-source, speaks the real S3 API (boto3,
aws-sdk-js, the aws s3 CLI, mc — all of it works unmodified), and you own the disk instead of
paying per-GB egress forever.
The annoying part is always the same: getting it actually running somewhere with a persistent
disk, HTTPS, and credentials that aren't minioadmin/minioadmin on a public URL. I run mine on
Railway and wrote up the two things that will bite you if you deploy the raw image yourself.
Gotcha 1 — the start command needs a shell. Railway execs your start command as argv, not
through /bin/sh. So if you write:
minio server /data --address ":${PORT:-9000}" --console-address ":9001"
that ${PORT:-9000} never gets expanded — it reaches MinIO as the literal seven characters, and
you get Unable to split host port :${PORT:-9000}: too many colons in address. Wrap it:
/bin/sh -c 'minio server /data --address ":${PORT:-9000}" --console-address ":9001"'
Gotcha 2 — Railway's edge proxy routes by $PORT, not by your domain's target port. I set the
domain's target port to 9000, deploy went SUCCESS, healthcheck passed internally — and the public
URL 502'd for a few minutes anyway. Setting PORT=9000 as an actual env var (so both the
container and the edge agree) fixed it on the next request. The container looking healthy from
inside tells you nothing about whether the edge can reach it.
Everything else is normal MinIO: mount a volume at /data or you lose every bucket on redeploy,
and the web console lives on 9001 separately from the S3 API if you want it exposed.
# pip install minio
from minio import Minio
import io
c = Minio("your-domain.up.railway.app", access_key="...", secret_key="...", secure=True)
c.make_bucket("test-bucket")
c.put_object("test-bucket", "hello.txt", io.BytesIO(b"hello"), length=5)
print(c.get_object("test-bucket", "hello.txt").read().decode())
If you want to do all this by hand: the official minio/minio image on Docker Hub, plus the
env vars above, is the whole recipe — nothing here is Railway-exclusive.
Full disclosure since this is my own template: I maintain a one-click Railway deploy for this
(persistent volume already wired, root credentials auto-generated per deploy instead of the
minioadmin default, PORT and the shell-wrapped start command already fixed) and I get a
kickback if you deploy through it:
Worth knowing before you commit to it either way: MinIO's web console is a separate port (9001)
from the S3 API, so if you want the browser UI you need to expose a second domain pointed at it —
the template doesn't do that for you automatically, you add it after deploy. And it's a real
always-on process (not serverless) — RAM scales roughly with your object metadata, so it's not
free to leave idle at scale, just cheap.
Top comments (0)