G.STANCUTA
Pubblicato · 2026 · 05 · 187 min di lettura

MinIO: Il Tuo S3 Personale sul Tuo Server

  • infrastructure
  • self-hosted
  • storage
  • devops

Non hai bisogno di AWS per ottenere uno storage a oggetti compatibile con S3. Un container, la stessa API, zero costi di egress e una via pulita verso il cloud quando ne avrai davvero bisogno.

Ogni progetto accumula media. NearYou archivia miniature di posizioni, avatar utenti, banner di eventi. Il riflesso condizionato è ricorrere a S3, pagare la tassa sulle richieste e accettare che ogni immagine servita da un'origine diversa costi un CORS preflight e un budget di latenza. Esiste un'alternativa migliore per la fase iniziale di un prodotto: MinIO, in esecuzione nello stesso stack Docker Compose della tua applicazione, che parla nativamente l'API S3.

L'API S3 non è una proprietà di Amazon. È uno standard de facto. Il pacchetto @aws-sdk/client-s3 non si preoccupa se l'endpoint è s3.amazonaws.com o localhost:9000. Punti a MinIO e le stesse chiamate PutObjectCommand e GetObjectCommand funzionano in modo identico. Quando NearYou passerà a un bucket gestito, la migrazione sarà una sola variabile d'ambiente.

Diagramma isometrico di uno stack Docker Compose con MinIO, un server API e una route proxy
MinIO si trova all'interno della stessa rete Compose, raggiungibile tramite nome del servizio.

Un Solo Container nello Stack

Lo snippet Compose è ridotto. MinIO pubblica due porte: 9000 per l'API S3 e 9001 per la console browser. La console è utile durante lo sviluppo. Monti un volume locale affinché i dati sopravvivano ai riavvii del container.

yaml
services:
  minio:
    image: quay.io/minio/minio:RELEASE.2025-04-08T15-41-24Z
    command: server /data --console-address ":9001"
    environment:
      MINIO_ROOT_USER: ${MINIO_ROOT_USER}
      MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
    ports:
      - "9000:9000"
      - "9001:9001"
    volumes:
      - minio_data:/data
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
      interval: 10s
      timeout: 5s
      retries: 3

  api:
    build: .
    environment:
      S3_ENDPOINT: http://minio:9000
      S3_ACCESS_KEY: ${MINIO_ROOT_USER}
      S3_SECRET_KEY: ${MINIO_ROOT_PASSWORD}
      S3_BUCKET: nearyou-media
    depends_on:
      minio:
        condition: service_healthy

volumes:
  minio_data:

All'interno del container api, MinIO è raggiungibile a http://minio:9000 perché entrambi i servizi condividono la rete Compose predefinita. Non è necessaria alcuna esposizione pubblica per le scritture interne. Il nome del bucket è semplicemente una stringa che crei una volta tramite la console o la CLI del client MinIO (mc mb myminio/nearyou-media).

Caricamento da TypeScript

Il percorso di upload in NearYou è un sottile wrapper attorno all'AWS SDK v3. L'intuizione chiave è forcePathStyle: true. AWS utilizza URL in stile virtual-hosted (bucket.host/key). MinIO in un ambiente locale usa lo stile path (host/bucket/key). Senza quel flag otterrai errori di risoluzione DNS non immediatamente evidenti.

ts
import {S3Client, PutObjectCommand, GetObjectCommand} from '@aws-sdk/client-s3';
import {getSignedUrl} from '@aws-sdk/s3-request-presigner';

const s3 = new S3Client({
  endpoint: process.env.S3_ENDPOINT,         // http://minio:9000 locally
  region: 'us-east-1',                       // MinIO ignores this; SDK requires it
  credentials: {
    accessKeyId: process.env.S3_ACCESS_KEY!,
    secretAccessKey: process.env.S3_SECRET_KEY!,
  },
  forcePathStyle: true,                      // required for MinIO
});

const BUCKET = process.env.S3_BUCKET ?? 'nearyou-media';

export async function uploadMedia(
  key: string,
  body: Buffer | Uint8Array,
  contentType: string,
): Promise<void> {
  await s3.send(
    new PutObjectCommand({
      Bucket: BUCKET,
      Key: key,
      Body: body,
      ContentType: contentType,
    }),
  );
}

export async function getPresignedReadUrl(key: string, expiresIn = 3600): Promise<string> {
  return getSignedUrl(
    s3,
    new GetObjectCommand({Bucket: BUCKET, Key: key}),
    {expiresIn},
  );
}

