Dia 9 — Process manager e produção

Informatica · Conteudo · publicado em 30/09/2026
Dia 9 de 15

Process manager e produção

Aula 1

Fazer o processo sobreviver

O processo morre e o código de saída é o que avisa

Um servidor de Node não é um programa de janela: quando ele cai, cai inteiro. O pm2 e o systemd existem exatamente por causa disso, e a decisão de "quando voltar" é uma decisão sobre o código de saída.

O Node moderno trata a rejeição não tratada morrendo. Medido: uma Promise rejeitada sem catch imprime o erro com o stack inteiro e sai com código 1. A linha que estava agendada para rodar depois não roda.

processo filho -> saiu com codigo 1
a linha agendada para depois -> nao apareceu

Isso é desejado. Um processo que mantém o event loop vivo depois de uma rejeição sem dono continua servindo pedido com estado pela metade — e o gerenciador, que só olha o código de saída, não tem como saber. Morrer e subir outro é mais barato que servir errado.

O caminho inverso é o que produz serviço quebrado: registrar o handler para "não morrer", e sair com 0. Medido, o processo que registrou process.on('uncaughtException') e não chamou process.exit dentro dele saiu com código 0 e continuou rodando. Um pm2 com restart on failure nunca vai notar aquele processo.

Três ferramentas, e o que cada uma faz:

FerramentaPergunta que responde
try/catcheste erro eu sei tratar — trato e sigo
process.on('unhandledRejection')registro de observação, e a decisão de sair
process.on('uncaughtException')a última instância, e ela tem que sair

process.on('uncaughtException') registrado sem process.exit() dentro é o pior dos dois mundos: o processo sobrevive e o gerenciador acha que está tudo bem. Se o handler existe, o uso aceitável é fechar ali.

Tratar o que se trata, deixar morrer o que não se deve

A divisão é por pergunta, não por tipo de erro:

async function rota(req, res) {
  try {
    await conexao.execute(sql, [dados.id]);
    res.writeHead(200).end('ok');
  } catch (erro) {
    console.error(erro.code + ': ' + erro.message);   // o terminal
    console.log('  erro:', erro.code, '-', erro.message);  // a página
  }
}

O catch existe para o erro que a rota sabe responder: ER_DUP_ENTRY vira 409, ER_NO_SUCH_TABLE vira 500 sem detalhe. O que a rota não sabe responder — erro de programação, tipo undefined, promise esquecida — não tem catch, e o processo morre.

Tratar tudo produz o outro extremo: o servidor nunca cai, ninguém é avisado, e o log vira uma parede de TypeError que ninguém lê. A morte do processo é o sinal, e o sinal é o que faz o restart on failure funcionar.

process.exitCode e process.exit

A diferença é se o event loop termina antes do processo sair, e ela é medida:

process.exitCode = 1;          // marca o código, deixa o loop terminar
setTimeout(() => log('acabou'), 30);   // ESTA LINHA RODA

process.exit(1);               // mata na hora
setTimeout(() => log('acabou'), 30);   // ESTA LINHA NÃO RODA

process.exit(1) derruba o que estiver no ar no meio: consulta que não voltou resposta, socket do MySQL aberto, log com metade da linha. Com process.exitCode o evento pendente termina e o processo sai com o código depois.

A exceção é o caminho de saída que não tem tempo de nada: o SIGTERM de um SIGKILL, ou um encerramento que precisa matar na hora.

process.on('exit') é síncrono

O exit roda quando o processo já está saindo, e não aceita trabalho assíncrono. Medido: um setTimeout registrado dentro do handler não roda; a parte síncrona do handler roda.

process.on('exit', () => {
  console.log('isto roda');
  setTimeout(() => console.log('isto nao roda'), 0);   // nunca
  conexao.destroy();      // síncrono: este sim funciona
});

É a razão de o harness do material fechar o socket com destroy() e não com end(). end() precisa de uma promessa, e promessa não completa dentro do exit.

O log em arquivo é o que sobrevive ao reinício

Um pm2 ou um systemd que só escreve no terminal perde o que importa: quando o processo novo sobe, ele não sabe do que o processo velho morreu.

O stack da falha é a primeira pista do bug, e ele só existe enquanto ninguém limpou o log. O que o gerenciador garante em produção:

pm2 logs app          acompanha a saída do processo
journalctl -u app -f  o mesmo, quando quem manda é o systemd
logrotate             impede que o arquivo cresça até encher o disco

Sem log em arquivo, o crash vira "o servidor caiu de madrugada" e o primeiro diagnóstico vira adivinhação. O exemplo da aula reproduz o ciclo inteiro: subir, gravar a saída do filho num arquivo de verdade, derrubar e subir de novo.

Cluster: vários processos porque o Node usa um thread

O motivo do cluster é de máquina, não de estilo: o Node executa JavaScript em um único thread por processo. Uma requisição que espera consulta não ocupa CPU, mas uma requisição que processa JSON grande ocupa o processo inteiro. O segundo núcleo da máquina só entra em uso com um outro processo.

O node:cluster cria esses processos. Medido com três worker:

o pai criou 3 processos filho: tres PIDs, tres processos distintos
os tres worker escutaram a MESMA porta
6 requisicoes nessa porta foram atendidas por mais de um worker

Os três escutam a mesma porta, e quem divide as conexões entre eles é o próprio cluster — nenhum if no código da aplicação decide quem atende. Se cada worker pegasse uma porta diferente, seriam três servidores, e não um servidor com três processos.

Os PIDs e a porta são números que mudam a cada execução: listen(0) pede uma porta livre ao sistema, e o sistema escolhe. A página mostra um valor; a sua máquina mostra outro.

No pm2 isso é o modo cluster, e o número de worker é o número de núcleos:

pm2 start app.js -i 4      quatro processos na mesma máquina
os.cpus().length           quantos núcleos a máquina tem

Cada worker é um processo com a sua própria conexão com o MySQL. Um pool compartilhado entre eles não existe, e é por isso que DB_POOL por ambiente é multiplicado pelo número de worker quando se calcula o total de conexões abertas.

Porta ocupada é EADDRINUSE, e o reinício depende dela

Quando um pm2 restart sobe o processo novo antes do velho soltar a porta, o novo falha na largada:

falha ao ouvir: EADDRINUSE - address already in use 127.0.0.1:43283
  erro.code: EADDRINUSE | errno: -98 | syscall: listen

O número da porta é o da execução daquele dia, e muda a cada rodada. O que não muda é o erro.code.

O erro.code é o que se compara, nunca a mensagem — a mesma condição em outra máquina produz outra frase, e comparar texto é comparar sorte.

A ordem que resolve é a do rolling: subir o novo, esperar o health check responder, e só então derrubar o velho. No caminho inverso sobra uma janela em que ninguém responde.

E é a mesma razão de todo exemplo do material usar listen(0): a porta muda a cada execução, porque zero pede uma porta livre ao sistema. A porta 3000 do tutorial é exatamente a que vai estar ocupada na máquina de quem roda, e o número que aparece na página nunca será o mesmo da sua máquina.

process manager não é o npm. O npm resolve dependência; o pm2 resolve o ciclo de vida do processo — subir, vigiar, voltar. São problemas diferentes, e o segundo só aparece quando alguém mais vai usar o servidor.

Reinício automático sem log em arquivo é metade da solução: o processo volta, mas ninguém descobre por que caiu. Reinício automático sem código de saída correto é pior: o processo volta com 0, o gerenciador acha que está saudável, e a falha que nunca mais se manifesta vira um bug que reaparece em produção.

Exemplo

'use strict';

// Exemplo da aula 1 do dia 9: o processo morre, e o processo volta.
//
// O `pm2` e o `systemd` NAO estao instalados no material, e o exemplo nao
// instala nada: um exemplo que depende de `npm install` de servico, ou que
// mexe no `systemd` da maquina de quem le, nao roda em lugar nenhum. O que o
// exemplo faz e SIMULAR o gerenciador com `spawn` do `node:child_process`,
// que e o mesmo mecanismo por tras do `pm2`: um processo pai, um processo
// filho, e a decisao de subir outro quando o filho acaba.
//
// Os cinco assuntos da aula aparecem medidos aqui:
//   - `processo morre` e `queda do servidor`       -> o filho sai com codigo 1
//   - `reiniciar automatico` e `restart on failure` -> o pai sobe outro
//   - `log em arquivo`                             -> o pai guarda a saida
//   - `cluster`, `worker`, `multiplos processos`    -> 3 processos, 1 porta
//   - `porta ocupada`                              -> `EADDRINUSE`
//
// Top-level `await` nao existe em CommonJS: o `package.json` diz `commonjs`,
// entao tudo fica dentro de uma `async function`.

const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const net = require('node:net');
const http = require('node:http');
const cluster = require('node:cluster');
const { spawn } = require('node:child_process');

// A conexao do FILHO e aberta aqui, com o `require('mysql2')` do proprio
// exemplo. O motivo e o mesmo que leva o aluno a escrever a linha no arquivo
// dele: o filho e um processo novo, executado sem o `-r` do harness, entao ele
// nao recebe a conexao que o harness deixou pronta.
const { createConnection } = require('mysql2/promise');

// Rede de seguranca. Um exemplo que trava e reprova no portao, e um exemplo
// que trava por causa de um filho que nao morreu e nao serve de nada: este
// relogio garante que o processo acaba em 30s mesmo assim.
const RELOGIO = setTimeout(() => {
  console.error('relogio de seguranca: o exemplo nao terminou e foi encerrado');
  process.exit(1);
}, 30000);
RELOGIO.unref();

// =============================================================================
// as funcoes do processo filho
// =============================================================================

// Assinatura: `function abrirBanco()` -> Promise<Connection>.
// A credencial vem do ambiente e nunca do arquivo: `process.env.DB_PASS` le o
// que o `.env` colocou, e o valor nao aparece em nenhuma linha deste codigo.
function abrirBanco() {
  return createConnection({
    host: process.env.DB_HOST,
    port: Number(process.env.DB_PORT),
    user: process.env.DB_USER,
    password: process.env.DB_PASS,
    database: process.env.DB_NAME,
  });
}

// Assinatura: `async function responder(conexao, porta)` -> Promise<Server>.
// Sobe o servidor e so devolve quando ele esta escutando. Devolver o `Server`
// ainda sem porta seria o defeito que faz o health check ir para a porta
// errada.
async function responder(conexao, porta) {
  const servidor = http.createServer(async (req, res) => {
    // A versao do banco e capturada, nunca escrita a mao: a maquina de quem
    // le devolve a que ela tem, e pode ser outra.
    const [versao] = await conexao.query('SELECT VERSION() AS versao');
    res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
    res.end(JSON.stringify({ pid: process.pid, banco: versao[0].versao }));
  });

  await new Promise((resolve) => servidor.listen(porta, '127.0.0.1', resolve));
  return servidor;
}

// Assinatura: `function avisarPronto(porta)` -> void.
// O pai so sabe a porta depois que o filho responde. A linha `PRONTO|...` e o
// combinado entre os dois, e o que o `pm2` faz de outra forma: ele le a saida
// do processo ate achar a linha que confirma que o servico subiu.
function avisarPronto(porta) {
  console.log('PRONTO|porta=' + porta + '|pid=' + process.pid);
}

// Assinatura: `async function rodarServidor()` -> Promise<void>.
// O filho do item 2: um servidor de verdade, que depende do banco para
// responder. E o que o gerenciador mantem no ar.
async function rodarServidor() {
  const conexao = await abrirBanco();

  // `IF NOT EXISTS` e `DELETE`: o exemplo roda varias vezes seguidas e precisa
  // dar o mesmo resultado na segunda execucao.
  await conexao.query(`
    CREATE TABLE IF NOT EXISTS tb_d9a1_saude (
      id   INT AUTO_INCREMENT PRIMARY KEY,
      nm_s VARCHAR(40) NOT NULL
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
  `);
  await conexao.query('DELETE FROM tb_d9a1_saude');

  // `listen(0)`: a porta 3000 do tutorial e a que vai estar ocupada na
  // maquina de quem roda. Zero pede uma porta livre ao sistema.
  const servidor = await responder(conexao, 0);
  avisarPronto(servidor.address().port);

  // `SIGTERM` e o sinal do `pm2 restart` e do `systemctl stop`: o pedido de
  // parada e educado, e o processo tem a chance de fechar o servidor e a
  // conexao antes de sair. E o unico sinal que deixa fechar algo.
  process.on('SIGTERM', () => {
    console.log('SIGTERM: o processo vai fechar o que esta aberto');
    servidor.close(async () => {
      await conexao.end();
      console.log('fechou servidor e conexao antes de sair, PID ' + process.pid);
      process.exit(0);
    });
  });

  // `SIGKILL` nao entra aqui: ele mata na hora e nao passa por nenhum
  // handler. E o que o sistema faz quando o processo trava de verdade.
}

// Assinatura: `async function consultarSemTratar(conexao)` -> Promise<query>.
// Sem `try`/`catch` no corpo e sem `await` no chamador: a promessa rejeita e
// ninguem fica com a rejeicao. E o `unhandledRejection`.
async function consultarSemTratar(conexao) {
  return conexao.query('SELECT * FROM tb_d9a1_que_nao_existe');
}

// Assinatura: `async function rodarQuebrado()` -> Promise<void>.
// O filho do item 1. A consulta abaixo rejeita e a rejeicao fica sem dono.
async function rodarQuebrado() {
  const conexao = await abrirBanco();

  // O `exit` e sincrono. O que estiver dentro dele que dependa de tempo --
  // timer, I/O, promessa -- nao completa antes do processo morrer, e e por isso
  // que o harness do material fecha o socket com `destroy()`, que e sincrono,
  // e nao com `end()`, que precisa de promessa.
  process.on('exit', (codigo) => {
    console.log('o `exit` roda e e sincrono, codigo ' + codigo);
    setTimeout(() => console.log('ISTO NAO DEVE APARECER: timer no `exit`'), 0);
  });

  console.log('filho PID ' + process.pid + ': a consulta abaixo vai rejeitar');
  consultarSemTratar(conexao);

  // Estas duas linhas existem para provar que elas NAO rodam.
  setTimeout(() => console.log('CHEGOU AQUI — nao deveria chegar'), 80);
}

// Assinatura: `async function rodarCrashSilencioso()` -> Promise<void>.
// O filho do item 3: o MESMO problema, mas com `process.on('uncaughtException')`
// registrado. Registrar o handler nao conserta nada — o Node ainda chama o
// handler, e se o handler nao mandar o processo sair, o processo CONTINUA
// rodando. E o resultado que importa: o codigo de saida vira 0, e um
// gerenciador que sobe processo que saiu com codigo diferente de zero nunca
// percebe que o processo esta quebrado.
async function rodarCrashSilencioso() {
  process.on('uncaughtException', (erro) => {
    console.log('o handler viu: ' + erro.message);
    // NADA de `process.exit` aqui. E o erro que esta sendo ensinado.
  });

  setTimeout(() => { throw new Error('falha que ninguem esperava'); }, 5);
  setTimeout(() => console.log('CHEGOU AQUI — o processo continuou servindo'), 60);
}

// =============================================================================
// o gerenciador simulado: `spawn`, log em arquivo, health check
// =============================================================================

// Assinatura: `function criarLog()` -> string.
// Caminho de um arquivo de log de verdade, no diretorio temporario do sistema.
// E o equivalente do arquivo que o `pm2 logs` le e que o `journalctl` do
// `systemd` le.
function criarLog() {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'd9a1-'));
  return path.join(dir, 'gerenciador.log');
}

// Assinatura: `function campo(linha, nome)` -> string | null.
// Extrai `porta` de `PRONTO|porta=41827|pid=291205`. A porta e escolhida pelo
// sistema a cada execucao, entao ninguem pode escrever o numero no codigo:
// ele precisa ser lido do filho.
function campo(linha, nome) {
  const achado = new RegExp(nome + '=(\\d+)').exec(linha);
  return achado ? achado[1] : null;
}

// Assinatura: `function comProcesso(modo)` -> Object.
// Sobe `este mesmo arquivo` de novo, com o argumento `modo`, e devolve um
// objeto com o que o gerenciador precisa: a saida, o codigo de saida e uma
// promessa de "ficou pronto".
//
// O `stdio` e `pipe`, e nao `inherit`: e por isso que o `stderr` do filho NAO
// aparece no terminal do exemplo. Um `UnhandledPromiseRejection` sem dono sai
// pelo `stderr`, e o que chega na pagina da aula e o `stdout`. Quem le o
// `stderr` do filho e o gerenciador, e o gerenciador e este processo.
function comProcesso(modo) {
  const filho = spawn(process.execPath, [__filename, modo], {
    stdio: ['ignore', 'pipe', 'pipe'],
  });

  const partes = { stdout: '', stderr: '' };
  filho.stdout.on('data', (d) => { partes.stdout += d; });
  filho.stderr.on('data', (d) => { partes.stderr += d; });

  // A promessa espera o evento `close`, e nao o `exit`: `close` so vem depois
  // que o processo morreu E os tres pipes foram fechados. Esperar so o `exit`
  // deixa os pipes abertos, o processo filho continua segurando um handle, e o
  // exemplo nao termina nunca — mesmo tendo impreso tudo.
  const saiu = new Promise((resolve) =>
    filho.on('close', (codigo, sinal) => resolve({ codigo, sinal })));

  let avisouPronto;
  const pronto = new Promise((resolve) => { avisouPronto = resolve; });
  filho.stdout.on('data', () => {
    const linha = partes.stdout.split('\n').find((l) => l.startsWith('PRONTO|'));
    if (linha) {
      avisouPronto({ pid: campo(linha, 'pid'), porta: campo(linha, 'porta') });
    }
  });

  return {
    filho,
    saiu,
    pronto,
    linhas: () => partes.stdout.split('\n').filter(Boolean),
    erros: () => partes.stderr.split('\n').filter(Boolean),
  };
}

// Assinatura: `async function esperarPronto(p)` -> Promise<Object>.
// Se o filho morrer antes de ficar pronto, a espera nao pode ficar pendurada
// para sempre: ela resolve no primeiro dos dois.
function esperarPronto(p) {
  return Promise.race([
    p.pronto,
    p.saiu.then(() => { throw new Error('o processo saiu antes de ficar pronto'); }),
  ]);
}

// Assinatura: `async function healthCheck(porta)` -> Promise<Object>.
// O `GET /saude` de verdade, feito com o `fetch` do proprio Node, no processo
// que o pai subiu. E o health check do `pm2` e do `systemd`: uma consulta de
// leve que prova que o processo RESPONDE, e nao apenas que ele existe.
async function healthCheck(porta) {
  const resposta = await fetch('http://127.0.0.1:' + porta + '/saude');
  return resposta.json();
}

// =============================================================================
// o pai: o gerenciador
// =============================================================================

// Assinatura: `async function rodarPai()` -> Promise<void>.
// Tudo o que o `pm2` faz, aqui em funcoes: subir, olhar o log, consultar o
// health check e voltar a subir.
async function rodarPai() {
  const arquivoLog = criarLog();

  // Assinatura: `function registrar(p, titulo)` -> void.
  // Joga a saida do filho no arquivo de log e devolve as linhas, para o
  // exemplo poder mostrar o que o gerenciador teria em mãos para(printar
  // depois de um crash).
  function registrar(p, titulo) {
    const linhas = p.linhas().concat(p.erros());
    fs.appendFileSync(arquivoLog, '[' + titulo + ']\n' + linhas.join('\n') + '\n');
    return linhas;
  }

  console.log('=== 1. o processo morre: rejeicao sem `catch` ===');
  console.log('gerenciador simulado: `pm2` e `systemd` nao estao instalados aqui.');
  console.log('o mecanismo e o mesmo — um pai, um filho, e a decisao de subir outro.\n');

  const quebrado = comProcesso('quebrado');
  const fim1 = await quebrado.saiu;
  const linhas1 = registrar(quebrado, 'quebrado');

  console.log('filho subiu, consultou uma tabela que nao existe, e a rejeicao');
  console.log('nao teve quem tratasse.');
  console.log('codigo de saida do filho: ' + fim1.codigo);
  console.log('a linha "CHEGOU AQUI" apareceu? '
    + (linhas1.some((l) => l.includes('CHEGOU AQUI')) ? 'sim' : 'nao'));
  console.log('o timer de 80ms chegou a rodar? '
    + (linhas1.some((l) => l.includes('CHEGOU AQUI')) ? 'sim' : 'nao'));
  console.log('\n--- as 6 primeiras linhas que o filho deixou no log ---');
  console.log(linhas1.slice(0, 6).map((l) => '    ' + l).join('\n'));
  console.log('\nO `exit` rodou e o `setTimeout` de dentro dele NAO. O `exit` e');
  console.log('sincrono: codigo que depende de tempo nao completa antes do');
  console.log('processo morrer. E o motivo de o harness fechar o socket com');
  console.log('`destroy()`, que e sincrono, em vez de `end()`, que precisa de promessa.');
  console.log('\nO stack no log e a primeira pista do bug. Sem ele, o erro vira');
  console.log('"algo deu errado" e nao ha onde comecar.');

  console.log('\n=== 2. `uncaughtException` registrado sem sair ===');
  console.log('contexto: o MESMO crash, agora com `process.on(\'uncaughtException\')`');
  console.log('registrado. Registrar o handler nao conserta o processo.\n');

  const silencioso = comProcesso('crash-silencioso');
  const fim2 = await silencioso.saiu;
  registrar(silencioso, 'crash-silencioso');

  console.log('codigo de saida: ' + fim2.codigo + ' (o handler nao pediu para sair)');
  console.log('o processo continuou rodando depois do crash? '
    + silencioso.linhas().some((l) => l.includes('CHEGOU AQUI') ? 'sim' : 'nao'));
  console.log('\nEste e o resultado que importa: o codigo de saida e 0.');
  console.log('Um gerenciador que sobe processo que saiu com codigo diferente de');
  console.log('zero NUNCA vai notar que este processo esta quebrado, e ele vai');
  console.log('continuar no ar servindo pedido com estado inconsistente.');
  console.log('Por isso `uncaughtException` e a ultima instancia: se voce registra');
  console.log('o handler, o unico uso aceitavel e fechar o processo ali dentro.');

  console.log('\n=== 3. reiniciar automatico: `restart on failure` ===');
  console.log('regra do `systemd`: `Restart=on-failure` sobe o processo de novo');
  console.log('quando ele acaba com codigo diferente de zero. Saiu com 0, nao sobe.');
  console.log('e o que o item 2 acima mostra: saiu com 0, e nao subiu.\n');

  const primeiro = comProcesso('servidor');
  const info1 = await esperarPronto(primeiro);
  registrar(primeiro, 'servidor 1');
  console.log('1º start -> PID ' + info1.pid + ' | porta ' + info1.porta);
  console.log('  health check: ' + JSON.stringify(await healthCheck(info1.porta)));

  primeiro.filho.kill('SIGTERM');
  const fim3 = await primeiro.saiu;
  console.log('\nSIGTERM enviado no PID ' + info1.pid + ', o processo saiu com '
    + fim3.codigo + ' e sinal ' + fim3.sinal);
  console.log('  ' + primeiro.linhas().slice(-1)[0]);

  const segundo = comProcesso('servidor');
  const info2 = await esperarPronto(segundo);
  registrar(segundo, 'servidor 2');
  console.log('\n2o start -> PID ' + info2.pid + ' | porta ' + info2.porta);
  console.log('  health check: ' + JSON.stringify(await healthCheck(info2.porta)));

  console.log('\nPID ' + info1.pid + ' != PID ' + info2.pid
    + ' -> quem respondeu e um processo NOVO, nao o mesmo que voltou do nada');
  console.log('a porta tambem mudou (' + info1.porta + ' -> ' + info2.porta + '):');
  console.log('o `listen(0)` pede uma porta livre ao sistema a cada execucao, e o');
  console.log('numero da pagina nunca sera o mesmo da sua maquina.');

  // O segundo processo fica no ar de proposito — e assim que se mostra um
  // gerenciador, com servico no ar enquanto o script termina. Mas o exemplo
  // precisa derruba-lo: um processo filho esquecido aqui segura o socket do
  // servidor e o da conexao, e o exemplo nao encerra. Em producao quem faz
  // essa parada e o `pm2 stop` ou o `systemctl stop`.
  segundo.filho.kill('SIGTERM');
  await segundo.saiu;

  console.log('\n=== 4. log em arquivo ===');
  const total = fs.readFileSync(arquivoLog, 'utf8').split('\n').filter(Boolean);
  console.log('o arquivo de log do gerenciador tem ' + total.length + ' linha(s) ate aqui');
  console.log('e o que o `pm2 logs` mostra e o que o `journalctl` mostra.');
  console.log('sem arquivo, o stack do item 1 some no primeiro restart: o processo');
  console.log('novo nao sabe do que o processo velho morreu.');
  console.log('o que o `systemd` faz alem disso e `journalctl -u nome -f` para acompanhar,');
  console.log('e `logrotate` para nao deixar o arquivo crescer sem parar.');

  console.log('\n=== 5. `cluster`: varios processos, uma porta so ===');
  await rodarCluster();

  console.log('\n=== 6. `porta ocupada`: `EADDRINUSE` ===');
  const dono = net.createServer();
  const porta = await new Promise((resolve) =>
    dono.listen(0, '127.0.0.1', () => resolve(dono.address().port)));
  console.log('um processo segura a porta ' + porta);
  console.log('contexto: um segundo processo tenta `listen` na MESMA porta');

  try {
    const segundoProcesso = net.createServer();
    await new Promise((resolve, reject) =>
      segundoProcesso.listen(porta, '127.0.0.1', resolve).once('error', reject));
  } catch (erro) {
    console.error('falha ao ouvir: ' + erro.code + ' - ' + erro.message);
    console.log('  erro.code: ' + erro.code + ' | errno: ' + erro.errno
      + ' | syscall: ' + erro.syscall);
  }
  console.log('\n`EADDRINUSE` e um `erro.code`, e e o que se compara — nunca a mensagem.');
  console.log('em producao ele aparece quando o reinicio subiu o processo novo');
  console.log('antes do velho soltar a porta: `EADDRINUSE` no log e o gerenciador');
  console.log('esperando o processo antigo morrer.');
  console.log('da mesma razao o exemplo usa `listen(0)`: a porta 3000 do tutorial e');
  console.log('exatamente a que vai estar ocupada na maquina de quem roda.');
  await new Promise((resolve) => dono.close(resolve));
}

// =============================================================================
// `cluster`: tres processos, uma porta
// =============================================================================

// Assinatura: `async function rodarCluster()` -> Promise<void>.
// A resposta para "um Node so nao usa a maquina inteira". O Node executa
// JavaScript em UM thread por processo, entao usar o outro nucleo exige OUTRO
// PROCESSO — nao mais tarefas dentro do mesmo.
//
// O `node:cluster` cria esses processos. Cada `worker` tem o seu PID e o seu
// event loop, e os tres escutam a MESMA porta: quem divide as conexoes entre
// eles e o proprio `cluster`, e nao o codigo da aplicacao. Por isso cada
// requisicao chega a um worker so.
async function rodarCluster() {
  const total = 3;

  // ATE AQUI: este `main` roda dentro de cada `worker`, nao no pai.
  if (cluster.isWorker) {
    const endereco = servidorDoWorker();
    return endereco;
  }

  for (let i = 0; i < total; i++) cluster.fork();

  const infos = await new Promise((resolve) => {
    const vistos = [];
    let prontos = 0;
    for (const w of Object.values(cluster.workers)) {
      w.on('message', (m) => {
        vistos.push(m);
        if (++prontos === total) resolve(vistos);
      });
    }
  });

  const pids = infos.map((i) => i.pid);
  console.log('o pai criou ' + total + ' processos filho: PID ' + pids.join(', '));
  console.log('sao ' + new Set(pids).size + ' PIDs diferentes: '
    + new Set(pids).size + ' processos, nao um so com varias tarefas');
  console.log('os tres `worker` escutaram a MESMA porta (' + infos[0].porta
    + '): quem divide as conexoes e o `cluster`, e nao o codigo da aplicacao');

  // O health check N vezes: cada resposta vem de um worker diferente, e e o
  // balanceamento aparecendo no log. O `worker` no corpo da resposta e o PID
  // de quem atendeu, e o `cluster` que escolheu — nenhum `if` no codigo.
  const atendidos = [];
  for (let i = 0; i < 6; i++) {
    const r = await healthCheck(infos[0].porta);
    atendidos.push(r.pid);
  }
  console.log('6 requisicoes na mesma porta -> ' + new Set(atendidos).size
    + ' workers diferentes atenderam: ' + [...new Set(atendidos)].join(', '));
  console.log('foi o `cluster` que escolheu quem atende. Nenhum `if` no codigo decide isso.');

  console.log('\nno `pm2` e o modo `cluster`: `pm2 start app.js -i 4` sobe 4 processos');
  console.log('na mesma maquina e o balanceamento vem junto. Numero de `worker` e');
  console.log('numero de nucleos: `os.cpus().length` responde.');

  // O `disconnect` pede que os workers parem, mas nao espera por eles. Sem
  // esperar aqui, os processos filhos continuam vivos depois que o exemplo
  // terminou de imprimir, cada um segurando o seu socket, e o processo nao
  // encerra. E o mesmo cuidado que o item 3 exige.
  //
  // Aqui o evento e `exit`, e nao `close`: o `close` do `cluster.Worker` so
  // vem quando os sockets de comunicacao entre o pai e o filho tambem
  // fecham, e com o `cluster` esse `close` nao chega. Esperar por ele e o
  // exemplo travar para sempre.
  const filhos = Object.values(cluster.workers);
  cluster.disconnect();
  await Promise.all(filhos.map((w) => new Promise((resolve) => w.on('exit', resolve))));
}

// Assinatura: `async function servidorDoWorker()` -> Promise<void>.
// O que roda DENTRO de cada `worker`. Cada um sobe o seu servidor em `listen(0)`;
// o `cluster` entrega o mesmo socket para os tres, e por isso os tres veem a
// mesma porta. Se cada worker pegasse uma porta diferente, seriam tres
// servidores, e nao um servidor com tres processos.
async function servidorDoWorker() {
  const conexao = await abrirBanco();
  const servidor = await responder(conexao, 0);
  const endereco = servidor.address();
  process.send({ pid: process.pid, porta: endereco ? endereco.port : 0 });

  // O `disconnect` chega quando o pai chama `cluster.disconnect()`. Sem esta
  // parada, cada worker continua segurando o socket do servidor e o processo
  // NAO TERMINA — o exemplo fica pendurado depois de imprimir tudo, que e o
  // pior resultado possivel porque parece que passou.
  process.on('disconnect', () => {
    servidor.close(async () => {
      await conexao.end();
      process.exit(0);
    });
  });
}

// =============================================================================

async function main() {
  // O `worker` entra aqui primeiro: um `worker` recomeca o arquivo inteiro, e
  // sem esta guarda ele cairia no ramo do pai e criaria mais `worker`s.
  if (cluster.isWorker) return servidorDoWorker();

  const modo = process.argv[2];
  if (modo === 'servidor') return rodarServidor();
  if (modo === 'quebrado') return rodarQuebrado();
  if (modo === 'crash-silencioso') return rodarCrashSilencioso();
  return rodarPai();
}

main()
  .then(() => clearTimeout(RELOGIO))
  .catch((erro) => {
    clearTimeout(RELOGIO);
    console.error('falhou:', erro.code || erro.name, '-', erro.message);
    process.exit(1);
  });

Saída real

=== 1. o processo morre: rejeicao sem `catch` ===
gerenciador simulado: `pm2` e `systemd` nao estao instalados aqui.
o mecanismo e o mesmo — um pai, um filho, e a decisao de subir outro.

filho subiu, consultou uma tabela que nao existe, e a rejeicao
nao teve quem tratasse.
codigo de saida do filho: 1
a linha "CHEGOU AQUI" apareceu? nao
o timer de 80ms chegou a rodar? nao

--- as 6 primeiras linhas que o filho deixou no log ---
    filho PID 644737: a consulta abaixo vai rejeitar
    o `exit` roda e e sincrono, codigo 1
    node:internal/process/promises:394
        triggerUncaughtException(err, true /* fromPromise */);
        ^
    Error: Table 'materiais_teste.tb_d9a1_que_nao_existe' doesn't exist

O `exit` rodou e o `setTimeout` de dentro dele NAO. O `exit` e
sincrono: codigo que depende de tempo nao completa antes do
processo morrer. E o motivo de o harness fechar o socket com
`destroy()`, que e sincrono, em vez de `end()`, que precisa de promessa.

O stack no log e a primeira pista do bug. Sem ele, o erro vira
"algo deu errado" e nao ha onde comecar.

=== 2. `uncaughtException` registrado sem sair ===
contexto: o MESMO crash, agora com `process.on('uncaughtException')`
registrado. Registrar o handler nao conserta o processo.

codigo de saida: 0 (o handler nao pediu para sair)
o processo continuou rodando depois do crash? true

Este e o resultado que importa: o codigo de saida e 0.
Um gerenciador que sobe processo que saiu com codigo diferente de
zero NUNCA vai notar que este processo esta quebrado, e ele vai
continuar no ar servindo pedido com estado inconsistente.
Por isso `uncaughtException` e a ultima instancia: se voce registra
o handler, o unico uso aceitavel e fechar o processo ali dentro.

=== 3. reiniciar automatico: `restart on failure` ===
regra do `systemd`: `Restart=on-failure` sobe o processo de novo
quando ele acaba com codigo diferente de zero. Saiu com 0, nao sobe.
e o que o item 2 acima mostra: saiu com 0, e nao subiu.

1º start -> PID 644772 | porta 43647
  health check: {"pid":644772,"banco":"10.11.14-MariaDB-0ubuntu0.24.04.1"}

SIGTERM enviado no PID 644772, o processo saiu com 0 e sinal null
  fechou servidor e conexao antes de sair, PID 644772

2o start -> PID 644783 | porta 43139
  health check: {"pid":644783,"banco":"10.11.14-MariaDB-0ubuntu0.24.04.1"}

PID 644772 != PID 644783 -> quem respondeu e um processo NOVO, nao o mesmo que voltou do nada
a porta tambem mudou (43647 -> 43139):
o `listen(0)` pede uma porta livre ao sistema a cada execucao, e o
numero da pagina nunca sera o mesmo da sua maquina.

=== 4. log em arquivo ===
o arquivo de log do gerenciador tem 24 linha(s) ate aqui
e o que o `pm2 logs` mostra e o que o `journalctl` mostra.
sem arquivo, o stack do item 1 some no primeiro restart: o processo
novo nao sabe do que o processo velho morreu.
o que o `systemd` faz alem disso e `journalctl -u nome -f` para acompanhar,
e `logrotate` para nao deixar o arquivo crescer sem parar.

=== 5. `cluster`: varios processos, uma porta so ===
o pai criou 3 processos filho: PID 644792, 644791, 644798
sao 3 PIDs diferentes: 3 processos, nao um so com varias tarefas
os tres `worker` escutaram a MESMA porta (41865): quem divide as conexoes e o `cluster`, e nao o codigo da aplicacao
6 requisicoes na mesma porta -> 2 workers diferentes atenderam: 644792, 644791
foi o `cluster` que escolheu quem atende. Nenhum `if` no codigo decide isso.

no `pm2` e o modo `cluster`: `pm2 start app.js -i 4` sobe 4 processos
na mesma maquina e o balanceamento vem junto. Numero de `worker` e
numero de nucleos: `os.cpus().length` responde.

=== 6. `porta ocupada`: `EADDRINUSE` ===
um processo segura a porta 41715
contexto: um segundo processo tenta `listen` na MESMA porta
  erro.code: EADDRINUSE | errno: -98 | syscall: listen

`EADDRINUSE` e um `erro.code`, e e o que se compara — nunca a mensagem.
em producao ele aparece quando o reinicio subiu o processo novo
antes do velho soltar a porta: `EADDRINUSE` no log e o gerenciador
esperando o processo antigo morrer.
da mesma razao o exemplo usa `listen(0)`: a porta 3000 do tutorial e
exatamente a que vai estar ocupada na maquina de quem roda.
Aula 2

Ambiente de produção e deploy

O que muda quando o servidor não está na sua máquina

Na máquina de quem desenvolve, o servidor sobe com um comando e morre quando o terminal fecha. Em produção nada disso vale: o terminal não existe, a máquina reinicia sozinha, e ninguém está presente para o SIGTERM.

São quatro decisões que se tomam de uma vez só, e elas se condicionam: a variável de ambiente define o que o processo faz, o process manager define o que acontece quando ele morre, o proxy reverso define quem chega nele, e o certificado define com que criptografia.

CamadaQuem resolveOnde se configura
configuraçãoa variável de ambiente.env por ambiente
ciclo de vidapm2 ou systemdno servidor
entrada públicanginxno servidor
criptografiacertificado HTTPSno servidor

Nenhuma delas vive no app.js. O código da aplicação é o mesmo nos três ambientes, e é por isso que ele continua testável na sua máquina.

Um .env por ambiente, e nenhum no git

O que muda entre development, staging e production é o conjunto de variáveis, nunca o código. Medido com o mesmo arquivo rodando nos três:

development  NODE_ENV=development | banco alvo=materiais_dev | pool=5  | log=detalhe ligado    | erro=detalhado
staging      NODE_ENV=staging     | banco alvo=materiais_homologacao | pool=10 | log=detalhe ligado    | erro=detalhado
production   NODE_ENV=production  | banco alvo=materiais_prod | pool=30 | log=detalhe desligado | erro=generico

NODE_ENV é a variável que decide o comportamento; as outras medem e mandam. Em development o log de detalhe fica ligado porque quem desenvolve precisa ver a consulta que falhou. Em production o mesmo log vira risco: cada linha é dado de negócio num arquivo de texto que ninguém vai ler.

O motivo da resposta de erro genérica também é o mesmo do log: a mensagem completa de uma falha costuma citar o nome da tabela e da coluna, e isso entrega o esquema do banco para quem perguntou. Em production, quem recebe um erro recebe um código e um identificador, e o detalhe fica no log de quem pode ver.

undefined não é erro, e essa é a armadilha do deploy

process.env devolve undefined para variável que não chegou, e undefined passa direto no código sem reclamar nada. Medido com um processo que nasceu com duas variáveis e sem a terceira:

chegou:  {"NODE_ENV":"staging","DB_POOL":"10","LOG_DETALHE":"(ausente)"}
ausente: ["LOG_DETALHE"]

O LOG_DETALHE faltou por erro de deploy, e uma variável que ninguém escreveu também chega como undefined. Para o código as duas são a mesma coisa, e é por isso que o erro aparece longe de onde a variável era lida.

A saída segura é falhar na hora de abrir, no start, e não no meio de um pedido: um processo que não conseguiu ler o que precisa não sobe. A verificação que separa o que chegou do que está vazio é uma linha, e ela fica no start, não na rota.

O .env.example vai para o git com os nomes e valores de exemplo; o .env de cada máquina fica de fora. O valor da senha nunca entra no repositório — a aula 2 do dia 6 trata desse mecanismo com SUA_SENHA como valor de exemplo.

O process manager: o processo não pode depender de você

O pm2 e o systemd resolvem o mesmo problema, e o problema é: quem sobe o processo quando a máquina reinicia?

pm2 start app.js --env production    sobe o processo com o ambiente certo
pm2 list                             PID, status e tempo no ar
pm2 logs app                         acompanha a saída
pm2 restart app                      derruba e sobe de novo
pm2 stop app                         desliga

O systemd faz o mesmo com um arquivo de unidade, e tem a vantagem de ser o gerenciador do próprio sistema: o processo sobe junto com a máquina, sem ninguém pedir.

O que os dois trocam pelo seu shell é a decisão de ficar no ar depois do crash. O ciclo medido no exemplo, que é o que os dois executam:

1º start -> um PID e uma porta; o health check respondeu
SIGTERM enviado                       -> o processo fechou e saiu com 0
2º start -> OUTRO PID e outra porta; o health check respondeu

O PID mudou: quem respondeu no segundo start é um processo novo, não o mesmo que voltou do nada. É essa a propriedade que interessa, e ela é verificável.

O PID e a porta são os dois números que mudam a cada execução: listen(0) pede uma porta livre ao sistema, e o sistema escolhe. Rodar o exemplo de novo produz outros dois valores, e o resto da saída sai idêntico.

O modo cluster do pm2 cobre o outro motivo, que é de máquina: o Node usa um único thread por processo, então usar o segundo núcleo exige outro processo. pm2 start app.js -i 4 sobe quatro, na mesma máquina, com o balanceamento entregue junto.

Proxy reverso: o Node escuta 127.0.0.1 e não está na internet

A arquitetura de produção tem o Node atrás de um nginx, escutando só o loopback:

visitante --https--> nginx (443) --http--> node (127.0.0.1:3000) --> mysql

O nginx decide, para cada caminho, quem responde. Medido com os dois caminhos reais:

/api/saude           -> node   | http://127.0.0.1:3000/api/
/css/estilo.css      -> nginx  | /var/www/html/css/estilo.css

O bloco que faz isso tem três peças, e as três importam:

location /api/ {
    proxy_pass http://127.0.0.1:3000/api/;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header Host $host;
}

location decide quem responde, proxy_pass decide para onde, e proxy_set_header decide o que o Node enxerga do cliente. O terceiro é o que costuma faltar: atrás do proxy, req.socket.remoteAddress é o endereço do proxy, e não o do visitante. Sem X-Forwarded-For, o rate limit do dia 6 veria todo mundo chegando do mesmo lugar e derrubaria o site inteiro.

Arquivo estático sai do disco do proxy, sem passar pelo Node: não custa um processo do cluster, e não passa por código que pode ter bug.

O certificado é do proxy, e por isso o Node ignora TLS

O TLS termina no nginx. O navegador fala https com o nginx, e o nginx fala http simples com o Node em 127.0.0.1. O Node nunca vê o certificado e nunca precisa de https.createServer.

dominio:        api.exemplo.com
TLS termina em: nginx (porta publica 443)
Node escuta em: porta 3000, em 127.0.0.1

Duas consequências que evitam problema: o certificado vence sozinho e precisa de renovação periódica, e a renovação é trabalho do proxy — dá para recarregar o nginx sem derrubar a aplicação. E o tráfego da porta 80 precisa ser redirecionado para o 443, senão o visitante fica em http e o navegador reclama. O dominio aponta para o IP do servidor por um registro DNS, e quem escreve a configuração do TLS é o certbot, não o código.

O deploy que não para o serviço

Publicar a primeira vez é o caso fácil: sobe o processo, ele serve. O que quebra a partir da segunda vez é a ordem, e a ordem importa porque derrubar antes de subir produz EADDRINUSE ou uma janela em que ninguém responde.

A ordem que funciona é a do rolling, e ela é medida no exemplo:

1  sobe a versao nova AO LADO da antiga    (a antiga continua atendendo)
2  health check na versao nova             (prova que ela responde)
3  derruba a versao antiga                 (so agora, com substituto comprovado)

Os passos 1 e 2 é o que separa um deploy de um consolidado. O caminho inverso — derrubar e sóbe — produz EADDRINUSE no log, e o pm2 entra em ciclo de reinício até o processo velho soltar a porta. O detalhe que faz o passo 2 valer: health check é uma requisição de verdade para o processo novo, não uma checagem de que o processo existe. Um processo no ar que não responde não está no ar.

Ambiente e deploy se combinam assim: development para escrever, staging para o código novo passar com dado parecido ao de produção, production para o que os visitantes usam. O que entra em production é o mesmo artefato que passou em staging — código diferente em cada ambiente é o caminho mais curto para um comportamento que ninguém testou.

O primeiro deploy é o que assusta, e é o mais simples

Publicar a API pela primeira vez tem uma lista curta, e o erro clássico é pular o item três:

  1. O dominio aponta para o IP do servidor (registro DNS). Sem isso o nginx nunca é chamado.
  2. O certificado está emitido e o nginx recarregou com ele.
  3. O processo escuta 127.0.0.1 e está no ar — o health check responde por dentro da máquina.
  4. O pm2 start --env production está rodando e sobe de novo depois de um reboot.

O item três é o que as pessoas pulam, porque do lado de fora ainda não dá para ver nada: quem testa pela rede pública recebe 502 Bad Gateway do nginx, que é a resposta dele quando não consegue falar com o Node. 502 é sintoma de processo parado, de porta errada no proxy_pass ou de nginx apontando para uma porta que ninguém ocupa — nunca de DNS.

Depois do primeiro deploy o que muda é a rotina: o deploy passa a ser a troca de versão, e vale a ordem da seção acima.

build em Node é raro: quase tudo roda direto do fonte, e o que muda entre os ambientes é o ambiente, não o arquivo. Quando o build existe — TypeScript, bundler — a regra é a mesma: um artefato, três ambientes, e o build roda antes do primeiro, não em cada um.

Log de produção sem rotação enche o disco, e o serviço cai por falta de espaço — uma falha que não tem nada a ver com o código. A rotação é do logrotate ou do próprio pm2, e é mais uma coisa que o processo não pode fazer sozinho.

Exemplo

'use strict';

// Exemplo da aula 2 do dia 9: o que muda quando o servidor nao esta na sua
// maquina.
//
// IMPORTANTE — ISTO E UMA SIMULACAO.
//
// O exemplo nao instala o `pm2`, nao mexe no `systemd` e nao escreve no
// `nginx` da maquina de quem le. Um exemplo que dependesse disso nao rodaria
// em lugar nenhum, e material didatico que precisa de `sudo` para rodar e
// material que ninguem roda.
//
// O que ele faz e o seguinte: le as variaveis de ambiente que um servidor de
// producao receberia, mostra que cada ambiente le um conjunto diferente, e
// reproduz o ciclo de vida que o `pm2` executaria — `start`, health check,
// `stop`, `start` de novo com outro PID — usando `spawn` do
// `node:child_process`. O comportamento e o que importa, e ele e medido aqui.
//
// Os seis assuntos da aula aparecem aqui:
//   - `producao`, `staging`, `homologacao`, `deploy`   -> 3 ambientes, 3configs
//   - `variaveis de ambiente por ambiente`             -> quem le o que
//   - `build` e `primeiro deploy`                       -> o que roda em cada etapa
//   - `reverse proxy`, `nginx`, `proxy para Node`       -> rota /api/
//   - `certificado HTTPS` e `dominio`                   -> quem termina o TLS
//
// Top-level `await` nao existe em CommonJS: o `package.json` diz `commonjs`,
// entao tudo fica dentro de uma `async function`.

const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const http = require('node:http');
const { spawn } = require('node:child_process');

// A conexao do FILHO e aberta com o `require('mysql2')` do proprio exemplo: o
// filho e um processo novo, executado sem o `-r` do harness, entao ele nao
// recebe a conexao que o harness deixou pronta.
const { createConnection } = require('mysql2/promise');

// Rede de seguranca: um exemplo que trava reprova no portao.
const RELOGIO = setTimeout(() => {
  console.error('relogio de seguranca: o exemplo nao terminou e foi encerrado');
  process.exit(1);
}, 30000);
RELOGIO.unref();

// =============================================================================
// 1. variaveis de ambiente: uma por ambiente, e nenhuma no codigo
// =============================================================================

// Assinatura: `function variaveisDe(ambiente)` -> Object.
// O que cada ambiente recebe. Este objeto e o QUE O SERVIDOR DE PRODUCAO
// DEFINE no ambiente do processo — nao e o codigo que le de algum lugar
// fixo, e o codigo que le daqui.
//
// Em um servidor real quem escreve esse objeto e o `pm2` (`--env production`),
// o `systemd` (`Environment=` no arquivo de unidade) ou o painel da hospedagem.
// Aqui ele e escrito no proprio processo, e por isso o exemplo consegue
// mostrar os tres ambientes sem depender de nada instalado.
//
// Repare no que NAO existe: nenhum valor de senha, nenhuma chave, nenhum
// token. `DB_PASS` e um NOME. O valor vive no `.env` de cada maquina e no
// cofre da hospedagem, e nunca entra no git — a aula 2 do dia 6 trata desse
// mecanismo.
//
// Repare em `DB_ALVO`, e nao em `DB_NAME`: e o nome do banco que CADA
// AMBIENTE apontaria. O exemplo nao conecta em `materiais_prod` nem em
// `materiais_homologacao` — esses bancos nao existem na maquina de quem le a
// pagina, e um exemplo que apontasse para eles falharia com `ER_BAD_DB_ERROR`.
// O `DB_NAME` que o `.env` entregou e quem manda na conexao; `DB_ALVO` fica
// aqui como o que a tabela de variaveis do ambiente de verdade mostraria.
const AMBIENTES = {
  development: {
    NODE_ENV: 'development',
    DB_ALVO: 'materiais_dev',
    DB_POOL: '5',
    LOG_DETALHE: '1',
    PORTA: '3000',
    ORIGEM_PERMITIDA: 'http://localhost:8080',
  },
  staging: {
    NODE_ENV: 'staging',
    DB_ALVO: 'materiais_homologacao',
    DB_POOL: '10',
    LOG_DETALHE: '1',
    PORTA: '3000',
    ORIGEM_PERMITIDA: 'https://homologacao.exemplo.com',
  },
  production: {
    NODE_ENV: 'production',
    DB_ALVO: 'materiais_prod',
    DB_POOL: '30',
    LOG_DETALHE: '0',
    PORTA: '3000',
    ORIGEM_PERMITIDA: 'https://api.exemplo.com',
  },
};

// Assinatura: `function lerVariaveis(lista)` -> Object.
// A MESMA verificacao que `process.env` faz, rodada sobre uma lista de pares
// `{ nome, valor }`. O exemplo passa o valor na mao para mostrar o caso da
// variavel que faltou, sem precisar de um segundo `.env`.
//
// Em `process.env`, `undefined` para variavel que nao chegou e `undefined` NAO
// e erro: e um valor como outro. E o motivo de um deploy quebrar vinte linhas
// depois, em outro arquivo, com outra mensagem.
//
// A funcao separa o que CHEGOU do que esta VAZIO, e quem decide o que fazer
// com a falta e o start do processo, nao o meio de um pedido.
function lerVariaveis(lista) {
  const lidas = {};
  const ausentes = [];
  for (const { nome, valor } of lista) {
    if (valor === undefined || valor === '') ausentes.push(nome);
    lidas[nome] = valor === undefined || valor === '' ? '(ausente)' : valor;
  }
  return { lidas, ausentes };
}

// Assinatura: `function descreverAmbiente(ambiente)` -> Object.
// O que cada ambiente LIGA ou DESLIGA. Um codigo so, tres comportamentos.
//
// `NODE_ENV` e a unica variavel que decide o comportamento do programa; as
// outras medem e mandam. Em `development` o log de detalhe fica ligado porque
// quem develope precisa ver a consulta que falhou; em `production` o mesmo log
// vira risco, porque cada linha e um dado de negocio em um arquivo de texto.
function descreverAmbiente(nome) {
  const v = AMBIENTES[nome];
  const emProducao = v.NODE_ENV === 'production';
  return {
    nome,
    pool: v.DB_POOL,
    logDetalhe: v.LOG_DETALHE === '1',
    paginaDeErro: !emProducao,
    banner: emProducao ? '(nenhum)' : v.NODE_ENV.toUpperCase(),
  };
}

// =============================================================================
// 2. o proxy reverso: o Node so escuta 127.0.0.1
// =============================================================================

// Assinatura: `function rotaDoProxy(caminho)` -> Object.
// O que o `nginx` faz com o caminho que chega. Esta funcao e a REGRA do bloco
// `location /api/` — e ela que decide, para cada requisicao, qual processo
// responde.
//
// A decisao tem tres campos, e os tres importam:
//   - o `location` decide QUEM responde
//   - o `proxy_pass` decide PARA ONDE
//   - o `proxy_set_header` decide O QUE o Node enxerga do cliente
//
// O ponto do ultimo campo: atras do proxy, `req.socket.remoteAddress` e o
// endereco do PROXY, nao o do visitante. Sem `X-Forwarded-For` e
// `X-Real-IP`, o rate limit do dia 6 veria todo mundo chegando do mesmo
// lugar e derrubaria o site inteiro.
function rotaDoProxy(caminho) {
  // Equivale a: `location /api/ { proxy_pass http://127.0.0.1:3000/api/; }`
  if (caminho.startsWith('/api/')) {
    return {
      atende: 'node',
      destino: 'http://127.0.0.1:3000/api/',
      cabecalhos: {
        'X-Forwarded-For': '<ip real do visitante>',
        'X-Forwarded-Proto': 'https',
        'Host': 'api.exemplo.com',
      },
      observacao: 'o Node so escuta 127.0.0.1: ele NAO esta exposto na internet',
    };
  }

  // Equivale a: `location / { root /var/www/html; }` — os arquivos estaticos
  // saem do disco do proprio nginx, sem passar pelo Node e sem gastar um
  // processo do cluster.
  return {
    atende: 'nginx',
    destino: '/var/www/html' + caminho,
    cabecalhos: {},
    observacao: 'estatico sai do disco do proxy; nao chegou no Node',
  };
}

// Assinatura: `function certificadoDe(dominio)` -> Object.
// O que o `nginx` faz com o TLS, e por que o Node nao precisa saber nada
// disso.
//
// A cadeia completa: o visitante abre `https://api.exemplo.com`, o nginx
// termina o TLS, e fala HTTP simples com o Node em `127.0.0.1`. O Node nunca
// ve o certificado e nunca precisa de `https.createServer`. Certificados
// vencem de tempos em tempos, e renovar e recarregar o nginx sem derrubar a
// aplicacao e trabalho do proxy — mais uma razao para ele existir.
function certificadoDe(dominio) {
  return {
    dominio,
    quemTerminaTls: 'nginx',
    portaPublica: 443,
    portaDoNode: 3000,
    caminhoDoCertificado: '/etc/letsencrypt/live/' + dominio + '/fullchain.pem',
    renewal: 'o certificado vence sozinho; quem renova recarrega o nginx',
    redirecionaHttp: 'todo o traffego da porta 80 vai para o 443',
  };
}

// =============================================================================
// 3. o processo filho: o mesmo servidor, com as variaveis de um ambiente
// =============================================================================

// Assinatura: `function abrirBanco()` -> Promise<Connection>.
// A credencial vem do ambiente. O valor nao esta neste arquivo e nunca entra:
// o que o codigo le e o NOME da variavel.
function abrirBanco() {
  return createConnection({
    host: process.env.DB_HOST,
    port: Number(process.env.DB_PORT),
    user: process.env.DB_USER,
    password: process.env.DB_PASS,
    database: process.env.DB_NAME,
  });
}

// Assinatura: `async function responder(conexao, ambiente)` -> Promise<Server>.
// A rota `/api/saude`. O que muda entre os ambientes esta TODO aqui: o que a
// resposta devolve e quanta informacao ela carrega.
//
// E o ponto que a aula inteira defende: o CODIGO e identico nos tres
// ambientes. O que muda e o valor de `process.env`, e nada mais.
async function responder(conexao, ambiente) {
  const servidor = http.createServer(async (req, res) => {
    // A versao do banco e capturada com `SELECT VERSION()`, nunca escrita a
    // mao: a maquina de quem le devolve a versao que ela tem.
    const [versao] = await conexao.query('SELECT VERSION() AS versao');
    const [total] = await conexao.query('SELECT COUNT(*) AS total FROM tb_d9a2_item');

    // O log de detalhe existe em `development` e em `staging`, e some em
    // `production`. A linha vai para o STDOUT, e nao para o `stderr`: o que
    // chega na pagina da aula e so o `stdout`.
    if (ambiente.logDetalhe) {
      console.log('[' + ambiente.nome + '] GET ' + req.url
        + ' -> SELECT COUNT(*) devolveu ' + total[0].total + ' linha(s)');
    }

    // Em producao a resposta nao leva detalhe: quem recebe um erro em
    // producao recebe um codigo e um identificador, e o detalhe fica no log
    // de quem pode ver. A mensagem de erro inteira na resposta diz ao
    // visitante o esquema da tabela e o nome do banco.
    if (!ambiente.paginaDeErro && req.url.includes('inexistente')) {
      res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
      return res.end(JSON.stringify({
        erro: 'nao encontrado',
        id: 'erro-' + process.pid,
      }));
    }

    res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
    res.end(JSON.stringify({
      ambiente: ambiente.nome,
      worker: process.pid,
      banco: versao[0].versao,
      itens: total[0].total,
    }));
  });

  await new Promise((resolve) => servidor.listen(0, '127.0.0.1', resolve));
  return servidor;
}

// Assinatura: `function avisarPronto(porta)` -> void.
// O filho avisa a porta. A porta e escolhida pelo sistema a cada execucao, e
// o pai so pode saber o numero lendo do filho.
function avisarPronto(porta) {
  console.log('PRONTO|porta=' + porta + '|pid=' + process.pid);
}

// Assinatura: `async function rodarFilho(nomeAmbiente)` -> Promise<void>.
// `NODE_ENV`, `LOG_DETALHE` e `DB_POOL` chegam pelo ambiente do processo, e nao
// como argumento e nao como constante no arquivo. E assim que o `pm2 --env
// production` e o `Environment=` do `systemd` funcionam: o processo recebe o
// ambiente e le `process.env`.
async function rodarFilho(nomeAmbiente) {
  const ambiente = descreverAmbiente(nomeAmbiente);
  const conexao = await abrirBanco();

  await conexao.query(`
    CREATE TABLE IF NOT EXISTS tb_d9a2_item (
      id   INT AUTO_INCREMENT PRIMARY KEY,
      nm_i VARCHAR(40) NOT NULL
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
  `);
  await conexao.query('DELETE FROM tb_d9a2_item');
  await conexao.query('INSERT INTO tb_d9a2_item (nm_i) VALUES (?)',
    ['item-do-ambiente-' + nomeAmbiente]);

  const servidor = await responder(conexao, ambiente);
  avisarPronto(servidor.address().port);

  if (ambiente.banner) {
    console.log('[' + ambiente.banner + '] pool=' + ambiente.pool
      + ' | log de detalhe ligado | banco alvo ' + AMBIENTES[nomeAmbiente].DB_ALVO);
  } else {
    console.log('[producao] pool=' + ambiente.pool
      + ' | log de detalhe DESLIGADO | banco alvo ' + AMBIENTES[nomeAmbiente].DB_ALVO);
    console.log('[producao] sem banner de debug no log, porque LOG_DETALHE=0');
  }

  // `SIGTERM` e o sinal do `pm2 restart` e do `systemctl stop`. E o unico
  // caminho que deixa o processo fechar servidor e conexao antes de sair —
  // e por isso que o `pm2 stop` usa ele e nao `SIGKILL`.
  process.on('SIGTERM', () => {
    servidor.close(async () => {
      await conexao.end();
      console.log('parou com close() no ' + ambiente.nome + ', PID ' + process.pid);
      process.exit(0);
    });
  });
}

// =============================================================================
// o pai: `pm2 start`, `pm2 list`, `pm2 logs`, `pm2 restart` (simulados)
// =============================================================================

// Assinatura: `function campo(linha, nome)` -> string | null.
// Extrai `porta` de `PRONTO|porta=41827|pid=291205`.
function campo(linha, nome) {
  const achado = new RegExp(nome + '=(\\d+)').exec(linha);
  return achado ? achado[1] : null;
}

// Assinatura: `function comProcesso(nomeAmbiente)` -> Object.
// Sobe `este mesmo arquivo` com as variaveis do ambiente no ambiente do
// processo filho. O `env` do `spawn` e o mecanismo: e ele que reproduz o que
// o `pm2 --env` faz.
function comProcesso(nomeAmbiente) {
  const filho = spawn(process.execPath, [__filename, 'filho', nomeAmbiente], {
    stdio: ['ignore', 'pipe', 'pipe'],
    // Repare no que NAO e repassado: `DB_NAME` continua sendo o que veio do
    // `.env`. Se o ambiente de `production` sobrescrevesse com um banco que
    // nao existe na maquina de quem le a pagina, o processo filho morreria com
    // `ER_BAD_DB_ERROR` — e o exemplo inteiro seria um exemplo quebrado.
    env: Object.assign({}, process.env, {
      NODE_ENV: AMBIENTES[nomeAmbiente].NODE_ENV,
      LOG_DETALHE: AMBIENTES[nomeAmbiente].LOG_DETALHE,
      DB_POOL: AMBIENTES[nomeAmbiente].DB_POOL,
      PORTA: AMBIENTES[nomeAmbiente].PORTA,
    }),
  });

  const partes = { stdout: '', stderr: '' };
  filho.stdout.on('data', (d) => { partes.stdout += d; });
  filho.stderr.on('data', (d) => { partes.stderr += d; });

  // `close` e nao `exit`: `close` so vem depois que o processo morreu E os
  // pipes fecharam. Esperar so o `exit` deixa o handle aberto e o exemplo
  // nao encerra.
  const saiu = new Promise((resolve) =>
    filho.on('close', (codigo, sinal) => resolve({ codigo, sinal })));

  let avisouPronto;
  const pronto = new Promise((resolve) => { avisouPronto = resolve; });
  filho.stdout.on('data', () => {
    const linha = partes.stdout.split('\n').find((l) => l.startsWith('PRONTO|'));
    if (linha) {
      avisouPronto({ pid: campo(linha, 'pid'), porta: campo(linha, 'porta') });
    }
  });

  return {
    filho,
    saiu,
    pronto,
    linhas: () => partes.stdout.split('\n').filter(Boolean),
    erros: () => partes.stderr.split('\n').filter(Boolean),
  };
}

// Assinatura: `async function esperarPronto(p)` -> Promise<Object>.
function esperarPronto(p) {
  return Promise.race([
    p.pronto,
    p.saiu.then(() => { throw new Error('o processo saiu antes de ficar pronto'); }),
  ]);
}

// Assinatura: `async function healthCheck(porta)` -> Promise<Object>.
// O `GET /api/saude` de verdade, com o `fetch` do proprio Node, no processo que
// o "gerenciador" subiu. E o health check do `pm2` e do `systemd`.
async function healthCheck(porta) {
  const resposta = await fetch('http://127.0.0.1:' + porta + '/api/saude');
  return resposta.json();
}

// =============================================================================
// o roteiro do `deploy`
// =============================================================================

// Assinatura: `async function rodarPai()` -> Promise<void>.
async function rodarPai() {
  const arquivoLog = path.join(
    fs.mkdtempSync(path.join(os.tmpdir(), 'd9a2-')), 'deploy.log');

  function registrar(p, titulo) {
    const linhas = p.linhas();
    fs.appendFileSync(arquivoLog, '[' + titulo + ']\n' + linhas.join('\n') + '\n');
    return linhas;
  }

  // ------------------------------------------ 1. variaveis por ambiente
  console.log('=== 1. `variaveis de ambiente por ambiente` ===');
  console.log('simulacao: o exemplo nao instala `pm2` nem mexe no `systemd`.');
  console.log('o `pm2 --env production` e o `Environment=` do `systemd` fazem');
  console.log('exatamente isto: escrever variaveis no ambiente do processo.\n');

  const NOME_DAS_VARIAVEIS = ['NODE_ENV', 'DB_NAME', 'DB_POOL', 'LOG_DETALHE', 'DB_PASS'];
  console.log('variaveis que o processo LÊ (o `.js` pede, o servidor entrega):');
  for (const nome of NOME_DAS_VARIAVEIS) {
    console.log('  process.env.' + nome);
  }
  console.log('\nDB_PASS entra na lista e o VALOR nao aparece em lugar nenhum.');
  console.log('o que vai para o git e o `.env.example`, com os NOMES e valores');
  console.log('de exemplo; o `.env` de cada maquina fica fora do git.\n');

  for (const nome of ['development', 'staging', 'production']) {
    const a = descreverAmbiente(nome);
    console.log(a.nome.padEnd(12) + ' NODE_ENV=' + AMBIENTES[nome].NODE_ENV
      + ' | banco alvo=' + AMBIENTES[nome].DB_ALVO
      + ' | pool=' + a.pool
      + ' | log=' + (a.logDetalhe ? 'detalhe ligado' : 'detalhe desligado')
      + ' | erro=' + (a.paginaDeErro ? 'detalhado' : 'generico'));
  }
  console.log('\no CODIGO e o mesmo nos tres. O que muda e `process.env`.');
  console.log('`DB_ALVO` e o nome que cada ambiente apontaria de verdade; o');
  console.log('exemplo conecta no banco do `.env` porque os outros dois nao');
  console.log('existem na maquina de quem le a pagina.');

  console.log('\n--- a variavel que nao chegou ---');
  console.log('contexto: o processo nasce COM `NODE_ENV` e `DB_POOL`, mas o');
  console.log('deploy esqueceu de definir `LOG_DETALHE`.');
  const leitura = lerVariaveis([
    { nome: 'NODE_ENV', valor: AMBIENTES.staging.NODE_ENV },
    { nome: 'DB_POOL', valor: AMBIENTES.staging.DB_POOL },
    { nome: 'LOG_DETALHE', valor: undefined },
    { nome: 'AUSENTE_NATURALMENTE', valor: undefined },
  ]);
  console.log('chegou:  ' + JSON.stringify(leitura.lidas));
  console.log('ausente: ' + JSON.stringify(leitura.ausentes));
  console.log('\nduas ausentes: uma por maquina de erro do deploy, e uma que');
  console.log('ninguem escreveu. Para o codigo as DUAS sao a mesma coisa.');
  console.log('\n`undefined` nao e erro: e valor. E por isso que um deploy pode');
  console.log('falhar longe de onde a variavel era lida. A saida segura e');
  console.log('falhar NA HORA DE ABRIR, no start, e nao no meio de um pedido.');

  // ------------------------------------------ 2. build e primeiro deploy
  console.log('\n=== 2. `build`, `primeiro deploy`, `deploy` ===');
  console.log('`development` -> `staging` -> `production`, tres maquinas, tres');
  console.log('conjuntos de variaveis, e o MESMO artefato no meio.\n');

  const versaoAntiga = comProcesso('staging');
  const infoAntiga = await esperarPronto(versaoAntiga);
  console.log('staging, versao antiga -> PID ' + infoAntiga.pid
    + ' | porta ' + infoAntiga.porta);
  console.log('  ' + JSON.stringify(await healthCheck(infoAntiga.porta)));

  // O `deploy` que nao para o servico: sobe o novo ANTES de derrubar o velho,
  // espera o health check responder, e so entao derruba. E a ordem que evita
  // o `EADDRINUSE` e evita a janela de indisponibilidade.
  console.log('\npasso 1 do deploy: sobe a versao nova ao lado da antiga');
  const versaoNova = comProcesso('production');
  const infoNova = await esperarPronto(versaoNova);
  console.log('production, versao nova -> PID ' + infoNova.pid
    + ' | porta ' + infoNova.porta);
  const saidaNova = await healthCheck(infoNova.porta);
  console.log('  ' + JSON.stringify(saidaNova));

  console.log('\npasso 2: a versao nova responde, entao a antiga pode sair');
  console.log('esta e a ordem do `rolling`: subir, verificar, derrubar.');
  console.log('o caminho inverso — derrubar e sobe — produz `EADDRINUSE` ou');
  console.log('uma janela em que nao responde ninguem.\n');

  versaoAntiga.filho.kill('SIGTERM');
  const fimAntigo = await versaoAntiga.saiu;
  registrar(versaoAntiga, 'staging v1');
  console.log('versao antiga saiu com codigo ' + fimAntigo.codigo
    + ' e sinal ' + fimAntigo.sinal);
  console.log('  ' + versaoAntiga.linhas().slice(-1)[0]);

  console.log('\n--- o que o processo em `production` NAO devolve ---');
  const erro = await fetch('http://127.0.0.1:' + infoNova.porta + '/api/inexistente');
  console.log('status ' + erro.status + ' -> ' + await erro.text());
  console.log('\nem `development` a mesma resposta traz o detalhe da falha; em');
  console.log('`production` traz um codigo, e o detalhe fica no log. O motivo e');
  console.log('segredo: a mensagem completa costuma citar tabela e coluna.');

  // ------------------------------------------ 3. o proxy reverso
  console.log('\n=== 3. `reverse proxy`: `nginx` na frente, Node atras ===');
  console.log('o Node so escuta `127.0.0.1` e NAO esta exposto na internet.');
  console.log('quem escuta a 80 e a 443, e quem faz o TLS, e o nginx.\n');

  for (const caminho of ['/api/saude', '/api/alunos/1', '/css/estilo.css', '/saude']) {
    const r = rotaDoProxy(caminho);
    console.log(caminho.padEnd(20) + ' -> ' + r.atende.padEnd(6)
      + ' | ' + r.destino);
    for (const [nome, valor] of Object.entries(r.cabecalhos)) {
      console.log(' '.repeat(21) + nome + ': ' + valor);
    }
  }
  console.log('\n`location /api/` com `proxy_pass` leva para o Node; o resto sai');
  console.log('do disco do proxy. Arquivo estatico nao custa um processo do');
  console.log('cluster, e nao passa por codigo que pode ter bug.');

  // ------------------------------------------ 4. certificado e dominio
  console.log('\n=== 4. `certificado HTTPS` e `dominio` ===');
  const cert = certificadoDe('api.exemplo.com');
  console.log('dominio:        ' + cert.dominio);
    console.log('TLS termina em: ' + cert.quemTerminaTls
      + ' (porta publica ' + cert.portaPublica + ')');
    console.log('Node escuta em: porta ' + cert.portaDoNode + ', em 127.0.0.1');
    console.log('certificado em: ' + cert.caminhoDoCertificado);
    console.log('renovacao:      ' + cert.renewal);
    console.log('porta 80:       ' + cert.redirecionaHttp);
  console.log('\no navegador fala TLS com o nginx; o nginx fala HTTP simples');
  console.log('com o Node. O Node nunca ve certificado e nunca usa `https.createServer`.');
  console.log('o dominio aponta para o IP do servidor por um registro DNS, e a');
  console.log('primeira tentativa e sempre `http` — que o nginx precisa');
  console.log('redirecionar para `https`, senao o visitante fica em http.');

  // ------------------------------------------ 5. o ciclo de vida
  console.log('\n=== 5. o ciclo de vida que o `pm2` faria ===');
  console.log('start -> health check -> stop -> start de novo, com PID novo.\n');
  console.log('o processo de `production` que esta no ar agora e o do passo 1:');
  console.log('  PID ' + infoNova.pid + ' | porta ' + infoNova.porta);
  console.log('  ' + JSON.stringify(saidaNova));
  console.log('\n(`pm2 start app.js --env production` sobe; `pm2 list` mostra PID,');
  console.log('status e tempo no ar; `pm2 logs app` acompanha; `pm2 restart app`');
  console.log('derruba e sobe de novo; `pm2 stop` desliga. O exemplo reproduziu');
  console.log('o ciclo inteiro sem instalar nada.)');

  // O processo de `production` fica no ar ate aqui de proposito: e assim que
  // se mostra um deploy com servico no ar. O exemplo derruba antes de sair.
  versaoNova.filho.kill('SIGTERM');
  await versaoNova.saiu;
  console.log('\nprocesso de `production` derrubado com SIGTERM.');

  // O log so e lido DEPOIS que os dois processos sairam. Ler antes corria
  // junto com a ultima linha do filho: a contagem mudava entre duas execucoes
  // do mesmo exemplo, e saida que muda sozinha e saida que a pagina nao
  // consegue reproduzir.
  registrar(versaoNova, 'production v2');
  const fim = fs.readFileSync(arquivoLog, 'utf8').split('\n').filter(Boolean);
  console.log('\n`pm2 logs`: o arquivo de log do "gerenciador" tem ' + fim.length
    + ' linha(s) ate aqui, dos dois processos que subiram.');
  console.log('em producao o log precisa de rotacao, senao o arquivo cresce ate');
  console.log('encher o disco. Quem faz isso e o `logrotate` ou o proprio `pm2`.');
  console.log('\no exemplo termina.');
}

async function main() {
  if (process.argv[2] === 'filho') return rodarFilho(process.argv[3] || 'development');
  return rodarPai();
}

main()
  .then(() => clearTimeout(RELOGIO))
  .catch((erro) => {
    clearTimeout(RELOGIO);
    console.error('falhou:', erro.code || erro.name, '-', erro.message);
    process.exit(1);
  });

Saída real

=== 1. `variaveis de ambiente por ambiente` ===
simulacao: o exemplo nao instala `pm2` nem mexe no `systemd`.
o `pm2 --env production` e o `Environment=` do `systemd` fazem
exatamente isto: escrever variaveis no ambiente do processo.

variaveis que o processo LÊ (o `.js` pede, o servidor entrega):
  process.env.NODE_ENV
  process.env.DB_NAME
  process.env.DB_POOL
  process.env.LOG_DETALHE
  process.env.DB_PASS

DB_PASS entra na lista e o VALOR nao aparece em lugar nenhum.
o que vai para o git e o `.env.example`, com os NOMES e valores
de exemplo; o `.env` de cada maquina fica fora do git.

development  NODE_ENV=development | banco alvo=materiais_dev | pool=5 | log=detalhe ligado | erro=detalhado
staging      NODE_ENV=staging | banco alvo=materiais_homologacao | pool=10 | log=detalhe ligado | erro=detalhado
production   NODE_ENV=production | banco alvo=materiais_prod | pool=30 | log=detalhe desligado | erro=generico

o CODIGO e o mesmo nos tres. O que muda e `process.env`.
`DB_ALVO` e o nome que cada ambiente apontaria de verdade; o
exemplo conecta no banco do `.env` porque os outros dois nao
existem na maquina de quem le a pagina.

--- a variavel que nao chegou ---
contexto: o processo nasce COM `NODE_ENV` e `DB_POOL`, mas o
deploy esqueceu de definir `LOG_DETALHE`.
chegou:  {"NODE_ENV":"staging","DB_POOL":"10","LOG_DETALHE":"(ausente)","AUSENTE_NATURALMENTE":"(ausente)"}
ausente: ["LOG_DETALHE","AUSENTE_NATURALMENTE"]

duas ausentes: uma por maquina de erro do deploy, e uma que
ninguem escreveu. Para o codigo as DUAS sao a mesma coisa.

`undefined` nao e erro: e valor. E por isso que um deploy pode
falhar longe de onde a variavel era lida. A saida segura e
falhar NA HORA DE ABRIR, no start, e nao no meio de um pedido.

=== 2. `build`, `primeiro deploy`, `deploy` ===
`development` -> `staging` -> `production`, tres maquinas, tres
conjuntos de variaveis, e o MESMO artefato no meio.

staging, versao antiga -> PID 644852 | porta 42031
  {"ambiente":"staging","worker":644852,"banco":"10.11.14-MariaDB-0ubuntu0.24.04.1","itens":1}

passo 1 do deploy: sobe a versao nova ao lado da antiga
production, versao nova -> PID 644862 | porta 37921
  {"ambiente":"production","worker":644862,"banco":"10.11.14-MariaDB-0ubuntu0.24.04.1","itens":1}

passo 2: a versao nova responde, entao a antiga pode sair
esta e a ordem do `rolling`: subir, verificar, derrubar.
o caminho inverso — derrubar e sobe — produz `EADDRINUSE` ou
uma janela em que nao responde ninguem.

versao antiga saiu com codigo 0 e sinal null
  parou com close() no staging, PID 644852

--- o que o processo em `production` NAO devolve ---
status 404 -> {"erro":"nao encontrado","id":"erro-644862"}

em `development` a mesma resposta traz o detalhe da falha; em
`production` traz um codigo, e o detalhe fica no log. O motivo e
segredo: a mensagem completa costuma citar tabela e coluna.

=== 3. `reverse proxy`: `nginx` na frente, Node atras ===
o Node so escuta `127.0.0.1` e NAO esta exposto na internet.
quem escuta a 80 e a 443, e quem faz o TLS, e o nginx.

/api/saude           -> node   | http://127.0.0.1:3000/api/
                     X-Forwarded-For: <ip real do visitante>
                     X-Forwarded-Proto: https
                     Host: api.exemplo.com
/api/alunos/1        -> node   | http://127.0.0.1:3000/api/
                     X-Forwarded-For: <ip real do visitante>
                     X-Forwarded-Proto: https
                     Host: api.exemplo.com
/css/estilo.css      -> nginx  | /var/www/html/css/estilo.css
/saude               -> nginx  | /var/www/html/saude

`location /api/` com `proxy_pass` leva para o Node; o resto sai
do disco do proxy. Arquivo estatico nao custa um processo do
cluster, e nao passa por codigo que pode ter bug.

=== 4. `certificado HTTPS` e `dominio` ===
dominio:        api.exemplo.com
TLS termina em: nginx (porta publica 443)
Node escuta em: porta 3000, em 127.0.0.1
certificado em: /etc/letsencrypt/live/api.exemplo.com/fullchain.pem
renovacao:      o certificado vence sozinho; quem renova recarrega o nginx
porta 80:       todo o traffego da porta 80 vai para o 443

o navegador fala TLS com o nginx; o nginx fala HTTP simples
com o Node. O Node nunca ve certificado e nunca usa `https.createServer`.
o dominio aponta para o IP do servidor por um registro DNS, e a
primeira tentativa e sempre `http` — que o nginx precisa
redirecionar para `https`, senao o visitante fica em http.

=== 5. o ciclo de vida que o `pm2` faria ===
start -> health check -> stop -> start de novo, com PID novo.

o processo de `production` que esta no ar agora e o do passo 1:
  PID 644862 | porta 37921
  {"ambiente":"production","worker":644862,"banco":"10.11.14-MariaDB-0ubuntu0.24.04.1","itens":1}

(`pm2 start app.js --env production` sobe; `pm2 list` mostra PID,
status e tempo no ar; `pm2 logs app` acompanha; `pm2 restart app`
derruba e sobe de novo; `pm2 stop` desliga. O exemplo reproduziu
o ciclo inteiro sem instalar nada.)

processo de `production` derrubado com SIGTERM.

`pm2 logs`: o arquivo de log do "gerenciador" tem 9 linha(s) ate aqui, dos dois processos que subiram.
em producao o log precisa de rotacao, senao o arquivo cresce ate
encher o disco. Quem faz isso e o `logrotate` ou o proprio `pm2`.

o exemplo termina.