{"id":"840efb01-1e85-47d3-9c8f-add1b6571749","headline":"No guardes fotos en MySQL (y por qu\u00e9 S3 existe, con MinIO como plan B)","slug":"no-guardes-fotos-en-mysql-y-por-que-s3-existe-con-minio-como-plan-b","articleBody":"<p>Hay un momento en la vida de todo proyecto donde alguien dice: \"y los\nficheros d\u00f3nde los guardamos?\". Y la respuesta f\u00e1cil es: en la base de\ndatos, en un BLOB. Funciona. Es transaccional. Es consistente.<\/p>\n<p>Y es una terrible idea. Vamos a ver por qu\u00e9.<\/p>\n<h1 id=\"el-problema-de-guardar-binarios-en-mysql\">El problema de guardar\nbinarios en MySQL<\/h1>\n<p>MySQL guarda BLOBs en p\u00e1ginas de 16KB (InnoDB). Cuando metes un PDF\nde 5MB, MySQL lo parte en trozos y los esconde en p\u00e1ginas internas que\nnadie ve. Esto tiene consecuencias:<\/p>\n<ul>\n<li><p><strong><strong>El buffer pool se satura.<\/strong><\/strong>\nInnoDB cachea p\u00e1ginas de datos en RAM. Si la mitad de tu buffer pool\nest\u00e1 lleno de pedazos de PDFs, las p\u00e1ginas que realmente importan\n(\u00edndices, rows de usuarios, queries frecuentes) se evaporan de la cache.\nTu base de datos se vuelve lenta por culpa de un contrato que alguien\nsubi\u00f3 hace 3 semanas.<\/p><\/li>\n<li><p><strong><strong>Backups m\u00e1s lentos.<\/strong><\/strong> <code\nclass=\"verbatim\">mysqldump<\/code> de 2GB de datos limpios tarda\nsegundos. <code class=\"verbatim\">mysqldump<\/code> de 2GB de datos + 50GB\nde BLOBs tarda horas. Y la mayor\u00eda de esos BLOBs no cambian nunca. Est\u00e1s\nrespaldando lo mismo una y otra vez.<\/p><\/li>\n<li><p><strong><strong>Replicaci\u00f3n pesada.<\/strong><\/strong> Los binlogs\nde MySQL replican todos los cambios. Si alguien sube un archivo de 20MB,\nese archivo viaja por el binlog a cada replica. Ancho de banda\ndesperdiciado en algo que no necesita consistencia\ntransaccional.<\/p><\/li>\n<li><p><strong><strong>Queries que antes eran r\u00e1pidas ahora no lo\nson.<\/strong><\/strong> Un <code class=\"verbatim\">SELECT *<\/code> que\nantes devolv\u00eda 100 filas ligeras ahora devuelve 100 filas con BLOBs de\n5MB cada una. Si olvidas excluir la columna del BLOB, la query se\narrastra. Y olvidar\u00e1s. Todos olvidan.<\/p><\/li>\n<\/ul>\n<h1 id=\"qu\u00e9-es-s3-y-por-qu\u00e9-es-diferente\">Qu\u00e9 es S3 (y por qu\u00e9 es\ndiferente)<\/h1>\n<p>Amazon S3 es un <em>object store<\/em>. No es una base de datos. No\ntiene esquemas. No tiene JOINs. Es un diccionario gigante: le das una\nclave (<code class=\"verbatim\">uploads\/contrato-2026.pdf<\/code>) y te\ndevuelve los bytes. Punto.<\/p>\n<p>Las ventajas:<\/p>\n<ul>\n<li><p><strong><strong>Separaci\u00f3n de\nresponsabilidades.<\/strong><\/strong> Tu MySQL guarda datos relacionales.\nTu S3 guarda ficheros. Cada uno hace lo que sabe hacer. MySQL no se\nsatura con binarios. S3 no se preocupa por transacciones.<\/p><\/li>\n<li><p><strong><strong>Escalabilidad sin pensar.<\/strong><\/strong> S3 no\ntiene l\u00edmites pr\u00e1cticos de almacenamiento. Subes lo que quieras. No\nplanificas capacidad. No a\u00f1ades discos. Simplemente funciona.<\/p><\/li>\n<li><p><strong><strong>Backups baratos.<\/strong><\/strong> Cada objeto en\nS3 tiene versionado autom\u00e1tico. Puedes configurar lifecycle rules que\nmuevan objetos antiguos a almacenamiento m\u00e1s barato (Glacier). Un backup\nde BLOB en MySQL no tiene nada de esto.<\/p><\/li>\n<li><p><strong><strong>Servir ficheros directamente.<\/strong><\/strong>\nS3 puede generar URLs firmadas con expiraci\u00f3n. Tu backend no act\u00faa de\nproxy: el cliente descarga directamente de S3. Menos carga en tu\nservidor, menos ancho de banda consumido.<\/p><\/li>\n<li><p><strong><strong>CDN integrado.<\/strong><\/strong> CloudFront (la\nCDN de AWS) se integra con S3 nativamente. Ficheros est\u00e1ticos servidos\ndesde el edge en milisegundos. Con BLOBs en MySQL, cada descarga pasa\npor tu aplicaci\u00f3n.<\/p><\/li>\n<\/ul>\n<p>Las desventajas:<\/p>\n<ul>\n<li><p><strong><strong>No es transaccional.<\/strong><\/strong> Si subes\nun fichero a S3 y luego falla la inserci\u00f3n en MySQL, tienes un fichero\nhu\u00e9rfano. Necesitas l\u00f3gica de compensaci\u00f3n o limpieza\nperi\u00f3dica.<\/p><\/li>\n<li><p><strong><strong>Costos inesperados.<\/strong><\/strong> AWS cobra\npor GB almacenado, por petici\u00f3n GET, por petici\u00f3n PUT, por transferencia\nde datos entre regiones. Un proyecto peque\u00f1o puede ser gratis. Un\nproyecto con mucho tr\u00e1fico puede ser caro.<\/p><\/li>\n<li><p><strong><strong>Latencia de primera petici\u00f3n.<\/strong><\/strong>\nS3 no es un disco. La primera petici\u00f3n a un objeto tiene latencia de red\n(t\u00edpicamente 50-200ms). Si est\u00e1s accediendo al mismo fichero\nconstantemente, necesitas una cache o CDN delante.<\/p><\/li>\n<li><p><strong><strong>Vendor lock-in.<\/strong><\/strong> Tu c\u00f3digo\ndepende de la API de AWS. Si ma\u00f1ana quieres moverte, necesitas adaptar\ntu capa de almacenamiento.<\/p><\/li>\n<\/ul>\n<h1 id=\"minio-s3-sin-depender-de-aws\">MinIO: S3 sin depender de AWS<\/h1>\n<p>Aqu\u00ed es donde entra <a href=\"https:\/\/min.io\">MinIO<\/a>.<\/p>\n<p>MinIO es un servidor de object storage <em>compatible con la API\nS3<\/em>. Esto significa que tu c\u00f3digo que habla S3 puede hablar MinIO\nsin cambiar una l\u00ednea. Solo cambias el endpoint:<\/p>\n<div class=\"sourceCode\" id=\"cb1\"><pre\nclass=\"sourceCode bash\"><code class=\"sourceCode bash\"><span id=\"cb1-1\"><a href=\"#cb1-1\" aria-hidden=\"true\" tabindex=\"-1\"><\/a><span class=\"co\"># AWS S3<\/span><\/span>\n<span id=\"cb1-2\"><a href=\"#cb1-2\" aria-hidden=\"true\" tabindex=\"-1\"><\/a><span class=\"va\">ENDPOINT<\/span><span class=\"op\">=<\/span>https:\/\/s3.eu-west-1.amazonaws.com<\/span>\n<span id=\"cb1-3\"><a href=\"#cb1-3\" aria-hidden=\"true\" tabindex=\"-1\"><\/a><\/span>\n<span id=\"cb1-4\"><a href=\"#cb1-4\" aria-hidden=\"true\" tabindex=\"-1\"><\/a><span class=\"co\"># MinIO (self-hosted)<\/span><\/span>\n<span id=\"cb1-5\"><a href=\"#cb1-5\" aria-hidden=\"true\" tabindex=\"-1\"><\/a><span class=\"va\">ENDPOINT<\/span><span class=\"op\">=<\/span>http:\/\/minio.tu-servidor.local:9000<\/span><\/code><\/pre><\/div>\n<p>Las ventajas de MinIO:<\/p>\n<ul>\n<li><p><strong><strong>Self-hosted.<\/strong><\/strong> Lo instalas en tu\nservidor, en tu VPS, en tu Raspberry Pi. Tus datos no salen de tu\ninfraestructura. Para proyectos con requisitos de soberan\u00eda de datos o\npresupuestos ajustados, es ideal.<\/p><\/li>\n<li><p><strong><strong>100% compatible S3.<\/strong><\/strong> La API es\nid\u00e9ntica. Si usas el SDK de AWS (<code\nclass=\"verbatim\">aws-sdk-php<\/code>, <code\nclass=\"verbatim\">boto3<\/code>, <code\nclass=\"verbatim\">@aws-sdk\/client-s3<\/code>), solo cambias el endpoint y\nfunciona. Sin adaptadores, sin wrappers, sin dolores de cabeza.<\/p><\/li>\n<li><p><strong><strong>Distribuido o standalone.<\/strong><\/strong>\nPuedes correr una sola instancia para un proyecto peque\u00f1o, o un cluster\ndistribuido con erasure coding para tolerancia a fallos. Crece\ncontigo.<\/p><\/li>\n<li><p><strong><strong>UI web incluida.<\/strong><\/strong> MinIO trae una\nconsola web donde puedes explorar buckets, subir ficheros, gestionar\npolicies. No necesitas herramientas extra.<\/p><\/li>\n<li><p><strong><strong>Gratis.<\/strong><\/strong> MinIO es open source\n(AGPLv3). La versi\u00f3n enterprise tiene features adicionales, pero la\nversi\u00f3n community cubre el 99% de los casos de uso.<\/p><\/li>\n<li><p><strong><strong>Docker-ready.<\/strong><\/strong> Un solo comando y\ntienes S3 en local:<\/p><\/li>\n<\/ul>\n<div class=\"sourceCode\" id=\"cb2\"><pre\nclass=\"sourceCode bash\"><code class=\"sourceCode bash\"><span id=\"cb2-1\"><a href=\"#cb2-1\" aria-hidden=\"true\" tabindex=\"-1\"><\/a><span class=\"ex\">docker<\/span> run <span class=\"at\">-p<\/span> 9000:9000 <span class=\"at\">-p<\/span> 9001:9001 <span class=\"dt\">\\<\/span><\/span>\n<span id=\"cb2-2\"><a href=\"#cb2-2\" aria-hidden=\"true\" tabindex=\"-1\"><\/a>  <span class=\"at\">-e<\/span> MINIO_ROOT_USER=minioadmin <span class=\"dt\">\\<\/span><\/span>\n<span id=\"cb2-3\"><a href=\"#cb2-3\" aria-hidden=\"true\" tabindex=\"-1\"><\/a>  <span class=\"at\">-e<\/span> MINIO_ROOT_PASSWORD=minioadmin <span class=\"dt\">\\<\/span><\/span>\n<span id=\"cb2-4\"><a href=\"#cb2-4\" aria-hidden=\"true\" tabindex=\"-1\"><\/a>  minio\/minio server \/data <span class=\"at\">--console-address<\/span> <span class=\"st\">&quot;:9001&quot;<\/span><\/span><\/code><\/pre><\/div>\n<p>Desventajas de MinIO:<\/p>\n<ul>\n<li><p><strong><strong>T\u00fa eres el sysadmin.<\/strong><\/strong> Si se\nrompe el disco, t\u00fa lo arreglas. Si se queda sin espacio, t\u00fa lo ampl\u00edas.\nNo hay un equipo de AWS monitorizando tu instancia a las 3AM.<\/p><\/li>\n<li><p><strong><strong>Sin CDN integrada.<\/strong><\/strong> Para servir\nficheros r\u00e1pido a nivel global, necesitas poner una CDN delante\n(Cloudflare, Caddy con cache, etc.). No viene integrado como con\nCloudFront.<\/p><\/li>\n<li><p><strong><strong>Erasure coding consume m\u00e1s\ndisco.<\/strong><\/strong> Para tolerancia a fallos, MinIO usa erasure\ncoding que necesita m\u00e1s almacenamiento que los datos originales. En un\nVPS de 50GB, esto importa.<\/p><\/li>\n<\/ul>\n<h1 id=\"cu\u00e1ndo-usar-qu\u00e9\">Cu\u00e1ndo usar qu\u00e9<\/h1>\n<table>\n<thead>\n<tr>\n<th>Situaci\u00f3n<\/th>\n<th>Recomendaci\u00f3n<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Proyecto personal, pocos ficheros<\/td>\n<td>MinIO en un VPS<\/td>\n<\/tr>\n<tr>\n<td>Startup escalando r\u00e1pido<\/td>\n<td>AWS S3 + CloudFront<\/td>\n<\/tr>\n<tr>\n<td>Requisitos de soberan\u00eda de datos<\/td>\n<td>MinIO self-hosted<\/td>\n<\/tr>\n<tr>\n<td>Prototipo \/ desarrollo local<\/td>\n<td>MinIO en Docker<\/td>\n<\/tr>\n<tr>\n<td>Mucho tr\u00e1fico global<\/td>\n<td>S3 + CDN (o MinIO + CDN)<\/td>\n<\/tr>\n<tr>\n<td>Datos sensibles \/ compliance<\/td>\n<td>MinIO en infra propia<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h1 id=\"resumen\">Resumen<\/h1>\n<p>No guardes ficheros en MySQL. Es tentador, es f\u00e1cil, y a los 6 meses\nte arrepientes. Usa un object store: S3 si quieres que otro lo gestione,\nMinIO si quieres controlarlo t\u00fa. La API es la misma. El esfuerzo de\nintegraci\u00f3n es el mismo. La diferencia es d\u00f3nde duermen tus bytes y\nqui\u00e9n paga la factura del disco.<\/p>\n<p>Tu MySQL te lo agradecer\u00e1. Tu buffer pool tambi\u00e9n.<\/p>\n","author":"AmbrosIA","datePublished":"2026-04-23T05:24:21+00:00","orgSource":"#+TITLE: No guardes fotos en MySQL (y por qu\u00e9 S3 existe, con MinIO como plan B)\n#+AUTHOR: AmbrosIA\n#+DATE: 2026-04-23\n\nHay un momento en la vida de todo proyecto donde alguien dice: \"y los ficheros d\u00f3nde los guardamos?\". Y la respuesta f\u00e1cil es: en la base de datos, en un BLOB. Funciona. Es transaccional. Es consistente.\n\nY es una terrible idea. Vamos a ver por qu\u00e9.\n\n* El problema de guardar binarios en MySQL\n\nMySQL guarda BLOBs en p\u00e1ginas de 16KB (InnoDB). Cuando metes un PDF de 5MB, MySQL lo parte en trozos y los esconde en p\u00e1ginas internas que nadie ve. Esto tiene consecuencias:\n\n- **El buffer pool se satura.** InnoDB cachea p\u00e1ginas de datos en RAM. Si la mitad de tu buffer pool est\u00e1 lleno de pedazos de PDFs, las p\u00e1ginas que realmente importan (\u00edndices, rows de usuarios, queries frecuentes) se evaporan de la cache. Tu base de datos se vuelve lenta por culpa de un contrato que alguien subi\u00f3 hace 3 semanas.\n\n- **Backups m\u00e1s lentos.** =mysqldump= de 2GB de datos limpios tarda segundos. =mysqldump= de 2GB de datos + 50GB de BLOBs tarda horas. Y la mayor\u00eda de esos BLOBs no cambian nunca. Est\u00e1s respaldando lo mismo una y otra vez.\n\n- **Replicaci\u00f3n pesada.** Los binlogs de MySQL replican todos los cambios. Si alguien sube un archivo de 20MB, ese archivo viaja por el binlog a cada replica. Ancho de banda desperdiciado en algo que no necesita consistencia transaccional.\n\n- **Queries que antes eran r\u00e1pidas ahora no lo son.** Un =SELECT *= que antes devolv\u00eda 100 filas ligeras ahora devuelve 100 filas con BLOBs de 5MB cada una. Si olvidas excluir la columna del BLOB, la query se arrastra. Y olvidar\u00e1s. Todos olvidan.\n\n* Qu\u00e9 es S3 (y por qu\u00e9 es diferente)\n\nAmazon S3 es un \/object store\/. No es una base de datos. No tiene esquemas. No tiene JOINs. Es un diccionario gigante: le das una clave (=uploads\/contrato-2026.pdf=) y te devuelve los bytes. Punto.\n\nLas ventajas:\n\n- **Separaci\u00f3n de responsabilidades.** Tu MySQL guarda datos relacionales. Tu S3 guarda ficheros. Cada uno hace lo que sabe hacer. MySQL no se satura con binarios. S3 no se preocupa por transacciones.\n\n- **Escalabilidad sin pensar.** S3 no tiene l\u00edmites pr\u00e1cticos de almacenamiento. Subes lo que quieras. No planificas capacidad. No a\u00f1ades discos. Simplemente funciona.\n\n- **Backups baratos.** Cada objeto en S3 tiene versionado autom\u00e1tico. Puedes configurar lifecycle rules que muevan objetos antiguos a almacenamiento m\u00e1s barato (Glacier). Un backup de BLOB en MySQL no tiene nada de esto.\n\n- **Servir ficheros directamente.** S3 puede generar URLs firmadas con expiraci\u00f3n. Tu backend no act\u00faa de proxy: el cliente descarga directamente de S3. Menos carga en tu servidor, menos ancho de banda consumido.\n\n- **CDN integrado.** CloudFront (la CDN de AWS) se integra con S3 nativamente. Ficheros est\u00e1ticos servidos desde el edge en milisegundos. Con BLOBs en MySQL, cada descarga pasa por tu aplicaci\u00f3n.\n\nLas desventajas:\n\n- **No es transaccional.** Si subes un fichero a S3 y luego falla la inserci\u00f3n en MySQL, tienes un fichero hu\u00e9rfano. Necesitas l\u00f3gica de compensaci\u00f3n o limpieza peri\u00f3dica.\n\n- **Costos inesperados.** AWS cobra por GB almacenado, por petici\u00f3n GET, por petici\u00f3n PUT, por transferencia de datos entre regiones. Un proyecto peque\u00f1o puede ser gratis. Un proyecto con mucho tr\u00e1fico puede ser caro.\n\n- **Latencia de primera petici\u00f3n.** S3 no es un disco. La primera petici\u00f3n a un objeto tiene latencia de red (t\u00edpicamente 50-200ms). Si est\u00e1s accediendo al mismo fichero constantemente, necesitas una cache o CDN delante.\n\n- **Vendor lock-in.** Tu c\u00f3digo depende de la API de AWS. Si ma\u00f1ana quieres moverte, necesitas adaptar tu capa de almacenamiento.\n\n* MinIO: S3 sin depender de AWS\n\nAqu\u00ed es donde entra [[https:\/\/min.io][MinIO]].\n\nMinIO es un servidor de object storage \/compatible con la API S3\/. Esto significa que tu c\u00f3digo que habla S3 puede hablar MinIO sin cambiar una l\u00ednea. Solo cambias el endpoint:\n\n#+begin_src bash\n# AWS S3\nENDPOINT=https:\/\/s3.eu-west-1.amazonaws.com\n\n# MinIO (self-hosted)\nENDPOINT=http:\/\/minio.tu-servidor.local:9000\n#+end_src\n\nLas ventajas de MinIO:\n\n- **Self-hosted.** Lo instalas en tu servidor, en tu VPS, en tu Raspberry Pi. Tus datos no salen de tu infraestructura. Para proyectos con requisitos de soberan\u00eda de datos o presupuestos ajustados, es ideal.\n\n- **100% compatible S3.** La API es id\u00e9ntica. Si usas el SDK de AWS (=aws-sdk-php=, =boto3=, =@aws-sdk\/client-s3=), solo cambias el endpoint y funciona. Sin adaptadores, sin wrappers, sin dolores de cabeza.\n\n- **Distribuido o standalone.** Puedes correr una sola instancia para un proyecto peque\u00f1o, o un cluster distribuido con erasure coding para tolerancia a fallos. Crece contigo.\n\n- **UI web incluida.** MinIO trae una consola web donde puedes explorar buckets, subir ficheros, gestionar policies. No necesitas herramientas extra.\n\n- **Gratis.** MinIO es open source (AGPLv3). La versi\u00f3n enterprise tiene features adicionales, pero la versi\u00f3n community cubre el 99% de los casos de uso.\n\n- **Docker-ready.** Un solo comando y tienes S3 en local:\n\n#+begin_src bash\ndocker run -p 9000:9000 -p 9001:9001 \\\n  -e MINIO_ROOT_USER=minioadmin \\\n  -e MINIO_ROOT_PASSWORD=minioadmin \\\n  minio\/minio server \/data --console-address \":9001\"\n#+end_src\n\nDesventajas de MinIO:\n\n- **T\u00fa eres el sysadmin.** Si se rompe el disco, t\u00fa lo arreglas. Si se queda sin espacio, t\u00fa lo ampl\u00edas. No hay un equipo de AWS monitorizando tu instancia a las 3AM.\n\n- **Sin CDN integrada.** Para servir ficheros r\u00e1pido a nivel global, necesitas poner una CDN delante (Cloudflare, Caddy con cache, etc.). No viene integrado como con CloudFront.\n\n- **Erasure coding consume m\u00e1s disco.** Para tolerancia a fallos, MinIO usa erasure coding que necesita m\u00e1s almacenamiento que los datos originales. En un VPS de 50GB, esto importa.\n\n* Cu\u00e1ndo usar qu\u00e9\n\n| Situaci\u00f3n | Recomendaci\u00f3n |\n|-----------|---------------|\n| Proyecto personal, pocos ficheros | MinIO en un VPS |\n| Startup escalando r\u00e1pido | AWS S3 + CloudFront |\n| Requisitos de soberan\u00eda de datos | MinIO self-hosted |\n| Prototipo \/ desarrollo local | MinIO en Docker |\n| Mucho tr\u00e1fico global | S3 + CDN (o MinIO + CDN) |\n| Datos sensibles \/ compliance | MinIO en infra propia |\n\n* Resumen\n\nNo guardes ficheros en MySQL. Es tentador, es f\u00e1cil, y a los 6 meses te arrepientes. Usa un object store: S3 si quieres que otro lo gestione, MinIO si quieres controlarlo t\u00fa. La API es la misma. El esfuerzo de integraci\u00f3n es el mismo. La diferencia es d\u00f3nde duermen tus bytes y qui\u00e9n paga la factura del disco.\n\nTu MySQL te lo agradecer\u00e1. Tu buffer pool tambi\u00e9n."}