Il campo region è una peculiarità. MinIO non applica le region, ma il client AWS SDK si rifiuterà di firmare le richieste senza una. us-east-1 è convenzionale e inoffensivo. Quando in seguito punti lo stesso codice a un vero bucket AWS, la region deve corrispondere a quella effettiva del bucket.

Proxy delle Letture Pubbliche tramite /api/media/*

Servire i file direttamente da http://minio:9000 significa che ogni richiesta del client lascia il dominio della tua applicazione. Perdi: caching same-origin, intestazioni Cache-Control che controlli tu, verifiche di accesso prima della consegna e la possibilità di riscrivere le chiavi senza cambiare gli URL. La route proxy costa un hop aggiuntivo nel server ma recupera tutto questo.

In un Route Handler di Next.js, il pattern è semplice. L'handler recupera da MinIO tramite la rete interna, trasmette in streaming la risposta al client e inoltra il content type. Puoi bloccare l'accesso con il tuo normale middleware di autenticazione prima che la fetch venga mai eseguita.

ts
// src/app/api/media/[...key]/route.ts
import {NextRequest, NextResponse} from 'next/server';
import {GetObjectCommand} from '@aws-sdk/client-s3';
import {s3} from '@/lib/s3';                // the client from the snippet above
import {Readable} from 'node:stream';

const BUCKET = process.env.S3_BUCKET ?? 'nearyou-media';

export async function GET(
  _req: NextRequest,
  {params}: {params: {key: string[]}},
) {
  const key = params.key.join('/');

  let object;
  try {
    object = await s3.send(new GetObjectCommand({Bucket: BUCKET, Key: key}));
  } catch (err: unknown) {
    if ((err as {name?: string}).name === 'NoSuchKey') {
      return new NextResponse('Not found', {status: 404});
    }
    throw err;
  }

  const contentType = object.ContentType ?? 'application/octet-stream';
  const stream = object.Body as Readable;

  return new NextResponse(stream as unknown as ReadableStream, {
    headers: {
      'Content-Type': contentType,
      'Cache-Control': 'public, max-age=31536000, immutable',
    },
  });
}

L'intestazione Cache-Control: immutable significa che browser e CDN non richiederanno nuovamente un asset una volta memorizzato nella cache. Funziona solo se le tue chiavi sono content-addressed (includono un hash o un segmento di versione). Per i media di NearYou, le chiavi seguono il pattern events/{eventId}/{hash}.webp. L'eventId è stabile, l'hash cambia quando cambia l'immagine.

Nessun Contatore di Egress

Il motivo per cui lo storage self-hosted ha senso nella fase iniziale di un prodotto non è la latenza o il controllo, anche se entrambi aiutano. È la prevedibilità dei costi. AWS S3 addebita per richiesta e per GB trasferiti. Quando si itera velocemente, si aggiungono formati di immagine, si ridimensiona al volo e si servono anteprime nelle dashboard, il conteggio delle richieste sale prima che tu te ne accorga. MinIO su un server che stai già pagando aggiunge zero a quella voce.

NearYou gira su un server dedicato Hetzner. Il costo mensile di quella macchina è fisso. Ogni immagine servita tramite la route proxy è banda interna. L'unica banda esterna è quella che il collegamento Hetzner porta all'utente finale, misurata separatamente ma in modo generoso. Per un prodotto nella fascia delle centinaia di utenti, lo storage self-hosted è il default corretto.

Non hai bisogno di storage con contatore di egress finché la tua bolletta di storage non diventa una frazione significativa del tuo fatturato. Prima di quel punto, la compatibilità con l'API S3 di MinIO ti offre il percorso di migrazione senza i costi.

Percorso di Migrazione verso un Bucket Cloud

Poiché il codice applicativo usa l'API S3 in modo uniforme, la migrazione a un vero bucket S3, R2 o Tigris è una modifica alle variabili d'ambiente. Le tre cose da verificare prima di fare il cambio sono:

  1. 01forcePathStyle deve essere false per AWS S3 e R2 (usano URL in stile virtual-hosted).
  2. 02Region deve corrispondere alla region effettiva del bucket per AWS. R2 e Tigris usano un placeholder fisso.
  3. 03Credentials devono essere sostituite con chiavi IAM con scope limitato al bucket, non le credenziali root.

La migrazione dei dati è un comando mc mirror. MinIO Client (mc) parla l'API S3 e può eseguire il mirror tra due endpoint: mc mirror myminio/nearyou-media s3prod/nearyou-media. Eseguilo con --watch per svuotare il backlog mentre il traffico colpisce ancora MinIO, poi fai il cutover. Il downtime è un singolo riavvio del container API con le nuove variabili d'ambiente.

Schema di un agente AI che legge file markdown di memoria per configurare e gestire MinIO
L'agente legge AGENTS.md e i documenti infra per ricostruire la conoscenza del sistema tra le sessioni.

Far Operare un Agente AI su Questo Senza Farlo Impazzire

Un agente AI di coding che lavora su NearYou in più sessioni non ha memoria persistente per impostazione predefinita. Dimenticherà che MinIO è nello stack, dimenticherà il requisito forcePathStyle e dimenticherà che esiste la route proxy. La soluzione è mantenere nella repository un documento di infrastruttura breve e preciso che l'agente legga all'inizio di ogni sessione.

Il documento si trova in AGENTS.md o in un dedicato docs/infra/storage.md. Non è un tutorial. È una tabella di riferimento per le cose su cui l'agente deve agire correttamente: nomi dei servizi, convenzioni sulle porte, pattern delle chiavi, problemi noti e comandi per le operazioni comuni. L'agente lo legge, ricostruisce il contesto e opera in modo affidabile senza che tu debba rispiegare la configurazione nella finestra di chat.

Ecco la sezione storage dei documenti infra di NearYou. È tenuta breve di proposito. L'agente non ha bisogno di prosa: ha bisogno di fatti su cui può agire.

md
# Storage (MinIO / S3)

## Service
- Compose service name: `minio`
- Internal S3 endpoint: `http://minio:9000`
- Console (dev only): `http://localhost:9001`
- Bucket: `nearyou-media`

## SDK conventions
- Client: `@aws-sdk/client-s3` v3
- Always set `forcePathStyle: true` for local MinIO
- Set `forcePathStyle: false` if endpoint is AWS or Cloudflare R2
- Region: use `us-east-1` placeholder (MinIO ignores it)
- Client singleton: `src/lib/s3.ts`

## Key schema
- Events: `events/{eventId}/{sha256}.webp`
- Avatars: `avatars/{userId}/{sha256}.webp`
- Keys are content-addressed — never mutate a key after upload

## Public reads
- All reads go through `/api/media/[...key]` route handler
- Cache-Control: `public, max-age=31536000, immutable`
- MinIO port 9000 is NOT exposed publicly on the server

## Common commands
- Create bucket: `mc mb myminio/nearyou-media`
- Mirror to cloud: `mc mirror --watch myminio/nearyou-media s3prod/nearyou-media`
- List bucket: `mc ls myminio/nearyou-media`

## Cloud migration checklist
1. Set `forcePathStyle=false` in env
2. Update `S3_ENDPOINT` to cloud bucket URL
3. Replace root credentials with scoped IAM keys
4. Verify region matches bucket
5. Run `mc mirror`, then restart API container

Quando inizia una nuova sessione e all'agente viene chiesto di aggiungere un nuovo tipo di media o di debuggare un 403 su un upload, legge prima questo file. Sa che il singleton del client è in src/lib/s3.ts, conosce lo schema delle chiavi e sa di non toccare il flag forcePathStyle senza verificare l'endpoint corrente. Questo è l'intero punto: il file markdown è la memoria a lungo termine dell'agente per questo sottosistema.

Lo stesso pattern si scala ad altri componenti infrastrutturali. Un file per sottosistema, tenuto breve e fattuale. L'agente accumula conoscenza operativa nella repository, non nella cronologia della chat.

Cosa Ti Porta Tutto Questo

  • Compatibilità API S3 senza vendor lock-in: sostituisci l'endpoint, cambia una variabile d'ambiente.
  • Zero fatturazione egress per i media interni durante lo sviluppo iniziale e la crescita.
  • Proxy same-origin con Cache-Control: immutable affinché browser e CDN si comportino correttamente.
  • Controllo degli accessi al livello proxy, prima che qualsiasi byte lasci il server.
  • Migrazione prevedibile: mc mirror più un riavvio del container, nessuna modifica al codice.
  • Infra operabile da agenti tramite un conciso documento markdown che ricostruisce il contesto in modo affidabile.

La configurazione predefinita per un nuovo prodotto dovrebbe essere la cosa più semplice che funziona e non si mette in mezzo. MinIO in uno stack Compose con una route proxy è esattamente questo. Sparisce nello sfondo, non costa nulla in più e non chiude nessuna porta.

Portfolio · Cartiglio
Disegnato da
G. STANCUTA
Disciplina
AI & AUTOMATION
Luogo
MORTER · SÜDTIROL
Stato
Disponibile
Lingue
IT · EN · RO · DE+
Stack
PLOI · HETZNER
Revisione
REV 2026.A
2026

© 2026 Gabriel Stancuta · jumpinotech.com — Progettato con l'AI, costruito per funzionare da solo.