Docker'da Laravel Queue: Job Oluşturma, Worker Yönetimi ve Hata Kontrolü
Önceki yazıda Docker'da scheduler kurmuştuk. Bu yazıda queue sistemini ele alacağız — ağır işleri arka plana atıp kullanıcıyı bekletmemek için.
PDF oluşturma, mail gönderme, resim işleme, API çağrısı — bunları request içinde yapmak kullanıcıyı saniyeler boyunca bekletir. Queue ile bu işleri arka planda çalışan worker'lara devredirsiniz. Kullanıcı "işleminiz alındı" mesajını görür, iş arka planda tamamlanır.
Queue Nedir, Neden Lazım?
Şöyle düşünün: bir restoranda garson siparişi alır, mutfağa iletir ve hemen diğer masaya geçer. Yemeğin pişmesini beklemez. Queue da aynı mantık — request gelir, iş kuyruğa atılır, response hemen döner. Worker (aşçı) kuyruktaki işleri sırayla halleder.
Tipik queue kullanım alanları:
- Mail gönderimi — SMTP bağlantısı 2-5 saniye sürebilir
- PDF/Excel oluşturma — CPU yoğun işlem
- Resim işleme — Thumbnail oluşturma, boyutlandırma
- Dış API çağrıları — Ödeme, kargo, bildirim servisleri
- Toplu veri işleme — Import, export, rapor oluşturma
Job Oluşturma
php artisan make:job ProcessOrderJob
namespace App\Jobs;
use App\Mail\OrderConfirmation;
use App\Models\Order;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;
class ProcessOrderJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $backoff = 30;
public int $timeout = 120;
public function __construct(
private Order $order
) {}
public function handle(): void
{
// Fatura PDF oluştur
$pdf = $this->order->generateInvoicePdf();
// Müşteriye onay maili at
Mail::to($this->order->user)
->send(new OrderConfirmation($this->order, $pdf));
// Siparişi "işlendi" olarak güncelle
$this->order->update(['processed_at' => now()]);
}
public function failed(\Throwable $exception): void
{
// Job başarısız olduğunda çalışır
logger()->error('Sipariş işlenemedi', [
'order_id' => $this->order->id,
'error' => $exception->getMessage(),
]);
// Admin'e bildirim at
$this->order->user->notify(new OrderProcessingFailed($this->order));
}
}
Property'leri açıklayalım:
$tries = 3— Başarısız olursa 3 kez dener$backoff = 30— Her deneme arasında 30 saniye bekler$timeout = 120— 2 dakikadan uzun sürerse timeout olurfailed()metodu — Tüm denemeler başarısız olursa çalışır
Job'ı Dispatch Etmek
// Controller'dan
public function store(OrderRequest $request)
{
$order = Order::create($request->validated());
ProcessOrderJob::dispatch($order);
return redirect()->route('orders.show', $order)
->with('success', 'Siparişiniz alındı, işleniyor.');
}
// Gecikmeli dispatch — 5 dakika sonra çalışsın
ProcessOrderJob::dispatch($order)->delay(now()->addMinutes(5));
// Belirli bir queue'ya gönder
ProcessOrderJob::dispatch($order)->onQueue('orders');
// Zincir halinde — sırayla çalışsın
Bus::chain([
new ProcessOrderJob($order),
new SendInvoiceJob($order),
new NotifyWarehouseJob($order),
])->dispatch();
Bus::chain() çok kullanışlı — ilk job başarılı olursa ikinci çalışır, ikinci başarılı olursa üçüncü. Birisi başarısız olursa zincir durur.
Queue Driver Seçimi
.env'de queue driver'ı ayarlarsınız:
QUEUE_CONNECTION=redis
| Driver | Ne Zaman? |
|---|---|
sync |
Development — job anında çalışır, debug kolay |
database |
Küçük projeler — Redis kurmak istemiyorsanız |
redis |
Production — hızlı, güvenilir, önerilen |
sqs |
AWS kullanan büyük projeler |
database driver için migration gerekiyor:
php artisan queue:table
php artisan migrate
Redis tercih ediyorum çünkü database'e ek yük bindirmiyor ve çok daha hızlı.
Docker Yapılandırması
docker-compose.yml
services:
app:
build: .
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
queue:
build: .
volumes:
- .:/var/www/html
command: php artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
restart: unless-stopped
depends_on:
- db
- redis
queue-orders:
build: .
volumes:
- .:/var/www/html
command: php artisan queue:work redis --queue=orders --sleep=3 --tries=3
restart: unless-stopped
depends_on:
- db
- redis
scheduler:
build: .
volumes:
- .:/var/www/html
command: >
sh -c "while true; do
php artisan schedule:run --verbose --no-interaction;
sleep 60;
done"
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
redis:
image: redis:alpine
volumes:
dbdata:
İki ayrı queue worker var — biri default queue'yu, biri orders queue'sunu dinliyor. Siparişlerin ayrı worker'da çalışması demek: mail gönderimi yoğunluğu sipariş işlemeyi yavaşlatmaz.
Worker Argümanları
php artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
--sleep=3— Queue boşken 3 saniye bekle (CPU israfını önler)--tries=3— Her job'ı 3 kez dene--max-time=3600— 1 saat sonra worker'ı yeniden başlat (memory leak önlemi)restart: unless-stopped— Worker çökerse Docker otomatik yeniden başlatır
--max-time yerine --max-jobs=1000 da kullanabilirsiniz — 1000 job işledikten sonra yeniden başla. Amaç aynı: uzun süre çalışan PHP process'in bellek sızıntısını önlemek.
Failed Jobs
Her şey her zaman çalışmaz. SMTP sunucusu erişilemez, API timeout olur, disk dolar. Failed job'ları yönetmek şart.
Migration
php artisan make:queue-failed-table
php artisan migrate
Kontrol Komutları
# Başarısız job'ları listele
php artisan queue:failed
# Belirli bir job'ı tekrar dene
php artisan queue:retry 5
# Tüm başarısız job'ları tekrar dene
php artisan queue:retry all
# Belirli bir job'ı sil
php artisan queue:forget 5
# Tüm başarısız job'ları temizle
php artisan queue:flush
Otomatik Temizlik (Scheduler ile)
// routes/console.php
Schedule::command('queue:prune-failed --hours=168')->daily(); // 7 günden eski failed job'ları sil
Job Batching — Toplu İşlemler
Laravel 8'den beri gelen batch özelliği, birden fazla job'ı grup halinde çalıştırıp topluca izlemenizi sağlar:
php artisan make:queue-batches-table
php artisan migrate
use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;
$users = User::where('newsletter', true)->get();
$jobs = $users->map(fn ($user) => new SendNewsletterJob($user));
$batch = Bus::batch($jobs->toArray())
->then(function (Batch $batch) {
logger()->info("Newsletter gönderildi: {$batch->totalJobs} kullanıcı");
})
->catch(function (Batch $batch, \Throwable $e) {
logger()->error("Newsletter hatası: {$e->getMessage()}");
})
->finally(function (Batch $batch) {
// Başarılı veya başarısız, her durumda çalışır
})
->allowFailures()
->dispatch();
// İlerlemeyi kontrol et
$batch = Bus::findBatch($batchId);
echo $batch->progress(); // 0-100
10.000 kullanıcıya newsletter göndermek istiyorsanız, her biri için ayrı job oluşturup batch'e atarsınız. Batch'in ilerleme durumunu frontend'de gösterebilirsiniz.
Horizon — Queue Monitoring
Production'da queue'larınızı izlemek için Laravel Horizon vazgeçilmez:
composer require laravel/horizon
php artisan horizon:install
Docker'da Horizon çalıştırmak:
horizon:
build: .
volumes:
- .:/var/www/html
command: php artisan horizon
restart: unless-stopped
depends_on:
- db
- redis
Horizon, queue:work yerine geçer — worker'ları kendisi yönetir. Dashboard'dan job'ları, failed job'ları, throughput'u, wait time'ı görebilirsiniz. /horizon adresinden erişilir.
Horizon kullanıyorsanız queue ve queue-orders container'larını kaldırın, tek horizon container'ı yeterli.
Development'ta Queue
Local development'ta iki seçenek:
1. Sync driver — Job anında çalışır, queue'ya gitmez:
QUEUE_CONNECTION=sync
Debug için en kolay yol. Ama gerçek queue davranışını test edemezsiniz.
2. docker-compose ile — Gerçek queue environment'ı:
docker-compose up -d
Production'a en yakın test ortamı. Önerdiğim yaklaşım bu.
Test Yazma
use Illuminate\Support\Facades\Queue;
public function test_order_dispatches_processing_job(): void
{
Queue::fake();
$order = Order::factory()->create();
$this->postJson('/api/orders', $order->toArray());
Queue::assertPushed(ProcessOrderJob::class, function ($job) use ($order) {
return $job->order->id === $order->id;
});
}
public function test_order_job_sends_confirmation_email(): void
{
Mail::fake();
$order = Order::factory()->create();
(new ProcessOrderJob($order))->handle();
Mail::assertSent(OrderConfirmation::class, function ($mail) use ($order) {
return $mail->hasTo($order->user->email);
});
}
İlk test job'ın dispatch edildiğini kontrol eder (Queue::fake ile gerçek çalıştırmadan). İkinci test job'ın handle metodunu direkt çağırıp mail gönderildiğini kontrol eder.
Checklist
Production'a çıkmadan önce:
-
QUEUE_CONNECTION=redis(sync değil!) - Failed jobs tablosu var mı?
- Worker container'da
restart: unless-stoppedvar mı? -
--max-timeveya--max-jobsile memory leak önlemi var mı? -
failed()metodu job'larda tanımlı mı? - Failed job temizliği scheduler'da var mı?
- Horizon veya başka bir monitoring var mı?
- Job timeout'ları makul mü? (default 60 saniye)
Queue sistemi doğru kurulduğunda uygulamanız çok daha hızlı ve dayanıklı olur. Kullanıcı beklemez, işler sırayla ve güvenle tamamlanır, hata olursa tekrar denenir. Docker ile bu yapıyı kurmak da sadece bir command satırı — geri kalanını Laravel hallediyor.