Dia 13: Cron e Agendamento no Linux — crontab e systemd timers

O cron é o agendador de tarefas mais antigo e universal do Linux — existe há mais de 40 anos e está presente em praticamente toda a distribuição. Quem vem do Windows pode pensar nele como o equivalente ao Task Scheduler, mas em linha de comandos e com uma sintaxe própria baseada em cinco campos temporais. O systemd timers é a alternativa moderna — integrado no init system padrão, com logging nativo, precisão ao microsegundo e capacidade de reagir a eventos do sistema em vez de apenas a relógios.

Este artigo cobre os quatro pilares do dia 13: crontab (sintaxe, gestão e localização dos ficheiros), at e batch (agendamento one-shot), systemd timers (a evolução do cron com vantagens reais) e anacron (para máquinas que desligam). No fim consegue escolher a ferramenta certa para cada cenário, configurar variáveis de ambiente correctamente e evitar os pitfalls mais comuns — incluindo o clássico % que escapa a comando em crontab.

O Que É o Cron e Como Funciona

O cron é um daemon (crond) que corre em background e verifica a cada minuto se há tarefas agendadas para executar. Quando encontra uma tarefa cuja hora coincide com a hora actual, executa o comando associado. O nome vem do grego chronos (tempo) — é literalmente um relógio que dispara comandos.

O daemon crond é tipicamente gerido pelo systemd nos sistemas modernos. Pode verificar o estado com:

# Verificar se o crond está a correr
systemctl status cron      # Debian/Ubuntu
systemctl status crond     # RHEL/Rocky/Fedora

# Activar ao arranque
sudo systemctl enable --now cron
sudo systemctl enable --now crond

O cron tem três componentes principais:

Componente Localização Função
crond Daemon (systemd) Processo que verifica e executa tarefas a cada minuto
crontab /usr/bin/crontab Comando para editar/listar/remover crontabs de utilizador
Ficheiros crontab /var/spool/cron/ e /etc/cron.d/ Onde as tarefas agendadas são armazenadas

Nota: O cron (daemon) e crontab (comando/ficheiro) são coisas diferentes. O daemon chama-se cron em Debian/Ubuntu e crond em RHEL/Fedora. O comando é sempre crontab em qualquer distribuição.

Sintaxe do crontab — Os 5 Campos e Atalhos

Cada linha num ficheiro crontab segue o formato de cinco campos temporais seguidos do comando:

# ┌───────────── minuto (0-59)
# │ ┌───────────── hora (0-23)
# │ │ ┌───────────── dia do mês (1-31)
# │ │ │ ┌───────────── mês (1-12 ou jan-dez)
# │ │ │ │ ┌───────────── dia da semana (0-7, 0 e 7 = Domingo)
# │ │ │ │ │
# │ │ │ │ │
  * * * * * comando-a-executar

Cada campo aceita valores específicos, intervalos, listas e passos:

Sintaxe Significado Exemplo
* Qualquer valor (always) * * * * * = todos os minutos
5 Valor exacto 5 * * * * = minuto 5 de cada hora
1-5 Intervalo (de A até B) * 1-5 * * * = horas 1-5 da madrugada
1,3,5 Lista de valores * * * * 1,3,5 = Segunda, Quarta, Sexta
*/15 Passo (a cada N unidades) */15 * * * * = a cada 15 minutos
0 9 * * 1 Combinação completa Todas as Segundas-feiras às 09:00

Atalhos @reboot, @daily, @weekly, @monthly, @yearly, @annually

O cron oferece atalhos em vez dos cinco campos — são equivalentes a padrões comuns:

Atalho Equivalente Descrição
@reboot Executa uma vez ao arranque do sistema
@yearly / @annually 0 0 1 1 * 1 de Janeiro às 00:00
@monthly 0 0 1 * * Dia 1 de cada mês às 00:00
@weekly 0 0 * * 0 Domingos às 00:00
@daily / @midnight 0 0 * * * Todos os dias às 00:00
@hourly 0 * * * * No minuto 0 de cada hora

