Skip to content

Public files don't render in Docker Compose, and 'private' isn't enforced by the bucket in Aspire #1410

Closed

Description

@iammukeshm

Follow-up from #1402. Uploads now work in the Docker Compose stack, but files uploaded as Public (for example the admin avatar) still don't render there. BuildPublicUrl returns PublicBaseUrl, or else ServiceUrl (http://rustfs:9000/fsh/...). Browsers can't resolve that host, and the compose bucket has no anonymous-read policy anyway.

Why this isn't a one-line fix

Visibility is not encoded in the storage key. StorageKeyBuilder puts every file under the same layout, so there's no way to grant anonymous read to public files only:

Stack Bucket policy today Effect
Aspire (AppHost.cs) anonymous s3:GetObject on fsh/* Public files render. Private files are also readable without auth by anyone who has the key. Keys contain a UUIDv7, so they're hard to guess, but "private" isn't enforced by the store.
Compose none Private files are safe, public URLs are broken
AWS Terraform off by default; app_s3_public_read_prefix (uploads/) if enabled Can only work if public files live under a known prefix, which they don't

Proposal

  1. Put visibility in the key. Have StorageKeyBuilder add a public/ or private/ root prefix (existing keys stay valid; only new uploads get it).
  2. Grant anonymous read only on public/* in all three stacks: the Aspire bootstrap policy, a new compose bootstrap policy, and the Terraform app_s3_public_read_prefix default changed to public/.
  3. Compose: set Storage__S3__PublicBaseUrl=${FSH_S3_PUBLIC_URL}/fsh.
  4. Changing visibility (if a file can go from public to private) becomes a copy plus delete. We need to check whether the Files module allows that today.

Alternative considered: an API endpoint that answers 302 with a presigned GET for public files. It needs no bucket policy anywhere, but it adds an API round trip per image and URLs change on every request, so browsers and CDNs can't cache them. Prefix + policy is better for public assets, since those are what you want cached.

Also worth doing

Touches src/BuildingBlocks/Storage (key builder, if it lives there) and the Files module, so it needs maintainer sign-off on the design before a PR.

Activity

  1. iammukeshm commented on Sep 30, 2026

    @iammukeshm
    MemberAuthor

    Signed off on the design: visibility goes in the key, and anonymous read is granted on public/* only, in all three stacks. I'm implementing it now. Decisions:

    • Key layout. New uploads go under public/ or private/, and the storage key builder stays the one place that decides.
    • Existing files. Narrowing the Aspire policy from fsh/* to public/* would break every public file uploaded before the change. The PR ships a one-time migration that copies existing public objects to public/… keys and updates their rows, so nothing is lost on upgrade. Existing private objects stay where they are and become non-public once the broad policy goes.
    • Visibility change. If the Files module allows changing a file's visibility, that becomes copy + delete under the new prefix.
    • Compose. Adds a bootstrap policy for public/* and sets Storage__S3__PublicBaseUrl=${FSH_S3_PUBLIC_URL}/fsh. The README gets a note to firewall the published S3 port, or bind it to 127.0.0.1 when the proxy runs on the same host.
    • Terraform. The app_s3_public_read_prefix default becomes public/.
  2. iammukeshm commented on Sep 30, 2026

    @iammukeshm
    MemberAuthor

    Fixed in #1422 (docs: fullstackhero/docs#262). Follow-ups are tracked in #1423.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions