Yerelde php artisan serve ile kusursuz çalışan bir projeniz var. Dosyaları sunucuya attınız, alan adını açtınız ve karşınıza ya beyaz bir sayfa geldi ya da /var/www/app dizininin içeriğini listeleyen bir dosya tarayıcısı. Adres çubuğuna /public eklediğinizde site açılıyor ama bu sefer tüm alt sayfalar 404 veriyor. Sonra bir de "The stream or file storage/logs/laravel.log could not be opened" hatası çıkıyor ve hatanın kendisi bile log'a yazılamıyor.
Bu üç belirti neredeyse her ilk Laravel kurulumunda görülür ve hepsinin sebebi aynıdır: Laravel, paylaşımlı hosting mantığıyla "dosyaları yükle, çalışsın" diye tasarlanmamıştır. Web sunucusunun kökü proje dizini değil, public alt dizini olmalıdır; storage ve bootstrap/cache dizinlerine PHP sürecinin yazma izni bulunmalıdır; bağımlılıklar sunucuda composer install ile kurulmalıdır.
Bu rehberde bir Ubuntu VDS üzerinde temiz bir Laravel yayına alma süreci anlatılıyor: gerekli paketler, Nginx server bloğu, izin modeli, üretim .env ayarları, artisan önbellekleri ve queue worker'ın systemd ile kalıcı hâle getirilmesi. Symlink hilesi, .htaccess yamaları ya da index.php taşıma gibi paylaşımlı hosting numaralarına hiç girmiyoruz — VDS'te bunlara ihtiyacınız yok.
Sunucuda Hangi Paketler Kurulu Olmalı?#
Laravel 11 ve 12 sürümleri PHP 8.2 ve üzerini ister. Ubuntu 24.04 deposundaki PHP 8.3 bu şart için yeterlidir; daha yeni bir sürüm istiyorsanız ondrej/php PPA'sını ekleyebilirsiniz. Eklentileri eksiksiz kurun, çünkü eksik bir eklenti genellikle composer install sırasında değil, uygulama ilk isteği aldığında ortaya çıkar:
sudo apt update
sudo apt install -y nginx mariadb-server unzip git \
php8.3-fpm php8.3-cli php8.3-mysql php8.3-mbstring \
php8.3-xml php8.3-curl php8.3-zip php8.3-bcmath \
php8.3-intl php8.3-gd
# Composer'ı global kur
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer
composer --version
Kurulum sonrası PHP'nin gerçekten hangi eklentileri gördüğünü doğrulayın. Web tarafında çalışan SAPI, CLI'dan farklı bir php.ini kullanır — bu ayrım çok sayıda "yerelde çalışıyor, sunucuda çalışmıyor" vakasının kaynağıdır:
php -m | grep -E "mbstring|pdo_mysql|openssl|tokenizer|xml|ctype|curl|bcmath"
php -i | grep "Loaded Configuration File"
Veritabanı tarafında MariaDB'yi kurduktan sonra mysql_secure_installation çalıştırmayı ve uygulamaya root değil, kendi kullanıcısını vermeyi ihmal etmeyin. Ayrıntılar için MariaDB kurulumu ve güvenliği yazısına bakabilirsiniz. Sunucuyu yeni aldıysanız güvenlik duvarını kapatmadan, root ile SSH girişini kapatıp anahtar tabanlı erişime geçmeyi ve deploy için ayrı bir kullanıcı açmayı bu adımdan önce halledin.
Projeyi Sunucuya Alma ve Dizin Yapısı#
Dosyaları FTP ile sürükleyip bırakmak yerine Git kullanın; hem sürüm takibi hem de sonraki deploy'lar için tek komutluk bir yol açar. Kodu ayrı bir kullanıcı sahipliğinde tutmak, web sunucusunun her dosyaya yazamamasını sağlar:
sudo mkdir -p /var/www/uygulama
sudo chown -R $USER:www-data /var/www/uygulama
cd /var/www
git clone https://github.com/hesap/proje.git uygulama
cd uygulama
Depoda .env dosyasının bulunmaması gerekir; Laravel'in .gitignore dosyası bunu zaten dışlar. Sunucuda örnek dosyadan kendi kopyanızı üretin:
cp .env.example .env
Dizin yapısında sizi ilgilendiren üç yer var:
| Dizin | Rolü | Yazma izni |
|---|---|---|
public/ | Web sunucusunun kökü, tek giriş noktası index.php | Gerekmez |
storage/ | Log, oturum, önbellek, yüklenen dosyalar | Gerekir |
bootstrap/cache/ | Derlenmiş config ve route önbellekleri | Gerekir |
vendor/ | Composer bağımlılıkları | Gerekmez |
.env | Ortam değişkenleri, veritabanı parolası | Gerekmez |
Web kökünün public/ olması yalnızca estetik bir tercih değildir: .env dosyanız, storage/ altındaki loglarınız ve vendor/ içindeki tüm kütüphane kaynak kodunuz proje kökündedir. Kökü /var/www/uygulama yaparsanız, ziyaretçi /.env adresini isteyerek veritabanı parolanızı düz metin olarak indirebilir. Bu, ilk kurulumda en sık yapılan ve en pahalıya patlayan hatadır.
composer install --no-dev Neden Üretimde Şart?#
vendor/ dizinini yerelden kopyalamayın; sunucudaki PHP sürümüne ve eklentilerine göre çözülmesi gereken bağımlılıklar var. Üretim kurulumunun komutu şudur:
cd /var/www/uygulama
composer install --no-dev --optimize-autoloader --no-interaction --prefer-dist
Bayrakların her biri bir işe yarıyor:
--no-dev—require-devaltındaki paketleri kurmaz. PHPUnit, Faker, debug araçları ve Laravel Telescope gibi geliştirme paketleri sunucuya hiç inmez. Bu hem disk ve kurulum süresi kazancıdır hem de güvenlik meselesidir: hata ayıklama araçları üretimde erişilebilir uçlar açabilir.--optimize-autoloader— Sınıf haritasını önceden üretir. PSR-4 çözümlemesi her istekte dosya sistemi taraması yapmak yerine hazır haritadan okunur; yüksek trafikte hissedilir bir fark yaratır.--no-interaction— Soru sormaz, otomasyon ve CI için gereklidir.--prefer-dist— Paketleri arşiv olarak indirir,.gitgeçmişlerini taşımaz.
⚠️ --no-dev kullandığınızda, config/app.php içindeki providers listesine elle eklenmiş bir geliştirme paketi varsa uygulama "Class not found" hatasıyla açılmaz. Böyle bir paket varsa kaydını koşullu hâle getirin ya da require-dev'den require'a taşımaya değip değmediğine karar verin.
composer install ile composer update arasındaki farkı da net tutun: sunucuda asla update çalıştırmayın. install, composer.lock dosyasındaki tam sürümleri kurar; update ise sürümleri yeniden çözer ve yerelde test etmediğiniz bir kütüphane sürümünü üretime sokar.
.env Dosyası ve Üretim Ayarları#
.env uygulamanın sırlarını taşır ve üretimde birkaç değeri yerelden farklı olmak zorundadır:
APP_NAME="Uygulama"
APP_ENV=production
APP_KEY=
APP_DEBUG=false
APP_URL=https://ornek.com
LOG_CHANNEL=daily
LOG_LEVEL=warning
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_DATABASE=uygulama
DB_USERNAME=uygulama_user
DB_PASSWORD=guclu-bir-parola
SESSION_DRIVER=database
QUEUE_CONNECTION=database
CACHE_STORE=database
APP_KEY boşsa uygulama şifreleme yapamaz ve "No application encryption key has been specified" hatası verir. Anahtarı sunucuda bir kez üretin:
php artisan key:generate
php artisan migrate --force
php artisan storage:link
migrate komutunda --force gereklidir; Laravel üretim ortamında onay istemeden migration çalıştırmaz ve etkileşimsiz oturumda komut sessizce iptal olur.
APP_DEBUG=false pazarlık konusu değildir. Debug açıkken oluşan bir istisna, Laravel'in hata ekranını üretir: dosya yolları, kod parçaları, veritabanı sorguları ve bazı durumlarda ortam değişkenlerinin tamamı bu ekranda görünür. Yani tek bir işlenmemiş hata, veritabanı parolanızı ve API anahtarlarınızı ziyaretçiye gösterir. APP_ENV=production ile birlikte kullanın; bazı paketler davranışlarını bu değere göre değiştirir.
Dosyanın kendisini de koruyun. Kökü public/ yaptığınız için web üzerinden erişilemez ama sunucudaki diğer kullanıcılara karşı izinleri daraltın:
chmod 640 /var/www/uygulama/.env
chown $USER:www-data /var/www/uygulama/.env
Bu kombinasyonda dosyayı siz okuyup yazabilir, www-data grubu yalnızca okuyabilir, diğer kullanıcılar hiç göremez.
Nginx Server Bloğu: Kök Dizin public Olmalı#
Şimdi başlangıçtaki üç belirtiyi de çözen yapılandırmaya geldik. /etc/nginx/sites-available/uygulama dosyasını oluşturun:
server {
listen 80;
server_name ornek.com www.ornek.com;
root /var/www/uygulama/public;
index index.php;
charset utf-8;
client_max_body_size 32m;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
error_page 404 /index.php;
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
Bloğu etkinleştirip test edin:
sudo ln -s /etc/nginx/sites-available/uygulama /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx
Kritik satırlar şunlar:
root .../public — Adres çubuğunda /public yazmanıza gerek kalmasının ve .env dosyasının web üzerinden erişilebilir olmasının sebebi budur. Doğru kök budur; başka hiçbir yama gerekmez.
try_files $uri $uri/ /index.php?$query_string — Laravel'in tüm yönlendirmesi tek bir index.php üzerinden geçer. Bu satır olmadan yalnızca ana sayfa açılır, /hakkimizda gibi her adres 404 verir. Sorgu dizesini de aktardığına dikkat edin; unutulursa ?sayfa=2 gibi parametreler uygulamaya ulaşmaz.
location ~ /\.(?!well-known).* — Nokta ile başlayan dosyaları engeller ama Let's Encrypt'in HTTP doğrulaması için kullandığı .well-known dizinini serbest bırakır. Sertifikayı kurmak için Let's Encrypt ile ücretsiz SSL adımlarını uygulayabilirsiniz; Certbot bu blokla uyumlu çalışır ve 443 dinleyicisini kendisi ekler.
PHP-FPM soket yolunun kurulu sürümle eşleştiğini doğrulayın; en sık görülen 502 sebebi budur. Havuz boyutlandırması ve timeout değerleri için Nginx ve PHP-FPM yapılandırması yazısındaki hesaplar buraya doğrudan uygulanabilir.
ls -l /run/php/
systemctl status php8.3-fpm --no-pager
storage ve bootstrap/cache İzin Hatası Nasıl Çözülür?#
"The stream or file could not be opened" hatası tek bir şey söyler: PHP-FPM'i çalıştıran kullanıcı (www-data) o dizine yazamıyor. Çözümde iki uç yaklaşım yanlıştır — her şeyi www-data'ya vermek de, chmod -R 777 yapmak da. Doğru model, kodun sizde kalması ve yalnızca iki dizinin gruba açılmasıdır:
cd /var/www/uygulama
# Tüm proje: sahibi siz, grubu www-data
sudo chown -R $USER:www-data .
# Dizinler 755, dosyalar 644
sudo find . -type d -exec chmod 755 {} \;
sudo find . -type f -exec chmod 644 {} \;
# Yalnızca yazılabilir olması gereken iki dizin
sudo chmod -R 775 storage bootstrap/cache
sudo chown -R $USER:www-data storage bootstrap/cache
# Yeni oluşan dosyalar da aynı grubu miras alsın
sudo find storage bootstrap/cache -type d -exec chmod g+s {} \;
Son satırdaki setgid biti (g+s) çoğu rehberde yoktur ve tam da bu yüzden hata birkaç gün sonra geri gelir: artisan komutlarını kendi kullanıcınızla çalıştırdığınızda üretilen yeni önbellek dosyaları sizin birincil grubunuza yazılır, www-data bir daha yazamaz. Setgid, alt dizinlerde oluşan her dosyanın grubunu dizinin grubundan devralmasını sağlar.
İzin mantığının tamamı için Linux dosya izinleri yazısı iyi bir referanstır. Hâlâ hata alıyorsanız gerçek kullanıcıyı doğrulayın; bazı kurulumlarda PHP-FPM havuzu farklı bir kullanıcıyla çalışır:
grep -E "^user|^group" /etc/php/8.3/fpm/pool.d/www.conf
sudo -u www-data test -w storage/logs && echo "yazabiliyor" || echo "YAZAMIYOR"
Config, Route ve View Cache: php artisan optimize#
Laravel her istekte onlarca yapılandırma dosyasını okur ve route tanımlarını yeniden derler. Üretimde bunları önceden derleyip tek dosyaya indirmek, istek başına 20-60 ms kazandırır:
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
Dördünü tek komutta toplamak isterseniz php artisan optimize yeterlidir. Tersini almak için php artisan optimize:clear kullanılır.
⚠️ config:cache sonrası env() çağrıları null döner. Laravel önbelleklenmiş yapılandırmayı kullanırken .env dosyasını hiç okumaz. config/ altındaki dosyalarda env() kullanmak sorunsuzdur — çünkü önbellek alınırken bir kez değerlendirilir — ama bir controller, model veya blade şablonu içinde doğrudan env('STRIPE_KEY') yazdıysanız üretimde boş değer alırsınız. Bu, kurulumdan sonra "yerelde çalışıyordu" denen hataların en yaygın sebebidir. Kural basit: env() sadece config/ içinde, uygulama kodunda daima config().
Aynı şekilde, .env dosyasını her düzenlediğinizde önbelleği yenilemeniz gerekir:
php artisan config:clear && php artisan config:cache
PHP tarafında OPcache'i de açık tutun; derlenmiş bytecode'u bellekte tutar ve Laravel gibi çok dosyalı framework'lerde en büyük tek kazançtır. Üretimde opcache.validate_timestamps=0 kullanıyorsanız her deploy sonrası PHP-FPM'i yeniden yüklemeyi unutmayın — ayrıntılar PHP OPcache yazısında.
Queue Worker'ı systemd ile Kalıcı Çalıştırma#
php artisan queue:work komutunu SSH oturumunda başlatırsanız, bağlantı koptuğu anda kuyruk durur. E-posta gönderimi, rapor üretimi ve bildirimler sessizce sıraya girer ve hiç işlenmez. İşçiyi bir servis hâline getirmelisiniz.
/etc/systemd/system/laravel-worker.service dosyasını oluşturun:
[Unit]
Description=Laravel queue worker
After=network.target mariadb.service
[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/uygulama
ExecStart=/usr/bin/php /var/www/uygulama/artisan queue:work \
--sleep=3 --tries=3 --max-time=3600 --backoff=10
Restart=always
RestartSec=5
StandardOutput=append:/var/log/laravel-worker.log
StandardError=append:/var/log/laravel-worker.log
[Install]
WantedBy=multi-user.target
Servisi devreye alın:
sudo systemctl daemon-reload
sudo systemctl enable --now laravel-worker
sudo systemctl status laravel-worker --no-pager
Parametrelerin gerekçeleri:
--max-time=3600— İşçi bir saat sonra kendini sonlandırır, systemd yeniden başlatır. Uzun ömürlü PHP süreçleri zamanla bellek biriktirir; bu, sızıntıyı düzenli olarak sıfırlar.--tries=3ve--backoff=10— Başarısız iş üç kez denenir, denemeler arasında 10 saniye beklenir. Bu olmadan hatalı bir iş sonsuz döngüye girer.Restart=always— Süreç herhangi bir sebeple ölürse systemd geri getirir.
Birden fazla işçi çalıştırmak isterseniz dosya adını [email protected] yapıp %i ile örneklendirin, sonra systemctl enable --now laravel-worker@1 laravel-worker@2 şeklinde başlatın. Servis birimlerinin ayrıntıları için systemd servis yönetimi yazısına bakabilirsiniz; Supervisor tercih ediyorsanız numprocs ayarıyla aynı işi tek yapılandırmadan görebilirsiniz.
⚠️ Queue worker kodu belleğe yükler ve orada tutar. Yeni kod deploy ettiğinizde işçiler eski sürümü çalıştırmaya devam eder. Her deploy'un son adımı şu olmalı:
php artisan queue:restart
Bu komut işçileri öldürmez; mevcut işi bitirdikten sonra nazikçe çıkmalarını söyler, systemd de onları yeni kodla ayağa kaldırır.
Zamanlanmış görevler için de tek bir cron satırı yeterlidir. Laravel'in kendi zamanlayıcısı hangi işin ne zaman çalışacağını içeride yönetir:
sudo crontab -u www-data -e
# Dosyaya ekleyin:
* * * * * cd /var/www/uygulama && php artisan schedule:run >> /dev/null 2>&1
Satırı www-data kullanıcısının crontab'ına yazmak önemlidir: kendi kullanıcınızla eklerseniz üretilen log ve önbellek dosyaları yanlış sahiplikle oluşur ve izin hatası geri döner.
Deploy Sonrası Kontrol Listesi ve Sık Hatalar#
Her yeni sürümde çalıştıracağınız komut sırası şudur — sıra önemlidir:
cd /var/www/uygulama
php artisan down --render="errors::503"
git pull origin main
composer install --no-dev --optimize-autoloader --no-interaction
php artisan migrate --force
php artisan optimize:clear
php artisan optimize
sudo systemctl reload php8.3-fpm
php artisan queue:restart
php artisan up
Uygulamanın gerçekte hangi ayarlarla çalıştığını tek komutla görebilirsiniz:
php artisan about
En sık karşılaşılan sorunlar ve gerçek sebepleri:
| Belirti | Sebep | Çözüm |
|---|---|---|
| Dizin listesi görünüyor | Nginx kökü public değil | root .../public; yapın |
| Ana sayfa açılıyor, alt sayfalar 404 | try_files satırı eksik | Bloğa try_files ekleyin |
| "could not be opened" | storage yazılamıyor | chmod 775 + chown www-data + setgid |
| "No application encryption key" | APP_KEY boş | php artisan key:generate |
.env değişti ama etkisi yok | Config cache eski | config:clear && config:cache |
Kodda env() null dönüyor | config:cache alınmış | config() kullanın |
| 502 Bad Gateway | fastcgi_pass soketi yanlış | ls /run/php/ ile doğrulayın |
| Kuyruk işleri işlenmiyor | Worker çalışmıyor | systemctl status laravel-worker |
| Deploy sonrası eski davranış | Worker eski kodu tutuyor | php artisan queue:restart |
| Yükleme 413 hatası | Nginx gövde limiti | client_max_body_size artırın |
Son bir öneri: bu adımları elle tekrarlamak yerine bir deploy.sh dosyasına yazın ve sunucuda tek komutla çalıştırın. Elle yapılan her deploy'da unutulan adım, genellikle queue:restart ya da config:cache olur ve hatanın kaynağını bulmak, komutu çalıştırmaktan çok daha uzun sürer.
Sıkça Sorulan Sorular#
Laravel için paylaşımlı hosting yerine neden VDS öneriliyor?#
Paylaşımlı hostingde web kökünü public dizinine taşıyamazsınız, SSH erişiminiz sınırlıdır ve arka planda sürekli çalışan bir queue worker başlatamazsınız. Bunları aşmak için index.php'yi köke taşımak veya symlink kurmak gibi hilelere başvurulur; her biri güncelleme sırasında kırılır. VDS'te kökü doğru ayarlar, systemd ile işçi çalıştırır ve Composer'ı doğrudan kullanırsınız.
composer install ile composer update arasındaki fark nedir?#
install, composer.lock dosyasında yazan tam sürümleri kurar; yerelde test ettiğiniz bağımlılıkların birebir aynısını üretime getirir. update ise sürümleri yeniden çözer ve composer.lock dosyasını değiştirir. Sunucuda update çalıştırmak, hiç test etmediğiniz bir kütüphane sürümünün canlıya girmesi demektir. Üretimde daima install kullanın.
storage dizinine 777 izni versem olmaz mı?#
Teknik olarak hatayı susturur ama sunucudaki her kullanıcı ve her süreç o dizine yazabilir hâle gelir. storage altında oturum dosyalarınız, önbelleğiniz ve kullanıcı yüklemeleriniz durur; bu dizine yazma yetkisi, bir web kabuğu bırakmak için yeterli bir zemindir. Doğru çözüm 775 izin, www-data grup sahipliği ve setgid bitidir.
Kodumda env() kullanıyorum, üretimde neden boş dönüyor?#
php artisan config:cache çalıştırıldığında Laravel tüm yapılandırmayı tek bir derlenmiş dosyaya yazar ve çalışma anında .env dosyasını hiç okumaz. Bu yüzden config/ dizini dışındaki env() çağrıları null döner. Değeri config/services.php gibi bir dosyaya taşıyın ve uygulama kodunda config('services.stripe.key') biçiminde okuyun.
Queue worker'ı Supervisor ile mi systemd ile mi çalıştırmalıyım?#
İkisi de aynı işi yapar. systemd her Ubuntu sunucusunda hazır bulunur, ek paket istemez ve journalctl ile log takibi doğal gelir. Supervisor ise numprocs ayarıyla aynı yapılandırmadan birden fazla işçi başlatmayı kolaylaştırır ve Laravel belgelerinde örneklenen yöntemdir. Tek uygulamalı bir VDS'te systemd, çok sayıda işçi ve kuyruk yönetiyorsanız Supervisor daha rahat olur.
Deploy sırasında siteyi kapatmam gerekir mi?#
Migration çalıştırıyorsanız evet, php artisan down ile bakım moduna almak veri tutarlılığı açısından güvenlidir. Yalnızca varlık dosyaları veya küçük kod değişiklikleri deploy ediyorsanız gerekmez. Bakım modunu atlamak istiyorsanız migration'ları geriye dönük uyumlu yazın: önce sütunu ekleyin, sonra kodu deploy edin, eski sütunu bir sonraki sürümde kaldırın.