Motivation
While building a Defang BYOC deploy bundle for Buzz (DefangLabs/buzz#2), we hit a gap: Buzz's deploy/compose/ bundle uses minio (S3-compatible, self-hosted) for object storage. There's no managed-storage extension for this today, so the Defang bundle had to be hand-written to take real S3-compatible endpoint/key config instead — asking users to provision a bucket and static credentials out-of-band, and only working cleanly on AWS/GCP. Azure has no native S3 API, so that bundle doesn't have a clean Azure story at all.
This is a generic gap, not Buzz-specific: any compose app that uses minio/S3 for storage hits the same wall. Proposing x-defang-storage (name TBD) to close it the same way x-defang-postgres/x-defang-redis closed it for databases/cache.
Scope note: there's a larger, prior design thread covering this same gap — defang-global PR 94 ("fold Fabric into the Portal repo"), section 4.3 of docs/plans/fold-fabric-into-portal.md — motivated by Fabric's own blob layer rather than a customer app. That plan is much bigger in scope (local/ephemeral dev engines, split-horizon DNS for presigned-URL portability, Fabric data-plane unification). This issue stays open as the narrower, Buzz-motivated slice: what's needed to deploy a real app's object storage on AWS/GCP/Azure today, without taking on the ephemeral-dev-environment or presigned-URL work. See that plan for the fuller design and the still-open decision on Azure (managed sidecar vs. documented v1 limitation) — Buzz is a second concrete motivating case for building the sidecar rather than shipping the limitation.
Proposal
A new extension, attached to a stub service the way x-defang-postgres/x-defang-redis attach to a postgres:/redis: service block (e.g. keep a minio:-shaped service in compose.yaml, tag it x-defang-storage: true). The minio image in that stub is a sentinel only — Defang elides it and never actually runs it, so MinIO Community Edition's reported archival (unmaintained, no security patches — flagged in the defang-global plan above) doesn't matter for this scope. Defang provisions real object storage per cloud and rewires the same env vars the app already expects — no compose.yaml rewrite needed per cloud.
Per-cloud plan
- AWS: native S3 bucket. No static keys — scope an IAM policy to the bucket on the task's execution role, leave access-key/secret-key env vars blank. Apps using the AWS default credential chain (most S3 SDKs, including Buzz's
rust-s3) pick this up automatically; this already works in Buzz's client today (Credentials::default() fallback in crates/buzz-media/src/storage.rs) but our current bundle doesn't use it — worth fixing there too.
- GCP: native GCS bucket via its S3-compatible XML/interop API — no proxy needed, GCS already speaks S3 well enough. Auth is HMAC keys minted for a dedicated service account. Not fully keyless (HMAC secret still exists), but Defang-managed/rotated rather than user-managed, and tied to a real IAM identity we control.
- Azure: no native S3 API, so this is the one cloud that needs a translation layer. Inject an S3-compatible proxy sidecar (e.g.
s3proxy, which already has an Azure Blob backend) alongside the service — same pattern as the existing LiteLLM sidecar for LLM-provider normalization. The sidecar authenticates to Blob Storage via a Managed Identity on the Container App — genuinely keyless on that leg. The app-to-sidecar leg should trust network isolation (sidecar bound to localhost/private network only) rather than requiring real SigV4 keys between them, same as the LiteLLM sidecar likely already does.
Explicitly out of scope for this issue (deferred to the defang-global PR 94 plan, phase 2+):
- Presigned URLs handed to third-party clients (browsers, etc.) — these break under a signature-less/localhost-trust sidecar, since SigV4 signs the
Host header and the host is part of what's signed. Buzz doesn't need this: confirmed via grep -rn presign across buzz-media/buzz-relay that Buzz never issues presigned URLs — the relay proxies all media itself.
- Split-horizon DNS / local ephemeral storage engine choice (SeaweedFS vs Garage vs Zenko CloudServer vs
s3proxy-on-filesystem) — not needed when the app always talks to real cloud storage or the Azure sidecar, no local dev engine in play here.
Open questions
- Final extension name/shape —
x-defang-storage here vs. x-defang-object-storage in the defang-global plan. Should align on one.
- Should Defang warn/refuse when it sees known NFS/S3-storage-unsafe patterns elsewhere in a compose file (e.g. a plain
minio on a mounted volume), separately from this proposal?
s3proxy vs Zenko CloudServer for the Azure sidecar (both came up independently in this issue and the defang-global plan).
- Companion work in
pulumi-defang (provider/compose/types.go + per-cloud provider code) once this design is agreed — this issue is scoping the design first.
Reference
The Buzz PR (DefangLabs/buzz#2) is a concrete example of the workaround this would eliminate — with x-defang-storage, that bundle's README/compose.yaml wouldn't need to diverge from deploy/compose/ at all.
Motivation
While building a Defang BYOC deploy bundle for Buzz (
DefangLabs/buzz#2), we hit a gap: Buzz'sdeploy/compose/bundle usesminio(S3-compatible, self-hosted) for object storage. There's no managed-storage extension for this today, so the Defang bundle had to be hand-written to take real S3-compatible endpoint/key config instead — asking users to provision a bucket and static credentials out-of-band, and only working cleanly on AWS/GCP. Azure has no native S3 API, so that bundle doesn't have a clean Azure story at all.This is a generic gap, not Buzz-specific: any compose app that uses
minio/S3 for storage hits the same wall. Proposingx-defang-storage(name TBD) to close it the same wayx-defang-postgres/x-defang-redisclosed it for databases/cache.Scope note: there's a larger, prior design thread covering this same gap —
defang-globalPR 94 ("fold Fabric into the Portal repo"), section 4.3 ofdocs/plans/fold-fabric-into-portal.md— motivated by Fabric's own blob layer rather than a customer app. That plan is much bigger in scope (local/ephemeral dev engines, split-horizon DNS for presigned-URL portability, Fabric data-plane unification). This issue stays open as the narrower, Buzz-motivated slice: what's needed to deploy a real app's object storage on AWS/GCP/Azure today, without taking on the ephemeral-dev-environment or presigned-URL work. See that plan for the fuller design and the still-open decision on Azure (managed sidecar vs. documented v1 limitation) — Buzz is a second concrete motivating case for building the sidecar rather than shipping the limitation.Proposal
A new extension, attached to a stub service the way
x-defang-postgres/x-defang-redisattach to apostgres:/redis:service block (e.g. keep aminio:-shaped service incompose.yaml, tag itx-defang-storage: true). Theminioimage in that stub is a sentinel only — Defang elides it and never actually runs it, so MinIO Community Edition's reported archival (unmaintained, no security patches — flagged in thedefang-globalplan above) doesn't matter for this scope. Defang provisions real object storage per cloud and rewires the same env vars the app already expects — no compose.yaml rewrite needed per cloud.Per-cloud plan
rust-s3) pick this up automatically; this already works in Buzz's client today (Credentials::default()fallback incrates/buzz-media/src/storage.rs) but our current bundle doesn't use it — worth fixing there too.s3proxy, which already has an Azure Blob backend) alongside the service — same pattern as the existing LiteLLM sidecar for LLM-provider normalization. The sidecar authenticates to Blob Storage via a Managed Identity on the Container App — genuinely keyless on that leg. The app-to-sidecar leg should trust network isolation (sidecar bound to localhost/private network only) rather than requiring real SigV4 keys between them, same as the LiteLLM sidecar likely already does.Explicitly out of scope for this issue (deferred to the
defang-globalPR 94 plan, phase 2+):Hostheader and the host is part of what's signed. Buzz doesn't need this: confirmed viagrep -rn presignacrossbuzz-media/buzz-relaythat Buzz never issues presigned URLs — the relay proxies all media itself.s3proxy-on-filesystem) — not needed when the app always talks to real cloud storage or the Azure sidecar, no local dev engine in play here.Open questions
x-defang-storagehere vs.x-defang-object-storagein thedefang-globalplan. Should align on one.minioon a mounted volume), separately from this proposal?s3proxyvs Zenko CloudServer for the Azure sidecar (both came up independently in this issue and thedefang-globalplan).pulumi-defang(provider/compose/types.go+ per-cloud provider code) once this design is agreed — this issue is scoping the design first.Reference
The Buzz PR (
DefangLabs/buzz#2) is a concrete example of the workaround this would eliminate — withx-defang-storage, that bundle's README/compose.yaml wouldn't need to diverge fromdeploy/compose/at all.