Exemplos práticos de entradas crontab:

# Backup completo todas as noites à 01:30
30 1 * * * /home/admin/scripts/backup.sh

# Actualizar base de dados de pacotes de Segunda a Sexta às 06:00
0 6 * * 1-5 /usr/local/bin/update-repos.sh

# Verificar certificados SSL a cada 6 horas
0 */6 * * * /usr/local/bin/check-certs.sh

# Limpeza de logs antigos no 1º dia de cada mês
@monthly /usr/local/bin/cleanup-logs.sh

# Executar script ao arranque do sistema
@reboot /home/admin/scripts/startup-check.sh

# Monitorizar a cada 10 minutos (passo)
*/10 * * * * /usr/local/bin/monitor.sh

Dica: Para testar a sintaxe de uma linha crontab antes de a colocar em produção, use o site crontab.guru — interpreta os 5 campos e mostra quando a próxima execução acontece. Não substitui o conhecimento da sintaxe, mas ajuda a evitar erros de off-by-one em intervalos complexos.

Gerir crontabs — crontab -e, -l, -r

O comando crontab é a interface para gerir os ficheiros de agendamento de cada utilizador. Cada utilizador tem o seu próprio crontab — não há um único ficheiro global (embora existam ficheiros de sistema em /etc/cron.d/, ver secção seguinte).

Comando Acção
crontab -e Edita o crontab do utilizador actual (abre o $EDITOR)
crontab -l Lista o conteúdo do crontab actual
crontab -r Remove o crontab do utilizador (sem confirmação!)
crontab -ri Remove com confirmação interactiva (recomendado)
crontab -u user -l Lista o crontab de outro utilizador (requer root)
crontab ficheiro Substitui o crontab actual pelo conteúdo de ficheiro
# Editar o meu crontab
crontab -e

# Ver o que está agendado
crontab -l

# Backup do crontab antes de editar
crontab -l > ~/crontab-backup-$(date +%Y%m%d).txt

# Restaurar a partir de um ficheiro
crontab ~/crontab-backup-20260721.txt

# Como root, ver o crontab do utilizador maria
sudo crontab -u maria -l

# Editar o crontab do utilizador www-data (comum para webapps)
sudo crontab -u www-data -e

Atenção: crontab -r apaga TODAS as entradas sem pedir confirmação. Um dedo escorregadio num terminal remoto pode eliminar horas de configuração. Use sempre crontab -ri (interactive) ou faça crontab -l > backup.txt antes de qualquer operação destrutiva.

Permissões — quem pode usar crontab?

O acesso ao comando crontab é controlado por dois ficheiros:

# /etc/cron.allow  — lista de utilizadores PERMITIDOS
# /etc/cron.deny   — lista de utilizadores PROIBIDOS

# Regra: se cron.allow existe, só os listados podem usar crontab
#        se cron.allow NÃO existe, todos podem EXCEPTO os de cron.deny
#        se ambos não existem, só root pode usar crontab

# Exemplo: permitir apenas admin e maria
echo -e "admin\nmaria" | sudo tee /etc/cron.allow

# Bloquear apenas o utilizador guest
echo "guest" | sudo tee /etc/cron.deny

Onde Ficam os Ficheiros — /etc/cron.d/ vs /var/spool/cron/

Existem três localizações distintas para ficheiros de agendamento cron, cada uma com um propósito específico:

Localização Gestão Quando usar
/var/spool/cron/ Via crontab -e Tarefas de utilizadores individuais
/etc/cron.d/ Ficheiros directos (root) Tarefas de sistema, empacotadas com software
/etc/crontab Ficheiro único (root) Crontab do sistema — tem campo de utilizador extra

A diferença crítica: os ficheiros em /etc/cron.d/ e /etc/crontab têm um campo adicional para o utilizador entre os 5 campos temporais e o comando:

# /var/spool/cron/admin (crontab de utilizador — 5 campos + comando)
30 1 * * * /home/admin/scripts/backup.sh

# /etc/cron.d/backup (ficheiro de sistema — 5 campos + UTILIZADOR + comando)
30 1 * * * admin /home/admin/scripts/backup.sh

# /etc/crontab (formato idêntico ao /etc/cron.d/)
30 1 * * * root /usr/local/bin/system-backup.sh

Existem ainda directórios especiais que executam todos os scripts neles contidos em horários pré-definidos:

# Directórios especiais — o cron corre todos os scripts dentro
/etc/cron.hourly/   # a cada hora (minuto 0)
/etc/cron.daily/    # todos os dias (configurável, tipicamente 06:25)
/etc/cron.weekly/   # todos os Domingos
/etc/cron.monthly/  # no dia 1 de cada mês

# Para colocar um script no cron.daily:
sudo cp ~/scripts/cleanup.sh /etc/cron.daily/cleanup
sudo chmod +x /etc/cron.daily/cleanup

Boa prática: Para tarefas de sistema (backups, manutenção), prefira /etc/cron.d/ em vez de crontab -e — os ficheiros ficam visíveis, podem ser versionados em git, e são geridos por pacotes (cada pacote pode instalar o seu próprio ficheiro em /etc/cron.d/ sem mexer no crontab de outros utilizadores).

at e batch — Agendamento One-Shot

Enquanto o cron é para tarefas recorrentes, o at é para executar um comando uma única vez num momento futuro. É o equivalente ao “run once” do Task Scheduler do Windows.

Instalação do at (nem todas as distribuições trazem por defeito):

# Debian/Ubuntu
sudo apt install at
sudo systemctl enable --now atd

# RHEL/Rocky/Fedora
sudo dnf install at
sudo systemctl enable --now atd

Exemplos de utilização do at:

# Agendar um backup para as 23:30 de hoje
echo "/home/admin/scripts/backup.sh" | at 23:30

# Agendar para amanhã às 09:00
echo "systemctl restart nginx" | at tomorrow 09:00

# Agendar para uma data específica
echo "/usr/local/bin/report.sh" | at 14:00 2026-12-25

# Agendar daqui a 30 minutos
echo "echo done > /tmp/ready" | at now + 30 minutes

# Agendar daqui a 2 horas
echo "systemctl reload apache2" | at now + 2 hours

# Modo interactivo — escrever comandos linha a linha
at 18:00
at> echo "Olá" | mail -s "Relatório" [email protected]
at> /usr/local/bin/generate-report.sh
at>   # termina com EOF (não Ctrl+C)

Comandos de gestão:

# Listar tarefas agendadas (mostra job ID e horário)
atq

# Remover uma tarefa pelo ID
atrm 3

# Ver o conteúdo de uma tarefa agendada
at -c 3

# batch — executa quando a carga do sistema baixa (< 1.5)
echo "/usr/local/bin/heavy-task.sh" | batch

O batch é uma variante do at que só executa quando a carga média do sistema cai abaixo de 1.5 (configurável). É útil para tarefas pesadas que não devem competir com carga de produção.

Ferramenta Frequência Caso de uso
cron Recorrente Backups diários, limpeza semanal, monitorização contínua
at Uma vez Reiniciar serviço às 02:00, deploy programado, aviso por email
batch Uma vez (carga baixa) Compilação pesada, processamento de batch fora de horas pico

systemd timers — A Evolução do Cron

Os systemd timers são a alternativa moderna ao cron. Em vez de um ficheiro de texto com sintaxe própria, usam dois ficheiros unit do systemd: um .service (o que executar) e um .timer (quando executar). Vimos isto no Dia 7: Systemd — aqui aprofundamos a comparação com o cron.

Vantagens dos systemd timers sobre o cron

Vantagem cron systemd timer
Execução ao boot @reboot (uma vez) OnBootSec= (com atraso configurável)
Precisão temporal Minuto (60s de jitter) Segundo/microsegundo (AccuracySec=)
Logging Email (se MAILTO configurado) Integrado no journald — journalctl -u
Dependências Não (script tem de gerir) Requires=, After=, Wants=
Execução falhada Sem retry automático Restart=on-failure
Tarefas perdidas Perde-se (máquina desligada) Persistent=true (corrige atrasadas)
Isolamento de recursos Não cgroups, CPUQuota=, MemoryMax=

OnCalendar — agendamento por relógio (estilo cron)

A directiva OnCalendar= é o equivalente aos 5 campos do crontab, mas com sintaxe mais legível:

# /etc/systemd/system/backup.service
[Unit]
Description=Backup completo do sistema

[Service]
Type=oneshot
ExecStart=/home/admin/scripts/backup.sh

# /etc/systemd/system/backup.timer
[Unit]
Description=Timer para backup diário

[Timer]
# Equivalente a "30 1 * * *" do crontab
OnCalendar=*-*-* 01:30:00

# Equivalente a "@daily" — todos os dias à meia-noite
# OnCalendar=daily

# Equivalente a "@weekly" — Domingos à meia-noite
# OnCalendar=weekly

# Dias úteis às 06:00 (Seg-Sex)
# OnCalendar=Mon..Fri 06:00

# A cada 15 minutos
# OnCalendar=*:0/15

# Com precisão de 1 minuto (reduz wakeups)
AccuracySec=1min

# Correr tarefas perdidas (equivalente ao anacron)
Persistent=true

[Install]
WantedBy=timers.target
# Activar e iniciar o timer
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer

# Verificar estado
systemctl status backup.timer

# Ver o serviço executado manualmente
sudo systemctl start backup.service

OnBootSec e OnUnitActiveSec — agendamento relativo

Para tarefas que não precisam de um horário fixo, os timers monotónicos (relativos) são mais adequados que o OnCalendar:

# /etc/systemd/system/health-check.timer
[Unit]
Description=Health check periódico

[Timer]
# 2 minutos após o boot (deixa o sistema estabilizar)
OnBootSec=2min

# Depois, a cada 5 minutos relativamente à última execução
OnUnitActiveSec=5min

# Não há problema se atrasar 30 segundos
AccuracySec=30s

[Install]
WantedBy=timers.target

A diferença entre OnCalendar (relógio real) e timers monotónicos (OnBootSec/OnUnitActiveSec):

Tipo Directiva Referência
Realtime OnCalendar= Relógio de parede (data/hora absoluta)
Monotónico OnBootSec= Segundos desde o boot do sistema
Monotónico OnUnitActiveSec= Segundos desde a última execução
Monotónico OnUnitInactiveSec= Segundos desde que o serviço ficou inactivo

systemctl list-timers — ver todos os timers

# Listar todos os timers activos
systemctl list-timers

# Incluir timers inactivos
systemctl list-timers --all

# Ver apenas timers do utilizador (não do sistema)
systemctl --user list-timers

# Output:
# NEXT                       LEFT LAST                       PASSED UNIT            ACTIVATES
# Tue 2026-07-21 18:30:00  5min left Tue 2026-07-21 18:25:00 5s ago  backup.timer    backup.service
# Tue 2026-07-21 19:00:00 35min left n/a                        n/a    health.timer    health.service

# Verificar a próxima execução de um timer específico
systemctl list-timers backup.timer

O list-timers mostra NEXT (próxima execução), LEFT (quanto tempo falta), LAST (última execução) e PASSED (há quanto tempo). É muito mais informativo que o crontab -l que apenas mostra a sintaxe sem indicar quando corre a seguir.

Quando migrar de cron para systemd timers? Se precisa de logging estruturado, dependências entre tarefas, retry automático, isolamento de recursos (cgroups), ou execução relativa ao boot — migre. Se tem scripts simples que já funcionam em cron e não precisa de nada disto, não há urgência. O cron não está deprecated e continua a ser mantido. Muitos sistemas usam ambos em paralelo sem conflito.

