Voltar ao blog Tutoriais

Como rodar cron jobs em Node.js com Docker em produção

Guia prático para configurar tarefas agendadas em Node.js usando node-cron, containerizar com Docker e publicar na Guara Cloud com logs e monitoramento.

8 min de leitura

Por Guara Cloud Editorial

Testado com Node.js 20 / Docker / node-cron 3.x

Tarefas agendadas em Node.js parecem simples até você precisar colocar em produção. Crontab do servidor some no redeploy, processos morrem sem aviso e não existe log centralizado. Se o seu projeto já usa Docker, dá pra resolver isso com node-cron dentro do próprio container e deixar a plataforma cuidar do resto.

Este tutorial mostra como criar um worker de cron jobs em Node.js, empacotar em Docker e publicar na Guara Cloud. O resultado é um serviço que roda suas tarefas no horário certo, com logs visíveis e reinício automático se algo falhar.

Resposta rápida

Para rodar cron jobs em Node.js com Docker em produção, instale a biblioteca node-cron, crie um processo separado que executa as tarefas agendadas, empacote tudo em um Dockerfile e publique como um serviço na Guara Cloud. A plataforma mantém o container rodando 24/7, reinicia automaticamente em caso de falha e centraliza os logs. Você não precisa de crontab do sistema operacional nem de ferramentas externas como Bull ou Agenda para tarefas simples.

Principais pontos

  • Use node-cron para cron jobs simples com sintaxe familiar do crontab. Para filas com retry e dead-letter, considere BullMQ.
  • Rode o worker como um serviço separado da API. Misturar os dois no mesmo container complica o debugging.
  • Configure timezone explicitamente no node-cron. O default é UTC, e isso pega muita gente de surpresa.
  • Log cada execução com timestamp e status. Sem isso, descobrir por que um job falhou às 3h da manhã vira um pesadelo.
  • Não esqueça do healthcheck no Docker. Se o processo travar, a plataforma precisa saber.

Quando se aplica

Essa abordagem funciona bem para tarefas com execução periódica que não dependem de filas: limpeza de registros expirados, envio de relatórios diários, sincronização com APIs externas, geração de snapshots, checagem de saúde de integrações. Basicamente qualquer coisa que você faria com uma crontab no servidor, mas quer manter dentro da aplicação.

Quando não usar

Se os seus jobs precisam de retry automático, dead-letter queue, prioridade ou distribuição entre múltiplos workers, node-cron sozinho não resolve. Nesse caso, BullMQ com Redis é uma escolha melhor. Outro cenário: se o job é muito pesado (processamento de vídeo, ETL grande) e precisa escalar horizontalmente, um worker de fila dedicado faz mais sentido.

Antes de começar

  • Node.js 20 ou superior instalado localmente
  • Docker instalado e funcionando
  • Conta na Guara Cloud (ou outra plataforma com suporte a containers)
  • Um repositório Git com o código do projeto

1. Instale o node-cron e crie o worker

Comece instalando a dependência:

npm install node-cron

Agora crie o arquivo src/worker.js com a estrutura básica:

import cron from 'node-cron';

console.log('[worker] Cron jobs iniciados. Aguardando execuções...');

// Roda todo dia às 8h horário de Brasília
cron.schedule('0 8 * * *', async () => {
  const start = Date.now();
  try {
    // Sua lógica aqui
    console.log(`[job:relatorio-diario] iniciado às ${new Date().toISOString()}`);
    await gerarRelatorioDiario();
    console.log(`[job:relatorio-diario] concluído em ${Date.now() - start}ms`);
  } catch (err) {
    console.error(`[job:relatorio-diario] falhou: ${err.message}`);
  }
}, {
  timezone: 'America/Sao_Paulo'
});

// Roda a cada 6 horas
cron.schedule('0 */6 * * *', async () => {
  try {
    await limparTokensExpirados();
  } catch (err) {
    console.error(`[job:limpar-tokens] falhou: ${err.message}`);
  }
}, {
  timezone: 'America/Sao_Paulo'
});

// Mantém o processo vivo
process.on('SIGTERM', () => {
  console.log('[worker] Recebido SIGTERM, encerrando...');
  process.exit(0);
});

async function gerarRelatorioDiario() {
  // Implementação real aqui
}

async function limparTokensExpirados() {
  // Implementação real aqui
}

Preste atenção no timezone: 'America/Sao_Paulo'. Esquece isso e o cron roda em UTC. O “relatório das 8h” dispara às 5h da manhã e ninguém percebe até alguém olhar o banco.

2. Separe o worker da API no package.json

Adicione dois scripts diferentes:

{
  "scripts": {
    "start": "node src/server.js",
    "worker": "node src/worker.js"
  }
}

Por que separar? Porque a API responde a requisições HTTP e o worker é um processo de longa duração que nunca responde nada na porta. Colocar os dois no mesmo container significa que se o cron job travar o event loop, a API fica lenta junto. Manter serviços diferentes evita esse acoplamento.

3. Crie o Dockerfile para o worker

FROM node:20-alpine

WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci --omit=dev

COPY src/ ./src/

CMD ["node", "src/worker.js"]

Note o --omit=dev na instalação. O worker em produção não precisa de jest, eslint ou typescript. Isso reduz o tamanho da imagem e o tempo de deploy.

Se o seu projeto usa TypeScript, o Dockerfile precisa compilar antes:

FROM node:20-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY tsconfig.json ./
COPY src/ ./src/
RUN npm run build

FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist/ ./dist/
CMD ["node", "dist/worker.js"]

4. Adicione um healthcheck

