Bir Laravel projesine yeni katılan geliştiricinin ilk günü genellikle kod yazmakla değil, ortam kurmakla geçer: doğru PHP sürümü, eksik intl eklentisi, farklı MySQL sürümü, Redis'in kurulu olmaması, storage klasörünün izinleri. Üstelik bu kurulum makineden makineye değişir ve "bende çalışıyordu" cümlesi doğar. Docker ile Laravel geliştirme ortamı kurmanın asıl kazancı hız değil, tekrarlanabilirliktir: projeyi klonlayan herkes tek komutla aynı PHP sürümüne, aynı eklenti setine ve aynı veritabanı sürümüne kavuşur.
Bu rehberde Laravel için dört servisli bir geliştirme ortamı kuracağız — uygulama (PHP-FPM), web sunucusu (Nginx), veritabanı (MySQL) ve önbellek/kuyruk (Redis). Kendi PHP imajımızı Laravel'in gerçekten istediği eklentilerle derleyecek, artisan ve composer komutlarını konteyner içinden nasıl çalıştıracağımızı, kuyruk işçisini nasıl ayrı servis yapacağımızı ve storage izin hatalarının kökünü nasıl kazıyacağımızı göreceğiz. Üretime taşırken nelerin değişmesi gerektiğine de ayrı bir bölüm ayırdım.
Laravel'i Docker'a Taşırken Neyi Çözmeye Çalışıyoruz#
Laravel'in çalışması için PHP tek başına yetmez. Framework belirli eklentileri şart koşar (mbstring, openssl, pdo, tokenizer, xml, ctype, json, bcmath, fileinfo), yaygın paketler bunlara intl, gd, zip, redis ekler. Bunları her geliştirici makinesinde elle kurmak yerine bir Dockerfile'a yazarsınız ve bir daha konuşulmaz.
İkinci problem servis çeşitliliğidir. Modern bir Laravel projesi genelde bir veritabanı, bir önbellek katmanı, bir kuyruk sürücüsü ve sıklıkla bir e-posta yakalayıcı ister. Bunları tek tek kurup yapılandırmak yerine compose dosyasında tanımlarsınız. Üçüncü problem ise sürüm sabitleme: üretimde MySQL 8 çalışırken geliştirmede MariaDB kullanmak, ancak canlıya çıkınca fark edilen sorgu farklarına yol açar.
| Sorun | Docker'sız durum | Docker ile |
|---|---|---|
| PHP eklentileri | Her makinede elle kurulur | Dockerfile'da bir kez tanımlanır |
| Veritabanı sürümü | Geliştiricinin makinesine bağlı | Compose'da sabitlenir |
| Redis / kuyruk | Ayrı kurulum gerekir | Tek satır servis |
| Yeni geliştirici | Yarım gün kurulum | docker compose up -d |
| Projeyi silmek | Sistemde artıklar kalır | Konteyner ve hacim silinir |
Nginx ve PHP-FPM ikilisinin nasıl konuştuğunu, SCRIPT_FILENAME yol eşleşmesinin neden kritik olduğunu daha önce Docker'da Nginx ve PHP-FPM yığını yazısında ayrıntılı ele almıştık; buradaki yapı onun Laravel'e uyarlanmış hâlidir.
Proje Yapısı ve PHP İmajı#
Laravel projesinin kök dizininde docker adında bir klasör açıp tüm altyapı dosyalarını orada toplayın; böylece uygulama kodu ile ortam tanımı karışmaz:
cd ~/projeler/laravel-uygulama
mkdir -p docker/{php,nginx}
docker/php/Dockerfile dosyası ortamın belkemiğidir. Burada hem eklentileri kuruyor hem de host kullanıcınızla aynı UID'ye sahip bir kullanıcı oluşturuyoruz — izin sorunlarının kalıcı çözümü budur:
FROM php:8-fpm-alpine
# Çalışma zamanı ve derleme bağımlılıkları
RUN apk add --no-cache libpng libzip icu-libs oniguruma git bash \
&& apk add --no-cache --virtual .build $PHPIZE_DEPS libpng-dev libzip-dev icu-dev oniguruma-dev \
&& docker-php-ext-install -j"$(nproc)" pdo_mysql mbstring bcmath gd zip intl exif pcntl opcache \
&& pecl install redis && docker-php-ext-enable redis \
&& apk del .build
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
# HOST kullanıcınızla aynı UID: dosya sahipliği sorunlarını kökten çözer
ARG UID=1000
ARG GID=1000
RUN addgroup -g ${GID} app && adduser -u ${UID} -G app -s /bin/bash -D app
WORKDIR /var/www/html
USER app
pcntl eklentisi kuyruk işçisi ve artisan sinyalleri için, exif görsel işleme için gereklidir; ikisi de sonradan "neden çalışmıyor" diye aranan tipik eksiklerdir. UID değerini kendi kullanıcınızdan alacağız, bunu birazdan compose dosyasında göreceksiniz. Bu tekniğin genel gerekçelerini Dockerfile en iyi pratikler yazısında topladık.
Nginx tarafı Laravel için standarttır; kök dizin public klasörüdür, geri kalan her şey index.php üzerinden geçer:
server {
listen 80;
server_name localhost;
root /var/www/html/public;
index index.php;
client_max_body_size 64m;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_pass app:9000;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_read_timeout 300;
}
location ~ /\. { deny all; }
}
docker-compose: Dört Servisli Ortam#
Şimdi servisleri birleştirelim. queue servisinin app ile aynı imajı kullandığına ama farklı komut çalıştırdığına dikkat edin; bu, aynı kod tabanını iki farklı rolde koşturmanın standart yoludur:
services:
app:
build:
context: ./docker/php
args:
UID: ${UID:-1000}
GID: ${GID:-1000}
restart: unless-stopped
volumes:
- ./:/var/www/html
depends_on:
- mysql
- redis
web:
image: nginx:alpine
restart: unless-stopped
ports:
- "8080:80"
volumes:
- ./:/var/www/html
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- app
mysql:
image: mysql:8
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: root-parolasi
MYSQL_DATABASE: laravel
MYSQL_USER: laravel
MYSQL_PASSWORD: laravel-parolasi
volumes:
- mysql-data:/var/lib/mysql
ports:
# Yalnızca localhost'a açık: DB istemcinizle bağlanmak için
- "127.0.0.1:3307:3306"
redis:
image: redis:alpine
restart: unless-stopped
queue:
build:
context: ./docker/php
args:
UID: ${UID:-1000}
GID: ${GID:-1000}
restart: unless-stopped
command: php artisan queue:work --tries=3 --timeout=90
volumes:
- ./:/var/www/html
depends_on:
- mysql
- redis
volumes:
mysql-data:
MySQL portunu 127.0.0.1:3307 biçiminde yayımlamak iki işe yarar: masaüstü veritabanı istemcinizle bağlanabilirsiniz ama port yalnızca yerel makineye açık kalır, ağdaki başka kimse göremez. Sunucuda çalışıyorsanız bu satırı tamamen kaldırın.
Laravel'in .env dosyasında ana makine adları konteyner adlarıyla değiştirilmelidir; localhost yazarsanız uygulama kendi konteynerine bağlanmaya çalışır ve bağlantı reddedilir:
APP_URL=http://localhost:8080
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=laravel
DB_PASSWORD=laravel-parolasi
REDIS_HOST=redis
CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis
İlk Çalıştırma: Bağımlılıklar, Anahtar ve Göç#
Ortamı ilk kez ayağa kaldırırken sıra önemlidir. UID değişkenini kabuktan geçirerek imajı kendi kullanıcı numaranızla derliyoruz:
# 1. Kullanıcı numaralarını dışa aktar (bir kez .bashrc'ye de eklenebilir)
export UID=$(id -u) GID=$(id -g)
# 2. İmajları derle ve servisleri başlat
docker compose up -d --build
# 3. Bağımlılıkları konteyner içinde çöz
docker compose exec app composer install
# 4. Ortam dosyası ve uygulama anahtarı
cp .env.example .env
docker compose exec app php artisan key:generate
# 5. Veritabanı hazır olana kadar bekleyip göçleri çalıştır
docker compose exec app php artisan migrate --seed
# 6. Depolama bağlantısı
docker compose exec app php artisan storage:link
Üçüncü adımda composer install komutunu host makinenizde değil konteyner içinde çalıştırmanız önemlidir. Konteynerdeki PHP sürümü ve eklenti seti farklıysa, host'ta çözülen bağımlılıklar konteynerde çalışmayabilir; bu, geliştirme makinesinde sorunsuz görünüp sunucuda patlayan hataların en yaygın kaynağıdır.
Beşinci adımda "connection refused" alırsanız MySQL henüz başlangıç sürecini bitirmemiştir. depends_on yalnızca başlama sırasını belirler, servisin hazır olmasını beklemez. Basit çözüm birkaç saniye bekleyip komutu tekrarlamaktır; kalıcı çözüm ise sağlık kontrolü tanımlamaktır:
mysql:
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
retries: 10
Günlük Geliştirme Akışı#
Konteyner içinde komut çalıştırmak zamanla yorucu olur; kabuk takma adları bu yükü tamamen kaldırır. Şunları ~/.bashrc dosyanıza ekleyin:
alias dc='docker compose'
alias art='docker compose exec app php artisan'
alias comp='docker compose exec app composer'
alias dsh='docker compose exec app bash'
Bundan sonra günlük iş akışınız şuna benzer:
art make:model Siparis -mcr # model, migration, controller, resource
art migrate:fresh --seed # veritabanını sıfırla
art tinker # etkileşimli kabuk
comp require spatie/laravel-permission
art queue:restart # kuyruk işçisine yeni kodu al dedirt
dc logs -f queue # işçinin ne yaptığını izle
Ön yüz varlıkları için Node'u da konteynerde tutabilirsiniz, ancak geliştirme sırasında sıcak yeniden yükleme (HMR) host makinede çalıştırmak genelde daha az sürtünme yaratır. Konteynerde çalıştıracaksanız Vite'ın dışarıdan erişilebilir olması için --host bayrağını vermeniz ve ilgili portu yayımlamanız gerekir. Node uygulamalarını konteynerleştirmenin ayrıntıları için Docker ile Node.js uygulaması yayınlama yazısına bakabilirsiniz.
Kuyruk işçisiyle ilgili kritik bir davranışı unutmayın: işçi kodu bir kez belleğe alır ve çalışmaya devam eder. Kod değiştirdiğinizde işçi eski kodu çalıştırmaya devam eder. art queue:restart komutu işçilere mevcut işi bitirip kapanmalarını söyler, restart: unless-stopped politikası da onları yeniden başlatır. Bu ikili olmadan "değişikliğim neden işlemiyor" sorusuyla saatler geçirirsiniz.
Sık Yapılan Hatalar#
storage klasörüne yazamıyor — The stream or file could not be opened hatası izin sorunudur. Yukarıdaki UID yaklaşımını kullandıysanız bu genellikle hiç oluşmaz; yine de sorun yaşarsanız sahipliği düzeltin:
sudo chown -R $(id -u):$(id -g) storage bootstrap/cache
docker compose exec app php artisan optimize:clear
SQLSTATE[HY000] [2002] Connection refused — .env içinde DB_HOST=localhost kalmıştır. Konteyner ağında veritabanının adı mysql'dir. Değiştirdikten sonra yapılandırma önbelleğini temizlemeyi unutmayın: art config:clear.
Değişiklikler yansımıyor — Laravel yapılandırma, rota ve görünüm önbelleklerini tutar. Geliştirmede art optimize:clear komutu hepsini birden temizler. Ayrıca üretim ayarlarıyla derlenmiş bir imaj kullanıyorsanız OPcache zaman damgası doğrulaması kapalı olabilir; geliştirme için açık olmalıdır.
Windows ve WSL'de aşırı yavaşlık — Proje dosyaları Windows dosya sisteminde durup konteynere bağlanıyorsa her dosya okuma bir çeviri katmanından geçer ve Laravel binlerce dosya okur. Çözüm, projeyi WSL'in kendi dosya sisteminde (/home/kullanici/...) tutmaktır; fark on kata varan hızlanma olabilir.
Kuyruk işi tekrar tekrar deneniyor — İş başarısız oluyor ama sebebini görmüyorsanız failed_jobs tablosuna bakın: art queue:failed. Sonsuz denemeyi önlemek için --tries değerini mutlaka verin; sınırsız deneme, hatalı bir iş yüzünden kuyruğun tıkanmasına yol açar.
Konteyner sürekli yeniden başlıyor — Genellikle queue servisinde .env eksik olduğu için artisan çöker ve restart politikası döngüye sokar. Logdan gerçek hatayı okuyun; bu döngünün genel nedenlerini konteyner sürekli yeniden başlıyor yazısında ele aldık.
Sıkça Sorulan Sorular#
Docker Laravel geliştirmesini yavaşlatır mı#
Linux üzerinde hissedilir bir fark olmaz; dosya sistemi doğrudan paylaşılır. macOS ve Windows'ta bağlı dizinlerin okuma performansı daha düşüktür ve Laravel çok sayıda küçük dosya okuduğu için bu fark sayfa yüklenmesinde görülebilir. WSL kullanıcıları projeyi Linux dosya sisteminde tutarak, macOS kullanıcıları ise dosya senkronizasyon ayarlarını düzenleyerek bunu büyük ölçüde giderebilir.
Laravel Sail ile bu kurulum arasındaki fark nedir#
Sail, Laravel ekibinin hazırladığı hazır bir Docker yapılandırmasıdır ve hızlı başlamak için idealdir. Buradaki yaklaşım ise imajı ve servisleri kendiniz tanımladığınız için tam kontrol sağlar: PHP eklentilerini seçersiniz, gereksiz servisleri hiç açmazsınız ve aynı Dockerfile mantığını üretime taşıyabilirsiniz. Sail ile başlayıp ihtiyaç büyüdükçe kendi yapılandırmanıza geçmek de makul bir yoldur.
Bu ortamı doğrudan üretimde kullanabilir miyim#
Doğrudan değil. Geliştirme ortamında kod bir hacimle bağlanır; üretimde kod imajın içine kopyalanmalıdır ki dağıtım değişmez (immutable) olsun. Ayrıca üretimde APP_DEBUG=false olmalı, art config:cache ve art route:cache çalıştırılmalı, OPcache zaman damgası doğrulaması kapatılmalı ve konteynerlere kaynak limiti konmalıdır. Yapı aynı kalır, ayrıntılar değişir.
Veritabanı verimi nasıl yedeklerim#
Veri adlandırılmış bir hacimde durur, bu yüzden klasör kopyalamak yerine mantıksal döküm almak en güvenli yoldur. docker compose exec mysql mysqldump -u root -p laravel > yedek.sql komutu tek dosyalık bir yedek üretir. Geri yüklerken aynı komutu ters yönde kullanırsınız. Konteyner verilerini topluca yedeklemenin yöntemlerini Docker uygulama yedekleme ve geri yükleme yazısında bulabilirsiniz.
Birden fazla Laravel projesini aynı anda çalıştırabilir miyim#
Evet, ancak yayımlanan portlar çakışmamalıdır. Her proje için farklı bir host portu seçin (8080, 8081, 8082) ya da hepsinin önüne bir ters proxy koyup alan adına göre yönlendirin. İkinci yöntem daha temizdir; her projeye proje1.test gibi bir yerel alan adı verirsiniz ve port numarası ezberlemekten kurtulursunuz.
E-postaları geliştirmede nasıl test ederim#
Gerçek bir SMTP sunucusuna bağlanmak yerine, gelen postaları yakalayıp bir web arayüzünde gösteren bir servis konteyneri ekleyin ve .env içindeki MAIL_HOST değerini o servise yönlendirin. Böylece test verileriyle gerçek kişilere posta gitmesi riski ortadan kalkar. Üretimde gerçek gönderim yapacaksanız SMTP ayarlarınızı SMTP test aracı ile doğrulayabilirsiniz.
Kapanış#
Laravel'i Docker'da rahat kullanmanın sırrı birkaç alışkanlıkta toplanıyor: imajı host kullanıcınızla aynı UID'ye sahip bir kullanıcıyla derlemek, composer ve artisan komutlarını daima konteyner içinde çalıştırmak, .env içindeki ana makine adlarını servis adlarıyla değiştirmek, kuyruk işçisini ayrı bir servis olarak tanımlayıp kod değişince queue:restart demek ve veritabanını adlandırılmış hacimde tutup düzenli döküm almak. Bu beşini oturttuğunuzda ortam kurulumu bir daha gündeminize gelmez.
Projeyi yayına alma vakti geldiğinde tam root erişimli bir ortam gerekir; VDS ve bulut sunucu paketlerimiz Docker tabanlı dağıtım için uygundur. Sunucu bakımını, güncellemeleri ve izlemeyi devretmek isterseniz sunucu yönetimi hizmetimiz bunu üstlenir; mevcut projenizi başka bir sağlayıcıdan taşıyacaksanız site taşıma hizmetimiz geçişi planlı biçimde yürütür.