🗄

MinIO Self-Hosted — MediosVivos

Almacenamiento de objetos compatible con S3 • VPS Hetzner CPX31 • storage.mediosvivos.com

✓ Produccion lista
Resumen del despliegue
Buckets configurados
3
uploads • backups • public
API endpoint
storage.
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
Privado Expiry: 7d (tmp/)
mv-backups
pg_dump diario + snapshots de config
Privado Versionado Expiry: 30d
mv-public
Thumbnails, logos, banners de campanas
Lectura publica
Arquitectura
Flujo de trafico — subida directa via URL prefirmada
Navegador
Next.js 14
Caddy
TLS + proxy
MinIO
:9000 API S3
↓ pg_dump
PostgreSQL
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 Fastify
2
Fastify genera URL prefirmada
Llama a getPresignedUploadUrl() y devuelve la URL PUT valida 5 minutos + la key del objeto
3
Navegador sube directamente a MinIO
fetch(presignedUrl, { method: 'PUT', body: file }) — el servidor Node.js no recibe el archivo
4
Cliente confirma la subida
Envia la key al backend; Fastify mueve el objeto de tmp/ a media/ y guarda la referencia en PostgreSQL
5
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)