Dia 9 — Process manager e produção
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:
| Ferramenta | Pergunta que responde |
|---|---|
try/catch | este 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 semprocess.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 managernão é onpm. Onpmresolve dependência; opm2resolve 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.
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.
| Camada | Quem resolve | Onde se configura |
|---|---|---|
| configuração | a variável de ambiente | .env por ambiente |
| ciclo de vida | pm2 ou systemd | no servidor |
| entrada pública | nginx | no servidor |
| criptografia | certificado HTTPS | no 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:
- O
dominioaponta para o IP do servidor (registro DNS). Sem isso onginxnunca é chamado. - O certificado está emitido e o
nginxrecarregou com ele. - O processo escuta
127.0.0.1e está no ar — o health check responde por dentro da máquina. - O
pm2 start --env productionestá 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.
buildem Node é raro: quase tudo roda direto do fonte, e o que muda entre os ambientes é o ambiente, não o arquivo. Quando obuildexiste — TypeScript, bundler — a regra é a mesma: um artefato, três ambientes, e obuildroda 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
logrotateou do própriopm2, 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.