· Admin

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 scheduler ile 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-dev production'da dev bağımlılıklarını kurma
  • --optimize-autoloader class map'i cache'ler, performans artar
  • rm -rf /var/lib/apt/lists/* imaj boyutunu küçültür
  • EXPOSE 9000 PHP-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 gereksiz
  • timezone() — UTC yerine kendi saat diliminizi belirtin
  • emailOutputOnFailure() — 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.