Escena Viva està configurada, observada, supervisada i empaquetada. El que encara no està és a internet. La Lucía no pot comprar les seves entrades per al Festival de Jazz de Primavera perquè l'aplicació continua executant-se a localhost:3000. Aquesta lliçó la posa en producció de debò. I ho fa pel camí que té més sentit per a un projecte d'aquesta mida: una plataforma com a servei, on el proveïdor s'ocupa de les màquines, el sistema operatiu, el balancejador, els certificats TLS i l'escalat, i tu t'ocupes de la teva aplicació. Veurem el procés complet amb Heroku com a cas canònic, entenent que els seus conceptes són idèntics a Railway, Render, Fly.io o Cloud Run.

Contingut

  1. Què és una PaaS i què t'estalvia
  2. Heroku i les seves alternatives actuals
  3. Desplegar Escena Viva pas a pas
  4. Complements gestionats i el problema de les connexions
  5. Migracions al desplegament i canvis d'esquema sense parada
  6. El sistema de fitxers efímer
  7. HTTPS, servidor intermediari invers i trust proxy
  8. Escalat horitzontal i vertical
  9. Dominis, preproducció i estratègies de desplegament
  10. Com es reverteix un desplegament
  11. Costos i dimensionament amb dades

  1. Què és una PaaS i què t'estalvia

Una plataforma com a servei rep el teu codi o la teva imatge, l'executa i s'encarrega de tot el que hi ha a sota: aprovisionar màquines, apedaçar el sistema operatiu, configurar el balancejador, emetre i renovar certificats TLS, recollir registres, reiniciar processos caiguts i escalar quan li ho demanes. A canvi et cobra diners —bastant més per unitat de còmput que un VPS— i control: no tries la versió del nucli, no instal·les paquets del sistema arbitraris, no controles on s'executa cada instància i estàs subjecte als límits del proveïdor.

Model Què gestiones tu Què gestiona el proveïdor Cost Quan triar-lo
VPS / servidor propi Tot: SO, Node, proxy, TLS, PM2, còpies, seguretat Només el maquinari El més baix per CPU Pressupost ajustat i algú amb temps per operar
PaaS Codi i configuració SO, TLS, balancejador, escalat, registres Mitjà-alt Equips petits que volen enviar producte
Contenidors gestionats (Cloud Run, ECS, App Runner) Imatge i configuració Execució, escalat, xarxa Mitjà, sovint per ús real Ja tens la imatge de 11-04 i vols control amb poca feina
Serverless (Lambda, Functions) Només funcions Absolutament tot Molt baix si hi ha poc trànsit Càrregues esporàdiques o pics extrems
Orquestrador (Kubernetes) Moltíssim Molt poc Alt en persones Molts serveis, diversos equips

Per a Escena Viva —tres sales, un equip petit, pics concrets a les estrenes— l'elecció honesta és entre PaaS i contenidors gestionats. Serverless queda descartat: tenim un consumidor de cua de vida llarga, connexions persistents a PostgreSQL i arrencades en fred que arruïnarien el p99 que tant vam cuidar al mòdul 10. Kubernetes és artilleria per caçar mosques.

  1. Heroku i les seves alternatives actuals

Heroku va inventar aquest model el 2007 i va definir el vocabulari que tothom fa servir: Procfile, dynos, buildpacks, complements, fase d'alliberament. Val la pena estudiar-lo per això, encara que avui convingui ser honest amb dues coses: la seva capa gratuïta va desaparèixer el novembre de 2022 —el «desplega gratis en cinc minuts» dels tutorials antics ja no existeix— i avui hi ha alternatives equivalents, algunes amb capa gratuïta o de cost molt baix.

Proveïdor Model Nota
Heroku Buildpacks o contenidor L'original; vocabulari de referència
Railway Detecta el projecte o usa Dockerfile Molt simple; bon graó per començar
Render Buildpacks o Dockerfile El més semblant a Heroku avui
Fly.io Contenidors a la vora Desplegament a prop de l'usuari; bon preu
DigitalOcean App Platform Buildpacks o Dockerfile Integrat amb la resta del seu ecosistema
Google Cloud Run Només contenidors Escala a zero; pagament per petició
AWS App Runner Contenidor o codi Bé si ja vius a AWS

Tots comparteixen els mateixos conceptes, i són aquests conceptes —no la interfície de cap proveïdor— el que cal aprendre: declarar tipus de procés (un web, altres de fons); escoltar al port que la plataforma assigna; rebre la configuració per variables d'entorn; consumir serveis gestionats a través d'una URL injectada; executar migracions en una fase d'alliberament; acceptar un sistema de fitxers efímer; i tenir TLS acabat a la vora, parlant HTTP per dins. Après això, canviar de proveïdor és qüestió d'hores.

  1. Desplegar Escena Viva pas a pas