anacron — Para Máquinas Que Desligam

O anacron resolve um problema específico do cron: se a máquina está desligada na hora agendada, a tarefa não corre e perde-se. Em portáteis, estações de trabalho que desligam à noite, ou VMs que são pausadas, isto é inaceitável para tarefas importantes como backups ou actualizações de segurança.

O anacron não tem campos de hora — apenas período (em dias), atraso (em minutos) e identificador da tarefa:

# /etc/anacrontab — formato:
# período  atraso  identificador  comando
#   1       5      backup.daily   /home/admin/scripts/backup.sh
#   7       10     cleanup.weekly /usr/local/bin/cleanup.sh
#   30      0      report.monthly  /usr/local/bin/generate-report.sh

# período: de quantos em quantos dias (1=diário, 7=semanal, 30=mensal)
# atraso: minutos de espera após o boot antes de executar (evita thundering herd)
# identificador: nome único usado em /var/spool/anacron/ para registar última execução

O anacron é tipicamente instalado por defeito em Debian/Ubuntu (parte do pacote cron) e corre via /etc/cron.d/0hourly ou como systemd timer. Os directórios /etc/cron.daily/, /etc/cron.weekly/ e /etc/cron.monthly/ são normalmente executados pelo anacron (não pelo cron directamente) em estações de trabalho.

# Verificar se o anacron está disponível
which anacron
cat /etc/anacrontab

# Forçar execução de todas as tarefas anacron (testing)
sudo anacron -f

# Executar apenas tarefas cujo período já passou
sudo anacron -d   # modo verbose/dry-run

# Ver última execução de cada tarefa
ls -la /var/spool/anacron/

Equivalência: O Persistent=true nos systemd timers oferece a mesma funcionalidade do anacron — quando o sistema arranca, o timer verifica se perdeu execuções e corre-as. Para novos sistemas com systemd, prefira Persistent=true em vez de anacron.

Variáveis de Ambiente em crontab — PATH e SHELL

Um dos pitfalls mais comuns do cron: o ambiente de execução é minimalista. O cron não carrega o .bashrc, .profile ou qualquer configuração de shell interativo. O PATH por defeito é tipicamente apenas /usr/bin:/bin — muitos comandos não são encontrados.

# crontab — definir variáveis de ambiente no topo do ficheiro
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
[email protected]
HOME=/home/admin

# Agora os comandos podem usar PATH completo
30 1 * * * backup.sh           # encontrado via PATH
0 6 * * 1 docker compose -f /opt/app/docker-compose.yml up -d

Variáveis suportadas em crontab:

Variável Default Função
SHELL /bin/sh Shell usado para executar os comandos
PATH /usr/bin:/bin Caminho de procura de executáveis
MAILTO utilizador local Para onde enviar stdout/stderr de cada tarefa
HOME home do utilizador Directório home usado durante a execução
CRON_TZ timezone do sistema Timezone para interpretar os campos temporais

Atenção: Definir MAILTO="" (vazio) desliga completamente o envio de email — útil quando as tarefas produzem muito output que não interessa. Mas se não definir MAILTO e não houver MTA instalado (postfix, exim), o stderr das tarefas desaparece silenciosamente. Sempre redireccione stderr: comando 2>&1 >> /var/log/minha-tarefa.log

Erros Comuns

1. O sinal % é especial em crontab

O % é interpretado pelo cron como newline — tudo depois do primeiro % é enviado como stdin ao comando. Isto quebra comandos com date +%Y%m%d:

# ERRADO — o % é interpretado como newline
0 1 * * * tar czf /backup/$(date +%Y%m%d).tar.gz /home

# O cron vê isto como:
#   tar czf /backup/$(date +
#   Y%m%d).tar.gz /home
# → stdin recebe "Y%m%d).tar.gz /home"

