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, RockyIsso 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 crondParece ó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>&1Compare 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 +aO 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).sqlA 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.shPermissã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 bashna primeira linha é executado por/bin/sh, que não entende[[ ]], arrays nemsourceem alguns sistemas. - Usuário errado.
crontab -eedita o crontab do usuário atual. Uma tarefa que precisa de root vai emsudo crontab -eou 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 -ecuida disso; um arquivo copiado por script, não. - Nome do arquivo em
/etc/cron.d/. Ponto no nome faz o arquivo ser ignorado porrun-parts. Usebackup-app, nãobackup-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:
timedatectlPrefira 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>&1O 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=$? ==="' EXITCom 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.targetsudo systemctl daemon-reload
sudo systemctl enable --now backup-app.timer
systemctl list-timers backup-app.timerSe 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.