Docker ile Laravel Scheduler: Cron Job'ları Container'da Çalıştırmak
Laravel'in scheduler'ı güzel bir şey — routes/console.php'de (veya eski sürümlerde Kernel.php'de) cron job'larınızı tanımlıyorsunuz, sunucuya tek bir crontab satırı ekliyorsunuz, bitti. Ama Docker kullanıyorsanız "sunucuya crontab ekle" kısmı biraz değişiyor. Container'ın kendi cron'u yok çünkü.
Bu yazıda Laravel scheduler'ını Docker ortamında çalıştırmanın birkaç yolunu ve production'da kullandığım yapıyı anlatacağım.
Laravel Scheduler Nasıl Çalışıyor?
Önce temeli hatırlayalım. Laravel'de zamanlanmış görevleri şöyle tanımlarsınız:
// routes/console.php (Laravel 11+)
use Illuminate\Support\Facades\Schedule;
Schedule::command('telescope:prune')->daily();
Schedule::command('backup:run')->dailyAt('03:00');
Schedule::command('queue:restart')->hourly();
Schedule::call(fn () => cache()->flush())->weekly();
Bunların çalışması için sunucuda tek bir crontab satırı yeterli:
* * * * * cd /var/www/html && php artisan schedule:run >> /dev/null 2>&1
Her dakika schedule:run çalışır, Laravel tanımlı görevlerden hangisinin zamanı geldiyse onu tetikler. Basit ve zarif.
Ama Docker'da bu crontab'ı nereye koyacaksınız? PHP-FPM container'ı web istekleri için ayağa kalkar, cron servisi çalıştırmaz. İşte burada birkaç yaklaşım var.
Yaklaşım 1: Ayrı Scheduler Container (Önerdiğim)
En temiz yol: scheduler için ayrı bir container çalıştırmak. Aynı Laravel imajını kullanır ama web isteği almak yerine schedule:run döngüsü çalıştırır.
docker-compose.yml
services:
app:
build:
context: .
dockerfile: Dockerfile
volumes:
- .:/var/www/html
depends_on:
- db
- redis
nginx:
image: nginx:alpine
ports:
- "8080:80"
volumes:
- .:/var/www/html
- ./.docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
depends_on:
- app
scheduler:
build:
context: .
dockerfile: Dockerfile
volumes:
- .:/var/www/html
command: >
sh -c "while true; do
php artisan schedule:run --verbose --no-interaction;
sleep 60;
done"
depends_on:
- db
- redis
queue:
build:
context: .
dockerfile: Dockerfile
volumes:
- .:/var/www/html
command: php artisan queue:work redis --sleep=3 --tries=3
depends_on:
- db
- redis
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: laravel
MYSQL_USER: laravel
MYSQL_PASSWORD: secret
volumes:
- dbdata:/var/lib/mysql
ports:
- "3307:3306"
redis:
image: redis:alpine
ports:
- "6379:6379"
volumes:
dbdata:
Scheduler container'ın command kısmı sihir burada:
while true; do
php artisan schedule:run --verbose --no-interaction;
sleep 60;
done
Her 60 saniyede schedule:run çalışır — cron ile aynı işi yapar. --verbose flag'i hangi görevin çalıştığını loglar.
Bu yaklaşımın avantajları:
- Web container'dan tamamen bağımsız — scheduler çökse bile site çalışır
docker-compose logs schedulerile scheduler loglarını ayrı görebilirsiniz- Scale etmenize gerek yok, tek instance yeterli (zaten her dakika kontrol ediyor)
Yaklaşım 2: Container İçi Cron (Alternatif)
Bazı projeler container içine gerçek cron servisini kurar. Daha "geleneksel" ama Dockerfile'ı karmaşıklaştırır:
FROM php:8.3-fpm
# ... PHP extension kurulumları ...
# Cron kurulumu
RUN apt-get update && apt-get install -y cron
# Crontab dosyasını kopyala
COPY .docker/crontab /etc/cron.d/laravel-scheduler
RUN chmod 0644 /etc/cron.d/laravel-scheduler
RUN crontab /etc/cron.d/laravel-scheduler
# Entrypoint: hem PHP-FPM hem cron çalıştır
CMD ["sh", "-c", "cron && php-fpm"]
.docker/crontab dosyası:
* * * * * www-data cd /var/www/html && php artisan schedule:run >> /var/log/cron.log 2>&1
Bu yaklaşımı önermiyorum çünkü:
- Tek container'da iki servis çalışıyor (PHP-FPM + cron) — single responsibility ihlali
- Cron çökerse fark etmezsiniz (PHP-FPM hâlâ çalışır, container "healthy" görünür)
- Log yönetimi zorlaşır
Dockerfile
Her iki yaklaşım için de temel Dockerfile aynı:
FROM php:8.3-fpm
WORKDIR /var/www/html
# Sistem bağımlılıkları
RUN apt-get update && apt-get install -y \
libzip-dev \
libpng-dev \
libonig-dev \
unzip \
&& docker-php-ext-install \
zip \
pdo_mysql \
mbstring \
gd \
bcmath \
&& rm -rf /var/lib/apt/lists/*
# Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Uygulama dosyaları
COPY . /var/www/html
# Bağımlılıkları kur
RUN composer install --no-dev --optimize-autoloader --no-interaction
# İzinler
RUN chown -R www-data:www-data storage bootstrap/cache
RUN chmod -R 775 storage bootstrap/cache
EXPOSE 9000
CMD ["php-fpm"]
Birkaç not:
--no-devproduction'da dev bağımlılıklarını kurma--optimize-autoloaderclass map'i cache'ler, performans artarrm -rf /var/lib/apt/lists/*imaj boyutunu küçültürEXPOSE 9000PHP-FPM'in dinlediği port (Nginx buraya proxy yapar)
Nginx Yapılandırması
.docker/nginx/default.conf:
server {
listen 80;
index index.php;
root /var/www/html/public;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass app:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
}
location ~ /\.ht {
deny all;
}
}
fastcgi_pass app:9000 — Docker network'ünde service ismi ile çözümleniyor. app service'inin 9000 portuna proxy yapıyor.
Örnek Scheduler Görevleri
// routes/console.php
use Illuminate\Support\Facades\Schedule;
// Her gece 3'te veritabanı yedeği
Schedule::command('backup:run')
->dailyAt('03:00')
->onOneServer()
->emailOutputOnFailure('[email protected]');
// Her saat başı Telescope loglarını temizle
Schedule::command('telescope:prune --hours=48')
->hourly();
// Her 5 dakikada queue health check
Schedule::command('queue:monitor redis:default --max=100')
->everyFiveMinutes();
// Her Pazartesi haftalık rapor
Schedule::command('reports:weekly')
->weeklyOn(1, '09:00')
->timezone('Europe/Istanbul');
// Her dakika cache istatistikleri
Schedule::call(function () {
logger()->info('Cache hit ratio', [
'ratio' => cache()->get('hit_count', 0) / max(cache()->get('total_count', 1), 1),
]);
})->everyMinute()
->environments(['production']);
Dikkat edilecekler:
onOneServer()— Birden fazla server/container varsa görev sadece birinde çalışır (Redis veya database lock kullanır)environments(['production'])— Sadece production'da çalışsın, development'ta gereksiztimezone()— UTC yerine kendi saat diliminizi belirtinemailOutputOnFailure()— Başarısız görevleri mail ile bildirin
Production: Supervisor ile Queue Worker
Scheduler ile birlikte genellikle queue worker da çalıştırırsınız. docker-compose'da ayrı bir queue service tanımlayabilirsiniz (yukarıdaki örnekte var) veya Supervisor kullanabilirsiniz:
; .docker/supervisord.conf
[supervisord]
nodaemon=true
[program:php-fpm]
command=php-fpm
autostart=true
autorestart=true
[program:queue-worker]
command=php /var/www/html/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
numprocs=2
process_name=%(program_name)s_%(process_num)02d
Ama tekrar söylüyorum: ayrı container yaklaşımı daha temiz. Her container tek iş yapsın.
Health Check
Scheduler'ın çalıştığından emin olmak için basit bir health check:
// Scheduler her çalıştığında cache'e timestamp yaz
Schedule::call(function () {
cache()->put('scheduler:last_run', now());
})->everyMinute();
Monitoring tarafında bu cache key'ini kontrol edin — 2 dakikadan eskiyse scheduler çalışmıyor demektir.
Özet: Hangi Yaklaşımı Kullanmalı?
| Yaklaşım | Ne Zaman? |
|---|---|
| Ayrı scheduler container | Çoğu proje (önerilen) |
| Container içi cron | Legacy projeler, tek container zorunluluğu |
schedule:work komutu |
Sadece local development |
Local development'ta php artisan schedule:work komutu var — cron kurmadan scheduler'ı çalıştırır. Ama production'da bunu kullanmayın, memory leak riski var.
Docker ile Laravel scheduler kurmak zor değil — while true; sleep 60 döngüsü kadar basit. Önemli olan doğru mimariyi seçmek: her container tek sorumluluk, loglar ayrı, health check mevcut. Böylece gece 3'te backup job'ı çalışmadığında sabah ilk iş fark edersiniz, müşteri aramadan önce.