← Voltar para o Blog

Por que o seu cron não rodou?

O script funciona quando você executa na mão e não roda no cron. Quase sempre é uma destas cinco coisas — e todas dão para confirmar em poucos minutos.

Equipe EasyOps Cloud · · 5 min de leitura

O script roda perfeitamente quando você executa na mão. Colocado no cron, não acontece nada — sem erro, sem log, sem pista. É uma das falhas mais irritantes de administrar servidor, porque o silêncio é o comportamento normal do cron.

A boa notícia é que a lista de causas é curta.

Primeiro: o cron tentou rodar?

Antes de investigar o script, confirme se a tarefa foi disparada. O cron registra cada execução no log do sistema:

# Debian/Ubuntu
grep CRON /var/log/syslog | tail -20

# systemd em geral
journalctl -u cron -n 50 --no-pager
journalctl -u crond -n 50 --no-pager   # RHEL, AlmaLinux, Rocky

Isso divide o problema em dois mundos bem diferentes:

  • Aparece uma linha CMD (...) com o seu comando — o cron disparou. A falha está no script ou no ambiente dele. Vá para a seção do PATH.
  • Não aparece nada — o cron nem tentou. O problema é a agenda, o serviço ou a permissão do crontab. Continue aqui.

Confirme também que o serviço está de pé:

systemctl status cron --no-pager     # ou crond

Parece óbvio, mas em servidor recém-criado, container ou imagem enxuta o cron frequentemente não está instalado nem habilitado.

A causa número um: PATH

O cron executa com um ambiente mínimo. Não lê seu .bashrc, não lê .profile, não tem o PATH da sua sessão. Na prática, o PATH de uma tarefa de cron costuma ser apenas /usr/bin:/bin.

O resultado é que tudo instalado fora disso — nvm, pyenv, rbenv, binários em /usr/local/bin, docker compose, aws — simplesmente não é encontrado.

Para ver o ambiente real, agende isto por um minuto:

* * * * * env > /tmp/cron-env.txt 2>&1

Compare com o env do seu shell. A diferença explica a maior parte dos casos.

Há duas correções, e a segunda é melhor:

# 1. Declarar o PATH no topo do crontab
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

# 2. Usar caminho absoluto em tudo — dentro do script, inclusive
0 3 * * * /usr/local/bin/aws s3 sync /var/backups s3://meu-bucket/

O caminho absoluto é preferível porque continua correto quando o script for executado de qualquer outro lugar, não só do cron.

Vale lembrar que o mesmo vale para variáveis que a sua aplicação espera: DATABASE_URL, AWS_ACCESS_KEY_ID, NODE_ENV. Se elas vêm do seu shell, não existem no cron. Carregue-as explicitamente dentro do script:

#!/usr/bin/env bash
set -euo pipefail
set -a
source /etc/meuapp/env
set +a

O caractere que ninguém espera: %

No crontab, % não é literal. Ele vira quebra de linha, e tudo depois do primeiro % é enviado ao comando como entrada padrão. É a causa clássica de backup com data no nome que nunca funcionou:

# Errado: quebra na primeira %
0 2 * * * pg_dump app > /backup/app-$(date +%Y-%m-%d).sql

# Certo: escape de cada %
0 2 * * * pg_dump app > /backup/app-$(date +\%Y-\%m-\%d).sql

A alternativa que evita o problema inteiro é não colocar lógica no crontab. Um crontab que só chama um script é mais fácil de ler, de testar e de versionar:

0 2 * * * /usr/local/bin/backup-app.sh

Permissão, dono e o formato do arquivo

Alguns detalhes que falham em silêncio:

  • Bit de execução. chmod +x /usr/local/bin/backup-app.sh. Sem ele, o cron registra a tentativa e o comando sai com erro imediatamente.
  • Shebang. Um script sem #!/usr/bin/env bash na primeira linha é executado por /bin/sh, que não entende [[ ]], arrays nem source em alguns sistemas.
  • Usuário errado. crontab -e edita o crontab do usuário atual. Uma tarefa que precisa de root vai em sudo crontab -e ou em um arquivo dentro de /etc/cron.d/.
  • Formato de /etc/cron.d/. Arquivos ali têm um campo a mais: o usuário, entre a agenda e o comando. Faltando esse campo, a linha é ignorada.
  • Linha final. Um crontab que não termina com quebra de linha pode ter a última entrada descartada. crontab -e cuida disso; um arquivo copiado por script, não.
  • Nome do arquivo em /etc/cron.d/. Ponto no nome faz o arquivo ser ignorado por run-parts. Use backup-app, não backup-app.sh.

Fuso horário

O cron usa o fuso do sistema, que em servidor costuma ser UTC. Uma tarefa marcada para 0 3 * * * roda às 3h UTC, ou seja, meia-noite em Brasília. Se o horário importa — janela de manutenção, fechamento contábil, relatório diário — confirme:

timedatectl

Prefira ajustar o fuso do sistema a compensar o horário na expressão do cron. Compensação manual quebra no horário de verão de outros países e confunde quem ler depois.

Pare de depender de adivinhação: registre log

A raiz do problema é que o cron manda a saída por e-mail local, que em servidor moderno ninguém lê nem entrega. Redirecionar para arquivo resolve de vez:

0 2 * * * /usr/local/bin/backup-app.sh >> /var/log/backup-app.log 2>&1

O 2>&1 é a parte essencial: sem ele, o erro — justamente o que você quer ver — continua indo para o e-mail. E vale registrar o rodapé do script:

#!/usr/bin/env bash
set -euo pipefail
echo "=== início: $(date -Is) ==="
trap 'echo "=== fim: $(date -Is) status=$? ==="' EXIT

Com isso, "o backup rodou ontem?" vira uma pergunta com resposta.

Se o log vai crescer, adicione uma rotação em /etc/logrotate.d/backup-app — um log de cron sem rotação é uma das formas mais bobas de encher o disco, e disco cheio derruba banco de dados.

Teste com o ambiente do cron, não com o seu

O teste que realmente vale é executar com um ambiente vazio, imitando o cron:

env -i /bin/bash --noprofile --norc -c '/usr/local/bin/backup-app.sh'

Se falhar aqui e funcionar no seu shell, você acabou de reproduzir o bug em um segundo em vez de esperar até as 3h.

Quando trocar por um systemd timer

O cron continua ótimo para tarefas simples. Vale considerar um timer do systemd quando você precisar de:

  • Log integrado: journalctl -u backup-app.service, sem redirecionamento manual.
  • Persistent=true: a tarefa que perdeu o horário porque a máquina estava desligada roda ao voltar. O cron simplesmente pula.
  • Controle de sobreposição: uma execução que demora mais que o intervalo não gera duas cópias concorrentes.
  • Ambiente declarado em EnvironmentFile=, versionado junto com a unit.

Um par mínimo fica assim:

# /etc/systemd/system/backup-app.service
[Unit]
Description=Backup da aplicação

[Service]
Type=oneshot
EnvironmentFile=/etc/meuapp/env
ExecStart=/usr/local/bin/backup-app.sh
# /etc/systemd/system/backup-app.timer
[Unit]
Description=Backup diário da aplicação

[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now backup-app.timer
systemctl list-timers backup-app.timer

Se a tarefa agendada que você está tentando consertar é justamente um backup, vale conferir o script de dump e envio para bucket: ele já sai com set -euo pipefail, retenção e a validação que evita subir um dump truncado. E backup automatizado não substitui snapshot antes de uma mudança arriscada — os dois resolvem problemas diferentes.

Suba um servidor em minutos

Preço em real, suporte em português e dados no Brasil.

Criar conta