Has après què és Docker, l'has instal·lat, en coneixes l'arquitectura, en manejes les comandes, entens les imatges i has creat els teus primers contenidors. Tot això són eines. El que falta és el problema que en justifica l'ús, i aquesta lliçó te'l posarà al davant amb nom, cognoms i codi real. Coneixeràs Aurora Libros S.L., una llibreria en línia fictícia que serà el fil conductor dels sis mòduls restants; en veuràs l'arquitectura objectiu, llegiràs el codi complet de la seva API amb Node.js 22, PostgreSQL 16 i Redis 7, i —el més important— intentaràs executar-lo sense Docker. Comptaràs els passos manuals que exigeix, veuràs els errors que apareixen i mesuraràs el cost real de l'onboarding d'un desenvolupador nou. Quan arribis a l'última secció, el full de ruta del curs ja no et semblarà un temari: et semblarà un pla de rescat.

Contingut

  1. Qui és Aurora Libros S.L. i què necessita
  2. L'arquitectura objectiu
  3. Estructura del repositori
  4. El codi de l'API: package.json
  5. El codi de l'API: server.js
  6. La base de dades: db/init.sql
  7. El web estàtic: web/index.html
  8. Executant l'API sense Docker: el calvari
  9. El recompte de l'onboarding
  10. El full de ruta del curs

  1. Qui és Aurora Libros S.L. i què necessita

Aurora Libros S.L. és una llibreria independent de València que ven per Internet des de fa tres anys. Van començar amb una botiga feta per un autònom i ara tenen un equip petit: dues desenvolupadores, un desenvolupador júnior acabat d'incorporar i una persona que s'ocupa de la infraestructura a mitja jornada.

La seva situació actual:

  • El web i l'API corren en un únic servidor llogat, configurat a mà fa dos anys. Ningú sap reconstruir-lo si s'espatlla.
  • El desplegament consisteix a connectar-se per SSH, fer git pull i reiniciar el procés a mà. Es fa els dimarts al matí "per si de cas".
  • Cada desenvolupador té el seu entorn local muntat de manera diferent. Una fa servir PostgreSQL 14 instal·lat amb apt, una altra la 16 de Homebrew al seu Mac, i el júnior fa tres dies que intenta deixar la seva màquina a punt sense aconseguir-ho.
  • No hi ha entorn de proves: es prova en local i es creuen els dits.
  • Fa un mes, una actualització del sistema del servidor va canviar la versió de Node i l'API va deixar d'arrencar. Van estar quatre hores caiguts.

El que necessiten és concret i no té res d'exòtic:

  1. Que qualsevol desenvolupador pugui tenir tota la plataforma funcionant en local en minuts, no en dies.
  2. Que l'entorn de desenvolupament, el de proves i el de producció siguin el mateix.
  3. Que desplegar sigui repetible i reversible, no un ritual manual.
  4. Que puguin escalar l'API en campanyes (Sant Jordi, Nadal) sense refer res.
  5. Que la configuració estigui versionada al costat del codi, no al cap d'una persona.

És exactament el catàleg de problemes de la lliçó 01-01, amb cares concretes. I és exactament el que Docker resol.

  1. L'arquitectura objectiu

Al final del curs, la plataforma d'Aurora Libros tindrà quatre peces, cadascuna al seu contenidor:

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

    subgraph PLAT["Plataforma Aurora Libros"]
        WEB["aurora-web<br/>Nginx<br/>web estàtic + proxy invers<br/>port 80"]
        API["aurora-api<br/>Node.js 22 + Express<br/>/salut · /llibres · /llibres/:id<br/>port 3000"]
        CACHE["aurora-cache<br/>Redis 7<br/>memòria cau del catàleg<br/>port 6379"]
        DB[("aurora-db<br/>PostgreSQL 16<br/>taula llibres<br/>port 5432")]
    end

    USER -->|"HTTP :80"| WEB
    WEB -->|"/api/* → proxy"| API
    API -->|"consulta a la memòria cau"| CACHE
    API -->|"SQL"| DB

Què fa cada peça i per què existeix:

Servei Tecnologia Responsabilitat Exposat a l'exterior
aurora-web Nginx Serveix l'HTML, el CSS i les imatges de la botiga i reenvia les crides /api/* a l'API , és la porta d'entrada
aurora-api Node.js 22 + Express Lògica de negoci i API REST del catàleg No directament; només a través d'aurora-web
aurora-cache Redis 7 Guarda en memòria les consultes més freqüents del catàleg per no colpejar la base de dades No
aurora-db PostgreSQL 16 Emmagatzema el catàleg de llibres de manera persistent No