El Procfile i els dos tipus de procés

El Procfile declara quins processos componen la teva aplicació. Escena Viva té els mateixos dos que vam declarar a PM2 (11-03) i a compose (11-04):

web: node src/servidor.js
worker: node src/processos/consumidor-entrades.js
release: npm run migrar

Tres línies i tres significats molt diferents:

  • web és l'únic tipus amb nom reservat: és el que rep el trànsit HTTP de l'enrutador de la plataforma, i l'únic al qual s'assigna un port.
  • worker és un nom lliure per a processos de fons. És el nostre consumidor de la cua BullMQ del mòdul 10: no escolta en cap port, es connecta a Redis i processa tasques. S'escala independentment del web, que és justament el que volem durant l'estrena del Festival de Jazz.
  • release és especial: s'executa una sola vegada per desplegament, abans que la versió nova rebi trànsit. Si falla, el desplegament s'avorta. L'apartat 5 viu d'aquí.

Fixa't que no hi ha npm start: per la mateixa raó que a Docker, un procés intermedi d'npm complica el reenviament de senyals. S'invoca node directament.

El port: process.env.PORT

Aquest és el punt on més desplegaments fallen la primera vegada, i enllaça directament amb el mòdul 4. La plataforma executa diversos contenidors a cada màquina i assigna a cadascun un port arbitrari, que comunica per la variable PORT. El teu procés ha d'escoltar en aquell port. Si fixes el 3000, l'enrutador de la plataforma trucarà al port que va assignar, no hi haurà ningú escoltant i el desplegament fallarà amb un temps d'espera exhaurit. El nostre esquema de 11-01 ja llegeix PORT, que és exactament el nom que injecta la plataforma, així que no cal traduir res: només cal no fixar-lo mai:

// src/config/index.js — la plataforma injecta PORT i el nostre esquema
// ja fa servir aquest mateix nom, aixi que no hi ha res a normalitzar.
// El valor de l'entorn mana sempre sobre el .default(3000) de l'esquema.
const resultat = esquemaConfiguracio.safeParse(process.env);

// src/servidor.js — escoltar a 0.0.0.0, NO a 127.0.0.1: dins d'un
// contenidor, 127.0.0.1 nomes es abastable des del mateix contenidor.
servidor.listen(configuracio.port, '0.0.0.0');

La regla general: en producció no es fixa mai un port. En desenvolupament tries el 3000 per comoditat; en producció el dicta l'entorn. És el mateix principi dels dotze factors que governa tota la configuració.

engines i la construcció

package.json ja declara la versió de Node, i les PaaS ho llegeixen per triar l'intèrpret:

{
  "engines": { "node": ">=24.5.0 <25", "npm": ">=10" },
  "scripts": { "start": "node src/servidor.js", "migrar": "sequelize-cli db:migrate" }
}

Sense engines, la plataforma tria la versió que li sembla —normalment la LTS del moment— i pots acabar en producció amb una versió diferent de la que vas provar. Un rang com aquest permet pedaços de seguretat sense saltar de major. A la construcció, la plataforma detecta Node i executa npm ci --omit=dev si existeix package-lock.json (per això està versionat des del mòdul 5) i després npm run build si existeix. Si prefereixes control total, gairebé totes accepten el teu Dockerfile de 11-04, i llavors la construcció és exactament la que tu vas escriure. Per a Escena Viva és el recomanable: la imatge provada a CI és la que s'executa en producció, sense traduccions intermèdies.

Configuració: el tauler, no el .env

A la PaaS no existeix el .env (i no ha d'existir: el .dockerignore de 11-04 se n'encarrega). Les variables es defineixen al tauler o per CLI:

heroku config:set NODE_ENV=production NIVELL_REGISTRE=info \
  CONFIAR_EN_PROXY=true ORIGEN_CORS=https://escenaviva.test --app escena-viva
heroku config:set JWT_SECRET=$(openssl rand -hex 32) \
  SESSIO_SECRET=$(openssl rand -hex 32) \
  CSRF_SECRET=$(openssl rand -hex 32) --app escena-viva
heroku config --app escena-viva   # Revisar el que hi ha

