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
- Què és una PaaS i què t'estalvia
- Heroku i les seves alternatives actuals
- Desplegar Escena Viva pas a pas
- Complements gestionats i el problema de les connexions
- Migracions al desplegament i canvis d'esquema sense parada
- El sistema de fitxers efímer
- HTTPS, servidor intermediari invers i
trust proxy - Escalat horitzontal i vertical
- Dominis, preproducció i estratègies de desplegament
- Com es reverteix un desplegament
- Costos i dimensionament amb dades
- 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.
- 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.
- 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):
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 haAquí 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.
- 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:
- Dimensionar el pool segons les instàncies, que és exactament el que fa
src/db/sequelize.jsdes 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));-
Fer servir un agrupador de connexions extern (PgBouncer, o el
pgbouncerque 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. -
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.
- 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.
- 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ò.
- HTTPS, servidor intermediari invers i
trust proxy
trust proxyAquí 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:
- 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. - Les galetes
secureno s'envien, perquè Express creu que la connexió no és segura. Les sessions deixen de funcionar sense cap missatge d'error útil. - 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.
- 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).
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.
- 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.
- 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-vivaCada 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ó:
- 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.
- 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.
- 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.
- 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.PORTi escolta a0.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 proxyatrue. 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
READMEi 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
- Què és Node.js?
- Instal·lació i Configuració de l'Entorn
- El Teu Primer Programa en Node.js
- El REPL de Node.js
- JavaScript Modern per a Node.js
- El Projecte del Curs: la Plataforma Escena Viva
Mòdul 2: Conceptes Bàsics
- Arquitectura de Node.js
- El Bucle d'Esdeveniments (Event Loop)
- Callbacks i Programació Asíncrona
- Promeses i async/await
- Esdeveniments i EventEmitter
- Mòduls CommonJS i require()
- Mòduls ES i Interoperabilitat
Mòdul 3: Sistema de Fitxers i E/S
- Lectura i Escriptura de Fitxers
- El Mòdul fs a Fons
- Rutes Multiplataforma amb el Mòdul path
- Treballant amb Streams
- Streams de Transformació i pipeline
- Buffers i Dades Binàries
Mòdul 4: HTTP i Servidors Web
- Creant un Servidor HTTP Simple
- Gestió de Sol·licituds i Respostes
- Enrutament Manual
- Servint Fitxers Estàtics
- Rebent Dades: Cossos de Petició i JSON
- Consumint APIs Externes des de Node.js
Mòdul 5: NPM i Gestió de Paquets
- Introducció a NPM i package.json
- Instal·lació i Ús de Paquets
- Versionat Semàntic i package-lock
- Scripts d'npm i Automatització del Projecte
- Creació i Publicació de Paquets
- Seguretat i Manteniment de Dependències
Mòdul 6: Framework Express.js
- Introducció a Express.js
- Configuració d'una Aplicació Express
- Enrutament a Express
- Middleware
- Middleware de Tercers Essencials
- Validació de Dades d'Entrada
- Gestió d'Errors
Mòdul 7: Bases de Dades i ORMs
- Introducció a les Bases de Dades
- Usant MongoDB amb Mongoose
- Operacions CRUD
- Relacions, Poblat i Consultes Avançades
- Usant Bases de Dades SQL amb Sequelize
- Migracions, Transaccions i Dades de Prova
Mòdul 8: Autenticació i Autorització
- Introducció a l'Autenticació
- Registre d'Usuaris i Hash de Contrasenyes
- Sessions i Galetes amb Passport.js
- Autenticació amb JWT
- Control d'Accés Basat en Rols
- Bones Pràctiques de Seguretat en APIs
Mòdul 9: Proves i Depuració
- Introducció a les Proves
- Proves Unitàries amb Mocha i Chai
- Dobles de Prova amb Sinon
- Proves d'Integració
- Cobertura i Automatització de les Proves
- Depuració d'Aplicacions Node.js
Mòdul 10: Temes Avançats
- El Mòdul Cluster
- Fils de Treball (Worker Threads)
- Memòria Cau i Cues de Treball amb Redis
- Optimització del Rendiment
- Construcció d'APIs RESTful
- GraphQL amb Node.js
Mòdul 11: Desplegament i DevOps
- Configuració i Variables d'Entorn
- Registre i Monitoratge en Producció
- Usant PM2 per a la Gestió de Processos
- Empaquetatge amb Docker
- Desplegant a Heroku i Altres PaaS
- Integració i Desplegament Continus
