decisionshared last reviewed 2026-05-21

MinIO is the default S3-compatible object store; one MinIO per customer-facing app

Context

Aspire apps need durable object storage for uploaded images, generated PDFs, design exports, AI-generated assets, etc. AWS S3 is the default-in-the-industry choice but creates a vendor dependency + per-GB egress costs that become meaningful at scale.

Detail

Decision

MinIO (self-hosted S3-compat) as the default object store. One MinIO instance per customer-facing app, not a single shared MinIO.

Rationale (storage choice)

Rationale (one instance per app, not one shared)

Architecture pattern (per app)

Coolify project: <app>
โ”œโ”€โ”€ <app> app container          (Next.js/etc)
โ”œโ”€โ”€ postgres container           (per app, see [[shared/decisions/use-postgresql-by-default]])
โ””โ”€โ”€ <app>-minio container        (this decision)
    โ”œโ”€โ”€ public S3 API   :9000 โ†’ https://s3.<app-domain>
    โ””โ”€โ”€ admin console  :9001 โ†’ https://s3-admin.<app-domain>

Live examples

Env-var contract (standard)

S3_ENDPOINT=https://s3.<app-domain>
S3_REGION=us-east-1                 # MinIO ignores region but SDK requires it
S3_BUCKET=<app>-uploads
S3_ACCESS_KEY_ID=<per-app>
S3_SECRET_ACCESS_KEY=<per-app>

Constraints we accepted

Revisit trigger

Actions

Related

๐Ÿ”— Relationships

graph LR minio_storage_per_app["minio-storage-per-app"]:::self minio_storage_per_app --> use_postgresql_by_default["use-postgresql-by-default"] minio_storage_per_app --> aspire_llm_gateway_only_egress["aspire-llm-gateway-only-egress"] minio_storage_per_app --> aspire_llm_gateway["aspire-llm-gateway"] classDef self fill:#715EE3,color:#fff,stroke:#291F50;