Aquí es cobra un altre dividend de 11-01: si oblides una variable obligatòria, la validació zod mata el procés a l'arrencada amb un missatge clar, la plataforma detecta que la versió nova no ha arrencat i manté l'anterior en servei. Fallar de pressa converteix un error de configuració en un desplegament avortat en comptes d'una caiguda. Dos avisos: canviar una variable reinicia l'aplicació a la majoria de plataformes (coherent amb la decisió de 11-01 de reiniciar en comptes de recarregar en calent), i qualsevol amb accés al tauler veu tots els secrets en clar, així que el control d'accessos del projecte és part de la teva seguretat.

  1. Complements gestionats i el problema de les connexions

Els complements són serveis que la plataforma aprovisiona i connecta a la teva aplicació injectant-ne l'URL com a variable d'entorn:

heroku addons:create heroku-postgresql:standard-0 --app escena-viva
heroku addons:create heroku-redis:premium-0 --app escena-viva
# MongoDB sol venir d'un tercer: MongoDB Atlas.

Es creen DATABASE_URL i REDIS_URL. Com que el nostre esquema espera URL_POSTGRES i URL_REDIS, es normalitza al mateix lloc on es llegeix tot:

// El nom del proveidor mana; el nostre queda com a alternativa local.
const entornNormalitzat = {
  ...process.env,
  URL_POSTGRES: process.env.DATABASE_URL ?? process.env.URL_POSTGRES,
  URL_REDIS: process.env.REDIS_URL ?? process.env.URL_REDIS,
};

Un sol fitxer absorbeix totes les particularitats del proveïdor. Aquest és exactament el benefici del punt únic de lectura de process.env. Detall pràctic: l'URL de la base de dades pot canviar sense avisar (manteniment, commutació per error). No la copiïs mai a un altre lloc ni la desis a la memòria cau: llegeix-la sempre de l'entorn.

El problema de les connexions, que és seriós

Aquí hi ha l'error que tomba aplicacions el dia de l'estrena. Els plans gestionats limiten el nombre de connexions simultànies. Un pla petit de PostgreSQL pot permetre'n 20. I l'aritmètica és implacable: l'aritmètica és instàncies web × pool + instàncies worker × (pool + connexions de BullMQ). Escenari realista per a l'estrena del Festival de Jazz:

Concepte Quantitat Pool Connexions
Instàncies web 4 10 40
Instàncies worker 3 5 15
Fase d'alliberament (migracions) 1 2 2
Total 57

Amb un límit de 20, la meitat dels processos no aconsegueixen connectar-se. I el pitjor: l'aplicació arrenca igualment (el pool crea connexions sota demanda) i falla sota càrrega, que és quan surt més car. Tres remeis, per ordre:

  1. Dimensionar el pool segons les instàncies, que és exactament el que fa src/db/sequelize.js des del mòdul 7 amb el nombre de treballadors. Aquí la variable és el nombre d'instàncies de la plataforma:
// src/db/sequelize.js (fragment)
const LIMIT_CONNEXIONS = Number(process.env.LIMIT_CONNEXIONS_BD ?? 20);
const INSTANCIES = Number(process.env.INSTANCIES_ESPERADES ?? 4);
// Reservem 4 connexions per a migracions i per connectar-se a depurar.
const poolMaxim = Math.max(2, Math.floor((LIMIT_CONNEXIONS - 4) / INSTANCIES));
  1. Fer servir un agrupador de connexions extern (PgBouncer, o el pgbouncer que ofereixen alguns plans). Multiplexa moltes connexions d'aplicació sobre poques de servidor. Compte: en mode transacció no es poden fer servir sentències preparades de sessió, i cal dir-ho a Sequelize.

  2. Pujar de pla. De vegades és simplement la resposta correcta, i costa menys que un redisseny.

Amb Redis passa el mateix, agreujat perquè BullMQ obre diverses connexions per treballador (una per escoltar esdeveniments blocadors, una altra per a ordres). Amb CONCURRENCIA_CUA=5 i 3 instàncies, compta 30-40 connexions a Redis, no 3.

  1. Migracions al desplegament i canvis d'esquema sense parada

La fase d'alliberament

La línia release: npm run migrar del Procfile s'executa una vegada per desplegament, després de construir i abans que la versió nova rebi trànsit. Si falla, el desplegament es cancel·la i la versió antiga continua servint. És la solució al problema que vam plantejar a 11-04: les migracions no van a l'arrencada de l'aplicació. Si anessin al CMD, amb quatre instàncies arrencant alhora tindries quatre processos migrant el mateix esquema en paral·lel. A la fase d'alliberament s'executen una sola vegada, en un contenidor efímer, amb la sortida visible als registres del desplegament.

Canvis d'esquema sense parada: expandir → migrar → contraure

