Cuando empiezas a desarrollar aplicaciones que van a correr dentro de contenedores como Docker, hay dos principios que marcan la diferencia entre un entorno limpio, mantenible y predecible, y uno lleno de problemas difíciles de depurar: enviar los logs a la salida estándar y usar almacenamiento externo en lugar del disco local del contenedor.
Puede parecer un detalle técnico menor, pero seguir estas dos reglas transforma completamente la forma en que tus aplicaciones se comportan, se escalan y se integran con su entorno. Vamos a ver por qué son tan importantes y cómo aplicarlas con un ejemplo práctico que puedes ejecutar en tu máquina en unos minutos.
Los logs deben ir a stdout: simplifica y estandariza
Una de las primeras tentaciones al crear una app es guardar los logs en un archivo. Pero dentro de un contenedor, eso es un error clásico. Los contenedores son efímeros: hoy existen, mañana se reinician o se eliminan. Y con ellos se van también tus archivos.
En cambio, si mandas los logs directamente a la salida estándar (stdout), todo fluye naturalmente:
Docker ya captura automáticamente lo que sale por
stdout. Puedes ver los logs con solo ejecutardocker logs.En entornos productivos, sistemas como Loki, ELK o Datadog recogen los logs desde ahí. No tienes que hacer nada especial.
Tu responsabilidad se reduce a una sola: emitir los logs. La infraestructura se encarga del resto.
Así, tus logs siempre estarán accesibles, centralizados y listos para analizar, sin configuraciones extra ni archivos que desaparecen.
Los datos persistentes deben vivir fuera del contenedor
El otro error común es guardar archivos (PDFs, imágenes, etc.) dentro del contenedor. Parece práctico, pero en cuanto el contenedor se reinicia o se reemplaza, todo se pierde.
La solución es tan sencilla como robusta: usa un almacenamiento externo. Un gran ejemplo es MinIO, una alternativa local a S3 que puedes usar incluso en desarrollo. Así, tus datos se guardan de forma persistente y accesible desde cualquier instancia, sin importar cuántas veces levantes o bajes el contenedor.
Un ejemplo completo con Docker Compose
Veamos un ejemplo práctico que combina ambos principios. Vamos a crear una pequeña aplicación en Node.js que genera un texto, lo sube a un bucket en MinIO y envía sus logs por stdout. La estructura del proyecto será la siguiente:
my-app/
├── app/
│ ├── index.js
│ ├── package.json
├── Dockerfile
├── docker-compose.yml
Dentro de la carpeta app, el archivo package.json define las dependencias básicas:
{
"name": "my-app",
"version": "1.0.0",
"dependencies": {
"express": "^4.18.2",
"minio": "^7.0.32"
}
}
El corazón de la aplicación está en index.js, donde se crea un servidor Express, se configura el cliente de MinIO y se define un endpoint que genera un archivo de texto y lo sube al bucket correspondiente. Cada paso se registra en consola, cumpliendo la primera regla de enviar los logs a stdout:
const express = require('express');
const Minio = require('minio');
const app = express();
const minioClient = new Minio.Client({
endPoint: 'minio',
port: 9000,
useSSL: false,
accessKey: 'minioadmin',
secretKey: 'minioadmin',
});
app.get('/save-text', async (req, res) => {
try {
const text = `Hola desde mi app - ${Date.now()}`;
const fileName = `text-${Date.now()}.txt`;
await minioClient.putObject('my-bucket', fileName, Buffer.from(text));
console.log(`Archivo ${fileName} subido a MinIO`);
res.send(`Archivo guardado como ${fileName}`);
} catch (error) {
console.error('Error subiendo archivo:', error.message);
res.status(500).send('Error al guardar archivo');
}
});
app.listen(3000, () => console.log('App corriendo en puerto 3000'));
Para construir la imagen de la aplicación, el Dockerfile usa una base ligera de Node.js y copia el código dentro del contenedor:
FROM node:18-alpine
WORKDIR /app
COPY app/package.json .
RUN npm install
COPY app .
CMD ["node", "index.js"]
Finalmente, docker-compose.yml define los servicios: la aplicación y MinIO, junto con sus puertos, credenciales y volumen persistente para almacenar los datos de forma externa al contenedor.
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
depends_on:
- minio
minio:
image: minio/minio:latest
ports:
- "9000:9000"
- "9001:9001"
environment:
- MINIO_ROOT_USER=minioadmin
- MINIO_ROOT_PASSWORD=minioadmin
command: server /data --console-address ":9001"
volumes:
- minio-data:/data
volumes:
minio-data:
Con los archivos creados, basta ejecutar docker-compose up -d para levantar el entorno. Luego, puedes crear un bucket en MinIO con:
curl -X PUT http://localhost:9000/my-bucket -H "x-amz-acl: public-read-write"
o acceder desde el panel web en http://localhost:9001 con las credenciales minioadmin / minioadmin.
Después de eso, genera un archivo con:
curl http://localhost:3000/save-text
y revisa los logs de la aplicación con:
docker logs <id-del-contenedor-app>
Verás algo como:
App corriendo en puerto 3000
Archivo text-1696641234567.txt subido a MinIO
Si miras en tu bucket my-bucket, el archivo estará allí, persistente y accesible aunque elimines y recrees el contenedor.
Enviar los logs a stdout y usar almacenamiento externo no son simples buenas prácticas: son los cimientos que permiten que las aplicaciones en contenedores sean mantenibles, portátiles y confiables. Adoptarlas desde el principio evita errores difíciles de rastrear y te prepara para desplegar en entornos reales sin sorpresas.