Has aprendido qué es Docker, lo has instalado, conoces su arquitectura, manejas sus comandos, entiendes las imágenes y has creado tus primeros contenedores. Todo eso son herramientas. Lo que falta es el problema que justifica usarlas, y esta lección te lo va a poner delante con nombres, apellidos y código real. Vas a conocer a Aurora Libros S.L., una librería online ficticia que será el hilo conductor de los seis módulos restantes; verás su arquitectura objetivo, leerás el código completo de su API con Node.js 22, PostgreSQL 16 y Redis 7, y —lo más importante— intentarás ejecutarlo sin Docker. Vas a contar los pasos manuales que exige, vas a ver los errores que aparecen y vas a medir el coste real del onboarding de un desarrollador nuevo. Cuando llegues a la última sección, la hoja de ruta del curso ya no te parecerá un temario: te parecerá un plan de rescate.

Contenido

  1. Quién es Aurora Libros S.L. y qué necesita
  2. La arquitectura objetivo
  3. Estructura del repositorio
  4. El código de la API: package.json
  5. El código de la API: server.js
  6. La base de datos: db/init.sql
  7. La web estática: web/index.html
  8. Ejecutando la API sin Docker: el calvario
  9. El recuento del onboarding
  10. La hoja de ruta del curso

  1. Quién es Aurora Libros S.L. y qué necesita

Aurora Libros S.L. es una librería independiente de Valencia que vende por Internet desde hace tres años. Empezaron con una tienda hecha por un freelance y ahora tienen un equipo pequeño: dos desarrolladoras, un desarrollador junior recién incorporado y una persona que se ocupa de la infraestructura a media jornada.

Su situación actual:

  • La web y la API corren en un único servidor alquilado, configurado a mano hace dos años. Nadie sabe reconstruirlo si se estropea.
  • El despliegue consiste en conectarse por SSH, hacer git pull y reiniciar el proceso a mano. Se hace los martes por la mañana "por si acaso".
  • Cada desarrollador tiene su entorno local montado de forma distinta. Una usa PostgreSQL 14 instalado con apt, otra la 16 de Homebrew en su Mac, y el junior lleva tres días intentando dejar su máquina lista sin conseguirlo.
  • No hay entorno de pruebas: se prueba en local y se cruza los dedos.
  • Hace un mes, una actualización del sistema del servidor cambió la versión de Node y la API dejó de arrancar. Estuvieron cuatro horas caídos.

Lo que necesitan es concreto y no tiene nada de exótico:

  1. Que cualquier desarrollador pueda tener toda la plataforma corriendo en local en minutos, no en días.
  2. Que el entorno de desarrollo, el de pruebas y el de producción sean el mismo.
  3. Que desplegar sea repetible y reversible, no un ritual manual.
  4. Que puedan escalar la API en campañas (Sant Jordi, Navidad) sin rehacer nada.
  5. Que la configuración esté versionada junto al código, no en la cabeza de una persona.

Es exactamente el catálogo de problemas de la lección 01-01, con caras concretas. Y es exactamente lo que Docker resuelve.

  1. La arquitectura objetivo

Al final del curso, la plataforma de Aurora Libros tendrá cuatro piezas, cada una en su contenedor:

flowchart TB
    USER["Usuario<br/>navegador"]

    subgraph PLAT["Plataforma Aurora Libros"]
        WEB["aurora-web<br/>Nginx<br/>web estática + proxy inverso<br/>puerto 80"]
        API["aurora-api<br/>Node.js 22 + Express<br/>/salud · /libros · /libros/:id<br/>puerto 3000"]
        CACHE["aurora-cache<br/>Redis 7<br/>caché del catálogo<br/>puerto 6379"]
        DB[("aurora-db<br/>PostgreSQL 16<br/>tabla libros<br/>puerto 5432")]
    end

    USER -->|"HTTP :80"| WEB
    WEB -->|"/api/* → proxy"| API
    API -->|"consulta cacheada"| CACHE
    API -->|"SQL"| DB

Qué hace cada pieza y por qué existe:

Servicio Tecnología Responsabilidad Expuesto al exterior
aurora-web Nginx Sirve el HTML, CSS e imágenes de la tienda y reenvía las llamadas /api/* a la API , es la puerta de entrada
aurora-api Node.js 22 + Express Lógica de negocio y API REST del catálogo No directamente; solo a través de aurora-web
aurora-cache Redis 7 Guarda en memoria las consultas más frecuentes del catálogo para no golpear la base de datos No
aurora-db PostgreSQL 16 Almacena el catálogo de libros de forma persistente No

Fíjate en la última columna, porque es una decisión de diseño importante que ya puedes entender con lo aprendido en la lección 01-06: solo aurora-web publicará puertos al exterior. La base de datos y la caché serán accesibles únicamente desde dentro de la red interna de contenedores. Eso reduce drásticamente la superficie de ataque, y es trivial de conseguir con Docker: simplemente no se usa -p en esos servicios.

Los endpoints de la API serán tres:

Endpoint Método Qué devuelve
/salud GET Estado del servicio y de sus dependencias. Sirve para health checks
/libros GET El catálogo completo, con caché en Redis
/libros/:id GET Un libro concreto por su identificador

  1. Estructura del repositorio

Crea esta estructura en tu máquina, porque la usarás durante todo el curso:

aurora-libros/
├── api/
│   ├── package.json
│   └── server.js
├── db/
│   └── init.sql
└── web/
    └── index.html
mkdir -p ~/aurora-libros/api ~/aurora-libros/db ~/aurora-libros/web
cd ~/aurora-libros

Tres carpetas, una por pieza que aporta código propio (la caché Redis no necesita ninguna). A lo largo del curso irás añadiendo ficheros aquí: Dockerfile en el módulo 2, .dockerignore, compose.yaml en el módulo 4, y ficheros de despliegue en el 6.

  1. El código de la API: package.json

Crea ~/aurora-libros/api/package.json:

{
  "name": "aurora-api",
  "version": "1.0.0",
  "description": "API REST del catálogo de Aurora Libros S.L.",
  "main": "server.js",
  "type": "commonjs",
  "engines": {
    "node": ">=22.0.0"
  },
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^4.21.2",
    "pg": "^8.13.1",
    "redis": "^4.7.0"
  }
}

Qué declara cada bloque:

  • name y version: identifican el paquete. La versión 1.0.0 será la primera etiqueta de la imagen que construyas en el módulo 2.
  • main: el fichero de entrada.
  • type: "commonjs": usaremos require() en lugar de import. Es la forma más compatible y evita distracciones.
  • engines.node: ">=22.0.0": este campo es clave para la lección. Declara que la aplicación necesita Node 22 o superior. Con Docker, esa exigencia se cumple sola; sin Docker, es responsabilidad de cada máquina.
  • scripts.start: cómo arrancar la aplicación.
  • dependencies: tres librerías. express es el framework web; pg el cliente de PostgreSQL; redis el cliente de Redis. Cada una de ellas implica un servicio externo que debe existir y estar accesible.

  1. El código de la API: server.js

Crea ~/aurora-libros/api/server.js:

const express = require('express');
const { Pool } = require('pg');
const { createClient } = require('redis');

// --- Configuración desde variables de entorno ---
// Nunca se escriben valores fijos: cada entorno (local, pruebas, producción)
// aporta los suyos. Los valores por defecto solo facilitan el desarrollo.
const PORT = process.env.PORT || 3000;
const DB_HOST = process.env.DB_HOST || 'localhost';
const DB_PORT = process.env.DB_PORT || 5432;
const DB_USER = process.env.DB_USER || 'aurora';
const DB_PASSWORD = process.env.DB_PASSWORD || 'aurora';
const DB_NAME = process.env.DB_NAME || 'aurora_libros';
const REDIS_HOST = process.env.REDIS_HOST || 'localhost';
const REDIS_PORT = process.env.REDIS_PORT || 6379;

const app = express();

// --- Conexión a PostgreSQL ---
const pool = new Pool({
  host: DB_HOST,
  port: Number(DB_PORT),
  user: DB_USER,
  password: DB_PASSWORD,
  database: DB_NAME,
  max: 10,
  connectionTimeoutMillis: 5000,
});

// --- Conexión a Redis ---
const cache = createClient({ url: `redis://${REDIS_HOST}:${REDIS_PORT}` });
cache.on('error', (err) => console.error('[cache] error:', err.message));

const TTL_CACHE = 60; // segundos que se mantiene el catálogo cacheado

// --- GET /salud : estado del servicio y sus dependencias ---
app.get('/salud', async (req, res) => {
  const estado = { servicio: 'aurora-api', version: '1.0.0', db: 'ko', cache: 'ko' };
  try {
    await pool.query('SELECT 1');
    estado.db = 'ok';
  } catch (err) {
    estado.errorDb = err.message;
  }
  try {
    await cache.ping();
    estado.cache = 'ok';
  } catch (err) {
    estado.errorCache = err.message;
  }
  const todoOk = estado.db === 'ok' && estado.cache === 'ok';
  res.status(todoOk ? 200 : 503).json(estado);
});

// --- GET /libros : catálogo completo, con caché ---
app.get('/libros', async (req, res) => {
  try {
    const cacheado = await cache.get('libros:todos');
    if (cacheado) {
      return res.json({ origen: 'cache', libros: JSON.parse(cacheado) });
    }
    const { rows } = await pool.query(
      'SELECT id, titulo, autor, isbn, precio FROM libros ORDER BY titulo'
    );
    await cache.setEx('libros:todos', TTL_CACHE, JSON.stringify(rows));
    res.json({ origen: 'db', libros: rows });
  } catch (err) {
    console.error('[/libros] error:', err.message);
    res.status(500).json({ error: 'No se pudo obtener el catálogo', detalle: err.message });
  }
});

// --- GET /libros/:id : un libro concreto ---
app.get('/libros/:id', async (req, res) => {
  const id = Number(req.params.id);
  if (!Number.isInteger(id) || id < 1) {
    return res.status(400).json({ error: 'El identificador debe ser un entero positivo' });
  }
  try {
    const { rows } = await pool.query(
      'SELECT id, titulo, autor, isbn, precio FROM libros WHERE id = $1',
      [id]
    );
    if (rows.length === 0) {
      return res.status(404).json({ error: 'Libro no encontrado' });
    }
    res.json(rows[0]);
  } catch (err) {
    console.error('[/libros/:id] error:', err.message);
    res.status(500).json({ error: 'Error consultando el libro', detalle: err.message });
  }
});

// --- Arranque ---
async function arrancar() {
  await cache.connect();
  app.listen(PORT, '0.0.0.0', () => {
    console.log(`[aurora-api] escuchando en el puerto ${PORT}`);
    console.log(`[aurora-api] base de datos: ${DB_HOST}:${DB_PORT}/${DB_NAME}`);
    console.log(`[aurora-api] caché: ${REDIS_HOST}:${REDIS_PORT}`);
  });
}

arrancar().catch((err) => {
  console.error('[aurora-api] fallo al arrancar:', err.message);
  process.exit(1);
});

Vamos por partes, porque hay decisiones aquí que serán importantes en módulos posteriores.

El bloque de configuración. Cada parámetro se lee de process.env, con un valor por defecto. Esto es fundamental: la aplicación no sabe dónde está su base de datos hasta que alguien se lo dice. Hoy, en tu máquina, DB_HOST será localhost; en el módulo 4, cuando todo esté en contenedores, será aurora-db, el nombre del servicio. El mismo código, sin tocar una línea, servirá para los dos escenarios. Esa es la razón de que las variables de entorno sean el mecanismo estándar de configuración en contenedores (lección 04-05).

El pool de PostgreSQL. Pool mantiene un conjunto de conexiones reutilizables en lugar de abrir una por petición. connectionTimeoutMillis: 5000 hace que, si la base de datos no responde, falle en 5 segundos en lugar de quedarse colgado. Ese detalle será visible en el apartado 8.

El cliente de Redis. Se construye con una URL del tipo redis://host:puerto. El manejador de error evita que un fallo de la caché tumbe el proceso entero.

/salud. Devuelve 200 si base de datos y caché responden, y 503 si no. No es decorativo: en el módulo 3 lo usarás para las health checks de Docker, y en el módulo 6 será lo que consulte el balanceador para decidir si un contenedor puede recibir tráfico.

/libros. Implementa el patrón cache-aside: primero mira en Redis; si hay resultado, lo devuelve marcado como origen: "cache"; si no, consulta PostgreSQL, guarda el resultado en Redis con 60 segundos de vida (setEx) y lo devuelve como origen: "db". Ese campo origen te permitirá comprobar de un vistazo si la caché funciona.

/libros/:id. Valida la entrada antes de consultar y usa consulta parametrizada ($1 con el valor aparte) en lugar de concatenar cadenas. Es la forma correcta de evitar inyección SQL.

El arranque. app.listen(PORT, '0.0.0.0', ...) escucha en todas las interfaces. Es un detalle que se convertirá en crítico dentro de un contenedor: si una aplicación escucha solo en 127.0.0.1, será inalcanzable desde fuera del contenedor por mucho -p que pongas. Anótalo, porque es una de las causas más frecuentes de "publiqué el puerto pero no responde".

  1. La base de datos: db/init.sql

Crea ~/aurora-libros/db/init.sql:

-- Esquema y datos iniciales del catálogo de Aurora Libros S.L.

CREATE TABLE IF NOT EXISTS libros (
    id          SERIAL PRIMARY KEY,
    titulo      VARCHAR(200)   NOT NULL,
    autor       VARCHAR(150)   NOT NULL,
    isbn        VARCHAR(17)    NOT NULL UNIQUE,
    precio      NUMERIC(8,2)   NOT NULL CHECK (precio >= 0),
    creado_en   TIMESTAMPTZ    NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_libros_autor ON libros (autor);

INSERT INTO libros (titulo, autor, isbn, precio) VALUES
    ('El jardín de senderos que se bifurcan', 'Jorge Luis Borges',      '978-84-206-3312-1', 14.50),
    ('Rayuela',                               'Julio Cortázar',         '978-84-376-0494-7', 19.90),
    ('Cien años de soledad',                  'Gabriel García Márquez', '978-84-397-2071-7', 17.95),
    ('La sombra del viento',                  'Carlos Ruiz Zafón',      '978-84-08-04364-5', 21.00),
    ('Nada',                                  'Carmen Laforet',         '978-84-233-4361-2', 12.75),
    ('La casa de los espíritus',              'Isabel Allende',         '978-84-9838-618-3', 18.40),
    ('Los detectives salvajes',               'Roberto Bolaño',         '978-84-339-6835-7', 23.60),
    ('El tiempo entre costuras',              'María Dueñas',           '978-84-8365-351-1', 20.15)
ON CONFLICT (isbn) DO NOTHING;

Comentarios sobre el diseño:

  • SERIAL PRIMARY KEY genera identificadores autoincrementales, que son los que usará /libros/:id.
  • isbn ... UNIQUE impide duplicados. Combinado con ON CONFLICT (isbn) DO NOTHING al final, hace que el script sea idempotente: puedes ejecutarlo mil veces sin duplicar libros ni provocar errores. Esta propiedad será importante en el módulo 4, donde PostgreSQL ejecuta automáticamente los scripts de inicialización.
  • NUMERIC(8,2) para el precio, nunca FLOAT: con dinero, la aritmética de coma flotante produce errores de redondeo.
  • CHECK (precio >= 0) es una restricción de integridad a nivel de base de datos.
  • TIMESTAMPTZ guarda la marca de tiempo con zona horaria, lo que evita la clase de problemas de la tabla de la lección 01-01.
  • CREATE TABLE IF NOT EXISTS y CREATE INDEX IF NOT EXISTS refuerzan la idempotencia.

Ocho libros de autores en español: datos ficticios pero verosímiles, suficientes para probar el catálogo, la caché y la consulta individual.

  1. La web estática: web/index.html

Reutiliza la página que hiciste en la lección 01-06 y amplíala para que consuma la API. Crea o sustituye ~/aurora-libros/web/index.html:

<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Aurora Libros · Librería online</title>
  <style>
    body { font-family: system-ui, sans-serif; max-width: 46rem; margin: 3rem auto; padding: 0 1rem; color: #222; }
    h1 { color: #6b3fa0; }
    table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
    th, td { text-align: left; padding: .5rem; border-bottom: 1px solid #ddd; }
    .estado { padding: .5rem .75rem; border-radius: .25rem; background: #f4f0fa; }
  </style>
</head>
<body>
  <h1>Aurora Libros</h1>
  <p class="estado" id="estado">Cargando catálogo…</p>
  <table id="tabla" hidden>
    <thead><tr><th>Título</th><th>Autor</th><th>ISBN</th><th>Precio</th></tr></thead>
    <tbody id="cuerpo"></tbody>
  </table>

  <script>
    // La web llama a /api/libros. En el módulo 4, Nginx reenviará esa ruta
    // hacia aurora-api mediante un proxy inverso.
    fetch('/api/libros')
      .then((r) => r.json())
      .then((datos) => {
        document.getElementById('estado').textContent =
          `${datos.libros.length} libros · origen de los datos: ${datos.origen}`;
        document.getElementById('cuerpo').innerHTML = datos.libros
          .map((l) => `<tr><td>${l.titulo}</td><td>${l.autor}</td><td>${l.isbn}</td><td>${l.precio} €</td></tr>`)
          .join('');
        document.getElementById('tabla').hidden = false;
      })
      .catch((err) => {
        document.getElementById('estado').textContent =
          'No se pudo contactar con la API: ' + err.message;
      });
  </script>
</body>
</html>

El punto interesante es que la página llama a /api/libros, una ruta relativa, no a http://localhost:3000/libros. Eso es deliberado: en la arquitectura final, Nginx recibirá esa petición y la reenviará internamente a aurora-api. Así el navegador solo habla con un origen, se evitan problemas de CORS y la API no necesita estar expuesta a Internet. La configuración del proxy inverso se hará en el módulo 4.

Ahora mismo, si abres esta página, el mensaje será "No se pudo contactar con la API". Es lo esperado: todavía no hay nada montado.

  1. Ejecutando la API sin Docker: el calvario

Vamos a hacer lo que haría el desarrollador junior de Aurora Libros en su primer día. Ponte en su piel.

Intento 1: simplemente arrancarla

cd ~/aurora-libros/api
node server.js
node:internal/modules/cjs/loader:1215
  throw err;
  ^
Error: Cannot find module 'express'
Require stack:
- /home/junior/aurora-libros/api/server.js

Faltan las dependencias. Lógico.

Intento 2: instalar dependencias

npm install

Pero antes hay que tener Node. Comprobemos qué versión hay:

node --version
v18.19.1

Node 18, y el package.json exige 22 o superior. Toca instalar la versión correcta, y no puedes simplemente reemplazar la del sistema porque otros proyectos de la máquina dependen de ella. Necesitas un gestor de versiones:

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.bashrc
nvm install 22
nvm use 22
node --version
v22.14.0

Cuatro comandos, un reinicio de shell y una herramienta nueva instalada en tu máquina solo para tener la versión correcta de Node. Ahora sí:

npm install
added 112 packages in 6s

Intento 3: arrancar de nuevo

node server.js
[aurora-api] fallo al arrancar: connect ECONNREFUSED 127.0.0.1:6379

ECONNREFUSED en el puerto 6379: no hay ningún Redis escuchando. El arranque falla porque cache.connect() no encuentra a nadie al otro lado.

Este es el error que da nombre a esta sección, y conviene entenderlo bien: "conexión rechazada" significa que la petición llegó a la máquina pero nadie estaba escuchando en ese puerto. No es un problema de firewall ni de credenciales: sencillamente no existe el servicio.

Intento 4: instalar Redis

sudo apt install -y redis-server
sudo systemctl enable --now redis-server
redis-cli ping
PONG

Un servicio más instalado permanentemente en tu sistema, arrancando en cada reinicio consuma o no recursos.

Intento 5: arrancar otra vez

node server.js
[aurora-api] escuchando en el puerto 3000
[aurora-api] base de datos: localhost:5432/aurora_libros
[aurora-api] caché: localhost:6379

¡Arranca! Probemos:

curl http://localhost:3000/salud
{"servicio":"aurora-api","version":"1.0.0","db":"ko","cache":"ok",
 "errorDb":"connect ECONNREFUSED 127.0.0.1:5432"}

La caché va, pero la base de datos no: otro ECONNREFUSED, ahora en el 5432. No hay PostgreSQL. Y observa que /salud devuelve HTTP 503, justo como se diseñó.

curl http://localhost:3000/libros
{"error":"No se pudo obtener el catálogo","detalle":"connect ECONNREFUSED 127.0.0.1:5432"}

Intento 6: instalar y configurar PostgreSQL

sudo apt install -y postgresql postgresql-contrib
sudo systemctl enable --now postgresql
psql --version
psql (PostgreSQL) 16.6

Con suerte, tu distribución trae la 16. Si trae la 14 —como el servidor de CI de la lección 01-01—, tendrías que añadir el repositorio oficial de PostgreSQL, importar su clave GPG y forzar la versión, repitiendo un baile muy parecido al de la instalación de Docker.

Ahora hay que crear el usuario y la base de datos, porque una instalación limpia no sabe nada de Aurora Libros:

sudo -u postgres psql -c "CREATE USER aurora WITH PASSWORD 'aurora';"
sudo -u postgres psql -c "CREATE DATABASE aurora_libros OWNER aurora;"
CREATE ROLE
CREATE DATABASE

Y cargar el esquema con los datos:

PGPASSWORD=aurora psql -h localhost -U aurora -d aurora_libros -f ~/aurora-libros/db/init.sql
CREATE TABLE
CREATE INDEX
INSERT 0 8

Si aquí falla con Peer authentication failed for user "aurora", tendrás que editar /etc/postgresql/16/main/pg_hba.conf, cambiar el método de autenticación a md5 o scram-sha-256 y reiniciar el servicio. Es un clásico, y cuesta entre diez minutos y una tarde según tu familiaridad con PostgreSQL.

Intento 7: por fin

node server.js
[aurora-api] escuchando en el puerto 3000
[aurora-api] base de datos: localhost:5432/aurora_libros
[aurora-api] caché: localhost:6379
curl http://localhost:3000/salud
{"servicio":"aurora-api","version":"1.0.0","db":"ok","cache":"ok"}
curl -s http://localhost:3000/libros | head -c 300
{"origen":"db","libros":[{"id":5,"titulo":"Cien años de soledad","autor":"Gabriel García Márquez","isbn":"978-84-397-2071-7","precio":"17.95"},...

Repite el mismo comando inmediatamente:

curl -s http://localhost:3000/libros | head -c 40
{"origen":"cache","libros":[{"id":5,...

origen ha cambiado de db a cache: la segunda petición se ha servido desde Redis, sin tocar PostgreSQL. La caché funciona.

Y un libro concreto:

curl -s http://localhost:3000/libros/2
{"id":2,"titulo":"El jardín de senderos que se bifurcan","autor":"Jorge Luis Borges","isbn":"978-84-206-3312-1","precio":"14.50"}

La API funciona. Ha costado siete intentos.

  1. El recuento del onboarding

Hagamos las cuentas de lo que acabas de hacer, que es exactamente lo que tiene que hacer cada persona nueva del equipo:

# Paso manual Riesgo de que falle
1 Clonar el repositorio Bajo
2 Descubrir que la versión de Node no vale
3 Instalar nvm (herramienta nueva en el sistema) Medio: depende del shell
4 nvm install 22 y nvm use 22 Bajo, pero hay que recordar nvm use en cada terminal nueva
5 npm install Bajo
6 Instalar Redis Medio: paquete distinto según SO
7 Habilitar y arrancar el servicio Redis Medio: no hay systemd en macOS
8 Instalar PostgreSQL 16 concretamente Alto: la distribución puede traer otra versión
9 Arrancar el servicio PostgreSQL Medio
10 Crear el usuario aurora Medio
11 Crear la base de datos aurora_libros Medio
12 Ajustar pg_hba.conf para la autenticación Alto: ruta y sintaxis distintas por versión y SO
13 Cargar init.sql Medio
14 Exportar las variables de entorno necesarias Medio: fáciles de olvidar
15 Arrancar la API Bajo

Quince pasos manuales, de los cuales dos son de riesgo alto y ocho de riesgo medio. Y esa lista asume que estás en Linux: en macOS cambian los gestores de paquetes (brew en vez de apt), la gestión de servicios (brew services en vez de systemctl) y las rutas de configuración. En Windows, más aún.

Los problemas que no desaparecen aunque completes los quince pasos:

  • Contaminación del sistema. Ahora tienes Redis y PostgreSQL arrancando en cada reinicio de tu máquina, uses Aurora Libros o no.
  • Conflicto entre proyectos. Si mañana entras en otro proyecto que necesita PostgreSQL 14, tienes un problema serio.
  • Sin garantía de igualdad. Tu PostgreSQL 16.6 y el 16.2 de tu compañera no son exactamente lo mismo, y ninguno de los dos es el de producción.
  • Nada versionado. Todo este proceso vive en un documento de Confluence que se desactualizará en tres semanas.
  • Onboarding lento. Entre 4 horas y 3 días según la experiencia de la persona y su mala suerte.
  • Imposible de replicar en CI. El servidor de integración necesitaría las mismas quince operaciones antes de cada ejecución de tests.

Y ahora la promesa, para que tengas el contraste presente: al terminar el módulo 4, todo esto se reducirá a un único comando, docker compose up, que cualquier persona podrá ejecutar en Linux, macOS o Windows, obteniendo exactamente las mismas versiones de todo, sin instalar Node, ni PostgreSQL, ni Redis en su máquina, y sin dejar rastro al terminar.

Antes de continuar, conviene dejar el sistema tranquilo. Si has instalado los servicios solo para este ejercicio, puedes pararlos:

sudo systemctl stop redis-server postgresql
sudo systemctl disable redis-server postgresql

stop los para ahora; disable evita que vuelvan a arrancar en cada reinicio. A partir del módulo 3 no los necesitarás: vivirán en contenedores.

  1. La hoja de ruta del curso

Este es el plan. Cada módulo añade una pieza concreta a Aurora Libros:

Módulo Qué aprendes Qué le añade a Aurora Libros
1. Introducción (el que acabas de terminar) Conceptos, instalación, arquitectura, comandos, imágenes, primer contenedor La plataforma presentada y el problema medido: 15 pasos manuales
2. Imágenes Docker Hub, Dockerfile, construcción, etiquetado, publicación El Dockerfile de aurora-api y su imagen aurora-api:1.0.0 publicada en un registro
3. Contenedores Ejecución, ciclo de vida, inspección, redes, volúmenes, límites Los cuatro servicios corriendo como contenedores, en una red propia, con un volumen que salva el catálogo de aurora-db
4. Docker Compose Servicios declarativos, variables de entorno, perfiles, desarrollo local El fichero compose.yaml con la pila completa: docker compose up y todo funciona
5. Avanzado Redes a fondo, almacenamiento, seguridad, optimización, BuildKit, monitorización, runtime Imágenes más pequeñas y seguras (usuario no root, multi-stage), logs centralizados, secretos bien gestionados
6. Producción Imágenes de producción, CI/CD, Swarm, Kubernetes, escalado, despliegues Aurora Libros desplegada de verdad, con pipeline automático, réplicas, balanceo y rollback
7. Ecosistema Aprovisionamiento, Compose frente a Kubernetes, Docker Desktop, herramientas, alternativas, futuro El contexto para decidir con criterio qué usar en cada caso

Fíjate en la progresión: cada módulo resuelve un problema real que el anterior deja abierto. El módulo 2 empaqueta la API, pero seguirá necesitando una base de datos. El 3 pone las cuatro piezas en marcha, pero arrancarlas a mano una a una será tedioso. El 4 lo automatiza, pero las imágenes serán mejorables y poco seguras. El 5 las endurece, pero seguirán viviendo solo en tu portátil. El 6 las lleva a producción.

Guarda bien la carpeta ~/aurora-libros: es tu proyecto para el resto del curso.

Errores Comunes y Consejos

  • ECONNREFUSED no significa "credenciales incorrectas". Significa que nadie escucha en ese puerto. Si ves ECONNREFUSED, comprueba primero que el servicio exista y esté arrancado, no la contraseña. Y en los próximos módulos, cuando esto ocurra entre contenedores, la causa suele ser un nombre de servicio mal escrito o una red mal configurada.
  • Escribir la configuración fija en el código. Si server.js tuviera host: 'localhost' escrito a fuego, la misma aplicación no podría funcionar en local y en contenedores. Las variables de entorno son lo que permite que el mismo artefacto sirva para todos los entornos.
  • Escuchar solo en 127.0.0.1. Dentro de un contenedor, una aplicación que hace app.listen(PORT, '127.0.0.1') es inalcanzable desde fuera aunque publiques el puerto. Usa 0.0.0.0, como hace server.js.
  • Olvidar nvm use en cada terminal. Un clásico del desarrollo sin contenedores: la terminal A tiene Node 22 y la B, Node 18, y el error resultante es incomprensible. Con Docker, la versión de Node va dentro de la imagen y no depende de la terminal.
  • Scripts SQL no idempotentes. Si init.sql no tuviera IF NOT EXISTS y ON CONFLICT DO NOTHING, ejecutarlo dos veces daría errores o duplicaría libros. En el módulo 4, PostgreSQL ejecutará ese script automáticamente; que sea idempotente evita sorpresas.
  • Consejo: guarda el recuento de los 15 pasos. Cuando en el módulo 4 escribas docker compose up y todo funcione, vuelve a esta tabla. Es la mejor forma de medir lo que has ganado.
  • Consejo: no borres ~/aurora-libros. Ese directorio es el proyecto del curso completo.

Ejercicios

Ejercicio 1: monta el proyecto y documenta el dolor

Crea la estructura completa aurora-libros/ con los cuatro ficheros (api/package.json, api/server.js, db/init.sql, web/index.html) e intenta arrancar la API sin Docker en tu sistema. Ve anotando en un fichero NOTAS-ONBOARDING.md:

  1. Cada comando que has tenido que ejecutar.
  2. Cada error que te ha aparecido, con su mensaje literal.
  3. El tiempo total invertido.
  4. Qué software queda instalado en tu máquina al terminar.

No importa si no consigues completarlo: lo que interesa es el registro del proceso.

Ejercicio 2: interpreta los diagnósticos

Para cada uno de estos mensajes, indica qué pieza falta o está mal, y qué comprobarías exactamente:

a) Error: Cannot find module 'pg'
b) [aurora-api] fallo al arrancar: connect ECONNREFUSED 127.0.0.1:6379
c) {"servicio":"aurora-api","db":"ko","cache":"ok","errorDb":"database \"aurora_libros\" does not exist"}
d) {"servicio":"aurora-api","db":"ko","cache":"ok","errorDb":"password authentication failed for user \"aurora\""}
e) Error: listen EADDRINUSE: address already in use 0.0.0.0:3000

Ejercicio 3: prepara el terreno para el módulo 2

Sin escribir todavía ningún Dockerfile (eso es el módulo 2), responde razonadamente:

  1. ¿Qué imagen base elegirías para aurora-api y por qué? Consulta su tamaño con docker pull y docker image ls.
  2. ¿Qué imágenes oficiales usarías para aurora-db y aurora-cache? Descárgalas y anota el tamaño de cada una.
  3. De los ficheros de ~/aurora-libros/api/, ¿cuáles deberían entrar en la imagen de la API y cuáles no? Justifícalo.
  4. Las variables DB_PASSWORD y similares, ¿deberían ir dentro de la imagen? ¿Por qué?

Soluciones

Solución al ejercicio 1

No hay una solución única, pero tu NOTAS-ONBOARDING.md debería parecerse a esto:

# Onboarding de Aurora Libros sin Docker — 4 de agosto de 2026

## Comandos ejecutados
1. mkdir -p ~/aurora-libros/{api,db,web}
2. node --version → v18.19.1 (insuficiente, se pide >=22)
3. curl ... nvm/install.sh | bash ; source ~/.bashrc
4. nvm install 22 && nvm use 22 → v22.14.0
5. cd api && npm install → 112 paquetes
6. node server.js → ECONNREFUSED :6379
7. sudo apt install -y redis-server && sudo systemctl enable --now redis-server
8. node server.js → arranca; /salud devuelve db:"ko"
9. sudo apt install -y postgresql
10. sudo -u postgres psql -c "CREATE USER aurora WITH PASSWORD 'aurora';"
11. sudo -u postgres psql -c "CREATE DATABASE aurora_libros OWNER aurora;"
12. Editar /etc/postgresql/16/main/pg_hba.conf (peer → scram-sha-256) + restart
13. PGPASSWORD=aurora psql -h localhost -U aurora -d aurora_libros -f ../db/init.sql
14. node server.js → OK

## Errores encontrados
- Cannot find module 'express'
- connect ECONNREFUSED 127.0.0.1:6379
- connect ECONNREFUSED 127.0.0.1:5432
- Peer authentication failed for user "aurora"

## Tiempo total
1 h 35 min (y ya conocía PostgreSQL)

## Software que queda instalado permanentemente
- nvm + Node 22 (además del Node 18 del sistema)
- redis-server (servicio activo al arrancar)
- postgresql-16 (servicio activo al arrancar) + fichero pg_hba.conf modificado

La reflexión importante: ese tiempo y esos residuos se multiplican por cada persona del equipo y por cada máquina, y ninguno de ellos garantiza que todos acaben con exactamente las mismas versiones.

Solución al ejercicio 2

(a) Cannot find module 'pg'. Falta la dependencia de npm, no un servicio. Es un error del arranque del proceso, anterior a cualquier conexión. Comprobarías que existe node_modules/ y que npm install se ejecutó en el directorio correcto (api/, donde está el package.json). Causa típica: haber lanzado node server.js desde la raíz del proyecto o desde otra carpeta.

(b) ECONNREFUSED 127.0.0.1:6379. Falta Redis: nadie escucha en el puerto 6379. Comprobarías si el servicio existe y está arrancado (systemctl status redis-server), si responde (redis-cli pingPONG) y si algo escucha en ese puerto (ss -tln | grep 6379). Ojo con la distinción: rechazado es "no hay nadie"; si el mensaje fuera un timeout, apuntaría a firewall o a una máquina inalcanzable.

(c) database "aurora_libros" does not exist. PostgreSQL sí está corriendo y sí acepta la conexión (fíjate en que el error ya no es de red, sino del servidor), pero la base de datos no ha sido creada. Falta el paso CREATE DATABASE aurora_libros OWNER aurora;. Comprobarías las bases existentes con sudo -u postgres psql -c "\l".

(d) password authentication failed for user "aurora". El servidor responde y la base existe, pero las credenciales no cuadran. Comprobarías: que el usuario exista (\du en psql), que la contraseña coincida con la de DB_PASSWORD, y que pg_hba.conf use un método compatible (scram-sha-256 en lugar de peer, que solo funciona por socket local). Es el error más lento de diagnosticar de los cinco.

(e) EADDRINUSE: address already in use 0.0.0.0:3000. No falta nada: sobra algo. Ya hay un proceso escuchando en el puerto 3000, casi siempre otra instancia de la propia API que dejaste corriendo en otra terminal. Lo localizarías con ss -tlnp | grep 3000 o lsof -i :3000, y lo pararías, o arrancarías esta instancia en otro puerto con PORT=3001 node server.js. Esta es la versión "sin Docker" del port is already allocated que viste en la lección 01-06.

Solución al ejercicio 3

  1. Imagen base para aurora-api: node:22-alpine. Razones: cumple el engines: node >=22.0.0 del package.json, es oficial (espacio library/), y la variante Alpine pesa unos 140 MB frente a los ~1,1 GB de node:22 completa. Compruébalo:
docker pull node:22-alpine
docker pull node:22
docker image ls node --format "table {{.Tag}}\t{{.Size}}"

En un pipeline que construye y descarga la imagen varias veces al día, esa diferencia de casi un gigabyte se traduce en minutos de espera y en coste de transferencia. La contrapartida es musl en lugar de glibc (lección 01-05), que aquí no supone problema porque las tres dependencias son JavaScript puro o traen binarios compatibles con Alpine.

  1. postgres:16-alpine para aurora-db y redis:7-alpine para aurora-cache. Ambas son oficiales y fijan la versión mayor, tal como exige el enunciado del proyecto.
docker pull postgres:16-alpine
docker pull redis:7-alpine
docker image ls --format "table {{.Repository}}:{{.Tag}}\t{{.Size}}"
REPOSITORY:TAG            SIZE
node:22-alpine            142MB
postgres:16-alpine        278MB
redis:7-alpine            41.4MB

Nota que no usamos latest en ninguna: sabemos exactamente qué versión mayor tendrá cada servicio, cumpliendo lo aprendido en la lección 01-05.

  1. Qué entra y qué no en la imagen:
Fichero o carpeta ¿Entra? Motivo
package.json Define las dependencias que hay que instalar
server.js Es la aplicación
package-lock.json Fija las versiones exactas; es lo que hace reproducible el npm ci
node_modules/ No Se instala dentro de la imagen. Copiar el de tu máquina puede meter binarios compilados para otro sistema operativo o arquitectura
.git/ No Historial pesado e irrelevante en ejecución; además puede contener información sensible
NOTAS-ONBOARDING.md, .env No Documentación local y, sobre todo, secretos, que jamás deben viajar dentro de una imagen

El mecanismo para excluirlos se llama .dockerignore y lo verás en el módulo 2.

  1. DB_PASSWORD no debe ir dentro de la imagen. Nunca. Tres razones acumulativas:

    • Seguridad: las capas de una imagen son inspeccionables por cualquiera que la tenga (docker image history te muestra las instrucciones). Una contraseña metida ahí es una contraseña publicada, y borrarla en una capa posterior no la elimina de la capa inferior (lección 01-05).
    • Portabilidad: la contraseña de desarrollo y la de producción son distintas. Si va dentro, necesitarías una imagen distinta por entorno, rompiendo la promesa de "construir una vez, desplegar en todas partes".
    • Rotación: cambiar una contraseña obligaría a reconstruir y redesplegar la imagen.

    Por eso server.js lee toda su configuración de process.env. Los valores se inyectan al ejecutar el contenedor, no al construirlo: con -e o --env-file (módulo 3), con variables en compose.yaml (lección 04-05), o con mecanismos específicos de secretos (lección 05-03).

Conclusión

Aurora Libros ya no es una idea abstracta: tiene un repositorio, una API con tres endpoints, un catálogo de ocho libros en PostgreSQL, una caché en Redis y una web estática esperando a que alguien la conecte. También tiene un problema perfectamente cuantificado. Has ejecutado la plataforma sin Docker y has necesitado quince pasos manuales, dos gestores de paquetes, tres servicios instalados permanentemente en tu sistema, una herramienta nueva para manejar versiones de Node y una edición a mano de pg_hba.conf. Por el camino has visto los ECONNREFUSED característicos de "no existe ese servicio", has descubierto que la caché funciona observando el campo origen, y has terminado con una máquina más sucia de lo que empezó y sin ninguna garantía de que tu entorno se parezca al de tus compañeros.

También has visto por qué el código está escrito como está: toda la configuración se lee de variables de entorno para que el mismo server.js valga en tu portátil y dentro de un contenedor; la API escucha en 0.0.0.0 para ser alcanzable desde fuera de su red; init.sql es idempotente para poder ejecutarse en cada arranque; y /salud existe porque en el módulo 6 será lo que consulte el balanceador antes de enviar tráfico a una réplica. Cada una de esas decisiones cobrará todo su sentido en los próximos módulos.

Con esto cierras el módulo 1. Sabes qué es Docker, lo tienes instalado y verificado, entiendes su arquitectura cliente-servidor con dockerd, containerd y runc, manejas la gramática de su CLI, comprendes las imágenes por dentro —capas, copy-on-write, digests y etiquetas— y has creado, publicado en un puerto, inspeccionado y destruido tus primeros contenedores. Tienes las herramientas y tienes el problema. En el módulo 2, Trabajando con Imágenes Docker, empezarás a unir ambas cosas: conocerás Docker Hub a fondo y escribirás tu primer Dockerfile para empaquetar aurora-api en una imagen propia, construible con un comando y ejecutable en cualquier máquina del mundo sin instalar Node en ella. El primer paso de los quince desaparecerá; los demás caerán uno a uno en los módulos siguientes.

Docker: De Principiante a Avanzado

Módulo 1: Introducción a Docker

Módulo 2: Trabajando con Imágenes Docker

Módulo 3: Contenedores Docker

Módulo 4: Docker Compose

Módulo 5: Conceptos Avanzados de Docker

Módulo 6: Docker en Producción

Módulo 7: Ecosistema y Herramientas de Docker

© Copyright 2026. Todos los derechos reservados