I aquí ve el concepte més valuós de la lliçó. Durant un desplegament conviuen, encara que sigui uns segons, la versió antiga i la nova del codi sobre la mateixa base de dades. Amb desplegament blau-verd o canari, aquella convivència dura minuts o hores. Per tant: tota migració ha de ser compatible amb el codi anterior i amb el nou. Un ALTER TABLE ... RENAME COLUMN trenca instantàniament totes les instàncies antigues, que continuen consultant el nom vell. La tècnica és dividir el canvi en tres desplegaments. Exemple real: a Escena Viva volem reanomenar preu (un nombre decimal, herència d'un model antic) per preuCentims (enter), respectant la nostra convenció de diners en cèntims. Desplegament 1 — Expandir. La migració afegeix sense treure res:

// migrations/20260815100000-afegir-preu-centims.js
'use strict';

module.exports = {
  async up(consultes, tipusDades) {
    // Columna nova i ANULLABLE: el codi antic la ignora sense problema.
    await consultes.addColumn('sessions', 'preuCentims', {
      type: tipusDades.INTEGER, allowNull: true,
    });
    // I s'omple amb les dades existents.
    await consultes.sequelize.query(
      'UPDATE sessions SET "preuCentims" = ROUND(preu * 100) WHERE "preuCentims" IS NULL'
    );
  },
  async down(consultes) {
    await consultes.removeColumn('sessions', 'preuCentims');
  },
};

El codi d'aquest desplegament escriu a les dues columnes i llegeix de l'antiga. Totes dues versions funcionen. Desplegament 2 — Migrar. El codi passa a llegir de preuCentims i continua escrivint a totes dues. Si cal revertir a la versió 1, tot continua funcionant perquè totes dues columnes estan al dia. Desplegament 3 — Contraure. El codi deixa de tocar preu, i només llavors la migració l'elimina amb await consultes.removeColumn('sessions', 'preu').

Desplegament Migració El codi escriu El codi llegeix Es pot revertir?
1. Expandir Afegeix preuCentims i omple Totes dues preu Sí
2. Migrar Cap Totes dues preuCentims Sí
3. Contraure Elimina preu preuCentims preuCentims Només al desplegament 2

És més lent i són tres passos en comptes d'un. A canvi, cap dels tres exigeix aturar l'aplicació ni impedeix revertir. En una plataforma de venda d'entrades, on una aturada de dos minuts durant l'estrena són centenars de vendes perdudes, aquesta lentitud és exactament el que vols. Regla pràctica per a migracions grans: compte amb els ALTER TABLE que bloquegen la taula sencera. Afegir una columna anul·lable és instantani a PostgreSQL modern; omplir dos milions de files no ho és, i convé fer-ho per lots.

  1. El sistema de fitxers efímer

El disc d'una instància és efímer. Quan la instància es reinicia —cada desplegament, cada canvi de configuració, cada reciclatge de la plataforma— tot el que s'hi ha escrit desapareix. I a més cada instància té el seu propi disc: el que escriu una no ho veu una altra. Això invalida completament el que vam fer al mòdul 3 amb el directori informes/, on guardàvem els PDF de les entrades generats pel pool de worker threads. En producció sobre PaaS: el PDF EV-2026-004182.pdf que genera el worker no existeix per a la instància web que atendrà la descàrrega de la Lucía, i encara que existís desapareixeria al desplegament següent. La solució és emmagatzematge d'objectes —S3, Cloud Storage, R2, Spaces—, amb aquest flux:

// src/serveis/magatzem-informes.js
'use strict';

const { S3Client, PutObjectCommand, GetObjectCommand } = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
const { configuracio } = require('../config/index.js');

const client = new S3Client({ region: configuracio.magatzem.regio });
const Bucket = configuracio.magatzem.cubell;

async function guardarEntradaPdf({ codiEntrada, contingut }) {
  const clau = `entrades/${codiEntrada}.pdf`;
  await client.send(
    new PutObjectCommand({
      Bucket, Key: clau, Body: contingut, ContentType: 'application/pdf',
      ACL: 'private',   // Mai public: les entrades son nominals.
    })
  );
  return clau;
}

// El PDF no passa pel nostre servidor: se signa una URL temporal.
async function urlDescarregaTemporal(clau, segons = 300) {
  return getSignedUrl(client, new GetObjectCommand({ Bucket, Key: clau }), {
    expiresIn: segons,
  });
}

module.exports = { guardarEntradaPdf, urlDescarregaTemporal };

Fixa't en el patró de l'URL signada: el worker puja el PDF al magatzem i en desa la clau; quan la Lucía demana la seva entrada, l'API comprova que és seva (exigir-propietat.js del mòdul 8) i retorna una URL signada que caduca en cinc minuts. El fitxer no travessa mai el nostre servidor, així que no consumeix ni memòria ni amplada de banda de la instància. La mateixa regla val per a qualsevol estat local: pujades d'usuaris, memòries cau en disc, fitxers de sessió, fitxers de registre. Res persistent al disc de la instància. Les sessions i la limitació de peticions ja són a Redis des del mòdul 10, precisament per això.

  1. HTTPS, servidor intermediari invers i trust proxy

Aquí complim la promesa del mòdul 8: HTTPS i certificats. La bona notícia és que en una PaaS no gestiones certificats. La plataforma posa un servidor intermediari invers a la vora que acaba el TLS: rep HTTPS del navegador, valida el certificat (emès i renovat automàticament, normalment via Let's Encrypt) i parla HTTP pla amb la teva instància per la xarxa interna.

flowchart LR
    N[Navegador de la Lucia] -->|HTTPS 443| B[Proxy de la vora: el TLS acaba aqui]
    B -->|HTTP + capcaleres X-Forwarded-*| W1[Instancia web 1]
    B -->|HTTP| W2[Instancia web 2]
    W1 --> R[(Redis)]
    W1 --> P[(PostgreSQL)]

La teva aplicació no necessita https.createServer ni certificats. Node serveix HTTP i això és correcte. Però té una conseqüència important, i és la promesa que vam deixar pendent al mòdul 6. Des del punt de vista d'Express, totes les peticions vénen del proxy: req.ip és la IP interna del balancejador, req.protocol és http i req.secure és false. La informació real viatja en capçaleres:

Capçalera Contingut
X-Forwarded-For Cadena d'IP: la del client primer
X-Forwarded-Proto Protocol original (https)
X-Forwarded-Host Host sol·licitat pel client

Express només les fa servir si l'hi dius, a src/app.js:

// Nombre de proxies de confianca al davant. La PaaS en posa un.
// MAI 'true' en produccio: confiar en qualsevol capcalera permet
// que un client falsifiqui la seva IP posant X-Forwarded-For a ma.
if (configuracio.confiarEnProxy) app.set('trust proxy', 1);

Sense trust proxy, tres coses es trenquen alhora:

  1. La limitació de peticions per IP (express-rate-limit, mòdul 6, amb magatzem Redis del mòdul 10) veu la mateixa IP —la del proxy— per a tots els usuaris. Resultat: o el límit global s'exhaureix en segons i bloqueja tothom, o el poses tan alt que no protegeix de res. És la fallada més greu de les tres.
  2. Les galetes secure no s'envien, perquè Express creu que la connexió no és segura. Les sessions deixen de funcionar sense cap missatge d'error útil.
  3. Els registres de 11-02 desen la IP del balancejador en comptes de la de l'usuari, i tota investigació d'abús es torna impossible.

El número importa: app.set('trust proxy', 1) significa «confia en l'últim salt». Si hi poses true, Express confia en tota la cadena d'X-Forwarded-For, i com que qualsevol pot enviar aquella capçalera, qualsevol pot fingir la seva IP i saltar-se la limitació de peticions. Compta els proxies reals i posa aquell número. Dos afegits que sí que depenen de tu: redirigir HTTP a HTTPS (moltes plataformes ho fan, però convé comprovar-ho) i activar HSTS, que ja ve amb helmet des del mòdul 6 i diu al navegador que no torni a intentar HTTP mai més.

  1. Escalat horitzontal i vertical

Vertical Horitzontal
Què fas Instàncies més grans Més instàncies
Límit La mida màxima que ofereixin Pràcticament cap
Tolerància a fallades Si cau, cau tot Si en cau una, queden les altres
Requisit Cap L'aplicació no pot tenir estat
A Node Poc útil: un procés fa servir un nucli El camí natural

L'escalat vertical és especialment fluix a Node: un procés fa servir un nucli, així que passar a una màquina de 8 CPU no accelera res per si sol. Per això existia el clúster del mòdul 10. En una PaaS, la unitat d'escalat ja és la instància, així que s'escala horitzontalment i s'executa un procés per instància (mateixa doctrina que en contenidors, 11-04).

heroku ps:scale web=4 worker=3 --app escena-viva

I ara l'important: Escena Viva ja està preparada per a això, i no per casualitat. Cada decisió del curs apuntava aquí:

Requisit d'escalat horitzontal On el vam resoldre
Sense estat a la memòria del procés Disseny des del mòdul 6
Sessions compartides connect-redis (mòdul 10)
Limitació de peticions compartida rate-limit-redis (mòdul 10)
Memòria cau compartida src/cache/cataleg.js a Redis (mòdul 10)
Feina de fons fora del procés web BullMQ i el consumidor (mòdul 10)
Fitxers fora del disc local Magatzem d'objectes (apartat 6)
Aturada ordenada en reduir instàncies SIGTERM (mòdul 6)
Sonda per retirar trànsit /salut/preparat (11-02)

Si alguna cosa d'això faltés, escalar a 4 instàncies produiria errors intermitents impossibles de reproduir: sessions que es perden en saltar d'instància, límits que no limiten, PDF que no apareixen. Que la llista sigui completa és la raó per la qual ps:scale web=4 simplement funciona.

  1. Dominis, preproducció i estratègies de desplegament

Dominis. S'afegeix el domini, s'apunta un CNAME al que indiqui el proveïdor i aquest emet el certificat automàticament. Recorda actualitzar ORIGEN_CORS amb el domini real: és l'error de configuració més freqüent en estrenar domini. Preproducció. Una segona aplicació amb la mateixa base de codi i la seva pròpia configuració:

Producció Preproducció
Aplicació escena-viva escena-viva-pre
NODE_ENV production staging
Base de dades Pla gran, amb còpies Pla petit, dades anonimitzades
Instàncies web=4, worker=3 web=1, worker=1
Passarel·la de pagament Real Mode prova
Correu Real A una bústia de captura .test

Dos avisos que valen diners: les dades de preproducció han d'estar anonimitzades (una còpia crua de producció és una filtració de dades personals esperant a passar), i la passarel·la en mode prova evita cobrar de debò a ningú durant una demostració. Estratègies de desplegament:

Estratègia Com funciona A favor En contra
Recreate Atura tot i arrenca el nou Simple Tall de servei
Progressiu (per defecte a gairebé totes) Reemplaça instàncies d'una en una Sense tall Conviuen dues versions
Blau-verd S'aixeca l'entorn complet nou i es commuta el trànsit de cop Reversió instantània Doble cost durant la finestra
Canari S'envia el 5 % del trànsit a la versió nova, s'observa i es puja Limita el dany d'una fallada Requereix mètriques i enrutament per pes

El progressiu és l'habitual i és el que imposa la disciplina d'expandir → migrar → contraure de l'apartat 5: durant el reemplaçament conviuen les dues versions sobre el mateix esquema. El canari brilla justament en un cas com el nostre: si el 5 % del trànsit va a la versió nova i la taxa d'error de /api/compres puja (mètrica de 11-02), es reverteix abans que ho noti el 95 % restant. Per fer-ho bé necessites les mètriques per versió, cosa que exigeix etiquetar els registres i les mètriques amb la versió desplegada — una altra raó per registrar la versió en arrencar, com vam recomanar a 11-02.

  1. Com es reverteix un desplegament

Això és el primer que cal saber fer. Abans de desplegar per primera vegada, assegura't de saber revertir. Un equip que sap revertir en trenta segons desplega amb tranquil·litat; un que no, desplega els dimarts al matí amb por.

$ heroku releases --app escena-viva
v52  Deploy 8f3a1c2    [email protected]  2026/08/15 18:02
v51  Deploy 4b9e0d7    [email protected]   2026/08/15 11:40
v50  Set JWT_SECRET    [email protected]   2026/08/14 09:15

$ heroku releases:rollback v51 --app escena-viva

Cada versió és immutable i conté codi i configuració, així que revertir retorna el conjunt exacte que funcionava. En segons. Tres coses que cal tenir clares sobre la reversió:

  1. Revertir el codi no reverteix la base de dades. Si la versió v52 va executar una migració destructiva, tornar a v51 deixa codi antic sobre un esquema nou. Per això les migracions es dissenyen compatibles cap enrere. L'estratègia de tres passos no és purisme: és el que fa que revertir sigui segur.
  2. Revertir primer, investigar després. Igual que amb un secret filtrat a 11-01: primer es restaura el servei, després es busca la causa. L'impuls de «deixa'm que ho miri cinc minuts» ha allargat moltes incidències.
  3. Assaja la reversió. Fes-la una vegada en preproducció, amb cronòmetre. Descobriràs detalls —permisos que falta donar, qui hi té accés, quant triga— que no vols descobrir amb la venda caiguda.

Un patró complementari molt útil: desacoblar desplegament d'activació amb indicadors de funcionalitat. Desplegues el codi de la nova venda anticipada apagat, l'actives quan toca i, si va malament, l'apagues sense desplegar res. Recorda de 11-01 que aquells indicadors van a base de dades, no a variables d'entorn.

  1. Costos i dimensionament amb dades

Una PaaS cobra per instància i hora, més els complements. Un exemple realista per a Escena Viva en operació normal:

Concepte Quantitat Cost mensual aproximat
Instàncies web 2 mitjanes 100 €
Instàncies worker 1 mitjana 50 €
PostgreSQL gestionat Pla estàndard 50 €
Redis gestionat Pla petit 15 €
Magatzem d'objectes 50 GB + trànsit 5 €
Registres i mètriques Retenció de 14 dies 20 €
Total ~240 €/mes

Un VPS equivalent costaria 40-60 €, però caldria sumar-hi el temps d'algú mantenint-lo, actualitzant-lo, gestionant còpies de seguretat i responent de matinada. Amb una persona, la PaaS sol sortir més barata en total. I el consell que tanca el mòdul 10 i aquesta lliçó: dimensiona amb dades, no amb intuïció. Ja saps mesurar. Abans de decidir quantes instàncies necessites per a l'estrena del Festival de Jazz, llança autocannon contra preproducció amb un perfil de càrrega realista, mira el p99, el retard del bucle d'esdeveniments i l'ús de CPU, i calcula. «Posem-hi vuit instàncies per si de cas» costa diners cada mes i sovint no ajuda: si el coll d'ampolla és la base de dades, vuit instàncies només consumeixen més connexions i empitjoren les coses. Consells de cost que valen la pena: escala abans d'un pic previst (l'estrena té data) i redueix després; vigila la despesa en registres, que creix amb el trànsit i sorprèn tothom; i activa alertes de facturació, perquè un bucle de reintents mal posat pot multiplicar la factura en un cap de setmana.

Errors Comuns i Consells

  • Fixar el port en producció. El desplegament falla amb temps d'espera exhaurit. Fes servir process.env.PORT i escolta a 0.0.0.0.
  • Oblidar trust proxy. La limitació per IP es trenca, les galetes segures no viatgen i els registres desen la IP del balancejador.
  • trust proxy a true. Permet falsificar la IP d'origen. Posa-hi el nombre de proxies reals.
  • Escriure fitxers al disc de la instància. Es perden a cada desplegament i no els veuen les altres instàncies.
  • Migracions a l'arrencada en comptes de a la fase d'alliberament. S'executen en paral·lel per cada instància.
  • Ignorar el límit de connexions del pla. Instàncies × pool exhaureix el pla i falla justament sota càrrega.
  • Desplegar sense saber revertir. L'error més car de tots.
  • Consell: documenta el procediment de reversió al README i assaja'l. L'ha de poder executar qualsevol de l'equip a les tres de la matinada.
  • Consell: desplega sovint i poc. Un desplegament amb dos dies de feina és un desplegament de risc baix; un amb dos mesos és una nit en blanc.

Exercicis

Exercici 1 — Auditoria de preparació

Repassa Escena Viva i enumera tot el que es trencaria en passar a 4 instàncies sense haver fet la feina dels mòduls 10 i 11: estat en memòria, fitxers locals, temporitzadors, memòria cau. Per a cada punt, indica el mecanisme que ho resol i en quin mòdul es va introduir.

Exercici 2 — Migració sense parada

Escena Viva necessita afegir el camp nomAssistent a la taula entrades, obligatori a partir d'ara. Dissenya la seqüència completa d'expandir → migrar → contraure indicant, per a cadascun dels tres desplegaments, què fa la migració i què fa el codi.

Exercici 3 — Pressupost de connexions

Amb un pla de PostgreSQL de 20 connexions i un de Redis de 30, calcula la configuració màxima d'instàncies web i worker per a Escena Viva, sabent que cada instància web fa servir un pool de PostgreSQL i que cada worker fa servir pool més 2 connexions de BullMQ per unitat de concurrència. Proposa valors concrets.

Solucions

Exercici 1.

Què es trencaria Per què Solució Mòdul
Sessions en memòria Cada instància té les seves; en saltar, l'usuari apareix desconnectat connect-redis M10
Limitació de peticions en memòria El límit es multiplica pel nombre d'instàncies rate-limit-redis M10
Memòria cau del catàleg en memòria Invalidacions inconsistents: una instància serveix dades velles Redis amb TTL M10
PDF a informes/ El worker escriu on el web no llegeix, i es perden en reiniciar Magatzem d'objectes 11-05
Tasques amb setInterval al procés web S'executen N vegades, una per instància Cua BullMQ amb tasca programada M10
Idempotència en memòria Una compra reenviada a una altra instància es duplicaria Claus a Redis M10
Comptadors de mètriques locals Cada instància exposa els seus Agregació a Prometheus 11-02

Exercici 2.

Desplegament 1 — Expandir. Migració: addColumn('entrades', 'nomAssistent', { type: STRING, allowNull: true }) i emplenat de les files existents amb el nom del comprador. Codi: escriu el camp si el rep, no l'exigeix i no el llegeix per a res. El zod del punt d'entrada el marca com a opcional. Desplegament 2 — Migrar. Sense migració. Codi: l'esquema zod passa a exigir nomAssistent a les compres noves i la interfície el mostra. Es pot revertir al desplegament 1 sense problema, perquè la columna continua sent anul·lable a la base de dades. Desplegament 3 — Contraure. Migració: changeColumn per posar allowNull: false, una vegada verificat amb una consulta que no queda cap fila nul·la. Codi: sense canvis. Aquest ordre és essencial: si posessis NOT NULL al desplegament 1, totes les instàncies antigues —que no envien el camp— començarien a fallar en inserir.

Exercici 3.

PostgreSQL, 20 connexions: en reservem 4 per a migracions i accés manual, en queden 16. Amb web=3 i worker=2, i pool de 3 per al web i 2 per al worker, surten 3×3 + 2×2 = 13 connexions i encaixa amb marge; amb web=4 i pool de 4 serien 16 només per al web, sense deixar res al worker. Redis, 30 connexions. BullMQ n'obre unes 2 per unitat de concurrència, més unes 2 per instància web per a sessions, límits i memòria cau. Web: 3 × 2 = 6. Worker: 2 instàncies × (5 de concurrència × 2) = 20, total 26: just, però hi cap. Amb CONCURRENCIA_CUA=3 en comptes de 5 serien 2 × 6 = 12, total 18, molt més folgat.

Configuració proposada: web=3 amb pool 3, worker=2 amb pool 2 i CONCURRENCIA_CUA=3. I la conclusió important: abans d'escalar cal fer aquest compte, perquè el símptoma d'exhaurir connexions (errors intermitents sota càrrega) és dels més difícils de diagnosticar en calent.

Conclusió

Escena Viva és a internet. Un Procfile declara les seves tres peces —el procés web, el worker que consumeix la cua del mòdul 10 i la fase release que executa les migracions una sola vegada—, escolta al port que assigna la plataforma, llegeix la seva configuració de les variables del proveïdor en comptes d'un .env, consumeix PostgreSQL i Redis gestionats amb el pool dimensionat per no exhaurir el pla, guarda els PDF de les entrades en un magatzem d'objectes perquè el disc de la instància és efímer, i serveix HTTPS amb el certificat que gestiona la vora mentre Express confia en un únic proxy i recupera així la IP real de cada usuari: la promesa del mòdul 6 complerta. Escala horitzontalment perquè cada peça d'estat es va treure del procés al seu degut temps, canvia d'esquema sense parada amb expandir → migrar → contraure, i es reverteix en segons, que és el primer que vam aprendre a fer. Queda una última baula, i és la que converteix tot això en rutina. Ara mateix desplegar continua sent una seqüència d'ordres que algú executa a mà, en l'ordre correcte, esperant no oblidar-ne cap. A l'última lliçó del mòdul, Integració i Desplegament Continus, automatitzem la cadena sencera: un fitxer de GitHub Actions que instal·la amb npm ci, passa el linter, executa les proves unitàries i d'integració contra PostgreSQL, MongoDB i Redis aixecats com a serveis, exigeix un llindar de cobertura, audita les dependències, construeix la imatge una sola vegada i la promou entre entorns, executa proves de fum contra /salut/preparat i reverteix sola si fallen. Fins a convertir pujar una versió en una cosa avorrida.

Curs de Node.js: De Principiant a Avançat

Mòdul 1: Introducció a Node.js

Mòdul 2: Conceptes Bàsics

Mòdul 3: Sistema de Fitxers i E/S

Mòdul 4: HTTP i Servidors Web

Mòdul 5: NPM i Gestió de Paquets

Mòdul 6: Framework Express.js

Mòdul 7: Bases de Dades i ORMs

Mòdul 8: Autenticació i Autorització

Mòdul 9: Proves i Depuració

Mòdul 10: Temes Avançats

Mòdul 11: Desplegament i DevOps

Mòdul 12: Projectes del Món Real

© Copyright 2026. Tots els drets reservats