Sem healthcheck, a plataforma não sabe se o worker travou. Um worker de cron job não expõe porta HTTP, então o truque é usar um arquivo de “última verificação”:

import { writeFileSync } from 'fs';
import cron from 'node-cron';

// Heartbeat a cada 30 segundos
cron.schedule('*/30 * * * * *', () => {
  writeFileSync('/tmp/healthy', Date.now().toString());
});

E no Dockerfile:

HEALTHCHECK --interval=60s --timeout=5s --retries=3 \
  CMD test -f /tmp/healthy && \
      node -e "const t=parseInt(require('fs').readFileSync('/tmp/healthy','utf8'));process.exit(Date.now()-t>120000?1:0)"

Isso garante que se o processo travar e parar de escrever o heartbeat, o Docker marca como unhealthy e a plataforma reinicia o container.

Variáveis de ambiente do worker

Nome Valor
NODE_ENV production
TZ America/Sao_Paulo
DATABASE_URL postgres://...
LOG_LEVEL info

5. Publique na Guara Cloud

Com o Dockerfile pronto, o deploy é direto:

Passo a passo

  1. Crie um novo serviço na Guara Cloud
  2. Escolha deploy via GitHub (apontando para o repositório) ou imagem Docker
  3. No comando de execução, use npm run worker (ou acesse o container diretamente)
  4. Configure as variáveis de ambiente (DATABASE_URL, TZ, etc)
  5. Inicie o deploy e confirme nos logs que o worker iniciou
Deploy do worker via CLI
guara deploy --name meu-worker --dockerfile Dockerfile.worker --env TZ=America/Sao_Paulo

Depois do primeiro deploy, verifique nos logs se aparece [worker] Cron jobs iniciados. Se aparecer, o serviço está rodando e vai executar os jobs nos horários configurados.

6. Monitore as execuções

Sem monitoramento, você só descobre que um job falhou quando alguém reclama. Esse feedback loop não é ideal.

Comece com logs estruturados. Use pino ou winston em vez de console.log. Com JSON estruturado, dá pra buscar nos logs da Guara Cloud por job name, status e duração. Depois adicione alertas de falha: se o job der erro, manda uma notificação para Discord, Slack ou email. Algo assim:

async function executarJob(nome, fn) {
  const start = Date.now();
  try {
    await fn();
    logger.info({ job: nome, duracao: Date.now() - start, status: 'ok' });
  } catch (err) {
    logger.error({ job: nome, erro: err.message, status: 'falha' });
    await notificarFalha(nome, err);
  }
}

Se você tem vários jobs, vale a pena registrar a última execução no banco e mostrar num endpoint interno. Qualquer pessoa do time pode checar se os jobs estão rodando sem precisar acessar logs.

Solução de problemas

Problema O job roda no horário errado (3h em vez de 8h)
Solução Adicione timezone: "America/Sao_Paulo" na opção do cron.schedule. Sem isso, ele usa UTC.
Problema O worker para de rodar depois de algumas horas
Solução Verifique se existe um tratamento de erro adequado. Uma promise rejeitada não catchada mata o processo Node. Adicione process.on("unhandledRejection").
Problema O container reinicia mas os logs não mostram nada
Solução O Dockerfile pode estar rodando o comando errado. Verifique se o CMD aponta para o arquivo do worker e não da API.
Problema Dois jobs rodam ao mesmo tempo e conflitam no banco
Solução Use um lock distribuído (Advisory Locks no Postgres) ou coloque os jobs em sequência usando async/await encadeado.
Problema O job demora mais que o intervalo e acumula execuções
Solução Adicione uma flag "executando" que impede sobreposição. Só inicia o próximo quando o anterior terminar.

Alternativas ao node-cron

Para contextos específicos, outras ferramentas fazem mais sentido:

BullMQ + Redis: quando precisa de retry, delay, prioridade ou dead-letter queue. Ideal para processamento assíncrono com garantia de entrega.

Agenda + MongoDB: semelhante ao BullMQ mas usa Mongo como backend. Bom se o projeto já tem MongoDB e não quer introduzir Redis.

Cron externo (GitHub Actions, cron-job.org): para tarefas muito simples que não precisam de contexto da aplicação. Exemplo: ping de healthcheck a cada 5 minutos.

A escolha entre eles depende do que você precisa garantir. Se é só “roda às 8h todo dia e loga se falhar”, node-cron resolve. Se precisa de retry e fila, vai de BullMQ.

Posso rodar cron jobs no mesmo container da API?

Pode, mas não deveria. Se o job travar o event loop ou consumir muita memória, a API vai sofrer junto. Serviços separados permitem escalar e debugar de forma independente.

O que acontece se o container reiniciar no meio de um job?

O job é interrompido e não retoma de onde parou. Se a tarefa precisa ser idempotente (pode rodar duas vezes sem problema), tudo bem. Senão, use um lock no banco para marcar "em execução" e verificar antes de começar.

node-cron aguenta quantos jobs simultâneos?

Não existe limite hardcoded. O gargalo é o que cada job faz. Se os 10 jobs são queries rápidas no banco, roda tranquilo. Se cada job faz chamadas HTTP pesadas, considere distribuir em workers separados.

Como testo cron jobs localmente sem esperar o horário?

Exporte a lógica do job em funções separadas e teste as funções diretamente. Para testar o agendamento, use intervalos curtos (a cada 10 segundos) durante desenvolvimento.

Publique seus cron jobs na Guara Cloud

Worker rodando 24/7 com reinício automático, logs centralizados e cobrança em Real. Sem gerenciar servidor.

Começar grátis