Resumen del despliegue
Buckets configurados
3
uploads • backups • public
API endpoint
storage.
mediosvivos.com
mediosvivos.com
TLS via Caddy
Retencion backups
30d
lifecycle auto-delete
Uploads temporales
7d
expiry rule activa
Buckets
mv-uploads
Videos, imagenes, PDFs de usuario • max 500 MB/archivo
mv-backups
pg_dump diario + snapshots de config
mv-public
Thumbnails, logos, banners de campanas
Arquitectura
Flujo de trafico — subida directa via URL prefirmada
Navegador
Next.js 14
Next.js 14
→
Caddy
TLS + proxy
TLS + proxy
→
MinIO
:9000 API S3
:9000 API S3
↓ pg_dump
PostgreSQL
backup diario
backup diario
El cliente obtiene una URL prefirmada PUT del servidor Fastify (
POST /api/upload-url), sube el archivo directamente a MinIO sin pasar por Node.js, y notifica al servidor con la key resultante para que la guarde en base de datos.
docker-compose.yml
docker-compose.yml
YAML
# MediosVivos — MinIO production deployment services: minio: image: minio/minio:latest command: server /data --console-address ":9001" restart: unless-stopped ports: - "127.0.0.1:9000:9000" # solo localhost — Caddy hace proxy - "127.0.0.1:9001:9001" # consola web volumes: - minio_data:/data environment: MINIO_ROOT_USER: ${MINIO_ROOT_USER} MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} MINIO_SITE_NAME: mediosvivos-prod healthcheck: test: ["CMD", "mc", "ready", "local"] interval: 30s timeout: 5s retries: 3 volumes: minio_data:
Scripts de configuracion
scripts/setup-buckets.sh
BASH
#!/bin/bash — ejecutar una vez tras el deploy set -euo pipefail ENDPOINT="https://storage.mediosvivos.com" ALIAS="mv" # Conectar alias mc alias set $ALIAS $ENDPOINT \ $MINIO_ROOT_USER $MINIO_ROOT_PASSWORD # Crear buckets mc mb $ALIAS/mv-uploads mc mb $ALIAS/mv-backups mc mb $ALIAS/mv-public # Versionado en backups mc version enable $ALIAS/mv-backups # Lectura publica en mv-public mc anonymous set download $ALIAS/mv-public # Lifecycle: borrar tmp/ tras 7 dias mc ilm rule add $ALIAS/mv-uploads \ --prefix "tmp/" --expiry-days 7 # Lifecycle: backups caducan a los 30 dias mc ilm rule add $ALIAS/mv-backups \ --expiry-days 30 echo "Buckets listos"
lib/storage.ts
TYPESCRIPT
import { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3' import { getSignedUrl } from '@aws-sdk/s3-request-presigner' const s3 = new S3Client({ endpoint: 'https://storage.mediosvivos.com', region: 'us-east-1', credentials: { accessKeyId: process.env.MINIO_ACCESS_KEY!, secretAccessKey: process.env.MINIO_SECRET_KEY!, }, forcePathStyle: true, // obligatorio con MinIO }) export async function getPresignedUploadUrl( filename: string, mimeType: string ) { const key = `tmp/${Date.now()}-${filename}` const url = await getSignedUrl(s3, new PutObjectCommand({ Bucket: 'mv-uploads', Key: key, ContentType: mimeType, }), { expiresIn: 300 } // 5 minutos ) return { url, key } }
Flujo de subida directa
1
Cliente solicita URL prefirmada
POST /api/upload-url con { filename, mimeType } al servidor Fastify2
Fastify genera URL prefirmada
Llama a
getPresignedUploadUrl() y devuelve la URL PUT valida 5 minutos + la key del objeto3
Navegador sube directamente a MinIO
fetch(presignedUrl, { method: 'PUT', body: file }) — el servidor Node.js no recibe el archivo4
Cliente confirma la subida
Envia la
key al backend; Fastify mueve el objeto de tmp/ a media/ y guarda la referencia en PostgreSQL5
Acceso posterior via URL prefirmada GET
Para archivos privados, se genera una URL GET con 1h de validez; los assets publicos (mv-public) se sirven directamente
Reglas de ciclo de vida
LIFECYCLE
| Bucket | Prefijo | Accion | Dias |
|---|---|---|---|
| mv-uploads | tmp/ | Expiry (borrado) | 7 |
| mv-backups | * | Expiry (borrado) | 30 |
| mv-uploads | media/ | Sin expiry (permanente) | ∞ |
| mv-public | * | Sin expiry (publico) | ∞ |
Script de backup PostgreSQL
scripts/backup-pg.sh
BASH — cron diario 02:00
#!/bin/bash — cron: 0 2 * * * /opt/mediosvivos/scripts/backup-pg.sh set -euo pipefail DATE=$(date +%Y-%m-%d) DUMP_FILE="/tmp/mediosvivos-${DATE}.sql.gz" S3_KEY="pg/${DATE}/mediosvivos.sql.gz" # 1. Volcar base de datos PGPASSWORD=$DB_PASSWORD pg_dump -h localhost -U $DB_USER $DB_NAME \ | gzip > $DUMP_FILE # 2. Subir a MinIO mc cp $DUMP_FILE mv/mv-backups/${S3_KEY} # 3. Limpiar archivo local rm $DUMP_FILE echo "Backup completado: mv-backups/${S3_KEY}"
Checklist de produccion
✓ Completado
✔ MinIO desplegado con Docker Compose
✔ TLS via Caddy (storage.mediosvivos.com)
✔ 3 buckets creados y configurados
✔ Reglas lifecycle para tmp/ (7d) y backups (30d)
✔ Versionado activado en mv-backups
✔ lib/storage.ts con presigned URL (PUT + GET)
✔ Script pg_dump -> mv-backups con cron
✔ Credenciales de produccion en .env (no defaults)
⚠ Recomendaciones adicionales
○ Configurar alertas de uso de disco (alerta al 80 %)
○ Access keys dedicadas por servicio (no usar root)
○ Habilitar auditoria de accesos en MinIO
○ Replica del bucket backups a Hetzner Object Storage
○ Limite de tamaño de objeto (500 MB) via policy
○ Scan antivirus en uploads (ClamAV sidecar)