Fixa't en l'última columna, perquè és una decisió de disseny important que ja pots entendre amb el que has après a la lliçó 01-06: només aurora-web publicarà ports a l'exterior. La base de dades i la memòria cau seran accessibles únicament des de dins de la xarxa interna de contenidors. Això redueix dràsticament la superfície d'atac, i és trivial d'aconseguir amb Docker: simplement no es fa servir -p en aquests serveis.

Els endpoints de l'API seran tres:

Endpoint Mètode Què retorna
/salut GET Estat del servei i de les seves dependències. Serveix per als health checks
/llibres GET El catàleg complet, amb memòria cau a Redis
/llibres/:id GET Un llibre concret pel seu identificador

  1. Estructura del repositori

Crea aquesta estructura a la teva màquina, perquè la faràs servir durant tot el curs:

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 carpetes, una per peça que aporta codi propi (la memòria cau Redis no en necessita cap). Al llarg del curs hi aniràs afegint fitxers: Dockerfile al mòdul 2, .dockerignore, compose.yaml al mòdul 4, i fitxers de desplegament al 6.

  1. El codi de l'API: package.json

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

{
  "name": "aurora-api",
  "version": "1.0.0",
  "description": "API REST del catàleg d'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 bloc:

  • name i version: identifiquen el paquet. La versió 1.0.0 serà la primera etiqueta de la imatge que construeixis al mòdul 2.
  • main: el fitxer d'entrada.
  • type: "commonjs": farem servir require() en lloc d'import. És la forma més compatible i evita distraccions.
  • engines.node: ">=22.0.0": aquest camp és clau per a la lliçó. Declara que l'aplicació necessita Node 22 o superior. Amb Docker, aquesta exigència es compleix sola; sense Docker, és responsabilitat de cada màquina.
  • scripts.start: com arrencar l'aplicació.
  • dependencies: tres llibreries. express és el framework web; pg el client de PostgreSQL; redis el client de Redis. Cadascuna implica un servei extern que ha d'existir i estar accessible.

  1. El codi de l'API: server.js

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

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

// --- Configuració des de variables d'entorn ---
// Mai no s'escriuen valors fixos: cada entorn (local, proves, producció)
// aporta els seus. Els valors per defecte només faciliten el desenvolupament.
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_llibres';
const REDIS_HOST = process.env.REDIS_HOST || 'localhost';
const REDIS_PORT = process.env.REDIS_PORT || 6379;

const app = express();

// --- Connexió 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,
});

// --- Connexió 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; // segons que es manté el cataleg a la memoria cau

// --- GET /salut : estat del servei i de les seves dependencies ---
app.get('/salut', async (req, res) => {
  const estat = { servei: 'aurora-api', version: '1.0.0', db: 'ko', cache: 'ko' };
  try {
    await pool.query('SELECT 1');
    estat.db = 'ok';
  } catch (err) {
    estat.errorDb = err.message;
  }
  try {
    await cache.ping();
    estat.cache = 'ok';
  } catch (err) {
    estat.errorCache = err.message;
  }
  const totOk = estat.db === 'ok' && estat.cache === 'ok';
  res.status(totOk ? 200 : 503).json(estat);
});

// --- GET /llibres : cataleg complet, amb memoria cau ---
app.get('/llibres', async (req, res) => {
  try {
    const enCache = await cache.get('llibres:tots');
    if (enCache) {
      return res.json({ origen: 'cache', llibres: JSON.parse(enCache) });
    }
    const { rows } = await pool.query(
      'SELECT id, titol, autor, isbn, preu FROM llibres ORDER BY titol'
    );
    await cache.setEx('llibres:tots', TTL_CACHE, JSON.stringify(rows));
    res.json({ origen: 'db', llibres: rows });
  } catch (err) {
    console.error('[/llibres] error:', err.message);
    res.status(500).json({ error: 'No s\'ha pogut obtenir el cataleg', detall: err.message });
  }
});

// --- GET /llibres/:id : un llibre concret ---
app.get('/llibres/:id', async (req, res) => {
  const id = Number(req.params.id);
  if (!Number.isInteger(id) || id < 1) {
    return res.status(400).json({ error: 'L\'identificador ha de ser un enter positiu' });
  }
  try {
    const { rows } = await pool.query(
      'SELECT id, titol, autor, isbn, preu FROM llibres WHERE id = $1',
      [id]
    );
    if (rows.length === 0) {
      return res.status(404).json({ error: 'Llibre no trobat' });
    }
    res.json(rows[0]);
  } catch (err) {
    console.error('[/llibres/:id] error:', err.message);
    res.status(500).json({ error: 'Error consultant el llibre', detall: err.message });
  }
});

