Como rodar um cron job sem precisar de um servidor
Siga isso no seu próprio servidor. Comece grátis, sem cartão
A tarefa em si é pequena: fazer backup de uma tabela, mandar um resumo semanal, expirar algumas linhas. Montar um servidor inteiro, ou uma aplicação inteira, pra rodar um comando num timer parece desproporcional pra tarefa, e é um instinto justo. Este guia cobre o que você de fato precisa pra um script que tem que rodar num horário fixo em vez de responder a requisições: primeiro a versão manual simples (uma VM barata, crontab, e a parte que todo mundo pula, confirmar que ele realmente rodou), e depois o recurso de Cron real do Railyard, descrito com precisão em vez de otimismo.
O que você vai precisar
- O próprio script, executável como um único comando (
node send-digest.js,python cleanup.py, uma linha de shell), com as variáveis de ambiente que ele precisa já identificadas. - Pro caminho manual: acesso SSH a qualquer VM Linux pequena. Um Hetzner CX23 (2 vCPU, 4 GB de RAM, €5,99/mês sem IVA) é mais do que suficiente pra um script que roda por segundos ou minutos num horário fixo, não continuamente.
A versão manual: uma VM, crontab, e prova de que rodou
Escreva a tarefa como um único comando
Envolva qualquer coisa com mais de uma linha num script, pra que o cron tenha só uma coisa pra chamar:
#!/usr/bin/env bash
set -euo pipefail
cd /opt/myjob
set -a
source /opt/myjob/.env
set +a
node send-digest.js >> /var/log/myjob/digest.log 2>&1
O set -euo pipefail importa especificamente aqui porque o cron não tenta de novo uma execução que falhou; sem ele, uma etapa que falha no meio pode deixar o resto do script rodando mesmo assim e reportando sucesso. O set -a antes do source exporta todas as variáveis que o arquivo .env define, pra que o comando da última linha de fato as receba; o próprio cron inicia tarefas com um ambiente quase vazio, não o do seu shell.
Dê permissão de execução e rode uma vez na mão antes de agendar:
chmod +x /opt/myjob/run.sh
/opt/myjob/run.sh
echo $?
Você deve ver 0.
Adicione a entrada no crontab, com lock
crontab -e
0 3 * * * /usr/bin/flock -n /tmp/myjob.lock /opt/myjob/run.sh
O flock -n pega um lock exclusivo antes de rodar o comando e desiste na hora, em vez de enfileirar uma segunda execução, se a execução anterior ainda estiver com o lock. Sem isso, uma tarefa que ocasionalmente demora mais acaba se sobrepondo à sua própria próxima execução agendada, muitas vezes nas mesmas linhas.
Confirme que ela de fato rodou, não só que estava agendada
Uma entrada no crontab existir não diz nada sobre se a tarefa deu certo na noite anterior. A versão honesta de “monitoramento” aqui é um dead man’s switch: a própria tarefa dá um ping numa URL quando dá certo, e algo te avisa se esse ping não chegar no horário esperado. Auto-hospedar essa checagem já é, em si, um pequeno serviço sempre ativo; veja auto-hospedando o Uptime Kuma ou auto-hospedando o Healthchecks pra essa parte especificamente. A entrada do crontab fica assim:
0 3 * * * /usr/bin/flock -n /tmp/myjob.lock /opt/myjob/run.sh && curl -fsS https://hc-ping.com/your-check-id
O && significa que o ping só acontece se o run.sh realmente terminou com sucesso; uma falha silenciosa agora aparece como um check-in perdido, em vez de não aparecer nada.
Pros detalhes de timezone e logging completo que essa versão resumida deixa de lado, substituindo o Heroku Scheduler por cron cobre o mesmo terreno com mais profundidade, incluindo por que o comportamento padrão do cron do sistema, UTC em vez de horário local, pega quase todo mundo que sai de um scheduler de plataforma de surpresa.
O que o Cron do Railyard realmente é
O Railyard tem um recurso real de tarefa agendada, chamado Cron no painel, e vale a pena descrevê-lo com precisão em vez de vagamente. Adicione uma entrada com um nome, um horário e um comando:
- O horário é uma expressão cron simples de cinco campos (
0 3 * * *), interpretada e avaliada do mesmo jeito que o cron do sistema lê uma, em UTC. - O comando roda dentro de uma instância nova da própria imagem da sua aplicação, exatamente o mesmo build que seus processos web e worker já estão rodando, não um container separado que você precisa manter sincronizado na mão.
- Cada execução é registrada: horário de início, código de saída e saída do comando, guardados como histórico de execução por tarefa, então “a tarefa de ontem à noite de fato rodou” é uma página que você consulta, não um arquivo de log que você precisa lembrar que existe.
Esse último ponto substitui a gambiarra do dead man’s switch da versão manual acima por algo nativo: o próprio histórico de execução é o registro de se a tarefa rodou e o que aconteceu, sem um segundo serviço avisando um terceiro pra te contar.
Uma limitação honesta, a mesma que você encontraria rodando isso você mesmo: o Cron do Railyard não pula uma execução enquanto uma execução anterior da mesma tarefa ainda está rodando. A proteção do flock -n da versão manual é algo que você ainda precisa colocar na própria tarefa se ela puder realisticamente ultrapassar o próprio intervalo, seja tornando o comando seguro pra rodar duas vezes em paralelo, seja fazendo ele checar e encerrar antecipadamente se detectar uma execução anterior ainda em andamento.
Veja workers e tarefas agendadas pra entender como o Cron fica ao lado da aba separada de Workers, que trata de processos de background de longa duração (um consumidor de fila, por exemplo) em vez de tarefas agendadas; os dois rodam a mesma imagem, então um deploy que corrige a aplicação corrige as tarefas agendadas dela ao mesmo tempo, por construção.
Configurando
Adicione a tarefa na aba Cron da aplicação: um nome, a expressão de horário e o comando, exatamente como você digitaria se tivesse entrado via SSH no container da própria aplicação e rodado direto. As variáveis de ambiente que a tarefa precisa são as mesmas já configuradas na aba Variables da aplicação, já que o comando roda nessa mesma imagem e ambiente; não existe um .env separado pra referenciar nem um crontab pra editar num servidor, porque não existe nenhum servidor separado rodando a tarefa. Veja variáveis e secrets.
Solução de problemas
A tarefa roda no horário errado. O Cron do Railyard avalia os horários em UTC; se você escreveu um horário assumindo horário local, converta pra UTC em vez de supor que a plataforma lê de outro jeito. É a mesma conversão que pega quem sai de um scheduler de plataforma que também usava UTC, como o do Heroku.
A tarefa parece se sobrepor com ela mesma. Como já dito, isso não é evitado automaticamente; adicione a mesma proteção que você adicionaria numa entrada de crontab comum, seja idempotência no próprio comando, seja uma checagem explícita de lock no começo do script, se a tarefa puder realisticamente demorar mais do que o próprio intervalo.
Nada aparece confirmando que a tarefa rodou. Confira o histórico de execução direto na aba Cron, em vez dos logs da aplicação primeiro; uma execução que terminou com código de saída diferente de zero ainda aparece lá com seu código de saída e sua saída de comando, o que geralmente é mais rápido de ler do que reconstruir a mesma informação a partir da aba Logs.
Perguntas frequentes
Eu preciso de uma aplicação inteira só pra rodar um script agendado? Não mais do que precisaria de um servidor inteiro pra versão manual. Se o script é de fato a coisa inteira que você está implantando, sem nenhum processo web, isso é um formato normal e suportado; uma tarefa agendada e uma aplicação só de worker significam apenas que não existe um tipo de processo web, não que está faltando alguma coisa.
Qual é o intervalo mínimo que um horário pode rodar? A sintaxe padrão de cron de cinco campos vai até uma vez por minuto (* * * * *), o mesmo piso do cron do sistema; nada abaixo de um minuto é expressável nesse formato, independentemente de onde é avaliado.
Qual a diferença disso pra um background worker? Um worker é um processo que começa uma vez e continua rodando indefinidamente, consumindo uma fila ou mantendo uma conexão aberta. Um cron job começa, faz uma coisa e termina, num horário fixo. São formatos de processo diferentes pra tarefas diferentes, cobertos por dois recursos separados e relacionados, em vez de um só tentando fazer as duas coisas.
O que acontece se o servidor por baixo reiniciar no meio de uma tarefa? A mesma coisa que acontece com qualquer processo derrubado de qualquer outro jeito: ele para, sem terminar. Uma tarefa segura pra rodar duas vezes (idempotente) tolera isso sem nenhum tratamento especial; uma que não é precisa se tornar idempotente independentemente de quem está agendando ela, já que o mesmo modo de falha existe num cron comum numa VM também.
Eu posso disparar a tarefa manualmente, não só no horário agendado? Sim, uma tarefa agendada suporta ser rodada por demanda, no mesmo lugar onde você consultaria seu histórico de execução, independentemente de esperar o próximo horário agendado, o que é útil pra testar uma mudança antes de confiar nela no horário real das 3 da manhã.
Seja uma linha de crontab numa VM de cinco dólares ou uma entrada num painel, um cron job é a mesma coisa por baixo: um comando, um horário e um jeito de saber que ele de fato rodou. Acertar o terceiro ponto geralmente é a parte que estava faltando, não os dois primeiros.
Por que escolhem o Railyard para isso
- Sua própria conta de cloud. Faça deploy no DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, ou em qualquer servidor Linux via SSH, cobrado direto pelo seu provedor, sem markup.
- Um preço fixo por equipe. O Free é grátis para sempre: um servidor mais um servidor de banco de dados, três apps, sem cartão. O Pro custa US$19 por mês para até cinco servidores e apps ilimitados. O Team custa US$49 por mês com servidores ilimitados. Sem cobrança por app, por deploy, por assento ou por gigabyte.
- Templates de um clique. Faça deploy de apps open source, como bancos de dados, analytics e ferramentas de dev, no seu próprio servidor em um clique.
- Bancos de dados gerenciados. Postgres, MySQL e Redis com backups programados, verificados por restauração, e recuperação point-in-time.
- Push-to-deploy. Faça deploy a partir do GitHub, GitLab ou da CLI, com HTTPS automático e releases sem downtime.