# CORRECTO — escapar % com backslash
0 1 * * * tar czf /backup/$(date +\%Y\%m\%d).tar.gz /home

# Alternativa — script wrapper
0 1 * * * /home/admin/scripts/backup.sh   # o script contém o date normalmente

2. Newlines no comando — cada linha é uma tarefa

O cron interpreta cada linha como uma tarefa separada. Não pode ter um comando multi-linha directamente no crontab:

# ERRADO — a segunda linha é interpretada como tarefa inválida
0 1 * * * docker compose -f /opt/app/docker-compose.yml \
  down && docker compose -f /opt/app/docker-compose.yml up -d

# CORRECTO — tudo numa linha com && ou ;
0 1 * * * docker compose -f /opt/app/docker-compose.yml down && docker compose -f /opt/app/docker-compose.yml up -d

# MELHOR — script wrapper
0 1 * * * /opt/app/restart.sh

3. Tarefa não executa e não há erro visível

Sem MAILTO configurado e sem MTA, o stderr desaparece. Diagnóstico:

# 1. Verificar se o crond está a correr
systemctl status cron   # ou crond

# 2. Ver logs do cron
journalctl -u cron --since "1 hour ago"
# ou em sistemas sem journald:
grep CRON /var/log/syslog | tail -20

# 3. Redireccionar output para ficheiro (debugging)
0 1 * * * /home/admin/scripts/backup.sh >> /tmp/backup.log 2>&1

# 4. Verificar permissões do script
ls -la /home/admin/scripts/backup.sh
# Deve ter -rwxr-xr-x ou similar com x

# 5. Verificar se o script funciona no ambiente cron (minimalista)
env -i SHELL=/bin/sh PATH=/usr/bin:/bin /home/admin/scripts/backup.sh

4. Dia 31 vs último dia do mês

O campo dia do mês aceita 1-31, mas meses como Fevereiro têm 28-29 dias. A tarefa 0 0 31 * * nunca corre em meses curtos. Para "último dia do mês", use uma verificação no script:

# Correr todos os dias 28-31 e verificar no script se é o último
0 0 28-31 * * [ "$(date -d tomorrow +%d)" = "01" ] && /usr/local/bin/monthly-task.sh

# Em systemd timer, OnCalendar=--31 não funciona igual
# Alternativa: OnCalendar=*-*-28..31 00:00:00 com verificação no serviço

5. Fuso horário — o cron usa a timezone do sistema

Se o sistema está em UTC mas se pensa em hora local, as tarefas correm na hora "errada". Verificar:

# Verificar timezone do sistema
timedatectl

# Para uma tarefa específica em timezone diferente
CRON_TZ=Europe/Lisbon
0 9 * * * /usr/local/bin/morning-report.sh

# Cuidado com mudanças de horário (verão/inverno)
# O cron não reavalia automaticamente — reinicia o crond após mudar timezone
sudo systemctl restart cron

Checklist Rápido

Antes de considerar uma tarefa de agendamento em produção, verificar todos os pontos:

  • O daemon (cron ou crond) está activo com systemctl status
  • PATH definido no crontab — não assumir que comandos são encontrados
  • Caminhos absolutos em todos os comandos e scripts (/usr/bin/rsync não rsync)
  • Sinais % escapados com \% ou script wrapper
  • MAILTO configurado ou stderr redireccionado para ficheiro de log
  • Script testado com env -i SHELL=/bin/sh PATH=/usr/bin:/bin script.sh (simula ambiente cron)
  • Permissões do script: chmod +x e owner correcto
  • Tarefas de sistema em /etc/cron.d/ (não em crontab -e de root)
  • Para máquinas que desligam: anacron ou Persistent=true no systemd timer
  • Se usar systemd timer: systemctl daemon-reload após editar units
  • Verificar com systemctl list-timers ou crontab -l que a tarefa está realmente agendada
  • Timezone confirmada com timedatectl — o cron usa a TZ do sistema

Artigos Relacionados