// --- Arrencada ---
async function arrencar() {
  await cache.connect();
  app.listen(PORT, '0.0.0.0', () => {
    console.log(`[aurora-api] escoltant al port ${PORT}`);
    console.log(`[aurora-api] base de dades: ${DB_HOST}:${DB_PORT}/${DB_NAME}`);
    console.log(`[aurora-api] memoria cau: ${REDIS_HOST}:${REDIS_PORT}`);
  });
}

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

Anem per parts, perquè aquí hi ha decisions que seran importants en mòduls posteriors.

El bloc de configuració. Cada paràmetre es llegeix de process.env, amb un valor per defecte. Això és fonamental: l'aplicació no sap on és la seva base de dades fins que algú l'hi diu. Avui, a la teva màquina, DB_HOST serà localhost; al mòdul 4, quan tot estigui en contenidors, serà aurora-db, el nom del servei. El mateix codi, sense tocar-ne ni una línia, servirà per als dos escenaris. Aquesta és la raó per la qual les variables d'entorn són el mecanisme estàndard de configuració en contenidors (lliçó 04-05).

El pool de PostgreSQL. Pool manté un conjunt de connexions reutilitzables en lloc d'obrir-ne una per petició. connectionTimeoutMillis: 5000 fa que, si la base de dades no respon, falli en 5 segons en lloc de quedar-se penjat. Aquest detall serà visible a l'apartat 8.

El client de Redis. Es construeix amb una URL del tipus redis://amfitrio:port. El gestor d'error evita que una fallada de la memòria cau tombi el procés sencer.

/salut. Retorna 200 si base de dades i memòria cau responen, i 503 si no. No és decoratiu: al mòdul 3 el faràs servir per als health checks de Docker, i al mòdul 6 serà el que consulti el balancejador per decidir si un contenidor pot rebre trànsit.

/llibres. Implementa el patró cache-aside: primer mira a Redis; si hi ha resultat, el retorna marcat com a origen: "cache"; si no, consulta PostgreSQL, desa el resultat a Redis amb 60 segons de vida (setEx) i el retorna com a origen: "db". Aquest camp origen et permetrà comprovar d'un cop d'ull si la memòria cau funciona.

/llibres/:id. Valida l'entrada abans de consultar i fa servir consulta parametritzada ($1 amb el valor a banda) en lloc de concatenar cadenes. És la manera correcta d'evitar la injecció SQL.

L'arrencada. app.listen(PORT, '0.0.0.0', ...) escolta a totes les interfícies. És un detall que es convertirà en crític dins d'un contenidor: si una aplicació escolta només a 127.0.0.1, serà inabastable des de fora del contenidor per molt -p que hi posis. Apunta-t'ho, perquè és una de les causes més freqüents de "he publicat el port però no respon".

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

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

-- Esquema i dades inicials del cataleg d'Aurora Libros S.L.

