El 25 abr 2026 MinIO archivó su community edition. Migramos producción de Click2Eat y yamltools.dev a Garage self-host (S3-compatible, AGPLv3). Cuatro escollos no documentados, el patrón S3 API compartido + CDN dedicado por producto, y checklist de migración completo.
El 25 de abril de 2026, MinIO archivó silenciosamente su repositorio principal con un mensaje seco: "THIS REPOSITORY IS NO LONGER MAINTAINED". La community edition se acabó. Si tienes producción corriendo sobre MinIO, ya no recibirás parches de seguridad. Si lo desplegabas con Coolify, además te lo quitaron del catálogo one-click hace meses.
Para quien no lo conozca: MinIO era el sistema de almacenamiento de objetos S3-compatible más popular en el mundo self-host. Cualquier app que necesite guardar archivos (imágenes de usuarios, PDFs, vídeos, backups) puede hablar con MinIO usando las mismas librerías que con S3 de Amazon, pero el servidor lo tienes tú. Para builders auto-suficientes era el default desde hace años.
Esta semana migramos dos productos en producción de MinIO a Garage (Rust, AGPLv3, mantenido activamente por la cooperativa Deuxfleurs). Click2Eat (60 MiB de imágenes de menú reales sirviendo restaurantes) y yamltools.dev. Cero downtime visible, todo en una sesión de tarde.
Esta es la guía técnica que hubiéramos querido tener al empezar.
MinIO Inc. sigue existiendo y vendiendo AIStor (su nueva línea comercial). Lo que se ha matado es la community edition. El código sigue en GitHub, pero archivado, sin issues, sin PRs, sin binarios precompilados nuevos. Si quieres MinIO community a partir de hoy, te toca compilar desde fuente con Go cada vez que aparezca una CVE.
Para un side project, irrelevante. Para producción, no aceptable a medio plazo.
Antes de decidir Garage, miramos las alternativas serias:
| Opción | Free tier | Cap de gasto duro | Egress fees | Self-host |
|---|---|---|---|---|
| Cloudflare R2 | 10 GB | No | Sin egress | No |
| Hetzner Object Storage | No | No | €1/TB después 1TB | No |
| Backblaze B2 | 10 GB | No | 3x storage gratis | No |
| Garage self-host | Sin límites | Sí (físico) | Lo que tu VPS dé | Sí |
El criterio que pesó: tener un cap de gasto duro real. Ningún cloud serio (R2, S3, Hetzner OS, Backblaze) tiene un toggle de "para si paso de €X". Solo alertas que avisan después. Con Garage self-host sobre un VPS, el techo lo pone físicamente el disco: si el volumen es 100 GB, no caben más de 100 GB, fin del problema.
R2 era atractivo (sin egress fees, free tier real), pero la falta de cap duro y el miedo razonable a "factura sorpresa por bug propio o ataque" lo descartó para nuestro caso.
Garage cumplía: AGPLv3, S3-compatible, mantenimiento activo, sucesor espiritual de MinIO en la comunidad self-host.
┌─────────────────────────────────┐
│ UN ÚNICO Garage en VPS │
│ │
s3.ipalseb.com ─────────│ S3 API :3900 │
(uploads autenticados │ ├─ bucket click2eat │
de TODOS los productos)│ ├─ bucket yamltools │
│ └─ bucket futuros │
│ │
cdn.click2eat.app ──────│ S3 Web :3902 → click2eat │
(lecturas públicas │ │
de Click2Eat) │ │
│ │
cdn.yamltools.dev ──────│ S3 Web :3902 → yamltools │
(lecturas públicas │ │
de yamltools) │ │
└─────────────────────────────────┘S3 API compartido + CDN dedicado por producto. Las apps suben siempre a s3.ipalseb.com
(autenticado, S3 path-style: s3.ipalseb.com/<bucket>/<key>). Los reads públicos pasan por un
dominio CDN específico de cada producto: cdn.click2eat.app, cdn.yamltools.dev. Cada producto
vive bajo su propia marca cuando se ve desde fuera; la infra interna está unificada.
Si en el futuro vendes uno de los productos, su CDN ya está bajo el dominio del producto: cero migración de URLs. Si añades un producto nuevo, solo añades un dominio CDN más al mismo Garage.
Estas son las cuatro horas que no te ahorras leyendo la doc oficial. Léelas antes de empezar.
El error literal es: Forbidden: Garage does not support anonymous access yet.
En MinIO, hacer un bucket público era cosa de aplicar una bucket policy public-read. Cualquier
GET https://minio.tudominio.com/<bucket>/<key> funcionaba sin auth. En Garage, eso no existe.
La forma de servir archivos públicos en Garage es usar el S3 Web (puerto 3902, no el 3900 de S3 API). Habilitas website mode en el bucket y le añades un alias = el hostname público que quieres usar:
garage bucket website --allow click2eat
garage bucket alias click2eat cdn.click2eat.appY configuras tu reverso (Caddy/Traefik/Nginx) para que cdn.click2eat.app enrute al puerto 3902.
Garage ve el Host header y resuelve el bucket por alias.
Esta separación física entre auth (3900) y público (3902) es deliberada del proyecto Garage: elimina toda una clase de errores de S3 (los famosos "bucket abierto" que vemos en noticias).
S3_REGION es obligatoria, no opcionalEl AWS SDK firma cada petición con la región configurada en el cliente S3. Garage tiene su propia
s3_region en garage.toml, y exige que coincida con la región que firma el SDK. Si no coinciden,
rechaza con AuthorizationHeaderMalformed:
AuthorizationHeaderMalformed: Authorization header malformed,
unexpected scope: 20260506/us-east-1/s3/aws4_requestSolución: setear S3_REGION=garage (o el valor que tengas en s3_region del garage.toml) en cada
app que sube/lee del bucket. En nuestro caso, las apps venían con us-east-1 heredado del setup
viejo de MinIO, de ahí el scope que ves en el error.
Esto nos costó 20 minutos hasta que llegó el primer upload real desde la app a producción. Las apps en local no fallaban porque sus uploads de test no llegaban a Garage.
Cuando despliegas Garage desde el catálogo de Coolify, los campos "S3 API URL", "Web URL" y "Admin
URL" aceptan un único valor cada uno. Eso choca con que necesitamos dos hostnames distintos al
mismo puerto 3902 (cdn.click2eat.app y cdn.yamltools.dev).
Solución: editar el Compose File del servicio Garage en Coolify y añadir labels Traefik manualmente:
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.garage-cdn-click2eat.rule=Host(`cdn.click2eat.app`)'
- 'traefik.http.routers.garage-cdn-click2eat.entrypoints=https'
- 'traefik.http.routers.garage-cdn-click2eat.tls=true'
- 'traefik.http.routers.garage-cdn-click2eat.tls.certresolver=letsencrypt'
- 'traefik.http.routers.garage-cdn-click2eat.service=garage-cdn-extra'
- 'traefik.http.routers.garage-cdn-click2eat.priority=1000'
- 'traefik.http.routers.garage-cdn-yamltools.rule=Host(`cdn.yamltools.dev`)'
- 'traefik.http.routers.garage-cdn-yamltools.entrypoints=https'
- 'traefik.http.routers.garage-cdn-yamltools.tls=true'
- 'traefik.http.routers.garage-cdn-yamltools.tls.certresolver=letsencrypt'
- 'traefik.http.routers.garage-cdn-yamltools.service=garage-cdn-extra'
- 'traefik.http.routers.garage-cdn-yamltools.priority=1000'
- 'traefik.http.services.garage-cdn-extra.loadbalancer.server.port=3902'El priority=1000 no es opcional. Si tu app principal tiene un router Traefik con HostRegexp
wildcard (típico cuando configuras un dominio multi-tenant tipo *.tuapp.com), ese wildcard captura
cualquier subdominio incluido el CDN. Sin priority alta, tu CDN nuevo aterriza en la app equivocada
y devuelve 404 o redirige al login.
Lo descubrimos cuando cdn.click2eat.app empezó a devolver el frontend de Click2Eat con un redirect
a /es. Una hora investigando hasta encontrar la regla Host(\click2eat.app`) ||
HostRegexp(`^.+.click2eat.app$`)` en las labels del container de la app.
rclone syncPara hacer backup pre-migración del MinIO viejo al NAS local, primero intentamos rclone sync
directo del MinIO al NAS montado vía SMB. Resultado: tres errores de checksum MD5 mismatch en
archivos pequeños, datos corruptos a mitad de la transferencia.
ERROR : corrupted on transfer: md5 hashes differ
src(S3 bucket yamltools) "86613f0dc3246fba874e9fa0a9d1ad02"
vs dst(Local file system) "2c1fdb865ed7bede40b6b973d831a282"Solución que sí funcionó: tar.gz monolítico desde el VPS, transferir el archivo único por scp al NAS, verificar SHA256:
# En el VPS
tar -czf /tmp/minio-backup.tar.gz -C /var/lib/docker/volumes/<vol>/_data .
sha256sum /tmp/minio-backup.tar.gz
# Desde el Mac
scp root@vps:/tmp/minio-backup.tar.gz /Volumes/NAS/backups/
shasum -a 256 /Volumes/NAS/backups/minio-backup.tar.gz # debe coincidirSHA256 verificado. Backup íntegro y atómico. No vuelvas a hacer rclone-many-files contra SMB.
Para que tengas un orden ejecutable la próxima vez:
PREPARACIÓN
[ ] Auditar buckets MinIO actuales: nombres, tamaños, conteos
[ ] Identificar todas las apps que escriben/leen del MinIO actual
[ ] Identificar todas las URLs hardcodeadas en BD apuntando al MinIO
DESPLIEGUE PARALELO
[ ] Crear DNS para s3.<tudominio> y cdn.<producto> apuntando al VPS
[ ] Desplegar Garage en Coolify (one-click); editar Compose para añadir
labels Traefik con priority=1000 si tienes apps con HostRegexp wildcard
[ ] Inicializar cluster: layout assign + apply
[ ] Crear bucket por producto + access key scope-separated por bucket
[ ] Habilitar website mode en cada bucket: garage bucket website --allow <name>
[ ] Añadir alias hostname al bucket: garage bucket alias <name> cdn.<producto>
MIGRACIÓN DE DATOS
[ ] rclone sync minio:<bucket> garage:<bucket> --transfers 8
[ ] Verificar conteos y tamaños en ambos lados
[ ] Backup completo del MinIO viejo (tar.gz al NAS, no SMB sync)
CUTOVER
[ ] Actualizar envs en cada app: S3_ENDPOINT, S3_REGION=garage,
nuevas keys, S3_PUBLIC_URL/S3_CDN_URL al CDN nuevo
[ ] Actualizar remotePatterns/CSP/CORS en cada app
[ ] UPDATE en BD para cambiar URLs hardcodeadas (con backup previo
y en transacción)
[ ] Redeploy de cada app
[ ] Smoke test: ver imágenes existentes + subir una nueva
LIMPIEZA
[ ] Stop del MinIO viejo en Coolify (no Delete inmediato)
[ ] Esperar 5-7 días observando errores
[ ] Borrar volumen Docker MinIO + servicio + DNS legacy
[ ] Quitar dominios viejos de remotePatternsGarage en un VPS Hetzner CX22 (€4,59/mes, ya estás pagándolo si tienes Coolify ahí) sirve N productos sin sumar nada al ticket. Disco: el que tenga el VPS. Cap duro: el tamaño físico del volumen.
Lo único que pagas es tu tiempo: ~4 horas la primera vez (incluyendo los 4 escollos), ~1 hora cada migración subsecuente cuando ya conoces el patrón.
Comparado con quedarte con MinIO sin parches, o pagar R2/Hetzner OS sin cap duro, este patrón es el que mejor encaja para builders auto-suficientes con varios productos pequeños sobre el mismo VPS.
Si tu setup es uno solo y mucho tráfico público, R2 con su free tier de 10 GB y cero egress probablemente sigue siendo más simple. La elección depende de tu perfil de riesgo respecto a la factura.
Suscríbete para más tutoriales y tips sobre crear productos con IA