Las aplicaciones no guardan todo en la base de datos: imágenes, documentos, los ficheros que suben los usuarios. Ese material pertenece al almacenamiento de objetos, cuyo estándar de facto es la API de S3 de AWS: la hablan todas las nubes y los SDK de todas las plataformas. Para el desarrollo se ejecuta esa API en local con MinIO, un servidor compatible desplegado con Docker (tema 02).
Un servicio de objetos guarda objetos en buckets:
miapp): la unidad de organización y de permisos.documentos/2026/libro.pdf): parece una ruta con carpetas, pero es una cadena, no un directorio.
La aplicación habla con la API; el navegador solo con URLs: directas si el bucket es público, o prefirmadas (sección 3).
| Sistema de ficheros | Almacenamiento de objetos | |
|---|---|---|
| Unidad | Fichero en un árbol de carpetas | Objeto con clave plana dentro de un bucket |
| Acceso | Rutas locales del sistema | API HTTP por red |
| Metadatos | Los del sistema operativo | Los que lleve cada objeto |
| Edición | Los ficheros se modifican | Los objetos se sobrescriben |
| Escalado | Un servidor, su disco | Distribuido en muchos nodos (pensado para nube) |
S3 (*Simple Storage Service*) es el servicio de objetos de AWS. Su API es el estándar de facto: la implementan las demás nubes y servidores compatibles como MinIO, y el mismo código del SDK funciona contra cualquiera de ellos cambiando la configuración (endpoint y credenciales), nunca el programa.
Las operaciones básicas son pocas:
| Operación | Qué hace | Como |
|---|---|---|
| CreateBucket | crea el bucket | mkdir |
| PutObject | sube datos y metadatos bajo una clave | cp |
| GetObject | descarga los datos de una clave | cat |
| HeadObject | solo los metadatos (tipo, tamaño, fecha) | stat |
| DeleteObject | borra el objeto | rm |
| ListObjectsV2 | lista las claves del bucket, filtradas por prefijo | ls |
La autenticación usa credenciales access key / secret key: el SDK firma cada petición con ellas. No hay sesión ni token: la firma viaja en la petición y las credenciales solo las conoce la aplicación, que las recibe como configuración, nunca en el código.
Cada bucket define una política de acceso para sus objetos:
https://servidor/bucket/clave sirve el objeto a cualquiera. Es el modo de portadas, avatares, CSS y demás ficheros que solo se descargan; escribir sigue exigiendo credenciales.Una URL prefirmada (*presigned URL*) la genera la aplicación con sus credenciales: lleva incrustadas la firma y una expiración. Quien la recibe descarga el objeto sin credenciales durante ese tiempo; al caducar, la URL vuelve a dar 403. Es la forma de compartir un objeto privado sin exponer credenciales ni hacer copias.
La tentación inicial es un bucket por sección de la aplicación (books, movies…). Lo idiomático es lo contrario: la organización por tipo de contenido se resuelve con prefijos, y los buckets se reservan para configuraciones distintas.
documentos/, portadas/). Datos con una configuración diferente, como ficheros privados de la aplicación frente a imágenes públicas: otro bucket.
MinIO es un servidor open source compatible con la API S3: un binario único que arranca en un segundo con Docker. En desarrollo simula AWS sin coste ni red; el código del SDK escrito contra MinIO funciona en producción contra S3 cambiando el endpoint y las credenciales, que viajan en configuración.
$ docker run -d --name minio \
-p 9000:9000 -p 9001:9001 \
-e MINIO_ROOT_USER=admin \
-e MINIO_ROOT_PASSWORD=secreto123 \
-v minio-data:/data \
elestio/minio server /data --console-address ":9001"
9000: la API S3, la que consumen el SDK y mc.9001: la consola web, para trabajar desde el navegador (http://localhost:9001).minio-data guarda los objetos fuera del contenedor.elestio/minio replica en Docker Hub las compilaciones oficiales de MinIO, cuya imagen original ya no se publica allí y se descarga desde quay.io.
La consola web permite crear buckets, subir y descargar ficheros, fijar la política de acceso de cada bucket y crear access keys para las aplicaciones (el usuario root del arranque no se usa en el código).
Para línea de comandos, mc (MinIO Client):
$ mc alias set local http://localhost:9000 admin secreto123 # conectar $ mc mb local/miapp # crear el bucket $ mc cp libro.pdf local/miapp/documentos/libro.pdf # subir $ mc ls local/miapp # listar
Con dos comandos más se practica el acceso público y prefirmado de la sección 3:
$ mc anonymous set download local/miapp # abre http://localhost:9000/miapp/documentos/libro.pdf en el navegador $ mc share download --expire 5m local/miapp/documentos/libro.pdf # URL prefirmada: funciona 5 minutos y luego da 403
El SDK de AWS apunta a MinIO con dos ajustes: el endpoint local y el acceso por ruta (path-style), que es como funciona MinIO:
S3Client s3 = S3Client.builder()
.endpointOverride(URI.create("http://localhost:9000"))
.region(Region.US_EAST_1)
.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create("admin", "secreto123")))
.forcePathStyle(true) // URL http://host:9000/bucket/clave
.build();
s3.putObject(PutObjectRequest.builder()
.bucket("miapp")
.key("documentos/libro.pdf")
.build(),
RequestBody.fromFile(Path.of("libro.pdf")));
El mismo SDK genera URLs prefirmadas desde el código: una vez creado un S3Presigner con la misma configuración que el S3Client, la petición presignada devuelve la URL con expiración:
URL url = presigner.presignGetObject(GetObjectPresignRequest.builder()
.signatureDuration(Duration.ofMinutes(10))
.getObjectRequest(GetObjectRequest.builder()
.bucket("miapp").key("documentos/libro.pdf").build())
.build())
.url();
endpoint, no el programa.
En el siguiente tema, el almacén de objetos entra en la arquitectura de la aplicación como un servicio más del compose.yaml.