PHP bir uygulamayı Docker'a taşırken herkesin karşılaştığı ilk çıkmaz şudur: Apache'nin mod_php alışkanlığıyla "tek konteynerde her şey" beklersiniz, ama Nginx PHP'yi kendisi çalıştıramaz. Nginx yalnızca bir HTTP sunucusudur; PHP dosyalarını FastCGI protokolüyle ayrı bir sürece devretmesi gerekir. İşte bu ayrım, Docker'ın felsefesiyle zaten örtüşür: bir konteyner bir iş yapar. Nginx istekleri karşılar, PHP-FPM kodu çalıştırır, veritabanı üçüncü konteynerde durur.
Bu rehberde üç servisli — Nginx, PHP-FPM ve MariaDB — üretime yakın bir yığını sıfırdan kuracağız. Dosya paylaşımının neden hem Nginx hem PHP konteynerinde gerekli olduğunu, fastcgi_pass satırının tam olarak ne yaptığını, eklenti kurmak için neden özel bir PHP imajı gerektiğini, dosya izinlerinin neden bu kadar sık soruna yol açtığını ve üretimde nelerin değişmesi gerektiğini adım adım göreceğiz. Sonunda da 502, 404 ve "File not found" hatalarının gerçek nedenlerini tek tek ele alacağım.
Neden Nginx ve PHP-FPM Ayrı Konteynerlerde#
Nginx bir .php isteği aldığında dosyayı okumaz ve çalıştırmaz; bunun yerine FastCGI protokolüyle PHP-FPM'e "şu yoldaki betiği çalıştır ve sonucu bana ver" der. PHP-FPM sonucu üretir, Nginx bunu istemciye iletir. Bu iki süreç birbirinden tamamen bağımsızdır ve TCP üzerinden konuşabilir — Docker'da tam olarak bunu kullanırız.
Ayrı tutmanın pratik faydaları var. PHP sürümünü yükseltmek istediğinizde yalnızca bir imaj etiketi değiştirir, Nginx'e hiç dokunmazsınız. PHP-FPM'in bellek ihtiyacı arttığında yalnızca o konteynere limit koyarsınız. Aynı Nginx örneğinin arkasında farklı PHP sürümleri çalıştırmak da mümkün hale gelir. Bunun karşılığında ödediğiniz bedel, iki konteynerin aynı dosyaları görmesi gerektiğidir; bu, aşağıda göreceğimiz en kritik yapılandırma noktasıdır.
| Bileşen | Görevi | Docker imajı |
|---|---|---|
| Nginx | HTTP isteklerini karşılar, statik dosyaları sunar | nginx:alpine |
| PHP-FPM | PHP kodunu çalıştırır, FastCGI konuşur | php:8-fpm-alpine |
| MariaDB | Veriyi saklar | mariadb:11 |
| Redis (isteğe bağlı) | Oturum ve önbellek | redis:alpine |
Konteynerlerin birbirini adıyla bulması, Docker'ın gömülü DNS'i sayesinde olur; compose ile başlatılan servisler otomatik olarak aynı ağa alınır ve servis adı bir ana makine adı gibi çözülür. Bu mekanizmaya yabancıysanız Docker Compose kullanımı yazısı temel kavramları oturtur.
Dizin Yapısı ve Dosya Paylaşımı Mantığı#
Projeyi düzenli kurmak, sonradan çıkacak yol karışıklıklarının yarısını önler. Şu yapıyı kullanacağız:
mkdir -p /opt/php-yigin/{src,nginx,php,db-data}
cd /opt/php-yigin
tree -L 2
# .
# ├── docker-compose.yml
# ├── nginx/
# │ └── default.conf # Nginx site yapılandırması
# ├── php/
# │ ├── Dockerfile # eklentili özel PHP imajı
# │ └── php.ini # üretim PHP ayarları
# └── src/
# └── index.php # uygulama kodu
Burada anlaşılması gereken kilit nokta şudur: src klasörü hem Nginx hem PHP-FPM konteynerine, aynı yola bağlanmalıdır. Sebebi FastCGI protokolünün çalışma biçimidir. Nginx, PHP-FPM'e dosyanın içeriğini göndermez; yalnızca dosyanın yolunu (SCRIPT_FILENAME) gönderir. PHP-FPM o yolu kendi dosya sisteminde arar. İki konteynerde yollar farklıysa PHP-FPM dosyayı bulamaz ve tarayıcıya meşhur File not found. mesajı düşer.
Bu yüzden her iki serviste de - ./src:/var/www/html yazacağız. Nginx tarafındaki root direktifi ile PHP tarafındaki çalışma dizini birebir aynı olmalı. Bu tek kuralı akılda tutmak, bu yığında en çok zaman kaybettiren hatayı baştan siler.
docker-compose.yml: Üç Servisli Yığın#
Şimdi yığını tanımlayalım. Veritabanı parolasını dosyaya gömmek yerine .env dosyasında tutmak, dosyayı sürüm kontrolüne kazara eklediğinizde yaşanacak kazayı önler:
cat > .env << 'EOF'
DB_ROOT_PASSWORD=cok-uzun-bir-root-parolasi
DB_NAME=uygulama
DB_USER=uygulama_kullanicisi
DB_PASSWORD=baska-bir-uzun-parola
EOF
chmod 600 .env
services:
web:
image: nginx:alpine
restart: unless-stopped
ports:
- "80:80"
volumes:
# KOD: Nginx'in statik dosyaları sunması ve yol eşleşmesi için
- ./src:/var/www/html
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- php
php:
build: ./php
restart: unless-stopped
volumes:
# AYNI YOL: SCRIPT_FILENAME burada çözülecek
- ./src:/var/www/html
- ./php/php.ini:/usr/local/etc/php/conf.d/zz-ozel.ini:ro
environment:
DB_HOST: db
DB_NAME: ${DB_NAME}
DB_USER: ${DB_USER}
DB_PASSWORD: ${DB_PASSWORD}
depends_on:
- db
db:
image: mariadb:11
restart: unless-stopped
environment:
MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MARIADB_DATABASE: ${DB_NAME}
MARIADB_USER: ${DB_USER}
MARIADB_PASSWORD: ${DB_PASSWORD}
volumes:
- db-data:/var/lib/mysql
# Port DIŞARIYA açılmıyor: yalnızca iç ağdan erişilir
volumes:
db-data:
Veritabanı konteynerinde ports: bölümünün bilerek olmadığına dikkat edin. PHP konteyneri db adıyla iç ağdan erişebildiği için 3306 portunu internete açmanın hiçbir gerekçesi yoktur; açık bırakılan veritabanı portları, otomatik tarayıcıların ilk denediği yerlerdendir. Verinin adlandırılmış hacimde durması da konteyneri silip yeniden oluşturduğunuzda veritabanının kaybolmamasını sağlar; bu ayrımı Docker volume ve veri yönetimi yazısında ayrıntılı ele almıştık.
Nginx Yapılandırması: fastcgi_pass ve Güvenlik#
nginx/default.conf dosyası yığının kalbidir. Buradaki fastcgi_pass php:9000 satırı, "PHP isteklerini php adlı konteynerin 9000 portuna gönder" demektir; php adı compose'daki servis adıdır ve Docker DNS'i onu otomatik çözer:
server {
listen 80;
server_name firmaniz.com www.firmaniz.com;
# PHP konteyneriyle BİREBİR AYNI yol olmalı
root /var/www/html;
index index.php index.html;
# Yükleme boyutu: php.ini ile uyumlu tutun
client_max_body_size 64m;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
# Yol geçişi (path traversal) saldırılarını kapatır
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass php:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_read_timeout 300;
}
# Gizli dosyaları ve yedekleri kapat
location ~ /\.(?!well-known) { deny all; }
location ~* \.(sql|bak|log|env)$ { deny all; }
# Statik dosyalarda uzun önbellek
location ~* \.(jpg|jpeg|png|gif|webp|svg|css|js|woff2)$ {
expires 30d;
access_log off;
}
}
try_files $uri =404; satırı isteğe bağlı görünür ama değildir: bu satır olmadan, yükleme klasörüne atılmış zararlı bir dosya belirli koşullarda PHP olarak çalıştırılabilir. Aynı şekilde .env ve .git gibi dosyaların açıkta kalması, sahada en çok veri sızıntısına yol açan yapılandırma hatalarındandır; bu konunun ayrıntısı için .git klasörü ve .env dosyası ifşası yazısına bakabilirsiniz.
Özel PHP İmajı: Eklentiler ve Composer#
Resmi php:8-fpm-alpine imajı çıplak gelir; pdo_mysql, gd, zip, intl gibi çoğu uygulamanın istediği eklentiler kurulu değildir. Bu yüzden kendi imajınızı türetirsiniz. php/Dockerfile:
FROM php:8-fpm-alpine
# Derleme bağımlılıkları geçici, çalışma bağımlılıkları kalıcı
RUN apk add --no-cache --virtual .build-deps $PHPIZE_DEPS libpng-dev libzip-dev icu-dev \
&& apk add --no-cache libpng libzip icu-libs \
&& docker-php-ext-install -j"$(nproc)" pdo_mysql gd zip intl opcache \
&& apk del .build-deps
# Composer'ı resmi imajdan kopyala
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html
# Kök yerine sınırlı kullanıcı ile çalış
USER www-data
Derleme bağımlılıklarını --virtual grubuyla kurup sonunda silmek, imaj boyutunu yüz megabaytlarca küçültür; bu tekniğin diğer örnekleri için Docker imaj boyutu optimizasyonu yazısına bakın. php/php.ini dosyasına da üretim ayarlarını koyun:
; Üretimde hata mesajları asla ekrana basılmaz
display_errors = Off
log_errors = On
error_log = /dev/stderr
memory_limit = 256M
upload_max_filesize = 64M
post_max_size = 64M
max_execution_time = 60
; OPcache: üretimde en büyük tek performans kazancı
opcache.enable = 1
opcache.memory_consumption = 128
opcache.max_accelerated_files = 20000
opcache.validate_timestamps = 0
opcache.validate_timestamps = 0 üretim için doğrudur ve ciddi hız kazandırır, ama kod değiştiğinde PHP-FPM'i yeniden başlatmanız gerektiği anlamına gelir. Geliştirme ortamında bu değeri 1 yapın, yoksa yaptığınız değişiklikleri göremez ve saatlerce hayalet hata kovalarsınız.
Kalıcı Veri, Loglar ve Dosya İzinleri#
PHP konteynerinde uygulama www-data kullanıcısıyla çalışır ve bu kullanıcının konteyner içindeki UID değeri genellikle 82 (Alpine) ya da 33'tür. Host makinedeki dosyalarınız ise sizin kullanıcınıza aittir. Bağlı hacimlerde izinler host tarafındaki değerlerle geçerlidir; bu yüzden yükleme klasörüne yazma denemesi "Permission denied" ile döner. En pratik çözüm, yazılabilir olması gereken klasörlerin sahipliğini konteynerdeki UID'ye vermektir:
# Konteynerdeki www-data UID/GID değerini öğren
docker compose exec php id www-data
# uid=82(www-data) gid=82(www-data)
# Yalnızca yazılması gereken klasörleri devret
sudo chown -R 82:82 src/storage src/uploads
sudo find src -type d -exec chmod 755 {} \;
sudo find src -type f -exec chmod 644 {} \;
Tüm proje klasörünü chmod 777 yapmak sorunu "çözer" ama sunucuyu da savunmasız bırakır; hiçbir üretim kurulumunda bunu yapmayın. Loglar tarafında ise Docker felsefesi gereği dosyaya değil standart çıktıya yazmak doğrudur; yukarıdaki error_log = /dev/stderr ayarı sayesinde PHP hataları doğrudan docker compose logs php çıktısında görünür. Log okuma ve konteyner içine girme yöntemlerini Docker konteyner loglarını okuma ve exec yazısında topladık.
Yığını başlatıp doğrulayın:
docker compose up -d --build
docker compose ps
curl -I http://localhost/
# PHP'nin veritabanını görüp görmediğini test et
docker compose exec php php -r 'new PDO("mysql:host=db;dbname=uygulama","uygulama_kullanicisi","baska-bir-uzun-parola"); echo "baglanti tamam\n";'
Sık Yapılan Hatalar#
File not found. — Tarayıcıda beyaz sayfada bu yazıyorsa Nginx isteği PHP-FPM'e iletmiş ama PHP dosyayı bulamamıştır. Sebep neredeyse her zaman yol uyuşmazlığıdır: Nginx'in root değeri ile PHP konteynerine bağlanan hacim yolu farklıdır. İkisini de /var/www/html yapın ve docker compose exec php ls /var/www/html ile dosyaların gerçekten orada olduğunu doğrulayın.
502 Bad Gateway — Nginx PHP-FPM'e hiç ulaşamıyor demektir. fastcgi_pass içindeki isim compose'daki servis adıyla aynı mı, PHP konteyneri ayakta mı (docker compose ps), PHP-FPM 9000'de dinliyor mu diye bakın. PHP konteyneri sürekli yeniden başlıyorsa asıl hata onun logundadır; bu döngünün genel nedenlerini konteyner sürekli yeniden başlıyor yazısında ele aldık.
PHP dosyası indiriliyor, çalışmıyor — location ~ \.php$ bloğu ya hiç yok ya da başka bir location bloğu tarafından gölgeleniyor. Nginx'te location eşleşme sırası önemlidir; düzenli ifade blokları önek bloklarından önce değerlendirilir ama ^~ kullanan bir önek bloğu düzenli ifadeleri tamamen devre dışı bırakır.
Veritabanına bağlanamıyor — Uygulamada ana makine adı olarak localhost yazmayın. Konteyner içinde localhost PHP konteynerinin kendisidir; veritabanı ayrı konteynerdedir ve adı db'dir. Aynı şekilde 127.0.0.1 de çalışmaz.
Kod değişiyor ama sayfa eski kalıyor — OPcache zaman damgası doğrulaması kapalıdır. Geliştirmede opcache.validate_timestamps = 1 yapın veya docker compose restart php ile önbelleği boşaltın.
Yükleme başarısız oluyor — Üç yerde sınır vardır ve en küçüğü geçerlidir: Nginx'te client_max_body_size, PHP'de upload_max_filesize ve post_max_size. Üçünü birbiriyle uyumlu ayarlayın, yoksa Nginx isteği 413 ile keser ve PHP tarafında hiçbir hata görmezsiniz.
Sıkça Sorulan Sorular#
Nginx ve PHP-FPM'i tek konteynerde çalıştırabilir miyim#
Teknik olarak mümkündür; bir süpervizör süreciyle iki servisi aynı konteynerde başlatan imajlar vardır. Ancak bu, konteyner başına tek süreç ilkesini bozar: loglar karışır, birini yeniden başlatmak diğerini de etkiler, sağlık kontrolü belirsizleşir. Tek konteyner çözümü ancak çok basit ve kısa ömürlü kurulumlarda kabul edilebilir; kalıcı bir yapı kuruyorsanız ayırmak uzun vadede daha az iş çıkarır.
PHP-FPM için Unix soketi mi TCP mi kullanmalıyım#
Aynı makinede çalışan iki süreç için Unix soketi biraz daha hızlıdır, ama Docker'da iki ayrı konteynerin soket paylaşması ek hacim yapılandırması gerektirir ve izin sorunlarına açıktır. Ayrı konteynerlerde TCP (php:9000) hem daha basittir hem de ileride PHP-FPM'i başka bir makineye taşımanıza imkân verir. Performans farkı, tipik bir web uygulamasının veritabanı gecikmesi yanında ölçülemeyecek kadar küçüktür.
PHP sürümünü nasıl yükseltirim#
php/Dockerfile dosyasındaki FROM satırındaki etiketi değiştirip docker compose up -d --build çalıştırmanız yeterlidir. Yükseltmeden önce uygulamanızın o sürümle uyumlu olduğunu bir kopya ortamda doğrulayın; özellikle kaldırılmış işlevler ve değişen varsayılanlar sorun çıkarabilir. Eski sürüme dönmek de aynı satırı geri almak kadar kolaydır, bu yüzden Docker'lı bir kurulumda sürüm denemesi risksizdir.
Bu yığını üretimde kullanabilir miyim#
Evet, ancak birkaç ekleme yapmalısınız: önüne SSL sonlandıran bir ters proxy koyun, konteynerlere kaynak limiti tanımlayın, restart: unless-stopped politikasını koruyun ve veritabanı hacminin düzenli yedeğini alın. Kaynak limitlerini nasıl belirleyeceğinizi Docker konteyner kaynak limitleri, SSL katmanını Traefik ile ters proxy yazısında bulabilirsiniz.
Composer komutlarını nasıl çalıştırırım#
PHP konteynerinin içinden çalıştırırsınız: docker compose exec php composer install. Böylece bağımlılıklar, uygulamanın gerçekten çalışacağı PHP sürümü ve eklenti setiyle çözülür. Host makinenizdeki PHP sürümü farklıysa orada çalıştırılan composer install yanlış paket sürümleri kilitleyebilir; bu, geliştirme makinesinde çalışıp sunucuda patlayan hataların klasik kaynağıdır.
Birden fazla siteyi aynı yığında barındırabilir miyim#
Evet. Her site için nginx/conf.d altına ayrı bir server bloğu ekler, farklı server_name ve root değerleri verirsiniz. Tüm siteler aynı PHP-FPM havuzunu paylaşır; bu, kaynak açısından verimlidir ama bir sitenin ağır isteği diğerlerini bekletebilir. Siteler birbirinden yalıtılmalıysa her biri için ayrı bir PHP-FPM servisi tanımlamak daha doğrudur.
Kapanış#
Bu yığında işleri yolunda tutan birkaç kural var: kod klasörünü hem Nginx hem PHP konteynerine aynı yola bağlamak, veritabanı portunu dışarıya hiç açmamak, try_files $uri =404; satırını PHP bloğundan silmemek, yükleme sınırlarını Nginx ve PHP tarafında birbiriyle uyumlu tutmak ve üretimde OPcache'i açıp geliştirmede zaman damgası doğrulamasını geri getirmek. Bir hata çıktığında ilk bakılacak yer bellidir: docker compose logs php ve docker compose exec php ls /var/www/html.
Bu mimariyi çalıştırmak için tam kontrol sahibi bir ortama ihtiyacınız var; VDS ve bulut sunucu paketlerimiz Docker kurulumu için doğrudan uygundur. Konteynerlerle uğraşmadan hazır bir PHP ortamı istiyorsanız cPanel'li web hosting paketlerimiz PHP sürüm seçimi ve eklenti yönetimini panelden sunar; sunucu bakımını devretmek isterseniz sunucu yönetimi hizmetimiz güncelleme ve izleme işini üstlenir.