CREATE TABLE IF NOT EXISTS llibres (
    id          SERIAL PRIMARY KEY,
    titol       VARCHAR(200)   NOT NULL,
    autor       VARCHAR(150)   NOT NULL,
    isbn        VARCHAR(17)    NOT NULL UNIQUE,
    preu        NUMERIC(8,2)   NOT NULL CHECK (preu >= 0),
    creat_el    TIMESTAMPTZ    NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_llibres_autor ON llibres (autor);

INSERT INTO llibres (titol, autor, isbn, preu) 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;

Comentaris sobre el disseny:

  • SERIAL PRIMARY KEY genera identificadors autoincrementals, que són els que farà servir /llibres/:id.
  • isbn ... UNIQUE impedeix duplicats. Combinat amb ON CONFLICT (isbn) DO NOTHING al final, fa que l'script sigui idempotent: el pots executar mil vegades sense duplicar llibres ni provocar errors. Aquesta propietat serà important al mòdul 4, on PostgreSQL executa automàticament els scripts d'inicialització.
  • NUMERIC(8,2) per al preu, mai FLOAT: amb diners, l'aritmètica de coma flotant produeix errors d'arrodoniment.
  • CHECK (preu >= 0) és una restricció d'integritat a nivell de base de dades.
  • TIMESTAMPTZ desa la marca de temps amb zona horària, cosa que evita la mena de problemes de la taula de la lliçó 01-01.
  • CREATE TABLE IF NOT EXISTS i CREATE INDEX IF NOT EXISTS reforcen la idempotència.

Vuit llibres d'autors en castellà: dades fictícies però versemblants, suficients per provar el catàleg, la memòria cau i la consulta individual.

  1. El web estàtic: web/index.html

Reutilitza la pàgina que vas fer a la lliçó 01-06 i amplia-la perquè consumeixi l'API. Crea o substitueix ~/aurora-libros/web/index.html:

<!DOCTYPE html>
<html lang="ca">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Aurora Libros · Llibreria en línia</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; }
    .estat { padding: .5rem .75rem; border-radius: .25rem; background: #f4f0fa; }
  </style>
</head>
<body>
  <h1>Aurora Libros</h1>
  <p class="estat" id="estat">Carregant el catàleg…</p>
  <table id="taula" hidden>
    <thead><tr><th>Títol</th><th>Autor</th><th>ISBN</th><th>Preu</th></tr></thead>
    <tbody id="cos"></tbody>
  </table>

  <script>
    // El web crida /api/llibres. Al mòdul 4, Nginx reenviarà aquesta ruta
    // cap a aurora-api mitjançant un proxy invers.
    fetch('/api/llibres')
      .then((r) => r.json())
      .then((dades) => {
        document.getElementById('estat').textContent =
          `${dades.llibres.length} llibres · origen de les dades: ${dades.origen}`;
        document.getElementById('cos').innerHTML = dades.llibres
          .map((ll) => `<tr><td>${ll.titol}</td><td>${ll.autor}</td><td>${ll.isbn}</td><td>${ll.preu} €</td></tr>`)
          .join('');
        document.getElementById('taula').hidden = false;
      })
      .catch((err) => {
        document.getElementById('estat').textContent =
          'No s\'ha pogut contactar amb l\'API: ' + err.message;
      });
  </script>
</body>
</html>

El punt interessant és que la pàgina crida /api/llibres, una ruta relativa, no http://localhost:3000/llibres. Això és deliberat: a l'arquitectura final, Nginx rebrà aquesta petició i la reenviarà internament a aurora-api. Així el navegador només parla amb un origen, s'eviten problemes de CORS i l'API no necessita estar exposada a Internet. La configuració del proxy invers es farà al mòdul 4.

Ara mateix, si obres aquesta pàgina, el missatge serà "No s'ha pogut contactar amb l'API". És l'esperat: encara no hi ha res muntat.

  1. Executant l'API sense Docker: el calvari

Farem el que faria el desenvolupador júnior d'Aurora Libros el seu primer dia. Posa't a la seva pell.

Intent 1: simplement arrencar-la

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

Falten les dependències. Lògic.

Intent 2: instal·lar dependències

npm install

Però abans cal tenir Node. Comprovem quina versió hi ha:

node --version
v18.19.1

Node 18, i el package.json exigeix la 22 o superior. Toca instal·lar la versió correcta, i no pots simplement reemplaçar la del sistema perquè altres projectes de la màquina en depenen. Necessites un gestor de versions:

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

Quatre comandes, un reinici de shell i una eina nova instal·lada a la teva màquina només per tenir la versió correcta de Node. Ara sí:

npm install
added 112 packages in 6s

Intent 3: arrencar de nou

node server.js
[aurora-api] fallada en arrencar: connect ECONNREFUSED 127.0.0.1:6379

ECONNREFUSED al port 6379: no hi ha cap Redis escoltant. L'arrencada falla perquè cache.connect() no troba ningú a l'altra banda.

Aquest és *l'*error que dona nom a aquesta secció, i convé entendre'l bé: "connexió rebutjada" significa que la petició va arribar a la màquina però ningú no escoltava en aquell port. No és un problema de tallafoc ni de credencials: senzillament el servei no existeix.

Intent 4: instal·lar Redis

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

Un servei més instal·lat permanentment al teu sistema, arrencant a cada reinici consumeixi recursos o no.

Intent 5: arrencar un altre cop

node server.js
[aurora-api] escoltant al port 3000
[aurora-api] base de dades: localhost:5432/aurora_llibres
[aurora-api] memoria cau: localhost:6379

Arrenca! Provem-ho:

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

La memòria cau va, però la base de dades no: un altre ECONNREFUSED, ara al 5432. No hi ha PostgreSQL. I observa que /salut retorna HTTP 503, just com es va dissenyar.

curl http://localhost:3000/llibres
{"error":"No s'ha pogut obtenir el cataleg","detall":"connect ECONNREFUSED 127.0.0.1:5432"}

Intent 6: instal·lar i configurar PostgreSQL

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

Amb sort, la teva distribució porta la 16. Si porta la 14 —com el servidor de CI de la lliçó 01-01—, hauries d'afegir el repositori oficial de PostgreSQL, importar-ne la clau GPG i forçar la versió, repetint un ball molt semblant al de la instal·lació de Docker.

Ara cal crear l'usuari i la base de dades, perquè una instal·lació neta no sap res d'Aurora Libros:

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

I carregar l'esquema amb les dades:

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

Si aquí falla amb Peer authentication failed for user "aurora", hauràs d'editar /etc/postgresql/16/main/pg_hba.conf, canviar el mètode d'autenticació a md5 o scram-sha-256 i reiniciar el servei. És un clàssic, i costa entre deu minuts i una tarda segons la teva familiaritat amb PostgreSQL.

Intent 7: per fi

node server.js
[aurora-api] escoltant al port 3000
[aurora-api] base de dades: localhost:5432/aurora_llibres
[aurora-api] memoria cau: localhost:6379
curl http://localhost:3000/salut
{"servei":"aurora-api","version":"1.0.0","db":"ok","cache":"ok"}
curl -s http://localhost:3000/llibres | head -c 300
{"origen":"db","llibres":[{"id":5,"titol":"Cien años de soledad","autor":"Gabriel García Márquez","isbn":"978-84-397-2071-7","preu":"17.95"},...

Repeteix la mateixa comanda immediatament:

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

origen ha canviat de db a cache: la segona petició s'ha servit des de Redis, sense tocar PostgreSQL. La memòria cau funciona.

I un llibre concret:

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

L'API funciona. Ha costat set intents.

  1. El recompte de l'onboarding

Fem els comptes del que acabes de fer, que és exactament el que ha de fer cada persona nova de l'equip:

# Pas manual Risc que falli
1 Clonar el repositori Baix
2 Descobrir que la versió de Node no val
3 Instal·lar nvm (eina nova al sistema) Mitjà: depèn del shell
4 nvm install 22 i nvm use 22 Baix, però cal recordar nvm use a cada terminal nou
5 npm install Baix
6 Instal·lar Redis Mitjà: paquet diferent segons el SO
7 Habilitar i arrencar el servei Redis Mitjà: no hi ha systemd a macOS
8 Instal·lar PostgreSQL 16 concretament Alt: la distribució pot portar una altra versió
9 Arrencar el servei PostgreSQL Mitjà
10 Crear l'usuari aurora Mitjà
11 Crear la base de dades aurora_llibres Mitjà
12 Ajustar pg_hba.conf per a l'autenticació Alt: ruta i sintaxi diferents per versió i SO
13 Carregar init.sql Mitjà
14 Exportar les variables d'entorn necessàries Mitjà: fàcils d'oblidar
15 Arrencar l'API Baix

Quinze passos manuals, dels quals dos són de risc alt i vuit de risc mitjà. I aquesta llista assumeix que ets a Linux: a macOS canvien els gestors de paquets (brew en lloc d'apt), la gestió de serveis (brew services en lloc de systemctl) i les rutes de configuració. A Windows, encara més.

Els problemes que no desapareixen encara que completis els quinze passos:

  • Contaminació del sistema. Ara tens Redis i PostgreSQL arrencant a cada reinici de la teva màquina, facis servir Aurora Libros o no.
  • Conflicte entre projectes. Si demà entres en un altre projecte que necessita PostgreSQL 14, tens un problema seriós.
  • Sense garantia d'igualtat. El teu PostgreSQL 16.6 i el 16.2 de la teva companya no són exactament el mateix, i cap dels dos és el de producció.
  • Res versionat. Tot aquest procés viu en un document de Confluence que es desactualitzarà en tres setmanes.
  • Onboarding lent. Entre 4 hores i 3 dies segons l'experiència de la persona i la seva mala sort.
  • Impossible de replicar en CI. El servidor d'integració necessitaria les mateixes quinze operacions abans de cada execució de tests.

I ara la promesa, perquè tinguis el contrast present: en acabar el mòdul 4, tot això es reduirà a una única comanda, docker compose up, que qualsevol persona podrà executar a Linux, macOS o Windows, obtenint exactament les mateixes versions de tot, sense instal·lar Node, ni PostgreSQL, ni Redis a la seva màquina, i sense deixar rastre en acabar.

Abans de continuar, convé deixar el sistema tranquil. Si has instal·lat els serveis només per a aquest exercici, els pots aturar:

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

stop els atura ara; disable evita que tornin a arrencar a cada reinici. A partir del mòdul 3 no els necessitaràs: viuran en contenidors.

  1. El full de ruta del curs

Aquest és el pla. Cada mòdul afegeix una peça concreta a Aurora Libros:

Mòdul Què aprens Què li afegeix a Aurora Libros
1. Introducció (el que acabes d'acabar) Conceptes, instal·lació, arquitectura, comandes, imatges, primer contenidor La plataforma presentada i el problema mesurat: 15 passos manuals
2. Imatges Docker Hub, Dockerfile, construcció, etiquetatge, publicació El Dockerfile d'aurora-api i la seva imatge aurora-api:1.0.0 publicada en un registre
3. Contenidors Execució, cicle de vida, inspecció, xarxes, volums, límits Els quatre serveis funcionant com a contenidors, en una xarxa pròpia, amb un volum que salva el catàleg d'aurora-db
4. Docker Compose Serveis declaratius, variables d'entorn, perfils, desenvolupament local El fitxer compose.yaml amb la pila completa: docker compose up i tot funciona
5. Avançat Xarxes a fons, emmagatzematge, seguretat, optimització, BuildKit, monitoratge, runtime Imatges més petites i segures (usuari no root, multi-stage), registres centralitzats, secrets ben gestionats
6. Producció Imatges de producció, CI/CD, Swarm, Kubernetes, escalat, desplegaments Aurora Libros desplegada de debò, amb pipeline automàtic, rèpliques, balanceig i rollback
7. Ecosistema Aprovisionament, Compose davant de Kubernetes, Docker Desktop, eines, alternatives, futur El context per decidir amb criteri què fer servir en cada cas

Fixa't en la progressió: cada mòdul resol un problema real que l'anterior deixa obert. El mòdul 2 empaqueta l'API, però continuarà necessitant una base de dades. El 3 posa les quatre peces en marxa, però arrencar-les a mà una a una serà tediós. El 4 ho automatitza, però les imatges seran millorables i poc segures. El 5 les endureix, però continuaran vivint només al teu portàtil. El 6 les porta a producció.

Guarda bé la carpeta ~/aurora-libros: és el teu projecte per a la resta del curs.

Errors Habituals i Consells

  • ECONNREFUSED no significa "credencials incorrectes". Significa que ningú no escolta en aquell port. Si veus ECONNREFUSED, comprova primer que el servei existeixi i estigui arrencat, no la contrasenya. I als propers mòduls, quan això passi entre contenidors, la causa sol ser un nom de servei mal escrit o una xarxa mal configurada.
  • Escriure la configuració fixa al codi. Si server.js tingués host: 'localhost' escrit a foc, la mateixa aplicació no podria funcionar en local i en contenidors. Les variables d'entorn són el que permet que el mateix artefacte serveixi per a tots els entorns.
  • Escoltar només a 127.0.0.1. Dins d'un contenidor, una aplicació que fa app.listen(PORT, '127.0.0.1') és inabastable des de fora encara que publiquis el port. Fes servir 0.0.0.0, com fa server.js.
  • Oblidar nvm use a cada terminal. Un clàssic del desenvolupament sense contenidors: el terminal A té Node 22 i el B, Node 18, i l'error resultant és incomprensible. Amb Docker, la versió de Node va dins de la imatge i no depèn del terminal.
  • Scripts SQL no idempotents. Si init.sql no tingués IF NOT EXISTS i ON CONFLICT DO NOTHING, executar-lo dues vegades donaria errors o duplicaria llibres. Al mòdul 4, PostgreSQL executarà aquest script automàticament; que sigui idempotent evita sorpreses.
  • Consell: guarda el recompte dels 15 passos. Quan al mòdul 4 escriguis docker compose up i tot funcioni, torna a aquesta taula. És la millor manera de mesurar el que has guanyat.
  • Consell: no esborris ~/aurora-libros. Aquest directori és el projecte del curs sencer.

Exercicis

Exercici 1: munta el projecte i documenta el dolor

Crea l'estructura completa aurora-libros/ amb els quatre fitxers (api/package.json, api/server.js, db/init.sql, web/index.html) i intenta arrencar l'API sense Docker al teu sistema. Ves anotant en un fitxer NOTES-ONBOARDING.md:

  1. Cada comanda que has hagut d'executar.
  2. Cada error que t'ha aparegut, amb el seu missatge literal.
  3. El temps total invertit.
  4. Quin programari queda instal·lat a la teva màquina en acabar.

No importa si no aconsegueixes completar-ho: el que interessa és el registre del procés.

Exercici 2: interpreta els diagnòstics

Per a cadascun d'aquests missatges, indica quina peça falta o està malament, i què comprovaries exactament:

a) Error: Cannot find module 'pg'
b) [aurora-api] fallada en arrencar: connect ECONNREFUSED 127.0.0.1:6379
c) {"servei":"aurora-api","db":"ko","cache":"ok","errorDb":"database \"aurora_llibres\" does not exist"}
d) {"servei":"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

Exercici 3: prepara el terreny per al mòdul 2

Sense escriure encara cap Dockerfile (això és el mòdul 2), respon raonadament:

  1. Quina imatge base triaries per a aurora-api i per què? Consulta'n la mida amb docker pull i docker image ls.
  2. Quines imatges oficials faries servir per a aurora-db i aurora-cache? Descarrega-les i anota la mida de cadascuna.
  3. Dels fitxers de ~/aurora-libros/api/, quins haurien d'entrar a la imatge de l'API i quins no? Justifica-ho.
  4. Les variables DB_PASSWORD i similars, haurien d'anar dins de la imatge? Per què?

Solucions

Solució a l'exercici 1

No hi ha una solució única, però el teu NOTES-ONBOARDING.md s'hauria d'assemblar a això:

# Onboarding d'Aurora Libros sense Docker — 4 d'agost de 2026

## Comandes executades
1. mkdir -p ~/aurora-libros/{api,db,web}
2. node --version → v18.19.1 (insuficient, es demana >=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 paquets
6. node server.js → ECONNREFUSED :6379
7. sudo apt install -y redis-server && sudo systemctl enable --now redis-server
8. node server.js → arrenca; /salut retorna 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_llibres 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_llibres -f ../db/init.sql
14. node server.js → OK

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

## Temps total
1 h 35 min (i ja coneixia PostgreSQL)

## Programari que queda instal·lat permanentment
- nvm + Node 22 (a més del Node 18 del sistema)
- redis-server (servei actiu en arrencar)
- postgresql-16 (servei actiu en arrencar) + fitxer pg_hba.conf modificat

La reflexió important: aquest temps i aquests residus es multipliquen per cada persona de l'equip i per cada màquina, i cap d'ells no garanteix que tothom acabi amb exactament les mateixes versions.

Solució a l'exercici 2

(a) Cannot find module 'pg'. Falta la dependència d'npm, no un servei. És un error de l'arrencada del procés, anterior a qualsevol connexió. Comprovaries que existeix node_modules/ i que npm install es va executar al directori correcte (api/, on hi ha el package.json). Causa típica: haver llançat node server.js des de l'arrel del projecte o des d'una altra carpeta.

(b) ECONNREFUSED 127.0.0.1:6379. Falta Redis: ningú no escolta al port 6379. Comprovaries si el servei existeix i està arrencat (systemctl status redis-server), si respon (redis-cli pingPONG) i si alguna cosa escolta en aquell port (ss -tln | grep 6379). Compte amb la distinció: rebutjat és "no hi ha ningú"; si el missatge fos un timeout, apuntaria a tallafoc o a una màquina inabastable.

(c) database "aurora_llibres" does not exist. PostgreSQL sí que està corrent i sí que accepta la connexió (fixa't que l'error ja no és de xarxa, sinó del servidor), però la base de dades no s'ha creat. Falta el pas CREATE DATABASE aurora_llibres OWNER aurora;. Comprovaries les bases existents amb sudo -u postgres psql -c "\l".

(d) password authentication failed for user "aurora". El servidor respon i la base existeix, però les credencials no quadren. Comprovaries: que l'usuari existeixi (\du a psql), que la contrasenya coincideixi amb la de DB_PASSWORD, i que pg_hba.conf faci servir un mètode compatible (scram-sha-256 en lloc de peer, que només funciona per socket local). És l'error més lent de diagnosticar dels cinc.

(e) EADDRINUSE: address already in use 0.0.0.0:3000. No falta res: sobra alguna cosa. Ja hi ha un procés escoltant al port 3000, gairebé sempre una altra instància de la mateixa API que vas deixar corrent en un altre terminal. El localitzaries amb ss -tlnp | grep 3000 o lsof -i :3000, i l'aturaries, o arrencaries aquesta instància en un altre port amb PORT=3001 node server.js. Aquesta és la versió "sense Docker" del port is already allocated que vas veure a la lliçó 01-06.

Solució a l'exercici 3

  1. Imatge base per a aurora-api: node:22-alpine. Raons: compleix l'engines: node >=22.0.0 del package.json, és oficial (espai library/), i la variant Alpine pesa uns 140 MB davant dels ~1,1 GB de node:22 completa. Comprova-ho:
docker pull node:22-alpine
docker pull node:22
docker image ls node --format "table {{.Tag}}\t{{.Size}}"

En un pipeline que construeix i descarrega la imatge diverses vegades al dia, aquesta diferència de gairebé un gigabyte es tradueix en minuts d'espera i en cost de transferència. La contrapartida és musl en lloc de glibc (lliçó 01-05), que aquí no suposa cap problema perquè les tres dependències són JavaScript pur o porten binaris compatibles amb Alpine.

  1. postgres:16-alpine per a aurora-db i redis:7-alpine per a aurora-cache. Totes dues són oficials i fixen la versió major, tal com exigeix l'enunciat del projecte.
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

Fixa't que no fem servir latest en cap: sabem exactament quina versió major tindrà cada servei, complint el que vas aprendre a la lliçó 01-05.

  1. Què entra i què no a la imatge:
Fitxer o carpeta Hi entra? Motiu
package.json Defineix les dependències que cal instal·lar
server.js És l'aplicació
package-lock.json Fixa les versions exactes; és el que fa reproduïble l'npm ci
node_modules/ No S'instal·la dins de la imatge. Copiar el de la teva màquina pot ficar-hi binaris compilats per a un altre sistema operatiu o arquitectura
.git/ No Historial pesat i irrellevant en execució; a més, pot contenir informació sensible
NOTES-ONBOARDING.md, .env No Documentació local i, sobretot, secrets, que mai no han de viatjar dins d'una imatge

El mecanisme per excloure'ls s'anomena .dockerignore i el veuràs al mòdul 2.

  1. DB_PASSWORD no ha d'anar dins de la imatge. Mai. Tres raons acumulatives:

    • Seguretat: les capes d'una imatge són inspeccionables per qualsevol que la tingui (docker image history et mostra les instruccions). Una contrasenya ficada allà és una contrasenya publicada, i esborrar-la en una capa posterior no l'elimina de la capa inferior (lliçó 01-05).
    • Portabilitat: la contrasenya de desenvolupament i la de producció són diferents. Si va a dins, necessitaries una imatge diferent per entorn, trencant la promesa de "construir un cop, desplegar a tot arreu".
    • Rotació: canviar una contrasenya obligaria a reconstruir i tornar a desplegar la imatge.

    Per això server.js llegeix tota la seva configuració de process.env. Els valors s'injecten en executar el contenidor, no en construir-lo: amb -e o --env-file (mòdul 3), amb variables a compose.yaml (lliçó 04-05), o amb mecanismes específics de secrets (lliçó 05-03).

Conclusió

Aurora Libros ja no és una idea abstracta: té un repositori, una API amb tres endpoints, un catàleg de vuit llibres a PostgreSQL, una memòria cau a Redis i un web estàtic esperant que algú el connecti. També té un problema perfectament quantificat. Has executat la plataforma sense Docker i has necessitat quinze passos manuals, dos gestors de paquets, tres serveis instal·lats permanentment al teu sistema, una eina nova per manejar versions de Node i una edició a mà de pg_hba.conf. Pel camí has vist els ECONNREFUSED característics de "no existeix aquest servei", has descobert que la memòria cau funciona observant el camp origen, i has acabat amb una màquina més bruta del que la vas començar i sense cap garantia que el teu entorn s'assembli al dels teus companys.

També has vist per què el codi està escrit com està: tota la configuració es llegeix de variables d'entorn perquè el mateix server.js valgui al teu portàtil i dins d'un contenidor; l'API escolta a 0.0.0.0 per ser abastable des de fora de la seva xarxa; init.sql és idempotent per poder executar-se a cada arrencada; i /salut existeix perquè al mòdul 6 serà el que consulti el balancejador abans d'enviar trànsit a una rèplica. Cadascuna d'aquestes decisions prendrà tot el seu sentit als propers mòduls.

Amb això tanques el mòdul 1. Saps què és Docker, el tens instal·lat i verificat, entens la seva arquitectura client-servidor amb dockerd, containerd i runc, manejes la gramàtica de la seva CLI, comprens les imatges per dins —capes, copy-on-write, digests i etiquetes— i has creat, publicat en un port, inspeccionat i destruït els teus primers contenidors. Tens les eines i tens el problema. Al mòdul 2, Treballant amb Imatges Docker, començaràs a unir totes dues coses: coneixeràs Docker Hub a fons i escriuràs el teu primer Dockerfile per empaquetar aurora-api en una imatge pròpia, construïble amb una comanda i executable a qualsevol màquina del món sense instal·lar-hi Node. El primer dels quinze passos desapareixerà; els altres cauran un a un als mòduls següents.

Docker: De Principiant a Avançat

Mòdul 1: Introducció a Docker

Mòdul 2: Treballant amb Imatges Docker

Mòdul 3: Contenidors Docker

Mòdul 4: Docker Compose

Mòdul 5: Conceptes Avançats de Docker

Mòdul 6: Docker en Producció

Mòdul 7: Ecosistema i Eines de Docker

© Copyright 2026. Tots els drets reservats