Profundizamos en n8n, tipos de nodos, buenas …
En el capítulo anterior repasamos qué es la automatización de procesos y por qué herramientas como …
leer másDomina el ciclo de vida de tus contenedores, redes y volúmenes antes de dar el salto a producción.
Si has seguido la serie hasta aquí, ya has recorrido un camino nada despreciable. Empezamos con el Capítulo 1, donde descubrimos qué es una imagen, creamos nuestros primeros contenedores con docker run
y aprendimos a limpiarlos. En el Capítulo 2 nos metimos de lleno en la administración básica: comandos como ps, logs, stop e inspect dejaron de sonarnos a chino y empezamos a sentirnos cómodos moviéndonos por el universo Docker. El viaje continuó con el Capítulo 3, en el que dockerizamos una API Node/Express: escribimos nuestro primer Dockerfile, compilamos la imagen y desplegamos la aplicación sin miedo a “en mi máquina funciona”. Y en el Capítulo 4 dimos un salto cualitativo al introducir Docker Compose: añadimos PostgreSQL y Redis, levantamos todo con un único docker compose up
y, sobre la marcha, prometimos que más adelante abriríamos la famosa “letra pequeña” de Docker.
Ha llegado ese momento. Antes de aventurarnos en temas un poco más avanzados —optimización de imágenes, despliegues en cloud u orquestación— necesitamos entender cómo Docker gestiona realmente los recursos fundamentales que sostienen tu infraestructura:
Recurso | Qué es | Por qué importa |
---|---|---|
Imágenes | Una instantánea inmutable de tu aplicación: incluye tu código, sus dependencias y un mini‑sistema operativo minimalista. Se construye por capas; si varias imágenes comparten capas, Docker las reutiliza. | Actúan como la “receta exacta” que garantiza que tu app se comporte igual en cualquier entorno. Cuanto más pequeñas sean, menos tardará tu CI/CD en enviarlas al registro y tus servidores en arrancarlas. |
Contenedores | Un proceso que se lanza a partir de una imagen y corre en aislamiento ligero (namespaces + cgroups). Para él, parece que tiene su propio sistema de archivos, pero en realidad es una capa de lectura y escritura (RW) temporal sobre la imagen. | Puedes crearlos, detenerlos y desecharlos en segundos — ideal para pruebas rápidas. Sin embargo, todo lo que escribas dentro se borra al destruirlo, salvo que uses volúmenes. |
Volúmenes | Carpetas persistentes que Docker monta dentro de los contenedores. Hay dos tipos principales: bind mounts (tú eliges la ruta exacta del host) y named volumes (Docker los gestiona en /var/lib/docker/volumes). | Aquí viven los datos que no deben perderse: bases de datos, subidas de usuarios, backups… Separar datos y contenedor te permite desplegar nuevas versiones sin tocar la información. |
Redes | Conexiones virtuales (bridges) que Docker crea para que los contenedores se descubran por nombre y se comuniquen sin chocar con otros servicios del host. Cada red ofrece DNS interno y su propia subred. | Evitan conflictos de puertos y te permiten aislar entornos (dev, test, prod) en la misma máquina. Además son tu primera línea de seguridad: si un contenedor no está en la red, simplemente “no existe” para los demás. |
A lo largo del capítulo iremos siguiendo un pequeño ejemplo práctico que funcionará como laboratorio de pruebas. Con él veremos, paso a paso, qué ocurre cuando detenemos, eliminamos o recreamos contenedores y recursos: los registros de Nginx se quedarán a salvo en un bind mount, la base de datos vivirá en un volumen persistente y la caché demostrará qué significa guardar los datos solo en memoria.
Cada bloque te presentará un concepto y, acto seguido, te pedirá uno o dos comandos para que veas el efecto en tiempo real. Nada de teoría suspendida en el aire: aprenderás tocando teclas.
En concreto, descubrirás cómo:
En concreto, descubrirás cómo:
¿Listo para ensuciarte las manos? Vamos a crear y explicar la estructura de archivos, luego empezaremos a experimentar.
Antes de profundizar, echemos un vistazo a los archivos que conforman nuestro entorno. Esta es la fotografía inicial:
.
├── docker-compose.yml # orquesta todos los servicios
├── api/ # código de la API Node/Express
│ ├── Dockerfile
│ ├── package.json
│ └── server.js
├── db/ # scripts para inicializar PostgreSQL
│ └── init.sql
├── nginx/ # proxy inverso
│ └── nginx.conf
└── logs/ # se crea en tiempo de ejecución (bind mount)
├── api/
└── nginx/
Para este ejemplo utilizaremos seis archivos de configuración, son los siguientes:
.env
— variables de entorno de ejemplo
# .env
POSTGRES_USER=labuser
POSTGRES_PASSWORD=labsupersecret
POSTGRES_DB=labdb
# otras variables que usa la API
PGHOST=db
PGPORT=5432
PGUSER=$POSTGRES_USER
PGPASSWORD=$POSTGRES_PASSWORD
PGDATABASE=$POSTGRES_DB
REDIS_HOST=cache
REDIS_PORT=6379
NODE_ENV=development
⚠ Advertencia
Este archivo está pensado solo para desarrollo: incluye contraseñas en texto plano y valores que no deberías exponer en producción.
En un entorno real utilizarías proveedores de secretos (Docker Secrets, HashiCorp Vault, variables de entorno del orquestador…) y generarías credenciales únicas por entorno.
docker-compose.yml
— la pieza central
version: "3.9"
services:
api:
build: ./api
container_name: lab_api
env_file: .env
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
volumes:
- ./logs/api:/usr/src/app/logs
networks:
- backend
- frontend
ports:
- "3000:3000"
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/health',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))"]
interval: 30s
retries: 3
db:
image: postgres:15
container_name: lab_db
env_file: .env
volumes:
- db-data:/var/lib/postgresql/data
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER"]
interval: 10s
retries: 5
networks:
- backend
cache:
image: redis:7
container_name: lab_cache
tmpfs:
- /data
networks:
- backend
nginx:
image: nginx:1.27-alpine
container_name: lab_proxy
ports:
- "80:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./logs/nginx:/var/log/nginx
depends_on:
- api
networks:
- frontend
volumes:
db-data:
networks:
backend:
driver: bridge
internal: true
frontend:
driver: bridge
Hay varios puntos clave a tener en cuenta en este compose
:
volumes:
declara db-data, un named volume que persiste la base de datos aunque borres el contenedor db
../logs/api
guarda los registros de la API directamente en tu máquina.backend
es internal: true
; ni Nginx ni tu host pueden acceder a PostgreSQL o Redis por error.healthcheck
evita que la API acepte tráfico antes de que PostgreSQL esté operativo.api/
— para comunicarnos con los servicios internosDockerfile
FROM node:lts-slim
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
node:lts-slim
para mantener la imagen ligera.package*.json
para aprovechar la caché de capas.package.json
{
"name": "docker-resources-api",
"version": "1.0.0",
"description": "Mini API para el laboratorio de gestión de recursos Docker",
"main": "server.js",
"type": "module",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^5.1.0",
"pg": "^8.16.3",
"ioredis": "^5.6.1"
}
}
server.js
El archivo api/server.js
es el “cerebro” de nuestro ejemplo práctico. Aglutina las rutas HTTP que usaremos para demostrar cómo viven los datos en Docker y, de paso, ilustra la conexión a dos servicios internos: Redis (memoria) y PostgreSQL (disco).
import express from 'express';
import pkg from 'pg';
import Redis from 'ioredis';
const { Pool } = pkg;
const app = express();
const pool = new Pool(); // se conecta a db:5432 por DNS interno
const redis = new Redis({ host: 'cache' });
app.get('/health', (_, res) => res.send('OK'));
app.get('/visits', async (_, res) => {
const visits = await redis.incr('counter');
res.json({ visits });
});
app.get('/pgvisits', async (_, res) => {
const { rows } = await pool.query(
'UPDATE pg_visits SET counter = counter + 1 WHERE id = 1 RETURNING counter;'
);
res.json({ pgVisits: rows[0].counter });
});
// server.js
app.get('/', (_, res) =>
res.send(`
<h1>Docker Resources API</h1>
<p>Rutas disponibles:</p>
<ul>
<li><a href="/visits">/visits</a> – contador Redis</li>
<li><a href="/pgvisits">/pgvisits</a> – contador PostgreSQL</li>
<li><a href="/health">/health</a> – health‑check</li>
</ul>
`)
);
app.listen(3000, () =>
console.log('API listening on http://localhost:3000')
);
El cliente no necesita saber los puertos internos: Docker resuelve los nombres de servicio (
db
,cache
) y la red frontend expone solo el puerto 80 del proxy Nginx.
Ruta | Propósito | Dónde se almacenan los datos | Por qué es útil en el capítulo |
---|---|---|---|
GET /health |
Devuelve OK si la API responde. Usado por el health‑check de Docker Compose. |
— | Verás cómo Docker decide si el contenedor está “healthy”. |
GET / |
Página HTML mínima con enlaces a los demás endpoints. | — | Punto de entrada rápido desde el navegador. |
GET /visits |
Incrementa la clave counter en Redis y devuelve el valor. |
Redis dentro de lab_cache (montaje tmpfs ) → no persiste. |
Demuestra qué pasa al reiniciar el servicio cache : el contador vuelve a 1. |
GET /pgvisits |
Incrementa la columna counter en la tabla pg_visits . |
PostgreSQL sobre el volumen db-data . |
Muestra que los datos sobreviven aunque borres y recrees el contenedor lab_db . |
db/
— script de inicialización de la base de datos
CREATE TABLE IF NOT EXISTS pg_visits (
id SERIAL PRIMARY KEY,
counter INT NOT NULL
);
INSERT INTO pg_visits (counter) VALUES (0);
db-data
está vacío.nginx/
— proxy inverso
http {
upstream api_upstream { server api:3000; }
server {
listen 80;
location / {
proxy_pass http://api_upstream;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
}
}
}
Con esta infraestructura clara, estás listo para explorar imágenes, contenedores, volúmenes y redes en las siguientes secciones.
Partiremos con los conceptos más básicos—y a la vez más frecuentes—: qué es una imagen, qué es un contenedor y cómo se comportan cuando los utilizamos.
docker compose up -d
# ¿Qué contenedores están vivos?
docker compose ps
# ¿Y qué imágenes hay disponibles?
docker image ls
Recuerda
Una imagen es solo‑lectura; un contenedor añade una capa RW encima que desaparece al destruirlo condocker rm
.
# Detén solo la API (la BD y Redis siguen vivos)
docker compose stop api
# Vuelve a levantarla
docker compose start api
La API recupera sin problema su estado porque no guardaba datos en el contenedor: los logs están en el bind mount ./logs/api
y la información persistente en db-data
o en la RAM de Redis.
Para ilustrar los diferentes tipos de almacenamiento de datos en Docker, vamos a ver varios mini‑experimentos que muestran cómo cambian (o no) según el tipo utilizado.
Por qué lo hacemos
Comprobar que los datos guardados solo en RAM se pierden al reiniciar el servicio.
Pasos
# 1 – consulta el contador
curl -s http://localhost/visits
# 2 – reinicia solo Redis
docker compose restart cache
# 3 – vuelve a consultar
curl -s http://localhost/visits
Resultado esperado
El contador vuelve a 1 → Redis estaba usando tmpfs
; al reiniciar se borró todo.
Por qué lo hacemos
Ver que un volumen nombrado persiste incluso si el contenedor se destruye o reinicia.
Pasos
# 1 - incrementa el contador varias veces
curl -s http://localhost/pgvisits
curl -s http://localhost/pgvisits
# 2 – reinicia Postgres
docker compose restart db
# 3 – comprueba de nuevo
curl -s http://localhost/pgvisits
Resultado esperado
El número sigue aumentando → la tabla vive en el volumen db-data
. Los datos persisten entre sesiones.
Por qué lo hacemos
Ver que Docker recrea la carpeta montada si no existe al arrancar el contenedor.
Pasos
# 1 – quita el contenedor del proxy
docker compose rm -sf nginx
# 2 – borra la carpeta de logs en el host
rm -rf logs/nginx
# 3 – vuelve a lanzar Nginx
docker compose up -d nginx
Resultado esperado
La carpeta logs/nginx
y los archivos access.log
/ error.log
se crean otra vez en tu host. Al simplemente haber reiniciado el contenedor con la API, los datos de visitas de Redis y PostgreSQL siguen intactos.
Antes de que un byte de información viaje de un contenedor a otro, debe haber una red que los conecte. En Docker las redes actúan como pequeñas LAN virtuales: determinan qué servicios “se ven” entre sí, qué puertos se expone al exterior y qué tráfico queda completamente aislado. Si dos contenedores no comparten red, es como si vivieran en máquinas distintas.
Objetivo
Saber cuántas redes ha creado Docker Compose, qué contenedores cuelgan de cada una y si la red back-end está realmente aislada del host.
# Muestra todas las redes del sistema
docker network ls
# Inspeccionando la red podemos ver su configuración
docker network inspect dockerresources_backend
# Ver si es interna (no accesible desde el host)
docker network inspect dockerresources_backend | grep "Internal"
backend es privada (internal: true
); el host no puede acceder.
Objetivo
Ver qué pasa cuando una aplicación pierde la conexión con otro servicio porque están desconectadas o en redes distintas.
# 1 – Averigua el nombre exacto de la red backend
docker network ls | grep _backend
# 2 – Desconecta el contenedor de Redis
docker network disconnect dockerresources_backend lab_cache
# 3 – Llama al contador volátil
curl -s --max-time 2 http://localhost/visits # ⟶ timeout
# 4 – Re‑conecta Redis
docker network connect dockerresources_backend lab_cache
# 5 – Inténtalo de nuevo
curl -s http://localhost/visits # ⟶ vuelve a contar
En este bloque vamos a borrar absolutamente todos los recursos que hemos creado en el laboratorio —contenedores, imágenes, volúmenes y redes—para que veas qué sobrevive y qué no.
Requisito: En caso de querer conservar el estado actual del entorno hasta este punto, asegúrate de hacer un backup de los volúmenes y aquello que quieras conservar.
# Detén y elimina todos los contenedores del proyecto
docker compose down
# Borra la imagen construida de la API
docker rmi docker-resources-api 2>/dev/null || true
# (Opcional) Borra las imágenes base si no las usas en otros proyectos
docker rmi node:lts-slim postgres:15 redis:7 nginx:1.27-alpine 2>/dev/null || true
# Elimina el volumen con datos de Postgres
docker volume rm dockerresources_db-data
# Elimina las redes creadas por Compose (si siguen ahí)
docker network rm dockerresources_backend dockerresources_frontend 2>/dev/null || true
En este punto tu proyecto está “vacío”: solo queda el código y los ficheros de configuración de los servicios Docker. Si levantamos el servicio de nuevo, éste creará todo de nuevo junto a una imagen fresca de la API.
Para terminar, hagamos balance. En este capítulo hemos abierto la tapa del “motor” de Docker y hemos jugado con todos sus engranajes principales: creamos imágenes ligeras con un Dockerfile
, vimos cómo los contenedores añaden una fina capa de lectura-escritura que puede borrarse sin piedad, descubrimos que los datos importantes deben vivir en volúmenes y comprobamos que las redes son autopistas internas que puedes desconectar y reconectar en caliente. Además, también vimos cómo identificar rápidamente qué hay corriendo en tu host, a limpiar recursos huérfanos sin desmontar el entorno, etc; todo ello utilizando comandos reales simulando un entorno funcional. Para próximos capítulos seguiremos ahondando en temas más avanzados. Happy Coding!
Quizá te puedan interesar
En el capítulo anterior repasamos qué es la automatización de procesos y por qué herramientas como …
leer másEn esta serie hemos explorado diversos motores de bases de datos, como SQLite, y los conceptos …
leer másHabiendo explorado los fundamentos de Docker y su poder para desplegar aplicaciones encapsuladas, …
leer másDe concepto a realidad