20 days ago
“How do I make my S3 bucket public for static/media file hosting?”
2 Replies
Status changed to Open Railway • 20 days ago
20 days ago
Is this a Minio Instance?
20 days ago
You can't. Railway's managed buckets are private-only by design, there's no "make public" toggle, no public bucket URL, no public-read ACL, and no custom domain you can attach to a bucket. Quoting their docs: "Buckets are private, but you can still work with their files in a few ways."
The two supported patterns
PatternWhen to useCost notePresigned URLs_(recommended)_Profile pics, user uploads, anything that's "public-ish" but you can hand out a per-request URL. Up to 90-day expiry. Zero service egress the file streams directly from the bucket to the browser.Backend proxyWhen you need a stable URL (e.g., /media/foo.jpg) embedded in HTML/email, or want CDN caching, or need auth gates.Costs service egress (counted against your service's bandwidth), but you can put Cloudflare/Bunny in front of your service to amortize that.
How that translates to typical use cases
- Public website img/href tags → backend proxy route + a CDN in front. Anything you put in raw HTML needs a stable URL, and presigned URLs aren't stable (they expire).
- App-rendered images (React/Vue/etc.) → presigned URLs generated on page load. Cheapest path.
- One-off downloads / "share this file" → presigned URL with a 1-hour expiry. Classic.
- Email-embedded images → backend proxy (presigned URLs may expire before the email is opened).
Generating a presigned URL with Railway's S3 creds
Pull credentials with railway bucket credentials --bucket <name> --json, then any standard S3 SDK works against the bucket's endpoint. Example with aws-sdk JS v3:
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const s3 = new S3Client({
endpoint: process.env.BUCKET_ENDPOINT,
region: "us-east-1", // any value; Railway ignores it
credentials: {
accessKeyId: process.env.BUCKET_ACCESS_KEY_ID,
secretAccessKey: process.env.BUCKET_SECRET_ACCESS_KEY,
},
forcePathStyle: true, // important for non-AWS S3
});
const url = await getSignedUrl(
s3,
new GetObjectCommand({ Bucket: "linko-photos", Key: "uploads/photo.jpg" }),
{ expiresIn: 3600 } // seconds; max 90 days
);
If you really need a public URL on a stable hostname
Their cleanest path on Railway is a tiny proxy service (~30 lines of Node/Hono/Express): receives GET /media/:key, streams the object through, sets Cache-Control: public, max-age=31536000, immutable, optionally puts Cloudflare in front of the service domain. That gets them effectively-public URLs and CDN caching, at the cost of routing the first uncached request through their service.