Zamanlanmış iş ne demek, sözdizimi nasıl okunur, hangi tuzaklar var, sistem geneli ve kullanıcı cron'u nasıl farklı, hata ayıklamak için nereye bakılır — komut-komut, alan-alan.
cron, arka planda sürekli çalışan ve belirli zamanlarda belirli komutları çalıştıran bir daemon'dur. Her dakikanın başında crontab dosyalarını okur, zamanı gelen işleri tetikler.
cron (Debian/Ubuntu) veya crond (RHEL/CentOS/Fedora) adında bir servis sürekli çalışır. Saniyelik çözünürlük yoktur — en küçük birim bir dakikadır. Çalışma mantığı şudur: her dakikanın başında tüm crontab dosyalarını tarar, o dakikada eşleşen satırları alt-süreç olarak çalıştırır.
systemctl status cron
systemctl status crond
Beklenen: Active: active (running). Değilse:
# Başlat ve boot'ta otomatik başlasın
sudo systemctl start cron
sudo systemctl enable cron
cron her işi tam dakika başında tetikler. Eğer bir iş 30 2 * * * için tanımlıysa, sistemde saat 02:30:00 olduğu anda başlar. Saniyeye ihtiyacın varsa cron yetmez — systemd timer veya uygulama-içi scheduler gerekir.
Kullanıcı cron tablolarını crontab komutu yönetir. Dosyayı doğrudan düzenlemezsin — komuta düzenleme modu açtırırsın, syntax hatası varsa komut uyarır.
-r ve -e klavyede yan yana. Yanlış tuşa basıp tüm crontab'ını bir anda silebilirsin — üstelik hiç onay sormaz. Her zaman crontab -i -r alışkanlığı edin. Ya da önce crontab -l > ~/crontab.bak ile yedek al.
İlk kez crontab -e çağırdığında hangi editor'ün açılacağını sorar. Sonra hatırlar. Değiştirmek istersen:
# Geçici (sadece bu çağrı için)
EDITOR=nano crontab -e
# Kalıcı (bir sonraki shell oturumundan itibaren)
echo 'export EDITOR=nano' >> ~/.bashrc
# Ya da sistem geneli
sudo update-alternatives --config editor
Kullanıcı crontab'ları aşağıdaki dizinde saklanır:
sudo ls -la /var/spool/cron/crontabs/
Her kullanıcı için bir dosya: /var/spool/cron/crontabs/ali, /var/spool/cron/crontabs/root gibi. Bu dosyalara elle dokunmak önerilmez — izinleri ve sahipliği bozulabilir. crontab -e bu işi güvenli şekilde yapar.
RHEL ailesinde dizin /var/spool/cron/ (crontabs alt dizini yok). Path bağımlı işler yazacaksan sisteminin hangisi olduğunu önce doğrula.
Bir cron satırı 5 zaman alanı + komutdan oluşur. Her alan bir zaman birimini temsil eder. Alanlar boşluk veya tab ile ayrılır.
┌─────────── dakika 0 – 59 │ ┌───────── saat 0 – 23 │ │ ┌─────── ayın günü 1 – 31 │ │ │ ┌───── ay 1 – 12 (jan, feb, mar...) │ │ │ │ ┌─── haftanın günü 0 – 7 (0 ve 7 = Pazar; sun, mon...) │ │ │ │ │ * * * * * /path/to/komut arg1 arg2
| Karakter | Anlamı | Örnek |
|---|---|---|
| * | Bu alanın tüm değerleri | * * * * * = her dakika |
| , | Liste ayırıcı | 0,15,30,45 = 0., 15., 30., 45. dakika |
| - | Aralık | 9-17 = 9'dan 17'ye kadar (dahil) |
| / | Adım aralığı | */5 = her 5'te bir, 0-30/10 = 0,10,20,30 |
Bu dört karakter birleştirilebilir: 0,30 9-17 * * 1-5 = Pazartesi-Cuma, saat 09:00-17:00 arası, her saatin 0. ve 30. dakikasında.
| İfade | Ne zaman çalışır |
|---|---|
| * * * * * | Her dakika |
| */5 * * * * | Her 5 dakikada bir (00, 05, 10, 15...) |
| 0 * * * * | Her saatin tam başında (01:00, 02:00...) |
| 0 */2 * * * | Her 2 saatte bir, tam saatte (00:00, 02:00, 04:00...) |
| 30 2 * * * | Her gece 02:30 |
| 0 9 * * 1 | Her Pazartesi sabah 09:00 |
| 0 9 * * 1-5 | Her hafta içi (Pzt-Cum) sabah 09:00 |
| 0 0 1 * * | Her ayın 1'inde gece yarısı |
| 0 0 1,15 * * | Her ayın 1'i ve 15'inde gece yarısı |
| 15 14 1 * * | Her ayın 1'inde saat 14:15 |
| 0 22 * * 1-5 | Pazartesi-Cuma, her akşam 22:00 |
| 23 0-23/2 * * * | Her çift saatin 23. dakikasında (00:23, 02:23, 04:23...) |
| 5 4 * * sun | Her Pazar 04:05 |
| 0 0 1 1 * | Yılda bir kez, 1 Ocak gece yarısı |
| 0 0 * * 6,0 | Hafta sonu (Cumartesi ve Pazar) gece yarısı |
| 0-59/10 * * * * | Her 10 dakikada bir (0, 10, 20, 30, 40, 50) |
Eğer hem ayın günü (3. alan) hem haftanın günü (5. alan) * değilse, cron bunları VEYA ile birleştirir, VE ile değil.
Örnek: 0 12 1 * 5 = "her ayın 1'i saat 12:00" VEYA "her Cuma saat 12:00". "Ayın ilk Cuma'sı saat 12:00" değil. Bu çok sık yapılan bir hatadır.
Ay ve haftanın günü alanlarında isim de kabul edilir (ilk 3 harf, İngilizce, büyük/küçük harf fark etmez):
# Her Cuma saat 17:00'de
0 17 * * fri /path/to/hafta-sonu-baslasin.sh
# Her Aralık ayının 25'inde
0 9 25 dec * /path/to/noel-maili.sh
Ancak isimleri aralıkta veya listede kullanırken dikkatli ol: mon-fri çalışır, ama mon,wed,fri bazı eski cron'larda çalışmaz. Sayısal form (1-5, 1,3,5) her yerde çalışır.
Sık kullanılan zaman kalıpları için kısayollar vardır. 5 alan yerine @ ile başlayan bir dize yazarsın.
| Dize | Eşdeğeri | Ne zaman çalışır |
|---|---|---|
| @reboot | — | Sistem başladığında (cron servisi ayağa kalktığında) bir kez |
| @yearly | 0 0 1 1 * | Yılda bir, 1 Ocak gece yarısı |
| @annually | 0 0 1 1 * | @yearly'nin eş anlamlısı |
| @monthly | 0 0 1 * * | Her ayın ilk günü gece yarısı |
| @weekly | 0 0 * * 0 | Her Pazar gece yarısı |
| @daily | 0 0 * * * | Her gün gece yarısı |
| @midnight | 0 0 * * * | @daily'nin eş anlamlısı |
| @hourly | 0 * * * * | Her saatin başında |
# Sistem her başladığında bir tünel aç
@reboot /home/ali/scripts/start-tunnel.sh
# Her gün gece yarısı log döndürme
@daily /usr/local/bin/logrotate-custom.sh
# Her saat başı disk kullanımını kontrol et
@hourly /home/ali/scripts/disk-check.sh
@reboot cron daemon'un başladığı anda tetiklenir — ki bu genellikle sistem boot'unun geç evrelerindedir. Ağ, disk, veritabanı servisleri henüz hazır olmayabilir. Gerçek boot-time init için systemd unit daha güvenilir. @reboot'u yalnızca basit, bağımlılıksız scriptler için kullan.
cron'un çalıştırdığı komutlar, senin shell oturumunla aynı ortama sahip değildir. PATH minimaldir, .bashrc yüklenmez, HOME kısıtlıdır. Burayı bilmezsen saatlerce "terminalde çalışıyor, cron'da çalışmıyor" ile uğraşırsın.
| Değişken | Varsayılan değer |
|---|---|
| SHELL | /bin/sh |
| PATH | /usr/bin:/bin |
| HOME | Kullanıcının home dizini |
| LOGNAME | Kullanıcı adı |
| MAILTO | Kullanıcı adı (çıktılar buraya mail edilir) |
PATH=/usr/bin:/bin — yani /usr/local/bin yok, /opt/... yok, ~/bin yok. Terminalde çalıştırdığın bir komut cron'da "command not found" verebilir. Bu sorunun iki çözümü var:
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=ali@example.com
# Artık PATH'teki her şey erişilebilir
*/5 * * * * python3 /home/ali/scripts/check.py
*/5 * * * * /usr/bin/python3 /home/ali/scripts/check.py
Komutun mutlak yolunu bulmak için: which python3 → /usr/bin/python3. Bu yaklaşım daha güvenlidir çünkü PATH'e güvenmezsin.
Her iki çözümü birden kullan: crontab'ın başında PATH tanımla ve script'lerin içinde mutlak yollar kullan. İkisi birden olursa bir yerde biri unutulmuş olsa bile diğeri kurtarır.
Cron shell'i interactive değil, login değil. Yani .bashrc, .bash_profile, .profile okunmaz. Senin tanımladığın alias'lar, fonksiyonlar, nvm, pyenv, rvm gibi version manager'lar hiçbiri çalışmaz. Bu yüzden cron script'leri kendi ortamlarını kurmalı:
#!/bin/bash
# Script başında ortamı açıkça yükle
export PATH=/usr/local/bin:/usr/bin:/bin
source /home/ali/.nvm/nvm.sh
nvm use 18
# Artık node, npm, vb. kullanılabilir
node /home/ali/app/backup.js
Cron bir komut çalıştırıp çıktı (stdout veya stderr) üretirse, bu çıktıyı varsayılan olarak kullanıcıya mail eder. MTA kurulu değilse çıktı kaybolur. İşlerin sessizce başarısız olduğunu fark etmek için bu davranışı bilinçli yönet.
*/5 * * * * /home/ali/scripts/check.sh
Eğer check.sh ekrana bir şey yazarsa (örneğin echo, ya da bir hata mesajı), cron bu çıktıyı alır ve /var/mail/ali içine düşürmeye çalışır. Çoğu modern sunucuda MTA (postfix, sendmail) kurulu değildir — çıktı kaybolur, syslog'a "(CRON) info (No MTA installed)" satırı düşer.
MAILTO=ali@example.com
*/5 * * * * /home/ali/scripts/check.sh
Üstte tanımlı MAILTO altındaki tüm işleri kapsar. Belirli bir işi kapsamdan çıkarmak için farklı bir MAILTO bloğu koyabilirsin.
MAILTO=""
# Bu işlerin çıktıları hiçbir yere gitmez — ama kaybolur
*/5 * * * * /home/ali/scripts/gurultulu-is.sh
En esnek yol, her komutun çıktısını doğrudan bir dosyaya yönlendirmek:
# stdout'u dosyaya, stderr'i mail'e (ayrıştır)
*/5 * * * * /home/ali/scripts/check.sh > /var/log/check.log
# stdout + stderr aynı dosyaya (append)
*/5 * * * * /home/ali/scripts/check.sh >> /var/log/check.log 2>&1
# Tamamen sustur — hem stdout hem stderr /dev/null'a
*/5 * * * * /home/ali/scripts/check.sh > /dev/null 2>&1
# Sadece stderr'i kaydet (hataları gör)
*/5 * * * * /home/ali/scripts/check.sh > /dev/null 2>> /var/log/check-err.log
> /dev/null 2>&1 her şeyi susturur. Bu işin aslında başarısız olduğunu bilemezsin — mail yok, log yok, hiçbir iz yok. Kritik işlerde en azından stderr'i bir dosyaya yönlendir ki bir şey patladığında fark edebilesin.
Log dosyasına yazılan her satıra otomatik zaman damgası eklemek için:
*/5 * * * * /home/ali/scripts/check.sh 2>&1 | ts >> /var/log/check.log
ts komutu moreutils paketinde gelir (apt install moreutils). Her satırın başına Apr 10 09:30:00 gibi bir zaman ekler.
Cron'da en sık karşılaşılan "neden çalışmıyor" sebeplerinin hepsi burada.
chmod +x /home/ali/scripts/backup.sh
İzin yoksa cron Permission denied verir. ls -l ile kontrol et — çıktı -rwxr-xr-x gibi "x" harfleri içermeli.
Script'in ilk satırı hangi yorumlayıcıyla çalıştırılacağını söylemeli:
#!/bin/bash
# Ya da python için:
#!/usr/bin/env python3
# ... script gövdesi
Shebang olmadan cron (varsayılan olarak /bin/sh kullanarak) script'i çalıştırır — bash'e özgü syntax ([[ ]], arrays) hata verir.
Script içinde göreli yol kullanma. Cron script'i kullanıcının home dizininden başlatabilir ama script'in kendi konumundan değil:
#!/bin/bash
cp ./data/* ./backup/ # HANGİ dizine göre?
#!/bin/bash
set -euo pipefail
# Script'in kendi dizinini bul (çok işe yarar)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cp "$SCRIPT_DIR/data/"* "$SCRIPT_DIR/backup/"
Script'lerin en üstüne koy. -e herhangi bir komut hata verirse script'i durdurur, -u tanımlanmamış değişkeni hata sayar, -o pipefail pipe'ın ortasındaki hatayı yakalar. Cron'da sessizce yarıda kalan script'lerin %90'ı bu satırla kurtulur.
Cron satırında % özel bir karakterdir — yeni satır anlamına gelir ve komutu keser. Eğer komutta % geçmesi gerekiyorsa (örneğin date format stringi) kaçış yap:
# Cron bunu "echo $(date +"%Y" (sonrası yeni satır)" olarak okur — hata
0 * * * * echo "$(date +"%Y-%m-%d")" >> /tmp/log.txt
0 * * * * echo "$(date +"\%Y-\%m-\%d")" >> /tmp/log.txt
Ya da daha temiz: komutu bir script'e koy, crontab'dan sadece script'i çağır. Bu sorun ortadan kalkar.
Kullanıcı crontab'larının yanında, root/paket sistemleri için farklı yerlerde yaşayan sistem geneli cron dosyaları da vardır. Formatları arasında tek küçük ama kritik bir fark var: kullanıcı adı alanı.
| Yol | Amacı | Kullanıcı alanı var mı |
|---|---|---|
| /etc/crontab | Sistem geneli ana crontab | EVET |
| /etc/cron.d/ | Paketlerin bıraktığı cron parçaları | EVET |
| /etc/cron.hourly/ | Her saat run-parts ile çalıştırılır | Hayır (script dizini) |
| /etc/cron.daily/ | Her gün run-parts ile çalıştırılır | Hayır |
| /etc/cron.weekly/ | Her hafta run-parts ile çalıştırılır | Hayır |
| /etc/cron.monthly/ | Her ay run-parts ile çalıştırılır | Hayır |
| /var/spool/cron/crontabs/ | Kullanıcı crontab'ları (crontab -e tarafından yönetilir) | Hayır |
Kullanıcı crontab'ından farklı olarak, /etc/crontab ve /etc/cron.d/ altındaki dosyalarda zaman alanları ile komut arasında bir kullanıcı adı alanı vardır:
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
# dak saat gün ay dow KULLANICI komut
17 * * * * root cd / && run-parts --report /etc/cron.hourly
25 6 * * * root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )
47 6 * * 7 root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly )
52 6 1 * * root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly )
6. alan (root) kullanıcı adı. Komut bu kullanıcı olarak çalıştırılır. Bu mekanizma sayesinde bir admin, crontab -e yerine doğrudan dosyayı düzenleyerek farklı kullanıcıların işlerini tek yerden yönetebilir.
/etc/cron.d/ altına kullanıcı crontab formatı (5 alan) ile dosya koyarsan cron o dosyayı yok sayar — hata vermez, sadece çalıştırmaz. Her zaman 6 alan + kullanıcı adı kullan.
Bir script'in her gün çalışmasını istiyorsan, en kolay yol onu /etc/cron.daily/ içine koymaktır. Cron (veya anacron) bu dizindeki her çalıştırılabilir dosyayı sırayla çalıştırır.
sudo cp backup.sh /etc/cron.daily/backup
sudo chmod +x /etc/cron.daily/backup
run-parts bazı karakterleri içeren dosya isimlerini yok sayar: nokta (.sh, .py), tilde, vb. Uzantı kullanma — backup.sh yerine sadece backup. Bu yüzden run-parts dizinlerindeki dosyalarda shebang zorunludur.
Cron saat X:YY olduğunda makine kapalıysa işi atlar — bir sonraki X:YY'e kadar beklemez, sessizce kaybeder. Dizüstü bilgisayarlar, akşam kapatılan masaüstleri için bu sorundur. anacron bu açığı kapatır: son çalıştırma zamanını bir dosyaya yazar, sistem açıldığında "bu iş en son ne zaman çalıştı, aradan bir gün geçti mi?" kontrolü yapar. Modern Ubuntu/Debian'da /etc/cron.daily vb. dizinler anacron tarafından yönetilir (yukarıdaki /etc/crontab'daki test -x /usr/sbin/anacron kontrolü bunu sağlar).
Gerçek dünyada karşına çıkacak senaryolar. Her biri crontab satırı + varsa yardımcı script ile.
MAILTO=admin@example.com
# Her gece 02:30'da yedek
30 2 * * * /home/ali/scripts/db-backup.sh >> /var/log/db-backup.log 2>&1
#!/bin/bash
set -euo pipefail
BACKUP_DIR=/var/backups/db
DATE=$(date +%Y-%m-%d)
FILE="$BACKUP_DIR/mydb-$DATE.sql.gz"
mkdir -p "$BACKUP_DIR"
pg_dump mydb | gzip > "$FILE"
# 30 günden eski yedekleri sil
find "$BACKUP_DIR" -name "mydb-*.sql.gz" -mtime +30 -delete
echo "[$(date -Iseconds)] Yedek tamamlandı: $FILE"
*/5 * * * * /usr/bin/curl -fsS https://example.com/health > /dev/null || echo "[$(date)] DOWN" >> /var/log/uptime.log
curl -f HTTP 4xx/5xx'te hata kodu döner. || ile sadece başarısızlık durumunda log yazılır.
# Her Pazar 03:00'te, 14 günden eski logları sil
0 3 * * 0 find /var/log/myapp -name "*.log" -mtime +14 -delete
@reboot sleep 30 && /home/ali/bin/start-bot.sh
30 saniye beklemek, network ve diğer servislerin ayağa kalkması için zaman kazandırır. Daha güvenilir yol: systemd unit.
Eğer bir iş uzun sürerse ve bir sonraki tetikleme geldiğinde önceki hâlâ devam ediyorsa, iki instance paralel çalışır. Bu genellikle istenmez. flock ile çözülür:
# -n: lock alınamazsa hemen çık, bekleme
*/5 * * * * /usr/bin/flock -n /tmp/myjob.lock /home/ali/scripts/slow-job.sh
İlk çalışma /tmp/myjob.lock'u kilitler. 5 dakika sonra ikinci deneme gelir — kilit hâlâ tutulu — -n sayesinde beklemeden çıkar. Önceki iş bitince kilit açılır.
# Root olarak ali'nin crontab'ını düzenle
sudo crontab -u ali -e
# Ya da /etc/cron.d/ altına dosya koy (6 alan formatı)
sudo tee /etc/cron.d/ali-backup << 'EOF'
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
30 2 * * * ali /home/ali/scripts/backup.sh
EOF
# Şu an 14:32 ise, 14:33'te tetikle
# crontab -e'ye ekle:
33 14 * * * /home/ali/scripts/test.sh >> /tmp/test.log 2>&1
Test bitince crontab'dan sil. Daha güzel bir deneme: bir dakika sonrayı bir scriptle otomatik hesapla, crontab -l ile mevcut crontab'ı dosyaya al, test satırını ekle, geri yükle.
"Terminalde çalışıyor, cron'da çalışmıyor" hikayesinin nasıl söküleceği.
Cron her tetiklediği işi syslog'a yazar. Görmek için:
# Syslog'da CRON satırlarını filtrele
grep CRON /var/log/syslog
# Ya da journalctl ile (daha yeni sistemler)
journalctl -u cron -f
# Sadece son 1 saat
journalctl -u cron --since "1 hour ago"
grep CROND /var/log/cron
journalctl -u crond -f
Beklenen satır şuna benzer:
Apr 10 14:33:01 host CRON[12345]: (ali) CMD (/home/ali/scripts/test.sh)
Bu satırı görüyorsan cron işi tetikledi. Gördüğün halde iş yapılmamışsa sorun cron'da değil — script'in içindedir. Görmüyorsan sorun crontab syntax'ında veya zamanlamadadır.
crontab -l
Yazdığın satır orada mı? Zaman alanı doğru mu? Alan sayısı 5 mi (veya /etc/crontab'daysa 6 mı)?
Cron'un kısıtlı PATH'ini simüle et:
# Temiz ortamda, sadece cron'un PATH'iyle çalıştır
env -i PATH=/usr/bin:/bin HOME=$HOME /bin/sh /home/ali/scripts/test.sh
Terminalde çalışan ama bu komutta çalışmayan bir script, cron'da da çalışmayacaktır. Sorun %90 PATH ile ilgilidir.
#!/bin/bash
exec > /tmp/test.debug.log 2>&1
set -x
echo "=== çalışma zamanı: $(date -Iseconds) ==="
echo "PATH=$PATH"
echo "HOME=$HOME"
echo "PWD=$(pwd)"
echo "USER=$(whoami)"
# ... asıl script ...
exec > dosya 2>&1 script'in kalan tüm çıktısını dosyaya yönlendirir. set -x her çalıştırılan komutu ekrana (artık log'a) basar. Bu üç satırla cron'da script'in gerçekten ne gördüğünü bir dakika sonra öğrenirsin.
| Belirti | Muhtemel sebep |
|---|---|
| Syslog'da CMD satırı yok | Crontab syntax hatalı, zaman ifadesi yanlış, veya servisi start edilmemiş |
| CMD var ama sonuç yok | Script'in çıktısı bir yere mail'leniyor, siz göremiyor. Çıktıyı dosyaya yönlendir. |
| "command not found" | PATH sorunu. Mutlak yol kullan veya crontab başında PATH tanımla. |
| "Permission denied" | chmod +x eksik, veya script'i çalıştıran kullanıcının dosyaya erişimi yok. |
| Bash özelliği hata veriyor | Script shebangsız ya da SHELL=/bin/sh. Shebang ekle ya da crontab'da SHELL=/bin/bash tanımla. |
| Göreli yol bulunamıyor | Cron script'i home dizinden değil, tanımsız bir dizinden çalıştırıyor. Mutlak yol kullan. |
| % karakteri kesiyor | % kaçışlanmamış. Ya \% yap ya da komutu script'e taşı. |
| Ayın 1'i + Cuma beklediğim gibi çalışmıyor | Cron VEYA ile birleştiriyor, VE ile değil. Bölüm 02'ye bak. |
| İş bazen kaçıyor | Makine o sırada kapalıydı. Çözüm: anacron veya systemd timer. |
cron, minimum ortamda (PATH = /usr/bin:/bin, .bashrc yok) senin adına komut çalıştıran dakika çözünürlükli bir zamanlayıcıdır — sessiz başarısızlıklarının çoğu mail gitmeyen çıktı, eksik PATH ve göreli yol üçlüsünden gelir; bu üç tuzağı bilirsen cron saatli bomba olmaktan çıkıp güvenilir bir otomasyon aracı olur.