# erho.dev - Tam içerik > Bu dosya tüm yayınlanmış yazıların ham markdown gövdesini ardışık olarak içerir. Ajan istemciler tek istekte tüm korpusu yükleyebilir. ## Hakkımda ## Merhaba, ben Erhan Back-end odaklı full-stack geliştirici olarak modern web teknolojileri ile ölçeklenebilir, sürdürülebilir ve performanslı uygulamalar geliştiriyorum. ### Ne Yapıyorum? Freelance olarak **Orizora Yazılım** bünyesinde projeler geliştiriyorum. Uzmanlık alanlarım: - **Backend**: Laravel, PHP, RESTful API tasarımı - **Frontend**: React, TypeScript, Tailwind CSS - **Veritabanı**: MySQL, PostgreSQL, Redis - **DevOps**: Docker, Linux, VPS yönetimi, CI/CD - **Mimari**: TDD, DDD, SOLID, DRY, KISS, YAGNI, Desing Pattern, Clean Architecture ### Deneyim ve Yaklaşım Yazılım geliştirme sürecinde **kod kalitesi**, **test edilebilirlik** ve **sürdürülebilirlik** prensiplerini ön planda tutuyorum. Her projede: - Temiz ve okunabilir kod yazmayı - Kapsamlı test coverage sağlamayı - Performans optimizasyonlarını - Güvenlik en iyi pratiklerini uygulamayı hedefliyorum. ### Bu Blog Neden Var? Bu blog, yazılım geliştirme sürecinde öğrendiklerimi, deneyimlerimi ve görüşlerimi paylaştığım kişisel bir alan. Burada: - **Teknik rehberler** ve derinlemesine analizler - **Mimari kararlar** ve tasarım desenleri - **Performans optimizasyonu** teknikleri - **Modern web geliştirme** pratikleri hakkında yazılar bulabilirsiniz. ### İletişim Benimle iletişime geçmek için [erho.me](https://icin.tr/me) adresini ziyaret edebilir veya aşağıdaki platformlardan ulaşabilirsiniz: - [GitHub](https://icin.tr/github) - [LinkedIn](https://icin.tr/linkedin) --- ## Self-Hosted Mail Sunucusu: Sıfırdan Adım Adım Kurulum Rehberi - URL: https://erho.dev/post/027-self-hosted-mail-sunucusu-sifirdan-kurulum-rehberi - Markdown: https://erho.dev/post/027-self-hosted-mail-sunucusu-sifirdan-kurulum-rehberi.md - Tarih: 2026-08-01 - Kategori: linux - Etiketler: linux, devops, security **Postfix + Dovecot + OpenDKIM + SpamAssassin + Roundcube | Hetzner VPS | Ubuntu 24.04** Yıllarca hosting kullandım. cPanel'e alışkındım, mail de hosting paketinin içinde geliyordu. Bir noktada yaptığım işler hosting'e sığmaz oldu ve VPS'e geçtim; son 2-3 yıldır aktif olarak kendi sunucularımı yönetiyorum. Performans, esneklik, kontrol... Hepsi hosting'den iyiydi. Eksik kalan tek şey mail'di. Hosting'de mail hazır gelirdi, sunucuya geçince o konfor gitti. Bir süre "Gmail'i, Hotmail'i kullanırım, olur biter" dedim. Ama içime sinmedi. Yıllarımın yazışmaları başkasının sunucusunda duruyordu ve bir gün otomatik bir "politika ihlali" kararıyla hesap kapanırsa itiraz edecek bir muhatap bile bulamayabilirdim. Kendi domain'imdeki adreslerin kontrolünün bende olmasını istiyordum. Aslında sunucuya geçtiğim günden beri kendi mail sunucumu kurmak istiyordum ama işin zahmeti gözümü korkutuyordu. Birkaç kez denedim, birkaç kez sunucuyu bozacak noktaya geldim, birkaç kez de "sonra yaparım" deyip bıraktım. Denemelerimden birinde [MailCow](https://mailcow.email) kurdum. Docker tabanlı, her şeyi içinde getiren bir çözüm: 18 container, kendi DNS resolver'ı, kendi yönetim arayüzü. "Ben sadece mail göndermek istiyorum" diye başlayıp kendimi koca bir altyapıyı yönetirken buldum. Isınamadım ve kaldırdım. Aradan epey zaman geçti, mail ihtiyacı yeniden kapıyı çaldı. O sırada cPanel'den, Plesk'ten tanıdığım [Roundcube](https://github.com/roundcube/roundcubemail-docker) aklıma geldi. Sade, hafif, açık kaynak. Aradığım tam buydu. Bu sefer erteleyecek hâlim yoktu ve sonunda oturdu... :) ("Bugün" dediğime bakmayın, bu rehberle aylardır uğraşıyordum.) Bu yazıda Hetzner üzerindeki bir VPS'e sıfırdan, eksiksiz bir mail sunucusu kurmayı karşılaştığım hatalarla birlikte adım adım anlatıyorum. Çayınızı ya da kahvenizi alın, rehber uzun. Hangi tarafından bakarsanız bakın 1-3 saatinizi alır, demedi demeyin... :) **Kullandığım teknolojiler:** - **Postfix** - SMTP sunucusu (mail gönderme/alma) - **Dovecot** - IMAP/POP3 sunucusu (mail kutusuna erişim) - **OpenDKIM** - Giden maili DKIM ile imzalar, gelen maildeki imzayı doğrular - **OpenDMARC** - Gelen maillerde DMARC politikasını uygular, sahte gönderici (spoofing) maillerini reddeder - **postfix-policyd-spf-python** - Gelen maillerde SMTP aşamasında SPF kontrolü - **SpamAssassin** - Spam puanlama - **Roundcube** - Web tabanlı mail istemcisi (Docker ile) - **Nginx** - Reverse proxy ve SSL sonlandırma - **Fail2ban** - Brute-force koruması - **Let's Encrypt** - Ücretsiz SSL sertifikası Sunucu ortamım Hetzner Cloud üzerinde Ubuntu 24.04, DNS'i Cloudflare'de yönetiyorum. Temel Linux bilginiz varsa rehberi rahatça takip edebilirsiniz. > **Not:** Daha önce MailCow denediğim için sunucumda Postfix, Dovecot, Certbot gibi bazı paketler zaten kuruluydu. Yer yer "bende zaten kurulu" dediğimi göreceksiniz ama her paketin kurulumunu da yazdım. > **GÜNCELLEME (19 Eylül 2026) - Rehberin ilk hâlini uyguladıysanız okuyun:** Rehberin güvenlik kısmını farklı spoofing senaryolarıyla test edip güçlendirdim, düzeltmeleri de rehberin içine işledim. Eski config'i kullanıyorsanız şunlara bakın: `smtpd_sender_restrictions` ve 465 portu override'ları (Adım 5), `non_smtpd_milters` satırının kaldırılması ve OpenDKIM `trustedhosts` dosyası (Adım 5 ve 6), OpenDMARC ve SPF kurulumu (Adım 6), Fail2ban jail ayarları (Adım 13). Neyin neden değiştiğini "Güvenlik Sıkılaştırma" altındaki **Spoofing Savunması** bölümünde anlattım. --- ## Başlamadan Önce: Bu Terimler Ne Demek? Bu işe ilk girdiğimde beni en çok kısaltmalar yormuştu: SMTP, IMAP, DKIM, SPF, PTR... Ezberlemenize gerek yok. Aşağıdaki mini sözlüğü, "bu neydi?" dediğiniz an dönüp bakmak için bırakıyorum: - **SMTP (Simple Mail Transfer Protocol):** Mail gönderim protokolü. Sunucular birbirine port 25'ten mail bırakır, siz de kendi sunucunuza gönderilecek maili 465 veya 587'den teslim edersiniz. - **IMAP (Internet Message Access Protocol):** Mail okuma ve senkronizasyon protokolü. Mesajlar sunucuda kalır, aynı kutuya birden fazla cihazdan bağlanırsınız (port 143 ve 993). - **POP3:** Eski usul okuma protokolü. Mesajları istemciye indirir, istemci ayarına göre de genelde sunucudan siler. Bugün neredeyse herkes IMAP kullanıyor. - **MTA (Mail Transfer Agent):** Maili sunucular arasında taşıyan yazılım. Bu rehberde Postfix. - **MDA (Mail Delivery Agent):** Maili kullanıcının kutusuna yazan program. Bu rehberde Dovecot'un LDA'sı (`dovecot-lda`). - **MUA (Mail User Agent):** Kullanıcının maili okuduğu arayüz: Gmail web, Outlook, Thunderbird... Bu rehberde webmail için Roundcube. - **MX kaydı:** "Bu domain'e gelecek mail hangi sunucuya gidecek?" sorusunun DNS'teki cevabı. `orneksite.com` için MX kaydı `mail.orneksite.com` gibi bir host gösterir. - **SPF (Sender Policy Framework):** Domain adına mail göndermeye yetkili IP'leri listeleyen DNS TXT kaydı. Alıcı sunucu, maili gönderen IP'nin bu listede olup olmadığına bakar. - **DKIM (DomainKeys Identified Mail):** Gönderen sunucu maili private key ile imzalar, alıcı sunucu DNS'teki public key ile imzayı doğrular. Böylece mailin yolda değiştirilmediği anlaşılır. DNS'te TXT kaydı, sunucuda private key gerekir. - **DMARC (Domain-based Message Authentication, Reporting & Conformance):** SPF ve DKIM sonucu olumsuzsa alıcının ne yapacağını söyleyen politika: kabul et, karantinaya al ya da reddet. - **PTR / Reverse DNS:** IP adresinden host adına giden ters DNS kaydı. Mail sunucusunun IP'si `mail.orneksite.com` gibi bir ada çözülmeli. Google'ın gönderici kuralları bunu açıkça istiyor, bu kayıt yoksa mailleriniz büyük olasılıkla reddedilir. - **Let's Encrypt / Certbot:** Ücretsiz SSL/TLS sertifikası veren otorite (Let's Encrypt) ve sertifikayı otomatik alan araç (certbot). Sertifikayı hem mail bağlantılarının TLS'i hem webmail'in HTTPS'i için kullanacağız. - **Milter:** "Mail filter" kısaltması. Postfix'in mail alırken ve gönderirken dışarıdan çağırdığı filtre programları. OpenDKIM ve OpenDMARC Postfix'e bu yolla bağlanıyor. - **Forward:** Bir adrese gelen maili otomatik olarak başka bir adrese ileten kural. Postfix'te `virtual_alias_maps` ile tanımlanıyor; cPanel'deki "Forwarders" ekranının sunucu tarafındaki karşılığı gibi düşünebilirsiniz. - **SRS (Sender Rewriting Scheme):** Forward edilen mailin zarf göndericisini (envelope sender) yeniden yazan mekanizma. Forward sırasında SPF'in kırılmasını önler. Ubuntu'da `postsrsd` paketiyle geliyor. Şimdilik bu kadarı yeter, gerisini yeri geldikçe anlatacağım. ## Ön Gereksinimler **Sunucu:** - Sabit IP'li bir VPS. Ben 2-3 yıldır Hetzner Cloud kullanıyorum; DigitalOcean, Linode, Turhost gibi başka bir sağlayıcı da olur. Önemli olan sabit IP ve port 25'in açılabilmesi. - Ubuntu 24.04 LTS. Rehberdeki komutların hepsi Ubuntu/Debian tabanlı sistemler için. - 2 GB RAM ve 20 GB disk yetiyor. **Domain ve DNS:** - En az bir domain (örneğin `orneksite.com`). Rehber birden fazla domain için de geçerli. Gerçek domain'lerimi güvenlik nedeniyle örnek adlarla değiştirdim. - DNS için Cloudflare kullanıyorum ve öneriyorum. Ücretsiz planı yetiyor, paneli de rahat. **Bilgi ve erişim:** - Temel Linux komut satırı bilgisi (dosya düzenleme, servis yönetimi) - SSH erişimi (root ya da sudo yetkili kullanıcı) - Bolca sabır :) İlk seferde her şey çalışmayabilir, bu çok normal. Tam da bu yüzden karşılaştığım her hatayı yazıya ekledim. ![Hetzner Cloud Panel - Sunucu genel görünümü](/images/posts/027-self-hosted-mail-sunucusu-sifirdan-kurulum-rehberi/img/ss1-hetzner-cloud-panel-sunucu-genel-gorunumu.webp) > **Cloud-init tuzağı (Hetzner Cloud ve benzerleri):** Bu tuzak sinsi, çünkü ilk reboot'a kadar her şey çalışıyor gibi görünüyor. Hetzner Cloud'da sunucu cloud-init ile hazırlanıyor. `/etc/cloud/cloud.cfg` içinde `manage_etc_hosts: True` varsa `/etc/hosts`'a elle eklediğiniz `127.0.1.1 mail.orneksite.com mail` satırı reboot'ta sessizce siliniyor. Aynı dosyada `preserve_hostname: false` ise cloud-init hostname'i de eski hâline döndürüyor. İlk kurulumumda reboot sonrası mail servisleri kendi adını bulamaz oldu ve ben uzun süre yanlışı başka yerde aradım. Çözüm iki yoldan biri: `manage_etc_hosts: false` ve `preserve_hostname: true` yapmak ya da `/etc/cloud/templates/hosts.debian.tmpl` dosyasına mail hostname'ini kalıcı olarak yazmak. > **Not:** Birden fazla domain'in nasıl ayarlandığını göstermek için iki domain kullanıyorum: `orneksite.com` (birincil) ve `ikincidomain.com` (ikincil). Komutlarda ve config dosyalarında bu adları gördüğünüz her yere kendi domain'inizi yazın. ## Gerekli Paketlerin Kurulumu Paket yöneticisi olarak [nala](https://gitlab.com/volian/nala) kullanıyorum. APT'nin üzerine kurulu bir arayüz: çıktısı daha okunaklı, paralel indirme yapıyor, işlem geçmişi tutuyor. Klasik APT kullanıyorsanız komutlardaki `nala` yerine `apt` yazmanız yeterli.
Nala kurulumu (isteğe bağlı) ```bash sudo apt install -y nala ``` Kurulduktan sonra en hızlı mirror'ları seçmek için: ```bash sudo nala fetch ```
Önce sistemi güncelliyorum: ```bash sudo nala update && sudo nala upgrade -y ``` > **Not:** Config dosyalarını alışkanlıktan `sudo vim ...` ile düzenliyorum. Sisteminizde yalnızca `vim-tiny` varsa tam sürümü `sudo nala install -y vim` ile kurabilirsiniz. Vim'e alışık değilseniz komutlardaki `vim` yerine `nano` yazın, fark etmez. Sonra paketleri tek tek kuruyorum. Her birinin ne işe yaradığını ve kurulduğunu nasıl doğrulayacağınızı da yazdım.
Postfix Kurulumu Postfix mail gönderip alan SMTP sunucusu, kurulumun merkezinde o duruyor. ```bash sudo nala install postfix -y ``` Kurulumda yapılandırma ekranı çıkarsa "Internet Site" seçin, "System mail name" alanına da ana domain'inizi yazın (örneğin `orneksite.com`). > **İpucu (sorusuz kurulum):** Postfix kurulurken iki sayfalık mor bir ekran açılıyor: mail tipi ve mailname. Bir kurulum betiği yazıyorsanız ya da SSH oturumunuzun bu ekranda asılı kalmasını istemiyorsanız cevapları önceden `debconf`'a verin. Ben `tmux` içinde çalışırken bu ekranı fark etmedim ve terminalin donduğunu sandım. > > ```bash > echo "postfix postfix/mailname string orneksite.com" | debconf-set-selections > echo "postfix postfix/main_mailer_type string 'Internet Site'" | debconf-set-selections > DEBIAN_FRONTEND=noninteractive apt install -y postfix postfix-pcre > ``` > > Kurulum böylece hiçbir şey sormadan bitiyor. `postfix-pcre` paketini de aynı anda kurun, ileride `login_maps.pcre` için lazım olacak. Doğrulama: ```bash postconf mail_version # Çıktı: mail_version = 3.8.6 gibi bir satır systemctl status postfix ```
Dovecot Kurulumu Dovecot mail kutusuna IMAP ve POP3 üzerinden erişimi sağlıyor. Roundcube da dahil her mail istemcisi, mailleri okumak için Dovecot'a bağlanıyor. ```bash sudo nala install dovecot-imapd dovecot-pop3d dovecot-sieve dovecot-lmtpd -y ``` - `dovecot-imapd` - IMAP desteği - `dovecot-pop3d` - POP3 desteği - `dovecot-sieve` - Mail filtreleme kuralları (örneğin spam'i otomatik Junk'a taşıma) - `dovecot-lmtpd` - LMTP ile yerel teslimat (bu rehberde teslimatı LDA yapıyor, paket ileride LMTP'ye geçmek isterseniz hazır dursun diye) Doğrulama: ```bash dovecot --version systemctl status dovecot ```
OpenDKIM Kurulumu OpenDKIM gönderdiğiniz maillere dijital imza ekliyor, alıcı sunucu da bu imzayla mailin gerçekten sizden geldiğini doğruluyor. SPF ve DMARC ile birlikte çalışıyor. ```bash sudo nala install opendkim opendkim-tools -y ``` > **Not:** Birçok rehber burada `usermod -aG opendkim postfix` ile Postfix'i `opendkim` grubuna eklemenizi söyler. Bu, OpenDKIM'e unix soket üzerinden bağlanırken gerekiyor. Ben OpenDKIM'i TCP (`inet:12301`) üzerinden bağlıyorum, o yüzden gerek yok. Üstelik OpenDKIM, anahtar dosyası grup tarafından okunabiliyorsa ve grupta başka üye varsa anahtarı "güvensiz" sayıp imzalamayı reddediyor (Troubleshooting tablosundaki 8. satır). Doğrulama: ```bash opendkim -V # veya nala show opendkim ```
OpenDMARC ve SPF Politika Servisi Kurulumu OpenDKIM bizim maillerimizi imzalıyor. Başkasının adına gelen sahte bir maili reddetmek ise ayrı bir iş. Onu OpenDMARC (gelen mailde DMARC politikasını uygular) ve `policyd-spf` (SMTP aşamasında SPF kontrolü) üstleniyor. Rehberin ilk hâlinde bu ikisi yoktu ve sunucum büyük markaların adresini taklit eden mailleri sorgusuz kabul ediyordu. Neden gerekli olduklarını "Spoofing Savunması" bölümünde anlattım. ```bash sudo nala install opendmarc postfix-policyd-spf-python swaks -y ``` - `opendmarc` - Gelen maillerde DMARC politikasını uygulayan milter - `postfix-policyd-spf-python` - Postfix için SPF policy servisi - `swaks` - SMTP test aracı (rehberin sonundaki spoofing testlerinde kullanacağız) > **Not:** Kurulumda OpenDMARC "veritabanı dbconfig-common ile yapılandırılsın mı?" diye sorarsa "No" deyin. O veritabanı yalnızca DMARC rapor geçmişi için, bu rehberde kullanmıyoruz. Doğrulama: ```bash opendmarc -V ls -la /usr/bin/policyd-spf ```
SpamAssassin Kurulumu SpamAssassin gelen maillere spam puanı veriyor. ```bash sudo nala install spamassassin spamc -y sudo systemctl enable spamd sudo systemctl start spamd ``` > **Dikkat (Ubuntu 24.04):** Servisin adı artık `spamassassin` değil, `spamd`. `systemctl enable spamassassin` yazınca `Unit not found` aldım. `systemctl` komutlarında hep `spamd` kullanın. Doğrulama: ```bash systemctl status spamd ```
Certbot / Let's Encrypt Kurulumu Sertifikasız bir mail sunucusu şifreleri ve mailleri açık metin taşır. Google'ın gönderici kuralları da mail aktarımında TLS istiyor; Gmail, TLS'siz gelen maili kırmızı kilit simgesiyle işaretliyor. ```bash sudo nala install certbot -y ``` Nginx henüz kurulu değilse sertifikayı standalone modda alabilirsiniz (port 80 boş olmalı): ```bash sudo certbot certonly --standalone -d mail.orneksite.com ``` Nginx çalışıyorsa: ```bash sudo certbot certonly --nginx -d mail.orneksite.com ``` Sertifikalar `/etc/letsencrypt/live/mail.orneksite.com/` altına düşüyor.
Nginx Kurulumu Nginx, Roundcube'un önünde reverse proxy olarak duracak ve HTTPS'i o karşılayacak. ```bash sudo nala install nginx -y sudo systemctl enable nginx ```
Docker ve Docker Compose Kurulumu Roundcube'u ve veritabanını Docker'da çalıştırıyorum, böylece ana sistemden ayrı duruyorlar. ```bash # Docker'ın resmi GPG anahtarını ekle sudo nala install ca-certificates curl gnupg -y sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpg # Docker repo'sunu ekle echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null # Kur sudo nala update sudo nala install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -y ``` Doğrulama: ```bash docker --version docker compose version ```
Fail2ban Kurulumu Fail2ban log'ları izleyip tekrar tekrar hatalı giriş deneyen IP'leri otomatik banlıyor. ```bash sudo nala install fail2ban -y sudo systemctl enable fail2ban sudo systemctl start fail2ban ```
UFW (Firewall) Kurulumu UFW, Ubuntu'nun sade firewall aracı. ```bash sudo nala install ufw -y ``` Şimdilik yalnızca SSH'yi açıp etkinleştiriyorum, gerisini Adım 11'de yapacağız: ```bash sudo ufw allow ssh sudo ufw enable ``` > **Uyarı:** SSH'ye izin vermeden `ufw enable` derseniz sunucuya erişiminizi kaybedersiniz.
## Mimari Genel Bakış Kuruluma geçmeden resmin tamamına bakalım. Hangi parçanın hangisiyle konuştuğunu bilmek, ileride hata ararken çok zaman kazandırdı. ```mermaid flowchart LR DNS["Cloudflare DNS
(DNS only)"] subgraph SUNUCU["SUNUCU"] direction TB Postfix["Postfix
:25 / :587 / :465"] Dovecot["Dovecot
:993 / :143"] Roundcube["Roundcube
:8080"] OpenDKIM["OpenDKIM
:12301"] OpenDMARC["OpenDMARC
:54321"] PolicydSPF["policyd-spf"] SpamAssassin["SpamAssassin"] PostgreSQL["PostgreSQL
(Docker)"] Nginx["Nginx
:443"] Fail2ban["Fail2ban"] Postfix --> Dovecot Roundcube --> Dovecot Postfix --> OpenDKIM Postfix --> OpenDMARC Postfix -. policy .-> PolicydSPF Postfix -. content_filter .-> SpamAssassin Roundcube --> PostgreSQL end DNS -->|MX/25| Postfix DNS -->|SMTP/587| Postfix DNS -->|HTTPS/443| Nginx ``` **Port haritası:** | Port | Protokol | Açıklama | |------|------------|---------------------------------| | 25 | SMTP | Sunucular arası mail alışverişi | | 143 | IMAP | Mail okuma (STARTTLS ile) | | 443 | HTTPS | Roundcube web arayüzü (Nginx) | | 465 | SMTPS | Mail gönderme (SSL/TLS sarmalı) | | 587 | Submission | Mail gönderme (STARTTLS) | | 993 | IMAPS | Mail okuma (SSL/TLS sarmalı) | **Neden bu mimari?** Roundcube ile PostgreSQL'i Docker'a koydum. Webmail internete açık tek PHP uygulaması; bir açık çıkarsa saldırgan önce container'ın içinde kalıyor ve host'a ulaşması zorlaşıyor. Postfix ve Dovecot'u host'ta bıraktım, çünkü portlara ve kullanıcıların home dizinlerine doğrudan erişmeleri gerekiyor. Bu karışık düzen bana hem güvenlik hem yönetim kolaylığı açısından dengeli geldi. ## Adım 1: Yedekleme Kullanımda olan bir sunucuda çalışıyorsanız hiçbir şeye dokunmadan önce yedek alın. "Bana bir şey olmaz" diyenlere söyleyeyim: bana oldu :( ```bash sudo cp -r /etc/postfix /etc/postfix.backup.$(date +%Y%m%d) sudo cp -r /etc/dovecot /etc/dovecot.backup.$(date +%Y%m%d) sudo cp -r /etc/opendkim.conf /etc/opendkim.conf.backup.$(date +%Y%m%d) sudo cp -r /etc/nginx /etc/nginx.backup.$(date +%Y%m%d) sudo cp -r /etc/fail2ban /etc/fail2ban.backup.$(date +%Y%m%d) ``` > **Not:** Sıfırdan kurulan bir sunucuda bu adımı atlayabilirsiniz, ama alışkanlık edinmekte fayda var. ## Adım 2: Sistem Kullanıcıları Bu kurulumda her mail kutusu bir Linux kullanıcısına bağlı. Bu kullanıcıların shell erişimi olmuyor, yalnızca mail kutusunun sahibi oluyorlar. ```bash # orneksite.com domain'i için kullanıcı sudo useradd -m -s /usr/sbin/nologin -G mail kullanici1 # ikincidomain.com domain'i için kullanıcı sudo useradd -m -s /usr/sbin/nologin -G mail kullanici2 ``` Şifre belirleyin (bu hash'i birazdan Dovecot'a taşıyacağız): ```bash sudo passwd kullanici1 sudo passwd kullanici2 ``` Maildir klasörlerini oluşturun: ```bash # kullanici1 sudo mkdir -p /home/kullanici1/Mail/Inbox sudo mkdir -p /home/kullanici1/Mail/Sent sudo mkdir -p /home/kullanici1/Mail/Drafts sudo mkdir -p /home/kullanici1/Mail/Trash sudo mkdir -p /home/kullanici1/Mail/Junk sudo chown -R kullanici1:kullanici1 /home/kullanici1/Mail # kullanici2 //... ``` ### PAM'dan passwd-file'a Geçiş: İlk Büyük Karar İlk planım Dovecot'un varsayılan PAM doğrulamasını kullanmaktı. O zaman kullanıcılar `kullanici1`, `kullanici2` gibi Linux kullanıcı adlarıyla giriş yapacaktı. Oysa ben `ben@orneksite.com` gibi adreslerle giriş yapmak istiyordum. Linux kullanıcısı `kullanici1`, adres `ben@orneksite.com`; PAM bu eşlemeyi bilemiyordu. Çözüm Dovecot'un `passwd-file` doğrulamasına geçmek oldu. Hangi adresin hangi kullanıcıya ait olduğunu kendim bir dosyada tanımlıyorum. Ayrıntısı Adım 7'de. ## Adım 3: Cloudflare DNS Yapılandırması "Daha Postfix'i kurmadan DNS'le neden uğraşıyoruz?" diye düşünebilirsiniz. Sebebi şu: Adım 9'da alacağımız Let's Encrypt sertifikası, `mail.orneksite.com` için doğrulama yapabilmek adına `A` kaydının yayında olmasını istiyor. DNS değişikliklerinin yayılması da bazen 5 dakika, bazen birkaç saat sürüyor. İlk kurulumumda bu sırayı bilmediğim için sertifika aşamasında takıldım. İkincisinde DNS'i en başa aldım; ben Postfix ve Dovecot'la uğraşırken kayıtlar çoktan yayılmıştı. DNS kayıtları yanlışsa mailleriniz hiçbir yere ulaşmaz. Burada bir kuralı öğrenmem de epey zamanımı aldı: > **Kural:** Mail ile ilgili bütün DNS kayıtları Cloudflare'de "DNS only" (gri bulut) modunda olmalı. Turuncu bulut (proxy) yalnızca web trafiğini taşıyor, SMTP'yi taşımıyor. Bunu atlarsanız mailin neden gitmediğini uzun süre başka yerde ararsınız. ### orneksite.com Domain'i | Tür | Ad | İçerik | Proxy | |------|-----|--------|-------| | A | mail | `SUNUCU_IP_ADRESINIZ` | DNS only (gri bulut) | | AAAA | mail | `SUNUCU_IPV6_ADRESINIZ` | DNS only (gri bulut) | | MX | orneksite.com | `mail.orneksite.com` (öncelik: 10) | - | | TXT | orneksite.com | `v=spf1 mx a:mail.orneksite.com ip4:SUNUCU_IP ip6:SUNUCU_IPV6 -all` | - | | TXT | _dmarc | `v=DMARC1; p=quarantine; rua=mailto:postmaster@orneksite.com; fo=1` | - | > **Not:** SPF'te sunucu IP'sini `ip4` ve `ip6` ile açıkça yazmayı tercih ediyorum. `mx` ve `a:` mekanizmaları her kontrolde ek DNS sorgusu gerektiriyor ve SPF'in 10 sorguluk sınırından yiyor; açık IP'nin böyle bir maliyeti yok. Sunucunuzun IPv6'sı varsa AAAA kaydını da ekleyin. Postfix mail gönderirken IPv6'yı da kullanabiliyor ve IPv6 adresi SPF'te yoksa Gmail gibi alıcılar o maili reddedebiliyor. > > Tabloda DKIM kaydı yok, çünkü anahtarı henüz üretmedik. Anahtarı Adım 6'da üreteceğiz, `mail._domainkey` TXT kaydını da orada ekleyeceğiz. Oraya geldiğimizde hatırlatacağım. ### ikincidomain.com Domain'i (Sıfırdan Ekleme) İkinci domain için aynı yapıyı tekrarlıyorum. `ip4`/`ip6` zaten aynı sunucuyu kapsıyor; SPF'e `a:mail.orneksite.com` eklemem, `mail.ikincidomain.com`'u ileride başka bir IP'ye taşısam bile ana sunucunun yetkili kalması için: | Tür | Ad | İçerik | Proxy | |------|-----|--------|-------| | A | mail | `SUNUCU_IP_ADRESINIZ` | DNS only (gri bulut) | | AAAA | mail | `SUNUCU_IPV6_ADRESINIZ` | DNS only (gri bulut) | | MX | ikincidomain.com | `mail.ikincidomain.com` (öncelik: 10) | - | | TXT | ikincidomain.com | `v=spf1 mx a:mail.ikincidomain.com a:mail.orneksite.com ip4:SUNUCU_IP ip6:SUNUCU_IPV6 -all` | - | | TXT | _dmarc | `v=DMARC1; p=quarantine; rua=mailto:postmaster@ikincidomain.com; fo=1` | - | ### DNS Kayıt Detayları **SPF:** `v=spf1 mx a:mail.orneksite.com ip4:... ip6:... -all` kaydı şunu söylüyor: "Benim adıma yalnızca MX sunucum, `mail.orneksite.com` ve bu IP'ler mail gönderebilir; geri kalan her şeyi reddedin." Sondaki `-all` bu "reddedin" kısmı. **DMARC:** `p=quarantine` ile başladım. Bu politika, SPF ve DKIM'den geçemeyen maillerin spam klasörüne atılmasını istiyor. Her şey oturunca `p=reject`'e geçmek gerekiyor; kademeli geçişi "Güvenlik Sıkılaştırma" bölümünde anlattım. ### DNS Doğrulama ```bash # Kur nala install -y bind9-dnsutils # Hata verirse veya terminal etkileşimi takılırsa bunu dene DEBIAN_FRONTEND=noninteractive nala install -y bind9-dnsutils # Zaten kuruluysa, dig binary'sinin PATH'te olduğunu doğrula which dig 2>&1; dpkg -L bind9-dnsutils 2>&1 | grep bin/dig # Kurulu ama "command not found" diyorsa shell'in komut önbelleğini temizleyin hash -r ``` ```bash # MX kaydını doğrula dig MX orneksite.com +short # Beklenen: 10 mail.orneksite.com. # SPF kaydını doğrula dig TXT orneksite.com +short # Beklenen: "v=spf1 mx a:mail.orneksite.com ip4:SUNUCU_IP ip6:SUNUCU_IPV6 -all" # DMARC kaydını doğrula dig TXT _dmarc.orneksite.com +short # Beklenen: "v=DMARC1; p=quarantine; ..." ``` `dig TXT mail._domainkey.orneksite.com` şu an boş döner, DKIM kaydını henüz eklemedik. > **Not:** DNS değişikliklerinin yayılması birkaç dakikadan birkaç saate kadar sürebiliyor. Beklemeyin, siz sonraki adımlara geçin. ![Cloudflare DNS kayıtları - orneksite.com](/images/posts/027-self-hosted-mail-sunucusu-sifirdan-kurulum-rehberi/img/ss2-cloudflare-dns-kayitlari-orneksite-com.webp) ## Adım 4: Hetzner PTR (Reverse DNS) Kaydı PTR, IP adresinden host adına giden ters DNS kaydı. Gmail, Outlook, Yahoo gibi büyük sağlayıcılar gelen mailde buna bakıyor. PTR yoksa ya da yanlışsa mailleriniz spam'e düşüyor veya doğrudan reddediliyor. Bu kaydı DNS ile birlikte baştan halletmek en iyisi. Hetzner panelinde iki dakikalık iş, sonra aklınızda kalmıyor. ### IPv4 PTR Kaydı 1. Hetzner Cloud Panel'e giriş yapın 2. Sunucunuzu seçin 3. **Networking** sekmesine gidin 4. IP adresinizin yanındaki **"..."** menüsüne tıklayın 5. **"Edit Reverse DNS"** seçin 6. `mail.orneksite.com` yazın ve kaydedin ### IPv6 PTR Kaydı Aynısını IPv6 için de yapın. Hetzner her sunucuya bir IPv6 /64 bloğu veriyor; sunucunun kullandığı birincil IPv6 adresine de `mail.orneksite.com` yazın. ![Hetzner Cloud Panel - Networking - Reverse DNS](/images/posts/027-self-hosted-mail-sunucusu-sifirdan-kurulum-rehberi/img/ss3-hetzner-cloud-panel-networking-reverse-dns.webp) ### Doğrulama ```bash # IPv4 PTR dig -x SUNUCU_IP_ADRESINIZ +short # Beklenen: mail.orneksite.com. # Veya host komutuyla host SUNUCU_IP_ADRESINIZ # Beklenen: 10.113.0.203.in-addr.arpa domain name pointer mail.orneksite.com. ``` ## Adım 5: Postfix Yapılandırması Sıra Postfix'te. Bütün kurulumda en çok ayar isteyen parça bu. ### main.cf - Ana Yapılandırma `/etc/postfix/main.cf` dosyasını düzenleyin: > **Uyarı:** Config'de `/etc/letsencrypt/live/mail.orneksite.com/` altındaki sertifika yollarını göreceksiniz. O dosyalar şu an yok; sertifikayı Adım 9'da alacağız. Postfix'i de Adım 10'a kadar başlatmıyoruz, o zamana kadar sertifika hazır olacak. Şimdilik yalnızca yolları yazıyoruz. ```ini # Temel ayarlar smtpd_banner = $myhostname ESMTP $mail_name (Ubuntu) biff = no append_dot_mydomain = no readme_directory = no compatibility_level = 3.6 # TLS parametreleri smtpd_tls_cert_file = /etc/letsencrypt/live/mail.orneksite.com/fullchain.pem smtpd_tls_key_file = /etc/letsencrypt/live/mail.orneksite.com/privkey.pem smtpd_tls_security_level = may smtp_tls_CApath=/etc/ssl/certs smtp_tls_security_level = may smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache # TLS kurulmadan AUTH yok (şifreler ağda açık metin dolaşmasın) smtpd_tls_auth_only = yes # Eski, güvensiz protokolleri devre dışı bırak smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1 smtp_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1 smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1 smtp_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1 # SASL kimlik doğrulama (Dovecot üzerinden) smtpd_sasl_auth_enable = yes smtpd_sasl_type = dovecot smtpd_sasl_path = private/auth smtpd_sasl_security_options = noanonymous, noplaintext smtpd_sasl_tls_security_options = noanonymous # Relay kısıtlamaları smtpd_relay_restrictions = permit_sasl_authenticated, reject_unauth_destination # Domain ve ağ ayarları myhostname = mail.orneksite.com mydomain = orneksite.com myorigin = /etc/mailname mydestination = $myhostname, $mydomain, ikincidomain.com, mail, localhost.localdomain, localhost, localhost.$mydomain relayhost = mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128 mail_name = orneksite.com alias_maps = hash:/etc/aliases alias_database = hash:/etc/aliases # Genel mailbox_size_limit = 0 recipient_delimiter = + inet_interfaces = all inet_protocols = all home_mailbox = Mail/Inbox/ # Gönderici kısıtlamaları # Bu satır fiilen yalnızca port 25'e (sunucular arası) uygulanır; 465/587 master.cf'te kendi # kurallarını taşır. Port 25'te AUTH kapalı olduğu için sahiplik kontrolü (login_mismatch) # burada çalışamaz; kendi domain'imizin zarf taklidini SPF (-all) + policyd-spf keser. smtpd_sender_login_maps = pcre:/etc/postfix/login_maps.pcre smtpd_sender_restrictions = reject_non_fqdn_sender, permit_mynetworks, reject_unknown_reverse_client_hostname, reject_unknown_sender_domain smtpd_recipient_restrictions = permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination, reject_unknown_recipient_domain, check_policy_service unix:private/policyd-spf policyd-spf_time_limit = 3600 # HELO kısıtlamaları smtpd_helo_required = yes smtpd_helo_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_invalid_helo_hostname, reject_non_fqdn_helo_hostname, reject_unknown_helo_hostname # Başlık gizleme (gizlilik için) header_checks = regexp:/etc/postfix/header_checks # Milter'lar: OpenDKIM (imza + doğrulama) ve OpenDMARC (gelen mailde DMARC politikası) # non_smtpd_milters BİLEREK YOK: SpamAssassin'den sendmail ile geri dönen mail # milter'dan ikinci kez geçerse OpenDKIM onu "yerel" sanıp bizim anahtarla imzalıyor. milter_default_action = accept milter_protocol = 6 smtpd_milters = inet:localhost:12301, inet:localhost:54321 # Dovecot LDA ile teslimat mailbox_command = /usr/lib/dovecot/deliver # SMTP smuggling koruması smtpd_forbid_bare_newline = normalize smtpd_forbid_bare_newline_exclusions = $mynetworks # Sanal alias eşlemeleri virtual_alias_maps = hash:/etc/postfix/virtual ``` Satırların çoğu kendini anlatıyor. Üzerinde durmam gerekenler şunlar: - **`mynetworks`**: Yalnızca loopback. İlk kurulumda buraya "Roundcube bağlanabilsin" diye `172.16.0.0/12` Docker aralığını da yazmıştım. Gereksizmiş, çünkü Roundcube 465'e şifreyle bağlanıyor. Daha komiği, benim sunucumda Roundcube container'ı `192.168.64.x` aralığındaydı; o satır onu kapsamıyordu bile. Yaptığı tek şey, sunucudaki herhangi bir container'a şifresiz bağlanıp yerel kutulara istediği gönderici adıyla mail bırakma imkânı vermekti. - **`smtpd_sender_restrictions`**: Postfix kısıtlamaları soldan sağa okuyor ve ilk `permit`'te duruyor. İlk hâlde bu satırda `permit_sasl_authenticated` başta, `reject_sender_login_mismatch` arkadaydı. Yani giriş yapmış birinin hangi adresle gönderdiğine hiç bakılmıyordu. 465 portu da bu satırı miras aldığı için, Laravel `.env` dosyasındaki `no-reply@` şifresini ele geçiren biri `ben@orneksite.com` adına mail atabilir ve alıcıda SPF/DKIM/DMARC'tan geçerdi. Sahiplik kontrolünü artık şifreyle giriş yapılan iki portta (465 ve 587) `master.cf` üzerinden yapıyorum. Buradaki satır yalnızca port 25'e kalıyor. Port 25'te AUTH kapalı olduğu için `reject_sender_login_mismatch` orada zaten çalışmıyor; Postfix onu yok sayıp her bağlantıda log'a `ignored: no SASL support` uyarısı basıyor. Bunu kendi log'umda görünce satırdan çıkardım. Port 25'te kendi domain'imin zarf taklidini SPF kesiyor (bir alttaki madde ve Test 4). - **`check_policy_service ... policyd-spf`**: Dışarıdan gelen mailde SMTP aşamasında SPF kontrolü. Gönderen domain'in SPF'i `-all` ile "bu IP yetkili değil" diyorsa mail içeri alınmadan reddediliyor. Giriş yapmış kullanıcılar ve `mynetworks` bu kontrole girmiyor, çünkü listede daha önce `permit` alıyorlar. - **`smtpd_forbid_bare_newline`**: SMTP smuggling saldırısına karşı koruma. Postfix 3.9 ile geldi, 3.8 serisinin güncel yamalarına da taşındı; Ubuntu 24.04'teki 3.8.6'da çalışıyor. - **`non_smtpd_milters` yok**: İlk hâlde vardı ve sessiz ama ciddi bir açığa yol açıyordu. Gelen mail SpamAssassin'den geçip `sendmail` ile Postfix'e geri veriliyor. Postfix bu yolla gelen maili milter'a "localhost, 127.0.0.1" olarak gösteriyor. OpenDKIM de 127.0.0.1'i iç ağ saydığı için dışarıdan gelen sahte bir `From: ceo@orneksite.com` mailini benim anahtarımla imzalıyordu. Test ederken log'da `DKIM-Signature field added (s=mail, d=...)` satırını görünce kanım dondu. Satırı kaldırınca SpamAssassin dönüşü milter'a hiç uğramıyor. Kendi maillerim yine imzalanıyor, çünkü onlar 465/587'den `smtpd_milters` yoluyla geçiyor. ### master.cf - Servis Tanımları `/etc/postfix/master.cf` dosyasının sonuna şunları ekleyin (dosyada `smtp`, `submission`, `smtps` tanımları zaten varsa onları bu hâle getirin): ```properties # SpamAssassin filtresi spamassassin unix - n n - - pipe user=debian-spamd argv=/usr/bin/spamc -f -e /usr/sbin/sendmail -oi -f ${sender} ${recipient} # SPF policy servisi (postfix-policyd-spf-python) policyd-spf unix - n n - 0 spawn user=policyd-spf argv=/usr/bin/policyd-spf # SMTP (25) - gelen mailler SpamAssassin'den geçer # Port 25 sunucular arası trafik içindir, burada şifreyle giriş (AUTH) kapalı smtp inet n - y - - smtpd -o content_filter=spamassassin -o smtpd_sasl_auth_enable=no # Submission (587) - kimlik doğrulamalı mail gönderme submission inet n - y - - smtpd -o syslog_name=postfix/submission -o smtpd_tls_security_level=encrypt -o smtpd_tls_auth_only=yes -o smtpd_enforce_tls=yes -o smtpd_client_restrictions=permit_sasl_authenticated,reject -o smtpd_sender_restrictions=reject_sender_login_mismatch -o smtpd_sender_login_maps=pcre:/etc/postfix/login_maps.pcre -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject_unauth_destination # SMTPS (465) - SSL sarmalı mail gönderme # Roundcube ve Laravel bu portu kullanıyor: kısıtlamalar 587 ile birebir aynı olmalı smtps inet n - y - - smtpd -o syslog_name=postfix/smtps -o smtpd_tls_wrappermode=yes -o smtpd_sasl_auth_enable=yes -o smtpd_client_restrictions=permit_sasl_authenticated,reject -o smtpd_sender_restrictions=reject_sender_login_mismatch -o smtpd_sender_login_maps=pcre:/etc/postfix/login_maps.pcre -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject_unauth_destination ``` > **465'i neden ayrıca kısıtlıyorum?** İlk hâlde `smtps` bloğunda yalnızca TLS ve AUTH ayarları vardı, gönderici kısıtlaması `main.cf`'ten geliyordu. Oradaki sıra da yanlış olunca 465 "giriş yaptıysan istediğin adresle gönder" kapısına dönmüştü. 587'yi sıkı tutup Roundcube'un ve Laravel'in kullandığı 465'i açık bırakmışım; ön kapıyı kilitleyip arka kapıyı açık unutmak gibi. Şimdi iki port aynı kuralları taşıyor. > **Port 25'te AUTH'u neden kapattım?** Port 25, diğer mail sunucularının bana mail bıraktığı kapı; orada kimse şifreyle giriş yapmaz. Kendi sunucumun bir haftalık log'unda port 25'te tek bir başarılı giriş yoktu, başarısız denemeler ise vardı. AUTH'u kapatınca bu saldırı yüzeyi ortadan kalktı. Kapatmadan önce kendi log'unuza bakın: `sudo grep 'postfix/smtpd\[' /var/log/mail.log | grep 'client=.*sasl_username'` boş dönüyorsa gönül rahatlığıyla kapatabilirsiniz. Tek yan etkisi, `reject_sender_login_mismatch` ailesinin SASL kapalı portta çalışmaması. O yüzden kendi domain'lerinizin SPF kaydı `-all` ile bitmeli; port 25'teki zarf taklidine karşı korumayı artık SPF yapıyor. ### login_maps.pcre - Gönderici Kısıtlamaları Hangi kullanıcının hangi adresle mail gönderebileceğini bu dosya belirliyor. `/etc/postfix/login_maps.pcre`: ```plaintext /^ben@orneksite\.com$/ ben@orneksite.com /^info@ikincidomain\.com$/ info@ikincidomain.com ``` Solda gönderici adresi, sağda o adresi kullanmaya yetkili SASL kullanıcı adı duruyor. Dovecot'ta kullanıcı adı zaten e-posta adresi olduğu için iki taraf aynı görünüyor. 465 ve 587'deki `reject_sender_login_mismatch` bu dosyaya bakıyor: giriş yapan kullanıcının kullandığı adres dosyada yoksa ya da sahibi başkasıysa mail `Sender address rejected: not owned by user ...` ile reddediliyor. Dışarıdan şifresiz gelip sizin adresinizi taklit eden bağlantılar (port 25) ise bu dosyaya değil, SPF'e takılıyor. > **Sınır:** Bu kontrol yalnızca zarf göndericisine, yani SMTP'deki `MAIL FROM` adresine bakıyor. Alıcının ekranda gördüğü `From:` başlığı ayrı bir alan ve Postfix onu bu dosyayla karşılaştırmıyor. Bu açığı "Spoofing Savunması" bölümünde anlattım. ### header_checks - Başlık Gizleme Giden maillerdeki bazı iç bilgileri gizlemek mümkün. Yalnız burada bir tuzağa düştüm. > **Uyarı: `Received` başlıklarını silmeyin.** > > İlk kurulumda gizlilik kaygısıyla `Received` başlıklarını da silmiştim. Sonuç: bütün maillerim spam'e düştü. SPF, DKIM, DMARC doğru, IP temiz... Uzun süre nedenini bulamadım. Sonunda SpamAssassin raporunda `NO_RECEIVED` ve `NO_RELAYS` işaretlerini gördüm. `Received` başlıkları mailin geçtiği sunucu zincirini gösteriyor; hiç olmaması, izini saklamaya çalışan bir gönderici izlenimi veriyor. `Received` silen kuralı kaldırdım. Gizlilik için yaptığım şey tam ters etki yapmıştı. > > `header_checks` dosyasında yalnızca `X-Originating-IP` satırını tutun, `Received` için kural yazmayın. `/etc/postfix/header_checks`: ```plaintext /^X-Originating-IP:/ IGNORE ``` `X-Originating-IP`, bazı istemcilerin eklediği ve sizin ev ya da ofis IP'nizi gösteren bir başlık; onu gizlemek makul. `Received` başlıkları ise mailin olağan parçası, onlara dokunmayın. ### virtual - Sanal Alias Eşlemeleri Mail adreslerini sistem kullanıcılarına bağlayan dosya. `/etc/postfix/virtual`: ```properties ben@orneksite.com kullanici1 info@ikincidomain.com kullanici2 ``` Hash dosyasını oluşturun: ```bash sudo postmap /etc/postfix/virtual ``` ## Adım 6: OpenDKIM - DKIM Anahtar Üretimi DKIM her giden maile kriptografik bir imza ekliyor, alıcı sunucu da DNS'teki public key ile bu imzayı doğruluyor. SPF ve DMARC ile birlikte, maillerimin spam'e düşmemesinde en çok işe yarayan ayar bu oldu. ### Anahtar Üretimi Her domain için ayrı anahtar üretiyorum: ```bash # orneksite.com için sudo mkdir -p /etc/postfix/dkim/orneksite.com sudo opendkim-genkey -b 2048 -d orneksite.com -D /etc/postfix/dkim/orneksite.com -s mail -v sudo chown opendkim:opendkim /etc/postfix/dkim/orneksite.com/mail.private # ikincidomain.com için sudo mkdir -p /etc/postfix/dkim/ikincidomain.com sudo opendkim-genkey -b 2048 -d ikincidomain.com -D /etc/postfix/dkim/ikincidomain.com -s mail -v sudo chown opendkim:opendkim /etc/postfix/dkim/ikincidomain.com/mail.private ``` DNS'e eklenecek public key, her domain klasöründeki `mail.txt` dosyasında duruyor. > **Şimdi Cloudflare'e dönüp `mail._domainkey` kaydını ekleyin:** Adım 3'te bu satırı anahtar henüz olmadığı için atlamıştık. Her domain için şu TXT kaydı gerekiyor: > > | Tür | Ad | İçerik | Proxy | > |------|-----|--------|-------| > | TXT | `mail._domainkey` | `v=DKIM1; h=sha256; k=rsa; p=MIIBIjANB...` (public key) | - | > > İçerik alanına `mail.txt`'nin tek satıra çevrilmiş hâli gidiyor; komutu hemen aşağıda veriyorum. `orneksite.com` ve `ikincidomain.com` için ayrı ayrı ekleyin. > **Cloudflare'e tek satırda yapıştırma:** `mail.txt`'yi `cat` ile açınca anahtar `"v=DKIM1; h=sha256; k=rsa; " "p=MIIBIjANB..." "..."` gibi birkaç satıra bölünmüş ve tırnaklı geliyor. Cloudflare'in TXT alanı tek satır istiyor. İlk seferinde tırnakları elle temizleyip satırları birleştirmeye çalıştım, bir karakter kaçırdım ve DKIM doğrulaması saatlerce başarısız oldu. Şimdi şu komutu kullanıyorum: > > ```bash > grep -oE '"[^"]*"' /etc/postfix/dkim/orneksite.com/mail.txt | tr -d '"' | tr -d '\n' > ``` > > Çıktıyı olduğu gibi Cloudflare'deki içerik alanına yapıştırıyorum, elle düzeltmeye gerek kalmıyor. ![DKIM public key örnek çıktısı (mail.txt)](/images/posts/027-self-hosted-mail-sunucusu-sifirdan-kurulum-rehberi/img/ss4-dkim-public-key-ornek-ciktisi-mail-txt.webp) ### OpenDKIM Yapılandırması `/etc/opendkim.conf`: ```properties Syslog yes SyslogSuccess yes Canonicalization relaxed/simple Mode sv OversignHeaders From AuthservID mail.orneksite.com UserID opendkim UMask 007 PidFile /run/opendkim/opendkim.pid TrustAnchorFile /usr/share/dns/root.key KeyTable file:/etc/postfix/dkim/keytable SigningTable refile:/etc/postfix/dkim/signingtable InternalHosts refile:/etc/postfix/dkim/trustedhosts Socket inet:12301@localhost ``` > **`AuthservID`'yi neden elle yazdım?** OpenDKIM doğrulama sonucunu maile `Authentication-Results: mail.orneksite.com; dkim=...` başlığıyla ekliyor. Birazdan kuracağımız OpenDMARC bu başlığa yalnızca tanıdığı bir isimle geliyorsa güveniyor. Varsayılan hâlde OpenDKIM Postfix'in adını (`mail.orneksite.com`) kullanıyor, OpenDMARC ise sunucunun hostname'ine güveniyor. Benim sunucumun hostname'i `mail.` ile başlamıyor, Hetzner'de sunucuya verdiğim ad duruyor; yani iki taraf farklı isimler görecekti. İki tarafa da aynı ismi elle yazınca bu risk kalkıyor. Bir de güzel yan etkisi var: OpenDKIM, dışarıdan gelen ve bu isimle sahte `dkim=pass` sonucu taşıyan `Authentication-Results` başlıklarını varsayılan olarak siliyor. Saldırgan maile "ben zaten doğrulandım" yazan bir başlık ekleyip kontrolü atlatamıyor. > **`/etc/default/opendkim` ile vakit kaybetmeyin.** İlk kurulumda refleksle bu dosyayı açtım, içinde `SOCKET=local:$RUNDIR/...` satırını görünce soketi buradan değiştirdim ve servisi yeniden başlattım. Çalışmadı. Bir daha denedim, yine olmadı. Yarım saat sonra dosyanın başındaki uyarıyı fark ettim: "This is a legacy configuration file. It is not used by the opendkim systemd service". Ubuntu 24.04'teki systemd servisi bu dosyayı hiç okumuyor. Gerçek soket ayarı `/etc/opendkim.conf`'taki `Socket inet:12301@localhost` satırı. ### KeyTable `/etc/postfix/dkim/keytable`: ```properties mail._domainkey.orneksite.com orneksite.com:mail:/etc/postfix/dkim/orneksite.com/mail.private mail._domainkey.ikincidomain.com ikincidomain.com:mail:/etc/postfix/dkim/ikincidomain.com/mail.private ``` ### SigningTable `/etc/postfix/dkim/signingtable`: ```properties *@orneksite.com mail._domainkey.orneksite.com *@ikincidomain.com mail._domainkey.ikincidomain.com ``` ### TrustedHosts `/etc/postfix/dkim/trustedhosts`: ```plaintext 127.0.0.1 ::1 ``` > **Bu dosyaya Docker aralığı yazmayın.** Rehberin ilk hâlinde burada `10.1.0.0/16` ve `172.16.0.0/12` de vardı, "Roundcube'dan giden mailler de imzalansın" diye. Yanılmışım. OpenDKIM bir maili iki durumda imzalıyor: istemci bu listedeyse ya da SMTP'de şifreyle giriş (SASL AUTH) yapmışsa. Bunu OpenDKIM'in kaynak kodunda da gördüm. Roundcube 465'e şifreyle bağlandığı için ikinci koşuldan zaten imzalanıyor; benim sunucumda Roundcube `192.168.64.x` aralığındaydı ve o satır onu kapsamıyordu bile. Docker aralığını buraya yazmak, sunucudaki her container'ın şifresiz gönderdiği maile sizin domain'inizin imzasını basmak demek. `127.0.0.1` yalnızca sunucunun kendi yerel süreçleri için duruyor; SpamAssassin dönüşünün bu kapıdan geçmemesini de `non_smtpd_milters` satırını kaldırarak sağladık. > **Anahtar dosyasının sahipliği: `opendkim:opendkim 600` mü, `root:opendkim 640` mı?** Rehberde iki farklı öneri göreceğiniz için açıklayayım. `opendkim:opendkim 600` hem çalışıyor hem en sıkı izin: anahtarı yalnızca `opendkim` okuyor, Postfix'le TCP üzerinden konuşuyor ve anahtar Postfix'in eline hiç geçmiyor. `root:opendkim 640` de çalışıyor (grupta başka üye olmadığı sürece) ve `postfix check`'in "not owned by root" uyarısını susturuyor, ama anahtarı grup tarafından okunur yapıyor. İkisini de denedim ve `opendkim:opendkim 600`'de kaldım. `postfix check` uyarısı bu durumda yalnızca gürültü, gerçek bir sorun değil. Servisi yeniden başlatın: ```bash sudo systemctl restart opendkim sudo systemctl enable opendkim ``` ### OpenDMARC: Gelen Mailde DMARC'ı Uygulamak Buraya kadar kurduğumuz SPF, DKIM ve DMARC kayıtları başka sunuculara "benim adıma gelen maili nasıl doğrularsın" diye tarif veriyordu. Peki başkasının adına sahte bir mail bize gelirse? Rehberin ilk hâlindeki en büyük boşluk buydu: sunucum DMARC politikası yayınlıyor ama başkalarının politikasını uygulamıyordu. Örnek vereyim. `usps.com` (ABD Posta Servisi) DMARC kaydında `p=reject` yayınlıyor, yani "benim adıma gelip doğrulamadan geçemeyen maili reddedin" diyor. Gmail bu talimata uyuyor, benim sunucum uymuyordu. `From:` başlığında bu domain'i taklit eden bir test maili attığımda SpamAssassin DMARC ihlalini gördü ama maile yalnızca 1.8 civarı puan ekledi. Toplam skor -1.9'da kaldı ve mail doğrudan Inbox'a düştü. OpenDMARC bu talimatı SMTP aşamasında uyguluyor. `/etc/opendmarc.conf`: ```properties AuthservID mail.orneksite.com TrustedAuthservIDs mail.orneksite.com Syslog true UMask 0002 UserID opendmarc PidFile /run/opendmarc/opendmarc.pid PublicSuffixList /usr/share/publicsuffix/public_suffix_list.dat Socket inet:54321@localhost RejectFailures true RequiredHeaders true IgnoreAuthenticatedClients true IgnoreHosts /etc/opendmarc/ignore.hosts SPFIgnoreResults true SPFSelfValidate true FailureReports false ``` `/etc/opendmarc/ignore.hosts`: ```plaintext 127.0.0.1 ::1 ``` ```bash sudo mkdir -p /etc/opendmarc printf '127.0.0.1\n::1\n' | sudo tee /etc/opendmarc/ignore.hosts sudo systemctl enable --now opendmarc sudo systemctl restart opendmarc ss -tlnp | grep 54321 ``` Ayarların ne işe yaradığı: - **`TrustedAuthservIDs`**: OpenDMARC, OpenDKIM'in eklediği `Authentication-Results` başlığına yalnızca bu isimle geliyorsa güveniyor. OpenDKIM'deki `AuthservID` ile birebir aynı olmalı. - **`RejectFailures true`**: Korumanın kendisi. Gönderen domain `p=reject` yayınlıyorsa DMARC'tan geçemeyen mail SMTP aşamasında `rejected by DMARC policy for usps.com` gibi bir yanıtla geri çevriliyor. `p=quarantine` yayınlayan domain'in maili kabul ediliyor ama başlığına `dmarc=fail` yazılıyor, `p=none` hiç etkilenmiyor. Yani kararı ben değil, domain sahibinin politikası veriyor. DNS o an cevap vermezse mail reddedilmiyor, 4xx ile "sonra tekrar dene" deniyor. - **`IgnoreAuthenticatedClients true`**: Şifreyle giriş yapan kendi kullanıcılarım bu kontrole girmiyor; onların hangi adresle gönderdiğine Postfix'in `reject_sender_login_mismatch` kuralı bakıyor. - **`SPFIgnoreResults` + `SPFSelfValidate`**: OpenDMARC maildeki SPF başlıklarına güvenmeyip SPF'i kendisi hesaplıyor. Başlığı saldırgan istediği gibi yazabilir, gönderen domain'in DNS kaydını yazamaz. - **`RequiredHeaders true`**: `From:` başlığı hiç olmayan ya da birden fazla olan bozuk mailleri reddediyor. İki ayrı `From:` koyup doğrulamayı birine, mail istemcisinin ekranını öbürüne yaptırmak bilinen bir DMARC atlatma hilesi; bu ayar o kapıyı kapatıyor. - **`FailureReports false`**: Başarısız doğrulamalar için dışarıya adli (forensic) rapor göndermiyorum. Hem gizlilik hem gereksiz trafik açısından. > **Neden unix soket değil de TCP?** Paket `local:/run/opendmarc/opendmarc.sock` unix soketiyle geliyor. Bizim `master.cf`'te `smtpd` servisleri chroot içinde (`/var/spool/postfix`) çalışıyor ve bu yolu göremiyor. OpenDKIM'de olduğu gibi `inet:54321@localhost` kullanınca dert kalmıyor. `/etc/default/opendmarc`'a da takılmayın; systemd servisi o dosyayı okumuyor, geçerli config `/etc/opendmarc.conf`. > **Milter sırası önemli:** `smtpd_milters = inet:localhost:12301, inet:localhost:54321` satırında OpenDKIM önce geliyor. OpenDMARC karar verirken OpenDKIM'in doğrulama sonucuna bakıyor; sıra ters olursa o sonuç henüz yazılmamış oluyor. ## Adım 7: Dovecot Yapılandırması Bir ara neyin çalışmadığını uzun süre anlayamadım, sonunda sorunun Dovecot ayarlarında olduğunu gördüm. Mail kutusuna erişimi yöneten bu parça, ayar hatasını en az affeden yer. ### Tam dovecot.conf `/etc/dovecot/dovecot.conf` dosyasının içeriğini tamamen aşağıdakiyle değiştirin: ```ini # Dovecot config # %u = kullanıcı adı (ben@orneksite.com) # %n = @ öncesi kısım (ben) # %d = domain (orneksite.com) # %h = kullanıcının home dizini # --- SSL / TLS bölümü --- ssl = required ssl_cert = **İzin tuzağı:** Bir keresinde dosyayı düzenledikten sonra sahibi `root:root`, izni `640` olarak kaldı ve bütün hesaplar "Oturum açılamadı" hatası vermeye başladı; yalnızca yeni eklenen değil, hepsi. Dovecot'un kimlik doğrulama süreci `dovecot` kullanıcısıyla çalışıyor (benim sistemimde uid 113) ve root grubunda değil, o yüzden dosyayı okuyamadı. Log'da şu satır vardı: > > `passwd-file: open(/etc/dovecot/users) failed: Permission denied (euid=113(dovecot) egid=117(dovecot) missing +r perm)` > > O gün aceleyle `chmod 644` yaparak çözmüştüm, ama bu hash'leri sunucudaki her kullanıcıya açıyor. Doğrusu grubu `dovecot` yapıp `640` bırakmak. Dosyayı her düzenledikten sonra sahibini ve iznini kontrol edin: `ls -l /etc/dovecot/users` çıktısında `-rw-r----- root dovecot` görmelisiniz. ### Sieve İzinleri Global Sieve betikleri için dizini hazırlayın: ```bash sudo mkdir -p /var/lib/dovecot/sieve sudo chmod 755 /var/lib/dovecot/sieve ``` ### Spam'i Junk Klasörüne Taşıyan Global Kural SpamAssassin spam bulduğu maile `X-Spam-Flag: YES` başlığı ekliyor, maili bir yere taşımıyor. Taşıma işi Sieve'in. Rehberin ilk hâlinde config'te bir Sieve dosyasının yolu vardı ama içeriğini yazmayı unutmuşum. `/var/lib/dovecot/sieve/spam.sieve`: ```plaintext require ["fileinto"]; if header :contains "X-Spam-Flag" "YES" { fileinto "Junk"; stop; } ``` Betiği önceden derleyin. Dovecot bu dizine kendi derlenmiş dosyasını yazamıyor; derlemezseniz her teslimatta log'a uyarı düşüyor: ```bash sudo sievec /var/lib/dovecot/sieve/spam.sieve sudo chmod 644 /var/lib/dovecot/sieve/spam.sieve /var/lib/dovecot/sieve/spam.svbin ``` > **Neden `sieve_default` değil de `sieve_before`?** İlk kurulumda bu kuralı `sieve_default` ile bağlamıştım. Sonra `sieve_default`'un yalnızca kullanıcının kendi Sieve betiği yoksa çalıştığını öğrendim. Kullanıcı bir mail istemcisiyle ya da Roundcube'un `managesieve` eklentisiyle (Dovecot'ta `dovecot-managesieved` paketi kuruluysa) "tatildeyim" yanıtı veya basit bir filtre kurduğu anda kişisel betiği oluşuyor ve spam kuralı o kullanıcı için sessizce devre dışı kalıyor. `sieve_before` ise her kullanıcının kişisel betiğinden önce, istisnasız çalışıyor. Bir faydası daha var: Dovecot sıradaki betiği yalnızca önceki betik maili "yerinde bırak" (keep) kararıyla bitirdiyse çalıştırıyor. `fileinto "Junk"` bu kararı iptal ettiği için spam mailde kullanıcının kişisel betiği hiç devreye girmiyor ve spam gönderene tatil yanıtı gitmiyor. Spamciye "bu adres aktif" diye haber vermenin âlemi yok. Servisi yeniden başlatın: ```bash sudo systemctl restart dovecot sudo systemctl enable dovecot ``` ## Adım 8: Roundcube Docker Kurulumu Mail alıp gönderen altyapı hazır, sıra web arayüzünde. Roundcube PHP ile yazılmış, açık kaynak bir webmail; cPanel'den ve Plesk'ten zaten tanıdığım bir arayüz. MailCow gibi koca bir altyapı yerine istediğim tam da bu sadelikti. Docker'ı seçmemin sebebi alışkanlık: sunucumdaki web projelerinin çoğunu zaten Docker Compose ile çalıştırıyorum. Bir şey ters giderse `docker compose down` deyip ana sisteme dokunmadan her şeyi geri alabiliyorum. MailCow'dan sonra geri alınabilirlik benim için en önemli ölçüt olmuştu. ### Dizin Yapısı ```bash sudo mkdir -p /mnt/HC_Volume_XXXXXXXX/websites/mail.orneksite.com/config sudo mkdir -p /mnt/HC_Volume_XXXXXXXX/websites/mail.orneksite.com/logs cd /mnt/HC_Volume_XXXXXXXX/websites/mail.orneksite.com ``` > **Not:** Ben Hetzner'in ek depolama volume'ünü (`/mnt/HC_Volume_XXXXXXXX`) kullanıyorum. Siz `/opt/roundcube` ya da `/srv/mail` gibi istediğiniz bir dizini seçebilirsiniz. ### docker-compose.yml ```yaml networks: roundcube-net: driver: bridge services: app-roundcube-db: image: postgres:16-alpine container_name: db-postgresql-roundcube restart: unless-stopped volumes: - ./db-data:/var/lib/postgresql/data environment: POSTGRES_DB: roundcube POSTGRES_USER: roundcube POSTGRES_PASSWORD: BURAYA_GÜÇLÜ_BİR_ŞİFRE_YAZIN networks: - roundcube-net healthcheck: test: ["CMD-SHELL", "pg_isready -U roundcube"] interval: 10s timeout: 5s retries: 5 app-roundcube: image: roundcube/roundcubemail:latest-apache container_name: app-roundcube restart: unless-stopped depends_on: app-roundcube-db: condition: service_healthy ports: - "127.0.0.1:8080:80" volumes: - ./config:/var/roundcube/config - ./logs:/var/roundcube/logs extra_hosts: - "host.docker.internal:host-gateway" environment: ROUNDCUBEMAIL_DB_TYPE: pgsql ROUNDCUBEMAIL_DB_HOST: app-roundcube-db ROUNDCUBEMAIL_DB_PORT: "5432" ROUNDCUBEMAIL_DB_NAME: roundcube ROUNDCUBEMAIL_DB_USER: roundcube ROUNDCUBEMAIL_DB_PASSWORD: BURAYA_GÜÇLÜ_BİR_ŞİFRE_YAZIN ROUNDCUBEMAIL_DEFAULT_HOST: ssl://host.docker.internal ROUNDCUBEMAIL_DEFAULT_PORT: "993" ROUNDCUBEMAIL_SMTP_SERVER: ssl://host.docker.internal ROUNDCUBEMAIL_SMTP_PORT: "465" ROUNDCUBEMAIL_PLUGINS: archive,zipdownload,managesieve ROUNDCUBEMAIL_SKIN: elastic ROUNDCUBEMAIL_ASPELL_DICTS: tr networks: - roundcube-net ``` Dikkat edilecek birkaç satır: - **`127.0.0.1:8080:80`**: Roundcube yalnızca sunucunun kendisinden erişilebilir, dışarıya Nginx üzerinden açılıyor. Bu `127.0.0.1:` önekini atlamayın; Docker'ın yayınladığı portlar UFW'yi atlıyor (Adım 11'de anlatıyorum). - **`host.docker.internal:host-gateway`**: Container'ın host'taki Postfix ve Dovecot'a ulaşabilmesi için bu tanım gerekiyor. - **`ssl://host.docker.internal:993` ve `:465`**: IMAPS ve SMTPS. Neden bu portlar olduğunu aşağıda hata hikayeleriyle anlatıyorum. > **`aspell-tr` bulunamadı uyarısını dert etmeyin:** Container ilk açılırken log'da `E: Unable to locate package aspell-tr` satırını görebilirsiniz. `ROUNDCUBEMAIL_ASPELL_DICTS: tr` ayarı yüzünden imajın başlangıç betiği Türkçe yazım sözlüğünü `apt-get install aspell-tr` ile kurmaya çalışıyor, container'ın deposunda da böyle bir paket yok. Roundcube uyarıyı geçip normal açılıyor, kaybettiğiniz tek şey Türkçe yazım denetimi. Ben ilk gördüğümde bir şey bozuldu sandım, ama webmail sorunsuz çalışıyordu. ### Roundcube Özel Yapılandırma `config/custom.config.inc.php`: ```php ['verify_peer' => false, 'verify_peer_name' => false], ]; $config['smtp_conn_options'] = [ 'ssl' => ['verify_peer' => false, 'verify_peer_name' => false], ]; ``` > **`refresh_interval` neden 60?** İlk config'imde 15 yazmıştım. Sonra Roundcube'un varsayılan ayarlarında `min_refresh_interval = 60` olduğunu gördüm; 60 saniyenin altındaki değerler zaten dikkate alınmıyor. Yanıltmasın diye 60 yaptım. `session_lifetime` dakika, `login_rate_limit` ise dakikada izin verilen başarısız giriş sayısı. > **`message_show_email` neden açık?** Roundcube varsayılan olarak gönderen satırında yalnızca görünen adı (örneğin "Müşteri Hizmetleri") gösteriyor, adresi görmek için fareyi üzerine getirmek gerekiyor. Spoofing'in en ucuz biçimi de bu: saldırgan kendi `rastgele123@gmail.com` adresinden, görünen adı bir şirketin müşteri hizmetleri gibi ayarlayıp mail atıyor. Mail SPF, DKIM, DMARC'tan geçer, çünkü gerçekten o Gmail adresinden geliyor. Bu ayarla Roundcube ad ile adresi yan yana gösteriyor ve kurumsal bir adın arkasında gmail adresi duran mail ilk bakışta göze batıyor. Sunucunun engelleyemediği bir saldırıya karşı arayüzde alınabilecek en basit önlem bu. ### Hata Deneyimleri: Docker ile İmtihanım Container'daki Roundcube'u host'taki Postfix ve Dovecot'a bağlamak düşündüğümden zor çıktı. Dört ayrı sorunla karşılaştım. **Hata 1: "Authentication not allowed until SSL/TLS is enabled"** İlk denemede IMAP için 143'ü kullandım. Dovecot'ta `ssl = required` olduğu için TLS'siz girişe izin verilmiyordu; container `host.docker.internal:143`'e düz bağlanıyordu ve Dovecot reddediyordu. **Çözüm:** IMAP'i `ssl://host.docker.internal:993`'e, SMTP'yi `ssl://host.docker.internal:465`'e çevirdim. Bağlantı baştan sona şifreli oldu. **Hata 2: SSL verify_peer hatası** Port değişince yeni bir hata çıktı. Sertifika `mail.orneksite.com` adına alınmıştı ama Roundcube `host.docker.internal`'a bağlanıyordu. Ad uyuşmadığı için PHP'nin sertifika doğrulaması başarısız oluyordu. **Çözüm:** Roundcube config'inde `verify_peer` ve `verify_peer_name` seçeneklerini `false` yaptım. Bağlantı aynı sunucunun içinde kaldığı için bunu kabul edilebilir buldum. ```php $config['imap_conn_options'] = [ 'ssl' => ['verify_peer' => false, 'verify_peer_name' => false], ]; $config['smtp_conn_options'] = [ 'ssl' => ['verify_peer' => false, 'verify_peer_name' => false], ]; ``` > **`verify_peer = false` nerede kabul edilebilir?** Yalnızca bu senaryoda: host ile container arasındaki, sunucunun dışına çıkmayan bağlantıda. Dış bir mail sunucusuna bağlanırken bu ayarı kullanmayın; araya giren biri sertifikayı değiştirip trafiği okuyabilir. Kullanıcıların Roundcube'a gelen bağlantısı ayrı bir katman: Nginx üzerinden HTTPS ile geliyor ve orada Let's Encrypt sertifikası normal şekilde doğrulanıyor. **Hata 3: Fail2ban Docker IP'sini banladı** Her şey çalışıyor gibiydi, bir süre sonra Roundcube yine bağlanamaz oldu. Log'lara bakınca Fail2ban'ın container'ın IP'sini (192.168.64.3 gibi bir adres) banladığını gördüm. Birkaç yanlış şifre denemesi, Roundcube'un kendi IP'sini kara listeye sokmuştu. **Çözüm:** Fail2ban jail'lerinde Docker aralıklarını (`172.16.0.0/12` ve `192.168.0.0/16`) `ignoreip` listesine ekledim. **Hata 4: Gönderici adresi "kullanici1@host.docker.internal"** Roundcube'dan ilk test mailimi attığımda alıcı gönderen olarak `kullanici1@host.docker.internal` gördü. Roundcube ilk girişte kullanıcının kimliğini (identity) otomatik oluşturuyor, domain kısmını da bağlandığı sunucunun adından türetiyordu. **Çözüm:** Roundcube'un PostgreSQL veritabanına girip `users` ve `identities` tablolarını elle düzelttim: ```bash docker exec -it db-postgresql-roundcube psql -U roundcube -d roundcube ``` ```sql -- Kullanıcı email adresini güncelle UPDATE users SET username = 'ben@orneksite.com' WHERE username LIKE '%kullanici1%'; -- Kimlik bilgilerini güncelle UPDATE identities SET email = 'ben@orneksite.com', name = 'Ad Soyad' WHERE email LIKE '%kullanici1%'; exit; ``` **Sorunu hiç yaşamamanın yolu:** SQL düzeltmesi işe yarıyor ama her yeni hesapta tekrarlamak sıkıcı. Config'de `$config['username_domain'] = '';` boş kalınca (yukarıdaki config'de öyle) Roundcube kısa ada domain eklemiyor. Dovecot'ta da yalnızca tam e-posta adresleri tanımlı olduğu için kullanıcı giriş ekranına `info@orneksite.com` gibi tam adresi yazmak zorunda kalıyor. Roundcube da kullanıcı adını doğrudan bu adresle kaydediyor ve `@host.docker.internal` tuzağı hiç oluşmuyor. Tek bedeli her girişte uzun adresi yazmak. İlk kurulumlarda bunu düşünmemiştim; ikinci kurulumda bu sayede SQL'e hiç girmedim. ### Container'ları Başlatma ```bash cd /mnt/HC_Volume_XXXXXXXX/websites/mail.orneksite.com docker compose up -d ``` Doğrulama: ```bash docker compose ps # İki container da "Up" görünmeli docker compose logs app-roundcube | tail -20 ``` ![Roundcube giriş ekranı](/images/posts/027-self-hosted-mail-sunucusu-sifirdan-kurulum-rehberi/img/ss5-roundcube-giris-ekrani.webp) ![Roundcube Gelen Kutusu görünümü](/images/posts/027-self-hosted-mail-sunucusu-sifirdan-kurulum-rehberi/img/ss6-roundcube-gelen-kutusu-gorunumu.webp) ## Adım 9: Nginx Reverse Proxy Roundcube `127.0.0.1:8080`'de çalışıyor. Dışarıya HTTPS ile açmak için Nginx'i ayarlıyorum. `/etc/nginx/sites-available/mail.orneksite.com`: ```nginx server { listen 80; listen [::]:80; server_name mail.orneksite.com; return 301 https://$host$request_uri; } server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name mail.orneksite.com; ssl_certificate /etc/letsencrypt/live/mail.orneksite.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/mail.orneksite.com/privkey.pem; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; client_max_body_size 25m; access_log /var/log/nginx/mail.orneksite.com.access.log; error_log /var/log/nginx/mail.orneksite.com.error.log; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # Hassas dizinlere ve gizli dosyalara erişimi engelle location ~ /\. { deny all; } location ~ ^/(config|temp|logs)/ { deny all; } } ``` Aktifleştirme: ```bash sudo ln -s /etc/nginx/sites-available/mail.orneksite.com /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx ``` `nginx -t` çıktısında `syntax is ok` ve `test is successful` görmelisiniz. Görmüyorsanız reload etmeyin, önce hatayı düzeltin. > **Sürüm tuzağı: `listen 443 ssl http2;` satırını koruyun.** Güncel nginx belgeleri (1.25.1 ve sonrası) bu yazımı eskimiş sayıyor ve `listen 443 ssl;` ile ayrı bir `http2 on;` satırı öneriyor. Bir keresinde belgeye bakıp config'i yeni yazıma çevirdim, `nginx -t` dediğimde `unknown directive "http2"` aldım. Ubuntu 24.04 depolarındaki nginx 1.24 ve `http2 on;` direktifini tanımıyor. 24.04 ve öncesinde eski yazımda kalın. ### SSL Sertifikası: Birden Fazla Domain İçin Genişletme Sertifikayı başta yalnızca `mail.orneksite.com` için aldım. İkinci domain'i ekleyince, `mail.ikincidomain.com`'a bağlanan mail istemcileri sertifika uyuşmazlığı hatası vermeye başladı. Çözüm, mevcut sertifikayı `--expand` ile genişletmek. Bunun için `mail.ikincidomain.com`'u önce Nginx'teki mail vhost'unun `server_name` satırlarına ekleyin, yoksa certbot doğrulamayı yapamıyor. ```bash sudo certbot certonly --nginx -d mail.orneksite.com -d mail.ikincidomain.com --expand ``` Komut iki hostname'i de sertifikanın SAN (Subject Alternative Name) alanına ekliyor. Sertifikanın yolu (`/etc/letsencrypt/live/mail.orneksite.com/`) değişmediği için Postfix ve Dovecot config'ine dokunmak gerekmiyor. Postfix ve Dovecot sertifikayı açılışta belleğe aldığı için, yenilemeden sonra reload edilmeleri gerekiyor. Yenileme ayarına bakın: ```bash cat /etc/letsencrypt/renewal/mail.orneksite.com.conf ``` `renew_hook` satırı Postfix ve Dovecot'u reload etmeli: ```ini renew_hook = echo "$RENEWED_DOMAINS" | grep -q 'mail.orneksite.com' && service postfix reload && service dovecot reload ``` > **İpucu:** Satır yoksa aynı işi `/etc/letsencrypt/renewal-hooks/deploy/` altına koyacağınız küçük bir betik de görebilir. Certbot bu dizindeki betikleri her başarılı yenilemeden sonra kendiliğinden çalıştırıyor. ## Adım 10: Postfix'i Aktif Etme Config'ler hazır, Postfix'i başlatmadan önce bir kontrol: ```bash sudo postfix check ``` Hata çıkarsa tek tek düzeltin. Benim en sık gördüğüm satır DKIM anahtarlarıyla ilgiliydi: ```plaintext postfix/postfix-script: warning: not owned by root: /etc/postfix/./dkim/ikincidomain.com/mail.private ``` Bu bir hata değil, uyarı. Anahtarı Adım 6'daki gibi `opendkim:opendkim 600` bıraktıysanız görmezden gelebilirsiniz. Susturmak isterseniz sahipliği `root:opendkim`, izni `640` yapın; anahtarı `root:root` yapmayın, OpenDKIM okuyamaz hâle gelir. Uyarı dışında bir şey kalmadıysa devam edin. ```bash sudo systemctl enable postfix sudo systemctl restart postfix ``` Port kontrolü: ```bash ss -tlnp | grep -E ':(25|587|465)\s' ``` 25, 587 ve 465'in `LISTEN` durumda olduğunu görmelisiniz. Bu çıktıyı gördüğüm an işin yarısının bittiğini hissettim. > **`ss -tlnp` çıktısı:** `LISTEN` portun bağlantı kabul ettiği anlamına geliyor. Portlar listede yoksa Postfix çalışmıyor ya da başka bir arayüzü dinliyor olabilir. `sudo systemctl status postfix` ile durumuna, `journalctl -u postfix -n 50` ile son log satırlarına bakın. ### `/var/log/mail.log` yoksa Bu beni ilk seferinde uzun süre yanılttı. Postfix `active (running)`, portlar `LISTEN`, test mailleri teslim ediliyor, ama `tail /var/log/mail.log` "No such file or directory" diyor. "Servis mi çalışmıyor?" diye defalarca yeniden başlattım. Sonunda iki şeyi düzeltince dosya oluştu ve log akmaya başladı. Ubuntu'da rsyslog kuruluysa normalde bu sorunla karşılaşmamanız gerekiyor. rsyslog paketi journald'a `ForwardToSyslog=yes` ayarını kendisi ekliyor, `/var/log` dizini de `root:syslog 775` olduğu için rsyslog dosyayı kendisi oluşturabiliyor. Dosya yine de yoksa önce şunlara bakın: ```bash systemctl is-active rsyslog systemd-analyze cat-config systemd/journald.conf | grep ForwardToSyslog stat -c '%U:%G %a' /var/log ``` **1. journald log'ları syslog'a iletmiyor olabilir.** Benim sunucumda iletim kapalıydı; mail log'ları journal'da kalıyor, rsyslog'a hiç ulaşmıyordu. Açmak için: ```bash sudo sed -i 's|^#\?ForwardToSyslog=.*|ForwardToSyslog=yes|' /etc/systemd/journald.conf sudo systemctl restart systemd-journald sudo systemctl restart rsyslog ``` **2. Dosyaları elle oluşturun.** İletimi açtıktan sonra dosyaları root ile oluşturup sahipliğini `syslog`'a verdim. `/var/log` izinleri yukarıdaki gibi `root:syslog 775` değilse rsyslog dosyayı kendisi oluşturamıyor; bu adım o durumda da işe yarıyor: ```bash sudo touch /var/log/mail.{log,err,warn,info} sudo chown syslog:adm /var/log/mail.{log,err,warn,info} sudo chmod 640 /var/log/mail.{log,err,warn,info} ``` `mail.log` olmadan Adım 13'teki Postfix jail'leri de çalışmıyor, çünkü onları doğrudan bu dosyadan okutacağız. Önce log'ları konuşturun, sonra fail2ban'a geçin. > **Geri alma:** Bir şey ters giderse yedeğe dönebilirsiniz: `sudo cp -r /etc/postfix.backup.* /etc/postfix && sudo systemctl restart postfix` ## Adım 11: Firewall (UFW) Dışarıya hangi portların açık olacağını belirliyorum: ```bash # SSH (zaten eklemiş olmalısınız) sudo ufw allow 22/tcp # SMTP - Sunucular arası mail alışverişi sudo ufw allow 25/tcp # IMAP - Mail istemcileri için (STARTTLS) sudo ufw allow 143/tcp # HTTPS - Roundcube web arayüzü sudo ufw allow 443/tcp # SMTPS - Mail gönderme (SSL/TLS) sudo ufw allow 465/tcp # Submission - Mail gönderme (STARTTLS) sudo ufw allow 587/tcp # IMAPS - Mail okuma (SSL/TLS) sudo ufw allow 993/tcp # Firewall'u etkinleştir sudo ufw enable # Durumu kontrol et sudo ufw status verbose ``` > **Uyarı:** SSH kuralını firewall'u etkinleştirmeden önce ekleyin. Yoksa sunucuya erişiminizi kaybeder, Hetzner'in web konsolundan müdahale etmek zorunda kalırsınız. Ben yandım, siz yanmayın :) > **Docker'ın yayınladığı portlar UFW'yi dinlemiyor.** Bunu kendi sunucumda, başka projelerin container'larına bakarken öğrendim. `docker-compose.yml`'de `ports: "5432:5432"` gibi bir satır yazdığınızda Docker kendi iptables kurallarını UFW'den önce ekliyor ve o port, UFW'de hiçbir izin olmasa da internete açılıyor. Benim sunucumda da UFW'de kapalı sandığım birkaç veritabanı ve yönetim paneli portu dışarıdan erişilebilir çıktı. Bu rehberdeki Roundcube'u `127.0.0.1:8080:80` diye yalnızca loopback'e bağlamamın sebebi bu. Dışarıya açılması gerekmeyen her container portunu `127.0.0.1:` önekiyle yayınlayın. Önekleyemediğiniz durumlarda Docker'ın bu iş için bıraktığı `DOCKER-USER` zincirine, dış arayüzden gelen trafiği kesen kurallar ekleyebilirsiniz. Kontrol için başka bir makineden `nc -zv SUNUCU_IP_ADRESINIZ PORT` deneyin. ## Adım 12: Hetzner Port 25 Engeli Sürecin en sinir bozucu anı buydu. Her şey ayarlı: DNS doğru, Postfix, Dovecot, Roundcube çalışıyor. Ama Gmail'e mail gidemiyor, log'da `Connection timed out` yazıyor. Saatlerce firewall'a, Postfix config'ine baktım. Sonra sebebi buldum: Hetzner, Cloud sunucularda dışarıya giden 25 ve 465 portlarını varsayılan olarak engelliyor. Spam'i önlemek için alınmış makul bir karar; Hetzner'in SSS sayfasında da yazıyor, ama ben kurulumdan önce okumamıştım. ### Nasıl Tespit Edilir ```bash timeout 5 bash -c 'echo "QUIT" | nc -w 3 gmail-smtp-in.l.google.com 25' ``` Komut zaman aşımına düşüyorsa port 25 engelli. Açıksa `220 mx.google.com ESMTP` gibi bir yanıt görürsünüz. ### Çözüm: Hetzner'e Limit Talebi Hetzner'in belgesine göre hesabınız bir ayı doldurup ilk faturasını ödedikten sonra bu portların açılmasını [destek panelinden](https://console.hetzner.com/support) bir limit talebiyle isteyebiliyorsunuz; kararı her talep için ayrı veriyorlar. Kısa ve net, İngilizce bir mesaj yazmanızı öneririm. Benim kullandığıma benzer bir örnek: ```plaintext Hello, I would like to request unblocking of outbound port 25 (SMTP) on my server (IP: SUNUCU_IP_ADRESINIZ). I am setting up a legitimate mail server for my domains orneksite.com and ikincidomain.com with proper SPF, DKIM, DMARC, and PTR records configured. Thank you. ``` Talepler genelde birkaç saat içinde yanıtlanıyor. > **İyi haber:** Port açılınca Postfix kuyrukta bekleyen mailleri kendiliğinden yeniden deniyor, tekrar göndermeniz gerekmiyor. Kuyruğu beklemeden boşaltmak isterseniz `sudo postqueue -f`. ![Hetzner Destek Talebi](/images/posts/027-self-hosted-mail-sunucusu-sifirdan-kurulum-rehberi/img/ss7-hetzner-destek-talebi.webp) ## Adım 13: Fail2ban - Roundcube ve Mail Servisleri Koruması Mail sunucusu internete açıldığı andan itibaren şifre deneyen botlar geliyor. Fail2ban başarısız girişleri log'lardan okuyup o IP'leri bir süreliğine banlıyor. Ama bu bölümü ilk yazdığım hâliyle uygularsanız Postfix ve Roundcube jail'leri Ubuntu 24.04'te hiçbir şey yakalamıyor. Bunu kendi sunucumda, bir haftada binin üzerinde başarısız SMTP girişi olduğu hâlde jail sayaçlarının sıfırda durduğunu görünce fark ettim. Aşağıya düzeltilmiş hâlini yazıyorum. > **Kök neden: Ubuntu'da varsayılan backend `systemd`.** Ubuntu'nun fail2ban paketi `/etc/fail2ban/jail.d/defaults-debian.conf` dosyasıyla varsayılan backend'i `systemd` yapıyor. Yani jail'ler log dosyası yerine systemd journal'ı okuyor. Postfix'in hazır filtresi journal'da `_SYSTEMD_UNIT=postfix.service` arıyor, oysa Ubuntu'da Postfix `postfix@-.service` adıyla çalışıyor ve eşleşme hiç olmuyor. Roundcube filtresi de `SYSLOG_IDENTIFIER=roundcube` arıyor; Docker'daki Roundcube ise journal'a değil dosyaya yazıyor. Çözüm, bu jail'lere dosyadan okumalarını açıkça söylemek. ### Roundcube Jail Önce Roundcube'un log dosyasını hazırlayın. `logs` klasörü ilk açılışta boş geliyor ve `errors.log` ilk hata olana kadar oluşmuyor; dosya yokken jail başlamayı reddediyor (`Have not found any log file for roundcube-auth jail`). Bunu fail2ban'ı kurduktan sonra, "jail neden başlamıyor" diye uğraşırken öğrendim. Container içinde Roundcube `www-data` (uid 33) kullanıcısıyla çalıştığı için dosyanın sahibi de o olmalı, yoksa log yazamıyor: ```bash cd /mnt/HC_Volume_XXXXXXXX/websites/mail.orneksite.com sudo touch logs/errors.log sudo chown 33:33 logs logs/errors.log ``` Adım 8'deki `log_driver = 'file'` satırı da bunun için vardı; Docker imajı varsayılan olarak log'u dosyaya değil stdout'a yazıyor. Bir sorun daha var. Roundcube 1.6 Nginx arkasında çalışırken başarısız girişi şöyle yazıyor: ```log IMAP Error: Login failed for ben@orneksite.com against host.docker.internal from 192.168.64.1 (X-Real-IP: 203.0.113.50,X-Forwarded-For: 203.0.113.50). AUTHENTICATE PLAIN: ... ``` `from` kısmında gerçek istemci değil, Docker ağ geçidinin IP'si duruyor. Hazır `roundcube-auth` filtresi bu satırı hiç eşleştirmiyor, eşleştirse bile banlayacağı IP ağ geçidi olurdu. Gerçek IP `X-Real-IP` alanında; Nginx bu başlığı her istekte `$remote_addr` ile ezdiği için güvenilir. Kendi filtremi yazdım: `/etc/fail2ban/filter.d/roundcube-auth-proxy.conf`: ```ini [Definition] failregex = IMAP Error: Login failed for \S+ against \S+ from \S+ \(X-Real-IP: [,)] ignoreregex = ``` Filtreyi mevcut log'a karşı deneyebilirsiniz: ```bash sudo fail2ban-regex /mnt/HC_Volume_XXXXXXXX/websites/mail.orneksite.com/logs/errors.log /etc/fail2ban/filter.d/roundcube-auth-proxy.conf ``` `/etc/fail2ban/jail.d/zzz-roundcube.local`: ```ini [roundcube-auth] enabled = true port = http,https filter = roundcube-auth-proxy backend = polling logpath = /mnt/HC_Volume_XXXXXXXX/websites/mail.orneksite.com/logs/errors.log maxretry = 3 findtime = 600 bantime = 7200 ``` > **Not:** Bu değerler makul bir başlangıç. Kendi sunucunuzdaki gerçek değerleri herkese açık bir yerde paylaşmamaya dikkat edin; eşiği bilen saldırgan hep onun bir altında kalır. Ben canlıda farklı değerler kullanıyorum. > **Dosya adı neden `zzz-roundcube.local`?** Fail2ban dosyaları şu sırayla okuyor: `jail.conf`, `jail.d/*.conf` (alfabetik), `jail.local`, `jail.d/*.local` (alfabetik). Sonra okunan önce okunanı eziyor. Benim sunucumda `jail.local` içinde daha önceden kalma bir `[roundcube-auth]` bloğu vardı. Jail'imi `jail.d/roundcube.conf` diye yazdığım için `jail.local` ondan sonra okunuyor ve benim `logpath`'imi var olmayan `/var/log/roundcube/errors.log`'a çeviriyordu. Ekranda doğru yolu görüp "ama yol doğru!" diye söylenirken `fail2ban-client -d | grep roundcube` ile gerçekten yüklenen ayarı görene kadar anlamadım. `.local` uzantısı ve alfabede sona düşen bir ad, dosyanın en son okunmasını garanti ediyor. ### Mail Servisleri Jail'leri `/etc/fail2ban/jail.d/emailwiz.local`: ```ini [postfix] enabled = true backend = auto logpath = /var/log/mail.log ignoreip = 127.0.0.1/8 172.16.0.0/12 192.168.0.0/16 [postfix-sasl] enabled = true backend = auto logpath = /var/log/mail.log ignoreip = 127.0.0.1/8 172.16.0.0/12 192.168.0.0/16 [sieve] enabled = true ignoreip = 127.0.0.1/8 172.16.0.0/12 192.168.0.0/16 [dovecot] enabled = true ignoreip = 127.0.0.1/8 172.16.0.0/12 192.168.0.0/16 ``` Postfix jail'leri `backend = auto` ve `logpath` ile artık `/var/log/mail.log`'u okuyor (Adım 10'da bu dosyanın oluştuğundan emin olmuştuk). Dovecot'un servis adı journal'da doğru eşleştiği için onu varsayılanda bıraktım. > **Docker aralıkları neden `ignoreip`'te?** Roundcube host'taki Dovecot'a Docker'ın iç IP'sinden bağlanıyor. Bir kullanıcı Roundcube'da birkaç kez yanlış şifre girince fail2ban bu denemeleri Dovecot log'unda görüp container'ın iç IP'sini banlıyordu. Sonuçta Roundcube kendi kendini kilitliyor, kullanıcı "bağlanamıyorum" derken sebebi fail2ban log'unda duruyordu. İç ağ aralıklarını `ignoreip`'e koymak bunu önlüyor. İnternetten gelen denemeler yine banlanıyor; Roundcube üzerinden gelenleri de yukarıdaki `roundcube-auth` jail'i gerçek IP ile yakalıyor. Fail2ban'ı yeniden başlatıp durumuna bakın: ```bash sudo systemctl restart fail2ban && sleep 2 && sudo fail2ban-client status ``` `restart` ile `status`'u arada beklemeden aynı satırda çalıştırınca soket henüz hazır olmadığı için şu hatayı aldım: `ERROR Failed to access socket path: /var/run/fail2ban/fail2ban.sock. Is fail2ban running?` Aradaki `sleep 2` bu yüzden. Jail'lerin gerçekten bir şey okuduğunu kontrol edin: ```bash sudo fail2ban-client status postfix-sasl sudo fail2ban-client status roundcube-auth ``` `File list` satırında log dosyasını görmelisiniz. Sunucu bir süredir açıksa `Total failed` sayacının da artmaya başladığını göreceksiniz. ## Laravel ve Web Uygulamalarında Mail Entegrasyonu Sunucu çalışınca ilk yaptığım şeylerden biri Laravel projelerimi ona bağlamak oldu: şifre sıfırlama, bildirimler, doğrulama kodları... Burada da saatlerimi yiyen bir tuzak vardı, aşağıda anlatıyorum. ### Laravel .env Yapılandırması ```ini MAIL_MAILER=smtp MAIL_SCHEME=smtps MAIL_HOST=mail.orneksite.com MAIL_PORT=465 MAIL_USERNAME=no-reply@orneksite.com MAIL_PASSWORD=BURAYA_SIFRE MAIL_FROM_ADDRESS=no-reply@orneksite.com MAIL_FROM_NAME="${APP_NAME}" ``` > **Port ve şifreleme:** Laravel 11 ve sonrasında `MAIL_ENCRYPTION` yok, yerini `MAIL_SCHEME` aldı. 465 için `MAIL_SCHEME=smtps` yazın. 587 kullanacaksanız `MAIL_SCHEME=smtp` yeterli, Symfony Mailer STARTTLS'i kendisi başlatıyor. Laravel 10 ve öncesinde ise 465 için `MAIL_ENCRYPTION=ssl`, 587 için `MAIL_ENCRYPTION=tls` kullanılıyordu. ### no-reply Hesabı Oluşturma Uygulamalar için ayrı bir `no-reply@` hesabı açmanızı öneririm. Kişisel mail şifrenizi `.env` dosyalarına yazmak, bir proje sızdığında bütün kutunuzu açık etmek demek. Adımlar: ```bash # Sistem kullanıcısı oluştur (tüm no-reply adresleri buna bağlanacak) sudo useradd -m -s /usr/sbin/nologin noreply # Mail dizinlerini oluştur sudo mkdir -p /home/noreply/Mail/{Inbox,Sent,Drafts,Trash,Junk}/{cur,new,tmp} sudo chown -R noreply:noreply /home/noreply/Mail ``` Bu hesabın şifre hash'ini shadow'dan almak yerine `doveadm pw` ile üretiyorum: ```bash doveadm pw -s CRYPT -p 'BURAYA_GUCLU_SIFRE' # Çıktı: {CRYPT}$2y$05$... ``` `/etc/dovecot/users` dosyasına ekleyin: ```plaintext no-reply@orneksite.com:{CRYPT}$2y$05$HASH_BURAYA...:UID:GID::/home/noreply::userdb_mail=maildir:~/Mail:INBOX=~/Mail/Inbox:LAYOUT=fs ``` Postfix tarafına da ekleyin: ```bash # /etc/postfix/virtual echo "no-reply@orneksite.com noreply" | sudo tee -a /etc/postfix/virtual sudo postmap /etc/postfix/virtual # /etc/postfix/login_maps.pcre echo '/^no-reply@orneksite\.com$/ no-reply@orneksite.com' | sudo tee -a /etc/postfix/login_maps.pcre ``` İzinleri kontrol edip servisleri yeniden başlatın: ```bash sudo chown root:dovecot /etc/dovecot/users && sudo chmod 640 /etc/dovecot/users sudo systemctl restart postfix dovecot ``` #### Paylaşımlı Mailbox: Birden Fazla no-reply Adresi Birden fazla domain'iniz varsa her biri için ayrı bir `no-reply@` adresi açıp hepsini aynı sistem kullanıcısına, yani aynı kutuya bağlayabilirsiniz. Yalnızca şifreler farklı olsun: ```plaintext # /etc/dovecot/users no-reply@orneksite.com:{CRYPT}$2y$05$HASH_1...:1012:1012::/home/noreply::userdb_mail=maildir:~/Mail:INBOX=~/Mail/Inbox:LAYOUT=fs no-reply@ikincidomain.com:{CRYPT}$2y$05$HASH_2...:1012:1012::/home/noreply::userdb_mail=maildir:~/Mail:INBOX=~/Mail/Inbox:LAYOUT=fs ``` Aynı UID/GID ve aynı home dizini, aynı kutu demek; şifreler ise ayrı. `no-reply` adreslerine gelen maile zaten kimse cevap vermeyeceği için hepsinin aynı yere düşmesi sorun değil. ### Gmail Phishing Tuzağı: FROM Domain Eşleşmesi Bu, en çok vakit kaybettiğim sorunlardan biriydi. Başta kolaylık olsun diye bütün projelerimde tek bir `no-reply@orizora.tr` adresi kullandım, `erhanurgun.tr` projesi dahil. Sonra `erhanurgun.tr`'den bir şifre sıfırlama maili gönderdim ve Gmail şu uyarıyı gösterdi: > *"Bu ileti tehlikeli olabilir... Buna benzer iletiler, kişisel bilgileri çalmak için kullanılmıştı."* Bu spam değil, phishing uyarısıydı. Bence Gmail'in gördüğü tablo şuydu: - Gönderen: `no-reply@orizora.tr` - İçerik: "erhanurgun.tr'de şifrenizi sıfırlayın" ve erhanurgun.tr linkleri - Sonuç: bir domain'den gelen mail, başka bir domain'de şifre sıfırlatmaya çalışıyor. Phishing'in tipik görüntüsü. Kuralım bu oldu: her projenin `MAIL_FROM_ADDRESS` değeri o projenin kendi domain'inden olmalı. | Proje | Doğru FROM | Yanlış FROM | |-------|-----------|-------------| | orneksite.com | `no-reply@orneksite.com` | `no-reply@baskadomain.com` | | ikincidomain.com | `no-reply@ikincidomain.com` | `no-reply@orneksite.com` | Yani her domain için sunucuda ayrı bir `no-reply@` hesabı, DKIM anahtarı ve DNS kayıtları (SPF, DKIM, DMARC) gerekiyor. Biraz daha iş, ama Gmail'in phishing filtresi bu konuda esnemiyor. ## Doğrulama ve Test Her şey ayarlandı, sıra gerçekten çalışıp çalışmadığını görmekte. Bu anı uzun zamandır bekliyordum; ilk başarılı testin keyfini anlatmak zor... :D ### DNS Doğrulama ```bash # MX kaydı dig MX orneksite.com +short # Beklenen: 10 mail.orneksite.com. # SPF dig TXT orneksite.com +short # Beklenen: "v=spf1 mx a:mail.orneksite.com ip4:SUNUCU_IP ip6:SUNUCU_IPV6 -all" # DKIM dig TXT mail._domainkey.orneksite.com +short # DMARC dig TXT _dmarc.orneksite.com +short # PTR (Reverse DNS) dig -x SUNUCU_IP_ADRESİNİZ +short # Beklenen: mail.orneksite.com. ``` ### DKIM Anahtar Doğrulama ```bash sudo opendkim-testkey -d orneksite.com -s mail -vvv ``` `key OK` görmelisiniz. Yanında `key not secure` uyarısı da çıkabilir; o DNSSEC ile ilgili ve çalışmayı etkilemiyor. > **"key not secure" için paniklemeyin:** İlk gördüğümde bir şeyi bozdum sanıp DKIM config'ini baştan sona taradım, epey vakit kaybettim. Uyarı, DNS kaydınız DNSSEC ile imzalı olmadığı için çıkıyor, yani domain'inizin DS kaydı yoksa. Gmail, Outlook, Yahoo gibi sağlayıcılar DKIM için DNSSEC şartı aramıyor; teslimatı etkilemiyor. Önemli olan `key OK` satırı. > **DNSSEC'i açmak isterseniz (isteğe bağlı):** Cloudflare'de domain'i seçin, DNS sekmesinin altındaki DNSSEC bölümünden "Enable DNSSEC" deyin. Cloudflare size bir DS kaydı (digest, algorithm, key tag) verecek; bunu domain'i aldığınız firmanın panelinde girmeniz gerekiyor. Yayılması genelde birkaç saat, bazen bir gün sürüyor. Bitince `opendkim-testkey` çıktısındaki "key not secure" yerini "key secure"a bırakıyor. Mail teslimatı için şart değil, ama DNS katmanını sağlamlaştırıyor. ### Fonksiyonel Testler 1. **Roundcube'a giriş:** `https://mail.orneksite.com` adresine gidip `ben@orneksite.com` ve şifrenizle giriş yapın. 2. **Roundcube'dan Gmail'e gönderim:** Yeni bir mail açıp bir Gmail adresine test maili gönderin. 3. **Gmail'den Roundcube'a alım:** Gmail'den `ben@orneksite.com`'a bir mail atın. Birkaç saniye içinde Roundcube'da görünmeli. 4. **SPF/DKIM/DMARC kontrolü:** Gmail'de gelen maili açın, sağ üstteki üç noktadan "Aslını göster"i (Show original) seçin. Şunları görmelisiniz: ```plaintext SPF: PASS DKIM: PASS DMARC: PASS ``` Günlerce uğraştıktan sonra bu üç PASS'i görmenin verdiği rahatlamayı anlatamam. ### Dış Doğrulama Araçları - **MXToolbox** (`mxtoolbox.com`): Domain'inizi girip MX, SPF, DKIM, DMARC kayıtlarını kontrol edebilirsiniz. "Blacklist Check" aracı IP'nizin bir kara listede olup olmadığını da gösteriyor. - **mail-tester.com**: Size verdiği adrese bir test maili atıp 10 üzerinden puanınızı görüyorsunuz. Ben 9 ve üstünü hedefliyorum. > **mail-tester puanını okurken:** İlk testte 7.8 görünce paniğe kapılıp config'leri boş yere karıştırdım. Sonra anladım ki önemli olan puanın nereden düştüğü. Rapor her kesintiyi ayrı ayrı gösteriyor: > > - Mailin çok kısa olması, yalnızca düz metin olması gibi SpamAssassin kalemleri çoğu zaman küçük kesintiler; test maili için normal. > - SPF, DKIM ya da DMARC başarısızsa rapor bunu ayrı bölümde açıkça yazıyor. Böyle bir satır varsa önce DNS kayıtlarına bakın. > - IP'niz ya da domain'iniz bir kara listedeyse o da ayrı bölümde görünüyor. Yeni sunucularda IP'nin itibarı zamanla oturuyor; haftalar geçip düzelmiyorsa Spamhaus'un kendi kontrol sayfasına bakın. > > 7-8 aldınız diye rehberin başına dönüp her şeyi baştan yapmayın. Önce raporun ayrıntısını okuyun. Ben 6'nın altını gerçek bir sorun işareti sayıyorum. ![mail-tester.com skoru](/images/posts/027-self-hosted-mail-sunucusu-sifirdan-kurulum-rehberi/img/ssx-mail-tester-com-skoru.webp) ### Parça Parça Doğrulama - Her Bileşeni Ayrı Ayrı Test Etmek Kurulum boyunca "burası çalışıyor mu?" diye emin olmak için kullandığım komutları topluca bırakıyorum. Bir yerde sorun çıkınca hangisinin takıldığına bakıp bozulan parçayı hızlıca buluyorum: ```bash # SMTP banner testi - hostname doğru şekilde yansıyor mu? (echo EHLO local; echo QUIT) | nc 127.0.0.1 25 # SMTPS TLS handshake - sertifika CN'i mail.orneksite.com mu? timeout 3 openssl s_client -connect 127.0.0.1:465 -brief <<< 'QUIT' # IMAPS TLS handshake - 993 portu doğru sertifikayla açılıyor mu? timeout 3 openssl s_client -connect 127.0.0.1:993 -brief <<< 'a1 LOGOUT' # Dovecot kimlik doğrulama - kullanıcı ve şifre gerçekten çalışıyor mu? # (şifreyi komut satırına yazmayın, doveadm kendisi sorar) doveadm auth test ben@orneksite.com # Dovecot mailbox listesi - namespace ve özel klasörler (Inbox, Sent vb.) görünür mü? doveadm mailbox list -u ben@orneksite.com # DKIM imzalama uçtan uca - kimlik doğrulamalı gönderim imzalanıyor mu? # (swaks şifreyi kendisi sorar, şifre history'ye düşmez) swaks --server 127.0.0.1 --port 587 --tls \ --auth LOGIN --auth-user ben@orneksite.com \ --from ben@orneksite.com --to info@ikincidomain.com \ --header "Subject: dkim testi" grep -l "DKIM-Signature" /home/kullanici2/Mail/Inbox/new/* ``` > **Not:** Bu testi ilk başta `sendmail -t` ile yapıyordum. `non_smtpd_milters` satırını kaldırdıktan sonra `sendmail` ile atılan yerel mail milter'a uğramıyor, dolayısıyla imzalanmıyor. Bu beklenen bir durum; gerçek gönderimler (Roundcube, Laravel, telefon) zaten şifreyle 465/587'den geçiyor ve orada imzalanıyor. Test gerçeği yansıtsın diye swaks ile şifreli gönderime çevirdim. Bu komutlar "SMTP banner doğru ama IMAPS sertifikası yanlış" ya da "giriş çalışıyor ama DKIM imzalamıyor" gibi hataları erkenden yakalamamı sağlıyor. Her adımı bitirince ilgili testi koşup öyle ilerliyorum. Testleri sona bıraktığım bir kurulumda sorunun hangi katmanda olduğunu bulmak işkenceye dönmüştü. ### Spoofing Testleri SPF/DKIM/DMARC PASS görmek benim maillerimin doğru gittiğini gösteriyor. Sunucunun sahte mailleri reddedip reddetmediği ise ayrı bir soru. Rehberin ilk hâlinde bu testler yoktu; eklediğimde kendi sunucumun iki sahte maili sorgusuz kabul ettiğini gördüm. Kurulumdan sonra bu testleri en az bir kez koşun. > **Püf noktası:** Testleri sunucunun içinden, sunucunun kendi public IP'sine bağlanarak yapıyorum. Bu IP `mynetworks` içinde olmadığı için Postfix bağlantıyı dışarıdan gelmiş gibi değerlendiriyor. Evden port 25'e çıkmaya uğraşmanız gerekmiyor; çoğu ev internet sağlayıcısı zaten port 25'i kapatıyor. **Test 1 - Başka bir domain'in taklidi (büyük bir markanın adresi):** ```bash swaks --server SUNUCU_IP_ADRESINIZ --port 25 --helo mail.orneksite.com \ --from test@orneksite.com --to ben@orneksite.com \ --header 'From: "Customer Service" ' \ --header 'Subject: [TEST] spoofing denemesi' ``` Beklenen: `550 5.7.1 rejected by DMARC policy for usps.com`. Zarf göndericisi (`test@orneksite.com`) SPF'ten geçiyor ama ekranda görünen `From:` domain'iyle (`usps.com`) hizalanmıyor; `usps.com` da `p=reject` yayınlıyor. OpenDMARC'tan önce bu mail benim sunucumda Inbox'a düşüyordu. **Test 2 - Kendi domain'imizin taklidi ("CEO fraud"):** ```bash swaks --server SUNUCU_IP_ADRESINIZ --port 25 --helo mail.orneksite.com \ --from test@gmail.com --to ben@orneksite.com \ --header 'From: "CEO" ' \ --header 'Subject: [TEST] acil havale' ``` > **Zarfta neden `gmail.com`?** Test sunucunun kendi IP'sinden yapılıyor ve bu IP `orneksite.com`'un SPF kaydında yetkili. Zarfa `test@orneksite.com` yazarsam SPF hem geçer hem `From:` ile hizalanır ve DMARC PASS verir; gerçek bir saldırganı canlandırmamış olurum. Saldırganın IP'si SPF kaydınızda olmayacağı için zarfta başka bir domain kullanmak onun durumunu birebir yansıtıyor. Kendi DMARC kaydınız `p=reject` ise beklenen yine `550 5.7.1 rejected by DMARC policy for orneksite.com`. `p=quarantine` aşamasındaysanız mail kabul ediliyor ama başlığında `dmarc=fail` yazıyor. Bu testte bakmanız gereken asıl şey: teslim edilen mailde sizin domain'inizle atılmış bir `DKIM-Signature` olmamalı. ```bash grep -l "acil havale" /home/kullanici1/Mail/Inbox/new/* | xargs grep -c "^DKIM-Signature" # Beklenen: 0 (p=reject ise mail hiç gelmediği için grep boş döner, o da geçerli sonuç) ``` İlk config'te bu sayı 1 çıkıyordu: SpamAssassin dönüşünde OpenDKIM sahte maili benim anahtarımla imzalıyordu (log'da `DKIM-Signature field added`). Bu mail bir Gmail adresine forward edilseydi orada DKIM ve DMARC'tan geçmiş görünecekti. **Test 3 - Giriş yapmış bir hesabın başkası adına göndermesi (çalınan şifre senaryosu):** ```bash swaks --server 127.0.0.1 --port 465 --tls-on-connect \ --auth LOGIN --auth-user no-reply@orneksite.com \ --from ben@orneksite.com --to ben@orneksite.com ``` Beklenen: `553 5.7.1 : Sender address rejected: not owned by user no-reply@orneksite.com`. İlk config'te 465 bu gönderimi kabul ediyordu. **Test 4 - Yetkisiz IP'den zarf taklidi (SPF):** ```bash swaks --server SUNUCU_IP_ADRESINIZ --port 25 --helo mail.orneksite.com \ --from test@usps.com --to ben@orneksite.com --quit-after RCPT ``` Beklenen: `550 5.7.23 : Recipient address rejected: Message rejected due to: SPF fail - not authorized...`. Bu testi kendi domain'inizle yapamazsınız, çünkü istek sunucunun kendi IP'sinden geliyor ve o IP sizin SPF kaydınızda yetkili. Onun yerine SPF'i `-all` ile biten ve sizin IP'nizi içermeyen bir domain kullanın (burada `usps.com`). Gerçek bir saldırgan `ben@orneksite.com` zarfıyla geldiğinde de aynı yanıtı alıyor, yeter ki sizin SPF kaydınız da `-all` ile bitsin. İlk config'te port 25'teki bu korumayı `reject_sender_login_mismatch` sağlıyordu (`not logged in` yanıtı) ve yalnızca `login_maps.pcre`'deki adresleri kapsıyordu. SPF ise domain'deki bütün adresleri koruyor. Testlerden sonra Inbox'a düşen test maillerini silmeyi unutmayın. ![Gmail'de gelen mailin başlık detayları (SPF/DKIM/DMARC PASS)](/images/posts/027-self-hosted-mail-sunucusu-sifirdan-kurulum-rehberi/img/ss8-gmailde-gelen-mailin-baslik-detaylari.webp) ![MXToolbox sonuçları](/images/posts/027-self-hosted-mail-sunucusu-sifirdan-kurulum-rehberi/img/ss9-mxtoolbox-sonuclari.webp) ## Karşılaştığım Sorunlar ve Çözümleri (Troubleshooting) Rehberi uygularken siz de aynı sorunlarla karşılaşabilirsiniz. Karşılaştığım hatalar ve çözümleri: | # | Sorun | Hata Mesajı / Belirti | Çözüm | |---|-------|----------------------|-------| | 1 | IMAP bağlantı hatası | "Authentication not allowed until SSL/TLS is enabled" | Roundcube'da IMAP bağlantısını `ssl://host.docker.internal:993` olarak değiştirin | | 2 | Docker'dan host'a bağlantı kesildi | "Connection refused" veya timeout | Fail2ban'ın Docker container IP'sini banlayıp banlamadığını kontrol edin: `sudo fail2ban-client status dovecot` | | 3 | Gönderici adresi yanlış | Mail `kullanici1@host.docker.internal` adresinden gelmiş görünüyor | Roundcube DB'sinde `users` ve `identities` tablolarını güncelleyin | | 4 | Gmail'e mail gönderilemiyor | Log'larda "Connection timed out" (port 25) | Hetzner giden 25 ve 465'i varsayılan olarak engelliyor; destek panelinden limit talebiyle açtırın (Adım 12) | | 5 | Sieve izin hatası | "sieve: Failed to stat" veya "Permission denied" | `chmod 755 /var/lib/dovecot/sieve && chmod 644 /var/lib/dovecot/sieve/*.sieve` | | 6 | Dovecot passwd-file okunamıyor | "Permission denied" users dosyası | `chown root:dovecot /etc/dovecot/users && chmod 640 /etc/dovecot/users` | | 7 | Gelen mail teslim edilemiyor | Dovecot LDA "user not found" hatası | Dovecot config'e fallback `userdb { driver = passwd }` ekleyin | | 8 | SMTP mail gönderilemiyor | `[451] 4.7.1 Service unavailable - try again later` | OpenDKIM anahtar izni sorunu. Anahtar grup tarafından okunabiliyorsa ve `opendkim` grubunda `postfix` gibi başka üyeler varsa OpenDKIM "key data is not secure" deyip imzalamayı reddediyor. Çözüm: `chown opendkim:opendkim /etc/postfix/dkim/*/mail.private && chmod 600 /etc/postfix/dkim/*/mail.private` | | 9 | Dovecot LDA izin hatası | `euid=65534(nobody)` -- `stat(/root/Mail/Inbox/tmp) failed: Permission denied` | `postmaster` ve `root` alias'ları gerçek mail kullanıcısına yönlendirilmemiş. `/etc/aliases` dosyasında `postmaster: kullanici1` ve `root: kullanici1` ekleyip `sudo newaliases` çalıştırın | | 10 | Dovecot stats-writer hatası | `net_connect_unix(/run/dovecot/stats-writer) failed: Permission denied` | Önce teslimat yapılan kullanıcının home dizini gerçekten var mı bakın: bende bu hata, home dizini olmayan bir kullanıcıya teslimat denenirken de çıktı. Dizin yerindeyse `/etc/dovecot/conf.d/10-master.conf` dosyasına `service stats { unix_listener stats-writer { mode = 0660; group = dovecot } }` bloğunu ekleyip Dovecot'u yeniden başlatın | | 11 | MySQL başlatılamıyor | `ExecStartPre=... status=203/EXEC` | MySQL systemd dosyaları silinmiş veya paket yarı bozuk (`rH` durumunda). `systemctl daemon-reload` yapın. Eğer service dosyası yoksa paketi `dpkg --purge --force-remove-reinstreq` ile temizleyip yeniden kurun. GPG key süresi dolmuşsa: yeni key'i `/usr/share/keyrings/mysql-archive-keyring.gpg` olarak kaydedin ve repo dosyasına `[signed-by=...]` ekleyin | | 12 | Ubuntu 24.04 servis adları | `Unit spamassassin.service not found` veya `Unit sshd.service not found` | Ubuntu 24.04'te servis adları değişti: `spamassassin` -> **`spamd`**, `sshd` -> **`ssh`**. Tüm `systemctl` komutlarında yeni adları kullanın | | 13 | Postfix DKIM key uyarısı | `not owned by root: /etc/postfix/./dkim/*/mail.private` | Anahtar `opendkim:opendkim 600` ise bu yalnızca uyarı, görmezden gelebilirsiniz. Susturmak isterseniz `root:opendkim 640` yapın; `root:root` yapmayın, OpenDKIM anahtarı okuyamaz (Adım 6 ve 10) | | 14 | APT Docker repo çift tanımlı | `Target Packages ... is configured multiple times` | Docker repo'su birden fazla dosyada tanımlanmış. `/etc/apt/sources.list.d/` altındaki duplicate dosyayı silin, sadece `signed-by` içeren dosyayı tutun | | 15 | Mailler spam'e düşüyor (Received stripping) | SpamAssassin loglarında `NO_RECEIVED, NO_RELAYS` bayrakları | `header_checks`'teki `Received` silme kuralını kaldırın, yalnızca `X-Originating-IP` satırını bırakın. `Received` başlıkları olmayan mail izini saklıyormuş gibi görünüyor | | 16 | Gmail "Bu ileti tehlikeli olabilir" (phishing) | Spam değil, phishing uyarısı: "Kişisel bilgileri çalmak için kullanılmıştı" | Gönderen domain'i ile içerikteki linklerin domain'i aynı olmalı. `no-reply@A.com`'dan gönderip içerikte `B.com` linkleri verince Gmail bunu phishing gibi görüyor. Her proje kendi domain'inden göndermeli | | 17 | Dovecot tüm hesaplarda "Oturum açılamadı" (düzenleme sonrası) | `open(/etc/dovecot/users) failed: Permission denied (euid=113 missing +r perm)` | Dosyayı düzenlemek sahipliğini ya da iznini değiştirebiliyor. Her düzenlemeden sonra `chown root:dovecot /etc/dovecot/users && chmod 640 /etc/dovecot/users` | | 18 | Çoklu domain SSL sertifika hatası | Mail istemcisinde sertifika uyuşmazlığı hatası | `certbot certonly --nginx -d mail.domain1.com -d mail.domain2.com --expand` ile mevcut sertifikaya yeni domain ekleyin | | 19 | Uygulama/istemci mail gönderemiyor (güncelleme sonrası) | `553 5.7.1 : Sender address rejected: not owned by user ...` | Giriş yapılan hesap, kullanılan gönderici adresinin sahibi değil. Ya `MAIL_FROM_ADDRESS`'i giriş yapılan hesapla aynı yapın ya da `login_maps.pcre`'de o adresin satırına hesabı ekleyin: `/^info@orneksite\.com$/ info@orneksite.com, ben@orneksite.com`. Sonra `sudo postfix reload` | | 20 | Meşru bir mail DMARC yüzünden reddediliyor | Gönderene `550 5.7.1 rejected by DMARC policy for ...` bounce'u dönüyor | Genelde maili değiştirerek ileten eski tip bir forwarder veya mail listesi (konuya etiket ekleyip DKIM imzasını bozan). Gönderen domain `p=reject` yayınladığı için ret doğru karar. Güvendiğiniz sabit bir aracı sunucuysa IP'sini `/etc/opendmarc/ignore.hosts`'a ekleyip `sudo systemctl restart opendmarc`. Her şeyi toptan açmak için `RejectFailures false` yapmayın, koruma tamamen gider | | 21 | Dışarıdan gelen mail SPF yüzünden reddediliyor | `550 5.7.23 Message rejected due to: SPF fail ...` | Gönderen domain'in SPF kaydı gönderen IP'yi yetkilendirmiyor ve `-all` ile bitiyor. Sorun karşı tarafta: SPF kaydını düzeltmeleri gerekiyor. `policyd-spf` varsayılan olarak yalnızca kesin `Fail` sonucunu reddediyor, `~all` (softfail) geçiyor | | 22 | Fail2ban hiç ban atmıyor | Log'da başarısız giriş dolu ama `fail2ban-client status postfix-sasl` çıktısında `Total failed: 0` | Ubuntu'da varsayılan backend `systemd`; Postfix jail'i journal'da yanlış servis adını arıyor. Jail'e `backend = auto` ve `logpath = /var/log/mail.log` ekleyin (Adım 13) | ### Log Dosyaları Bir şey ters gidince cevap neredeyse her zaman log'larda: ```bash # Postfix logları sudo tail -f /var/log/mail.log # Dovecot logları sudo tail -f /var/log/mail.log | grep dovecot # Fail2ban logları sudo tail -f /var/log/fail2ban.log # Roundcube logları tail -f /mnt/HC_Volume_XXXXXXXX/websites/mail.orneksite.com/logs/errors.log # Nginx logları sudo tail -f /var/log/nginx/mail.orneksite.com.error.log ``` ## Güvenlik Sıkılaştırma Kurulum bitti ama iş bitmedi. Mail sunucusu internete doğrudan açık ve açıldığı gün deneme trafiği başlıyor. Benim aldığım önlemler:
SSH Güvenliği Root ile girişi kapatın, yalnızca SSH anahtarıyla girişe izin verin: ```bash sudo vim /etc/ssh/sshd_config ``` ```properties PermitRootLogin no PasswordAuthentication no PubkeyAuthentication yes ``` ```bash sudo systemctl restart ssh ``` > **Dikkat (Ubuntu 24.04):** Servisin adı `sshd` değil, `ssh`. `systemctl restart sshd` yazınca `Unit sshd.service not found` alıyorsunuz. Ubuntu 24.04'te SSH ayrıca socket activation (`ssh.socket`) ile başlıyor; bu normal. > **Uyarı:** Bu ayarlardan önce SSH anahtarınızı sunucuya yükleyin ve sudo yetkili bir kullanıcıyla anahtarla girebildiğinizi ayrı bir oturumda deneyin. Yoksa sunucuya erişiminizi kaybedersiniz. SSH portunu 22'den başka bir porta taşıyabilirsiniz ama bunun güvenliğe katkısı az; log'daki gürültüyü azaltıyor, o kadar. Brute-force denemelerini zaten Fail2ban'ın `sshd` jail'i banlıyor. Portu değiştirirseniz Ubuntu 24.04'te socket activation yüzünden `sudo systemctl daemon-reload && sudo systemctl restart ssh.socket` da gerekiyor, yalnızca `ssh`'ı yeniden başlatmak yetmiyor.
Kurulum Sonrası Düz Metin Şifre Temizliği Kurulum boyunca elinizden bir sürü düz metin şifre geçiyor: `sudo passwd` için yazdığınız şifre, `doveadm pw -p 'SIFRE'` komutunun parametresi, `docker-compose.yml`'deki `POSTGRES_PASSWORD`, "sonra hash'lerim" diye kenara not ettiğiniz dosya... Kurulum bitince bunların diskte izi kalmamalı. Kalırsa sunucuda okuma yetkisi kazanan biri ya da yedeğinize erişen biri hazır bir anahtar destesi bulur. **Kontrol edilecek yerler:** ```bash # 1. "Geçici olarak tutayım" scratch dosyaları ls -la /root/*.txt /root/.*passwords* /tmp/*pass* 2>/dev/null # 2. Bash history (şifreyi komut satırında yazdıysanız, örn. doveadm pw -p 'xxx') grep -iE 'passwd|doveadm pw -p|PASSWORD=' ~/.bash_history 2>/dev/null # 3. Editor cache'leri: nano .save, vim .swp, .viminfo find /root -maxdepth 2 \( -name '*.swp' -o -name '*.save' -o -name '.viminfo' \) 2>/dev/null ``` **Neden `rm` değil de `shred`?** `rm` yalnızca dosyanın kaydını siliyor, içerik diskte duruyor ve üzerine başka veri yazılana kadar `extundelete`, `photorec` gibi araçlarla geri getirilebiliyor. `shred -u` önce dosyanın üzerine birkaç kez rastgele veri yazıyor, sonra siliyor: ```bash shred -u /root/.mail-setup-passwords.txt ``` Bash history için: ```bash history -c && shred -u ~/.bash_history && touch ~/.bash_history ``` > **Uyarı (dosya sistemi):** Varsayılan ayarlı ext4'te `shred` çoğu durumda işini görüyor. Btrfs, ZFS gibi copy-on-write dosya sistemlerinde ya da TRIM açık SSD'lerde ise üzerine yazılan blok her zaman eski veriyle aynı yere denk gelmiyor; garanti yok. En güvenlisi şifreyi hiç diske yazmamak: `doveadm pw`'yi `-p` olmadan çalıştırın (şifreyi terminalden sorar, history'ye düşmez), `docker-compose.yml`'de şifreyi `${VAR}` ile `.env`'den okutun ve `.env`'i `.gitignore`'a ekleyin, şifreleri kurulum biter bitmez bir parola yöneticisine (Bitwarden, 1Password, KeePassXC) taşıyın. **Benim rutinim:** Her hesap için `openssl rand -base64 15` ile şifre üretiyorum ve hemen parola yöneticisine koyuyorum. Oradan kopyalayıp `doveadm pw -s CRYPT`'nin şifre istemine yapıştırıyorum, çıkan hash'i `/etc/dovecot/users`'a yazıyorum. Şifre ne terminalde ne dosyada ne history'de görünüyor, kurulum sonunda silinecek bir şey de kalmıyor.
Postfix Güvenlik Ayarları Bunların hepsi Adım 5'teki config'te zaten var; toplu hâlde ne işe yaradıklarını yazayım: - `smtpd_helo_required = yes`: İstemci kendini tanıtmadan konuşamıyor. - `reject_unknown_sender_domain`: Gönderen domain'in DNS'te A veya MX kaydı yoksa mail reddediliyor. - `reject_unknown_reverse_client_hostname`: IP'sinin PTR kaydı olmayan sunucular reddediliyor. - `smtpd_tls_auth_only = yes`: Şifreyle giriş yalnızca TLS üzerinden. - `smtpd_forbid_bare_newline = normalize`: SMTP smuggling saldırısına karşı koruma. - `smtpd_sasl_security_options = noanonymous, noplaintext`: TLS'siz bağlantıda anonim girişe ve şifrenin açık gittiği yöntemlere izin yok. - 465 ve 587'deki `reject_sender_login_mismatch`: Giriş yapan kullanıcı yalnızca kendi adresiyle gönderebiliyor. - `check_policy_service unix:private/policyd-spf`: Dışarıdan gelen mailde SPF kontrolü, kesin `Fail` reddediliyor. - Port 25'te `smtpd_sasl_auth_enable=no`: Şifre denemeleri bu kapıdan yapılamıyor. - Yalnızca loopback'ten oluşan `mynetworks` ve olmayan `non_smtpd_milters`: Docker container'larına ve SpamAssassin dönüşüne "iç ağ" ayrıcalığı verilmiyor.
Spoofing Savunması Bu bölümü rehberi yayınladıktan yaklaşık bir buçuk ay sonra, kurulumu farklı spoofing senaryolarıyla test ederek ekledim ve açıkçası biraz utanarak yazıyorum... :) İşin temelinde şu var: `From:` başlığı, zarfın üstüne elle yazılan gönderici adı gibi serbest bir metin ve SMTP onu kendiliğinden doğrulamıyor. Yani teknik olarak herkes `From:` satırına istediği adresi yazabiliyor. SPF, DKIM ve DMARC bu açığı kapatmak için var. Örneğin `usps.com`'un DMARC kaydı: ```bash dig TXT _dmarc.usps.com +short # "v=DMARC1; p=reject; ..." ``` `p=reject` ile domain sahibi herkese "benim adıma gelip doğrulamadan geçemeyen maili reddedin" diyor. Gmail, Outlook gibi DMARC uygulayan bir sunucu böyle bir sahte maili kapıdan çeviriyor. Yani büyük bir markanın adresini taşıyan sahte mail ancak DMARC uygulamayan bir sunucuda Inbox'a düşebiliyor. Sonra aklıma şu soru geldi: benim sunucum DMARC uyguluyor mu? Test ettim, uygulamıyormuş. Büyük bir markanın adresini taklit eden test maili spam puanı -1.9 ile doğrudan Inbox'a düştü; SpamAssassin DMARC ihlalini görmüş ama 5 puanlık eşiğin çok altında küçük bir ceza vermişti. Kendi domain'imin taklidi (`ceo@...`) daha da kötü çıktı: SpamAssassin dönüşünde OpenDKIM o sahte maili benim anahtarımla imzaladı. Rehberin ilk hâli SPF/DKIM/DMARC'ı gönderen taraf için doğru kuruyor, alan taraf için hiç kurmuyordu. Test ettiğim senaryolar ve karşılıkları: | # | Senaryo | Örnek | Ne koruyor | Sonuç | |---|---------|-------|-----------|-------| | 1 | Başka bir domain'in taklidi | `From: support@usps.com` | OpenDMARC (`RejectFailures true`) + `policyd-spf` | Gönderen domain `p=reject` yayınlıyorsa SMTP'de `550` ile ret | | 2 | Dışarıdan kendi domain'imizin taklidi ("CEO fraud") | `From: ceo@orneksite.com` | Zarf için SPF (`-all` + `policyd-spf`), başlık için OpenDMARC ve sizin DMARC kaydınızın `p=reject` olması; OpenDKIM artık sahte maili imzalamıyor | `p=quarantine`'de `dmarc=fail` ile işaretlenir, `p=reject`'te ret | | 3 | Çalınan şifreyle başka hesap adına gönderim | `no-reply@` şifresiyle `MAIL FROM:` | 465 ve 587'deki `reject_sender_login_mismatch` | `553 ... not owned by user` ile ret | | 4 | Giriş yapmış hesabın zarfı doğru, `From:` başlığı başka | Zarf `no-reply@`, başlık `From: ben@...` | Postfix'te yerleşik kontrol yok | Kalan risk (aşağıda) | | 5 | Görünen ad veya benzer domain | `"Müşteri Hizmetleri" `, `0rneksite.com` | Sunucu tarafında teknik doğrulama yakalayamaz, çünkü mail gerçekten o adresten geliyor | Roundcube `message_show_email` + kullanıcının dikkati | **Kalan risk (senaryo 4):** Postfix'in `reject_sender_login_mismatch` kuralı yalnızca zarf göndericisine (`MAIL FROM`) bakıyor. Postfix'in kendi belgesi de `From:` başlığını zarfla eşleştirmek için harici bir milter gerektiğini söylüyor ve örnek olarak [milterfrom](https://github.com/magcks/milterfrom)'u gösteriyor; [Rspamd](https://rspamd.com) de bu işi yapabiliyor. Roundcube üzerinden bu açık kullanılamıyor, çünkü Roundcube zarfı her zaman seçili kimlikten dolduruyor. Risk, ele geçirilmiş bir uygulama şifresiyle doğrudan SMTP konuşan bir betikten geliyor. Tek kişilik ya da birkaç hesaplı bir sunucuda bunu kabul edilebilir buluyorum. Yine de uygulama şifrelerini her proje için ayrı bir `no-reply@` hesabında tutmak ve "Rate Limiting" bölümündeki limitleri açmak bu yüzden önemli. Çok kullanıcılı bir sunucuda milter kurardım. **Kendi DMARC kaydınız neden `p=reject` olmalı?** Madalyonun öbür yüzü bu. `p=quarantine` ya da `p=none`'da kalırsanız bir saldırgan başka sunuculara sizin domain'inizden sahte mail atabilir, alıcı da en fazla spam klasörüne koyar. Üstelik OpenDMARC kendi sunucunuzda da kendi domain'inizin taklidini reddetmiyor, yalnızca işaretliyor (senaryo 2). Kademeli geçişi "DMARC'ı Kademeli Sıkılaştırma" bölümünde anlattım. Sunucunuzun bu senaryolara nasıl cevap verdiğini "Doğrulama ve Test" altındaki Spoofing Testleri ile görebilirsiniz.
Dovecot Güvenliği - `ssl = required`: TLS olmadan şifreyle giriş yapılamıyor. - `ssl_min_protocol = TLSv1.2`: TLS 1.0 ve 1.1 kapalı. Log'da `SSL_accept() failed: ... version too low` satırları görürseniz, eski sürümle bağlanmaya çalışan tarayıcı botlardır; reddedilmeleri doğru. - Cipher listesi: 3DES, RC4, MD5 gibi zayıf algoritmalar kapalı. - `ssl_prefer_server_ciphers = yes`: Şifreleme algoritmasını istemci değil sunucu seçiyor.
Brute-force Koruması (Fail2ban Detayları) Kurduğumuz jail'ler: | Jail | Koruduğu Servis | Açıklama | |------|-----------------|----------| | postfix | Postfix SMTP | Hatalı SMTP komutları | | postfix-sasl | Postfix SASL | Başarısız SMTP kimlik doğrulama | | dovecot | Dovecot IMAP/POP3 | Başarısız IMAP/POP3 giriş denemeleri | | sieve | Dovecot Sieve | Başarısız ManageSieve giriş denemeleri | | roundcube-auth | Roundcube | Başarısız webmail giriş denemeleri | Başlangıç için önerdiğim değerler: ```ini # 10 dakika içinde 3 başarısız deneme, 2 saat ban maxretry = 3 findtime = 600 bantime = 7200 ``` Yorumu ayrı satıra yazmamın bir nedeni var: fail2ban satır sonu yorumu olarak yalnızca `;` tanıyor. `maxretry = 3 # açıklama` yazarsanız `#` sonrası değerin parçası sayılıyor ve ayar okunamıyor. Yoğun saldırı altındaysanız `bantime`'ı artırabilir, `bantime = -1` ile kalıcı ban da verebilirsiniz. Gerçek değerlerinizi Git'e ya da herkese açık bir yere koymayın; eşiği bilen saldırgan hep onun bir altında kalmaya çalışır. Aktif banlar: ```bash sudo fail2ban-client status postfix-sasl sudo fail2ban-client status dovecot sudo fail2ban-client status roundcube-auth ```
SpamAssassin Yapılandırması SpamAssassin Postfix'e bağlı ve gelen mailleri puanlıyor. Eşik ve davranış ayarları `/etc/spamassassin/local.cf` dosyasında: ```bash sudo vim /etc/spamassassin/local.cf ``` ```properties required_score 5.0 rewrite_header Subject [SPAM] report_safe 0 dns_server 127.0.0.1:5335 ``` - `required_score 5.0`: 5 ve üstü puan alan mail spam sayılıyor. Varsayılan da 5; daha sıkı filtre isterseniz 3-4 arası deneyebilirsiniz. - `rewrite_header`: Spam mailin konusunun başına `[SPAM]` ekleniyor. - `report_safe 0`: Mail olduğu gibi kalıyor, SpamAssassin yalnızca `X-Spam-*` başlıklarını ekliyor. Varsayılan hâlde spam maili bir raporun içine ek olarak sarıyor; bu da mailin DKIM imzasını bozuyor. - `dns_server`: Aşağıda anlattığım yerel resolver. **Kara liste kontrolleri için yerel resolver.** Kendi sunucumda bir test mailinin başlıklarına bakarken `RCVD_IN_ZEN_BLOCKED_OPENDNS`, `URIBL_BLOCKED` gibi kuralları gördüm. Spamhaus ve URIBL, Hetzner'inki gibi çok kullanıcılı resolver'lardan gelen sorgulara cevap vermiyor. Sonuçta SpamAssassin'in kara liste kontrolleri hiç çalışmıyordu. Çözüm, yalnızca SpamAssassin'in kullanacağı küçük bir yerel resolver: ```bash sudo nala install unbound -y sudo tee /etc/unbound/unbound.conf.d/spamassassin.conf > /dev/null <<'EOF' server: interface: 127.0.0.1 port: 5335 access-control: 127.0.0.0/8 allow EOF sudo systemctl disable --now unbound-resolvconf.service sudo unbound-checkconf && sudo systemctl restart unbound dig +short @127.0.0.1 -p 5335 2.0.0.127.zen.spamhaus.org # 127.0.0.2 gibi cevaplar gelmeli; 127.255.255.254 ise sorgu engelleniyor demek ``` Unbound'u 53 yerine 5335'te dinlettim ve `unbound-resolvconf` servisini kapattım; sistemin kendi DNS ayarına dokunmuyor, yalnızca SpamAssassin kullanıyor. **Kural güncellemelerini açın.** Aynı gün SpamAssassin kurallarının hiç güncellenmediğini de fark ettim. Güncellemeyi yapan `spamassassin-maintenance.timer` bende kapalıydı. Açmak ve ilk güncellemeyi hemen yapmak için: ```bash sudo systemctl enable --now spamassassin-maintenance.timer sudo systemctl start spamassassin-maintenance.service ls /var/lib/spamassassin/ # 4.000000 gibi bir klasör görmelisiniz sudo systemctl restart spamd ``` > **SpamAssassin filtre değil, puanlayıcı:** Maili reddetmiyor, puan verip başlık ekliyor. Spam işaretli maili Junk'a taşıyan Adım 7'deki `sieve_before` kuralı; o kural olmazsa `[SPAM]` etiketli mail Inbox'ta kalıyor. Spoofing konusunda da SpamAssassin'e güvenmeyin: DMARC ihlaline verdiği ceza 5'lik eşiğin çok altında kalıyor, benim testimde büyük bir markayı taklit eden mail -1.9 puanla geçti. Spoofing kararını SMTP aşamasında OpenDMARC veriyor, SpamAssassin ondan sonra gelen ikinci katman.
Nginx Güvenlik Başlıkları Adım 9'daki Nginx config'inde şu başlıklar var: - `Strict-Transport-Security`: Tarayıcıya bu siteye her zaman HTTPS ile gelmesini söylüyor (HSTS). - `X-Frame-Options "SAMEORIGIN"`: Sayfanın başka sitelerde çerçeve içine alınmasını (clickjacking) engelliyor. - `X-Content-Type-Options "nosniff"`: Tarayıcının dosya türünü tahmin etmesini engelliyor. Hassas dizinler de kapalı: ```nginx location ~ /\. { deny all; } # Gizli dosyalar (.env, .git, vb.) location ~ ^/(config|temp|logs)/ { deny all; } # Yapılandırma ve log dizinleri ```
Open Relay Kontrolü Sunucunuzun "open relay", yani herkesin üzerinden başka adreslere mail gönderebildiği bir sunucu olmadığından emin olun. Open relay'ler kısa sürede spamcilerin eline geçiyor ve IP kara listeye giriyor. Test: ```bash # Başka bir sunucudan veya yerel makinenizden telnet mail.orneksite.com 25 EHLO test.com MAIL FROM: RCPT TO: ``` `554 5.7.1 : Relay access denied` alıyorsanız sunucunuz open relay değil. Bunu `smtpd_relay_restrictions = permit_sasl_authenticated, reject_unauth_destination` satırı sağlıyor: başka domain'lere yalnızca giriş yapmış kullanıcılar mail gönderebiliyor.
Rate Limiting Deneme yanılma saldırılarını yavaşlatmak için hız sınırları: **Roundcube:** ```php $config['login_rate_limit'] = 5; // dakikada en fazla 5 başarısız giriş ``` Bu satır `custom.config.inc.php`'de zaten var. **Postfix** (isteğe bağlı, `main.cf`'ye eklenebilir; sınırlar dakika başına ve `mynetworks` dışındaki istemciler için geçerli): ```ini smtpd_client_connection_rate_limit = 10 smtpd_client_message_rate_limit = 30 anvil_rate_time_unit = 60s ```
Düzenli Güncellemeler Mail sunucusu internete açık bir sistem, güvenlik yamalarını geciktirmemek gerekiyor. Otomatik güvenlik güncellemeleri için: ```bash sudo nala install unattended-upgrades -y sudo dpkg-reconfigure -plow unattended-upgrades ``` Ya da düzenli olarak elle: ```bash sudo nala update && sudo nala upgrade -y ``` Docker imajlarını güncelleme: ```bash cd /mnt/HC_Volume_XXXXXXXX/websites/mail.orneksite.com docker compose pull docker compose up -d ```
Log İzleme Takip ettiğim log dosyaları: | Dosya | İçerik | |-------|--------| | `/var/log/mail.log` | Postfix, Dovecot, OpenDKIM ve OpenDMARC kayıtları | | `/var/log/fail2ban.log` | Ban/unban kayıtları | | `/var/log/nginx/mail.orneksite.com.access.log` | Roundcube web erişimleri | | `/var/log/nginx/mail.orneksite.com.error.log` | Nginx hataları | | Roundcube `logs/errors.log` | Roundcube uygulama hataları | Canlı izlemek için: ```bash sudo tail -f /var/log/mail.log ``` Hata ve uyarıları süzmek için: ```bash sudo grep "error\|warning\|fatal" /var/log/mail.log | tail -50 ```
DMARC'ı Kademeli Sıkılaştırma (Gradual Rollout) Rehberde DMARC'ı `p=quarantine` ile başlattık ve çoğu durum için makul bir orta yol bu. Kitaba uygun olanı ise `p=none` ile başlayıp kademeli sıkılaştırmak. Ben ilk kurulumda doğrudan `p=quarantine` yazdım; ilk günlerde bir SaaS'ın alt domain'den gönderdiği bildirim mailleri karantinaya düştü, servisi SPF'e ekleyince düzeldi. Bir sonraki kurulumda şu sırayı izleyeceğim: 1. **`p=none`** (ilk 2-4 hafta): `v=DMARC1; p=none; rua=mailto:postmaster@orneksite.com; fo=1` - Uyumsuzluklar yalnızca raporlanıyor, teslimat etkilenmiyor. - `rua` adresine Google, Yahoo gibi alıcılardan günlük toplu raporlar geliyor. - Raporlardan, sizin adınıza mail atan ama SPF/DKIM'den geçemeyen meşru kaynakları buluyorsunuz. Sık görülen örnek: unutulmuş bir SaaS (Mailchimp, Stripe gibi) sizin domain'inizden mail atıyordur; ya IP bloğunu SPF'e eklersiniz ya da o serviste DKIM imzalamayı ayarlarsınız. 2. **`p=quarantine`** (sonraki 2-4 hafta): `v=DMARC1; p=quarantine; rua=mailto:postmaster@orneksite.com; fo=1` - Geçemeyen mailler alıcının spam klasörüne düşüyor, kaybolmuyor. - "Şu mailim spam'e düştü" diye geri bildirim gelirse kurtarması kolay. 3. **`p=reject`** (son adım): `v=DMARC1; p=reject; rua=mailto:postmaster@orneksite.com; fo=1` - Geçemeyen mail doğrudan reddediliyor, spam klasörüne bile gitmiyor. - Bu adıma, raporlarda beklenmedik bir meşru başarısızlık kalmadığında geçin. Bu geçişlerin hepsi DNS'te yapılıyor; sunucuda bir satır değiştirmeniz gerekmiyor, yalnızca Cloudflare'deki `_dmarc` kaydını güncelliyorsunuz. `p=reject`'in bir faydası da kendi sunucunuzda görünüyor: OpenDMARC, dışarıdan gelip sizin domain'inizi taklit eden maili (`ceo@orneksite.com` gibi) `p=quarantine`'de yalnızca işaretliyor, `p=reject`'e geçince SMTP aşamasında reddediyor. Yani kendi kullanıcılarınızı "CEO dolandırıcılığına" karşı korumanın son adımı da bu. Raporları okumak için [dmarcian](https://dmarcian.com) ya da [Postmark'ın DMARC aracı](https://dmarc.postmarkapp.com) gibi ücretsiz araçlar kullanabilirsiniz; ham XML raporlarını elle okumak zor.
Düzenli Bakım Kontrol Listesi Mail sunucusu "kurup unuttum" diyebileceğiniz bir şey değil. Başta öyle sanmıştım. Düzenli baktığım şeyler: - **Sertifika yenileme testi** (ayda bir): `sudo certbot renew --dry-run`. Gerçek yenileme yapmıyor, sürecin çalıştığını gösteriyor. Yenilemeyi certbot'un systemd zamanlayıcısı (`certbot.timer`) zaten yapıyor; ben hook'ların da çalıştığını görmek için bakıyorum. - **Mail kuyruğu** (haftada bir): `sudo postqueue -p` ile takılı mail var mı bakıyorum. Onlarca mail birikmişse ya alıcı sunuculara ulaşılamıyordur ya da teslim edilemeyen bir adres vardır; nedenini log'dan bulun. `sudo postqueue -f` kuyruğu hemen yeniden deniyor. - **Fail2ban** (haftada bir): `sudo fail2ban-client status` ile jail'lerdeki ban sayılarına bakıyorum. Ani bir artış varsa log'ları inceliyorum; sayaçlar hep sıfırsa jail'ler log okuyamıyor olabilir (Adım 13). - **Disk kullanımı** (haftada bir): `sudo du -sh /home/*/Mail /var/log /opt/roundcube` (Roundcube dizini sizde farklıysa onu yazın). Kutuların ve log'ların büyümesini, kullanıcı "kutum doldu" demeden önce ben görmek istiyorum. - **Log döndürme** (otomatik, bir kez teyit edin): rsyslog'un logrotate ayarı (`/etc/logrotate.d/rsyslog`) varsayılan olarak geliyor. `sudo logrotate -d /etc/logrotate.conf` ile deneme çalıştırması yapıp bir kez görün. - **mail-tester puanı** (ayda bir): Yeni bir test adresine mail atıp puanın hâlâ 9-10 arasında olduğuna bakıyorum. IP itibarı zamanla değişebiliyor. - **Spoofing testleri** (config değişikliğinden sonra): "Doğrulama ve Test" bölümündeki dört testi yeniden koşuyorum. - **Yeni hesap ya da şifre değişikliği** (gerektikçe): "Yeni Mail Hesabı Ekleme" bölümündeki adımlar. Bunları aklımda tutmaya çalışmak yerine takvime koydum; her pazartesi sabahı 10 dakikamı alıyor. Alışkanlık olunca sıradan bir iş, olmayınca sorunlar sürpriz olarak çıkıyor.
## Yeni Mail Hesabı Ekleme Sunucu oturduktan sonra en sık yaptığım iş yeni hesap açmak oldu. Örnek olarak `yeni@orneksite.com` için adımlar: ### 1. Sistem Kullanıcısı Oluşturun ```bash sudo useradd -m -s /usr/sbin/nologin -G mail yenikullanici sudo passwd yenikullanici ``` Mail dizinlerini oluşturun: ```bash sudo mkdir -p /home/yenikullanici/Mail/{Inbox,Sent,Drafts,Trash,Junk} sudo chown -R yenikullanici:yenikullanici /home/yenikullanici/Mail ``` ### 2. Dovecot Users Dosyasına Ekleyin Şifre hash'ini iki yoldan alabilirsiniz: **Yöntem 1: `doveadm pw` (benim tercihim)** ```bash doveadm pw -s CRYPT # Şifreyi iki kez soruyor, history'ye düşmüyor # Çıktı: {CRYPT}$2y$05$... ``` **Yöntem 2: Sistem kullanıcısının shadow hash'i** ```bash sudo grep yenikullanici /etc/shadow | cut -d: -f2 ``` UID ve GID'yi öğrenin: ```bash id yenikullanici ``` `/etc/dovecot/users` dosyasına yeni satırı ekleyin ve izinleri kontrol edin: ```plaintext yeni@orneksite.com:{CRYPT}$y$j9T$HASH_BURAYA...:UID:GID::/home/yenikullanici::userdb_mail=maildir:~/Mail:INBOX=~/Mail/Inbox:LAYOUT=fs ``` ```bash sudo chown root:dovecot /etc/dovecot/users && sudo chmod 640 /etc/dovecot/users ``` ### 3. Postfix Virtual Alias Ekleyin `/etc/postfix/virtual` dosyasına: ```properties yeni@orneksite.com yenikullanici ``` Hash'i yeniden oluşturun: ```bash sudo postmap /etc/postfix/virtual ``` ### 4. login_maps.pcre'ye Ekleyin `/etc/postfix/login_maps.pcre` dosyasına: ```plaintext /^yeni@orneksite\.com$/ yeni@orneksite.com ``` ### 5. Servisleri Yeniden Başlatın ```bash sudo systemctl restart dovecot sudo systemctl restart postfix ``` ### 6. Test Edin Roundcube'a `yeni@orneksite.com` ve şifresiyle giriş yapın, giriş olursa bir test maili gönderin. > **Not:** Başka bir domain için hesap açıyorsanız (örneğin `yeni@ikincidomain.com`), o domain'in `main.cf`'teki `mydestination` satırında, OpenDKIM'in `keytable` ve `signingtable` dosyalarında tanımlı olduğundan, DKIM anahtarının üretilip DNS'e eklendiğinden emin olun. > **İpucu:** Mevcut bir domain'e adres ekliyorsanız DNS'e (SPF, DKIM, DMARC, MX) dokunmanız gerekmiyor; bu kayıtlar adres değil domain bazında çalışıyor. > **Not:** Yeni kutu yerine yalnızca başka bir adrese yönlendirme istiyorsanız bir sonraki bölüme bakın; basit forward'dan catch-all'a kadar senaryoları orada anlattım. ## E-Posta Yönlendirme (Forwarding) Sunucuyu kullanmaya başlayınca bazen yeni bir kutu açmak yerine maili başka bir adrese iletmenin daha pratik olduğunu gördüm. Örneğin `info@orneksite.com`'a gelen müşteri maillerini hem yerel kutuda saklamak (arşiv ve felaket durumu için) hem de telefondan kolayca görmek için Gmail'e kopyalamak istiyordum. Ya da tatildeyken `support@orneksite.com`'a gelenleri ekipten birine yönlendirmek... Hosting günlerinde bunu cPanel'in "Forwarders" ekranından tek tıkla yapıyordum. cPanel'in arkasında Exim çalışıyor ama yaptığı iş aynı: bir adrese geleni başka adrese eşlemek. Postfix'te bunun karşılığı `virtual_alias_maps` ve bizim sunucuda zaten kurulu, yalnızca ne yazacağımızı bilmemiz gerekiyor. :) Aşağıda dört senaryo var, ihtiyacınıza uyanı seçin. Sonunda da çoğu kişinin atladığı bir konuya geleceğim: forward edilen mailler neden Gmail'de spam'e düşüyor ve `postsrsd` bunu nasıl çözüyor. ### Forwarding Nedir, Alias'tan Farkı Ne? Önce terimleri ayıralım. Postfix'te "alias" ve "forward" aynı dosyada tanımlanıyor ama farklı şeyler: - **Alias:** Bir adresi yerel bir kullanıcıya bağlıyor. `info@orneksite.com`'a gelen mail aslında `kullanici1`'in kutusuna düşüyor. Yukarıdaki `info@ikincidomain.com kullanici2` satırı tam olarak bu. - **Forward:** Gelen maili Gmail, Outlook ya da başka bir domain'deki harici bir adrese iletiyor veya kopyalıyor. İkisi de `/etc/postfix/virtual` dosyasına yazılıyor. Postfix hedefin yerel kullanıcı mı harici adres mi olduğuna bakıp ona göre davranıyor. Yani forward için yeni bir şey kurmaya gerek yok, `virtual` dosyasına satır eklemek yetiyor. > **Gmail benzetmesi:** Gmail'deki "Filtreler > Yeni filtre > Şuraya yönlendir" akışının sunucu tarafındaki karşılığı bu. Farkı şu: Gmail filtresi mail kutuya düştükten sonra iletiyor ve bir kopyası kutuda kalıyor; Postfix'in `virtual_alias_maps`'i ise adresi mail kutuya düşmeden, teslimat zincirinin en başında yeniden yazıyor. ### Senaryo 1: Sadece Harici Hedefe Yönlendirme En basiti: `info@orneksite.com`'a gelen her mail doğrudan `ornek@gmail.com`'a gitsin, sunucuda saklanmasın. `/etc/postfix/virtual` dosyasını açın: ```bash sudo vim /etc/postfix/virtual ``` Sona ekleyin: ```properties # Tüm info@ mailleri Gmail'e ilet, yerel kopya tutma info@orneksite.com ornek@gmail.com ``` Hash dosyasını yeniden üretip Postfix'i reload edin: ```bash sudo postmap /etc/postfix/virtual sudo systemctl reload postfix ``` > **`reload` mı `restart` mı?** Alias değişikliklerinde `reload` yetiyor. `restart` Postfix'i durdurup yeniden başlatıyor, o an süren teslimatlar yarıda kesilip daha sonra yeniden deneniyor. `reload` ise config'i yeniden okutuyor ve çalışan süreçleri işlerini bitirdikten sonra yeniliyor. Test için Gmail'den `info@orneksite.com`'a bir mail atıp log'u izleyin: ```bash sudo tail -f /var/log/mail.log | grep info@orneksite.com ``` Log akışı kabaca şöyle görünmeli. Port 25'ten gelen mail önce SpamAssassin'e gidiyor, oradan dönüp Gmail'e çıkıyor: ```log postfix/smtpd[...]: A1B2C3: client=mail-...google.com[...] postfix/qmgr[...]: A1B2C3: from=, size=..., nrcpt=1 (queue active) postfix/pipe[...]: A1B2C3: to=, orig_to=, relay=spamassassin, ..., status=sent (delivered via spamassassin service) postfix/qmgr[...]: D4E5F6: from=, size=..., nrcpt=1 (queue active) postfix/smtp[...]: D4E5F6: to=, relay=gmail-smtp-in.l.google.com[...]:25, ..., status=sent ``` `orig_to=` kısmına bakın: Postfix `info@`'nun karşılığını `virtual` dosyasında bulup alıcıyı `ornek@gmail.com` yapmış. SpamAssassin dönüşünde mail yeni bir kuyruk numarasıyla doğrudan Gmail'e gidiyor, sunucuda saklanmıyor. `SRS0=...` ile başlayan gönderici adresini aşağıda anlatıyorum. ### Senaryo 2: Hem Yerel Kutuya Hem Harici Adrese Kopyalama En çok kullandığım senaryo bu. `info@orneksite.com`'a gelen mailin bir kopyası Gmail'e gitsin (dışarıdayken hızlıca okumak için), bir kopyası da yerel kutuda kalsın (Roundcube'tan erişim, arşiv, yedek). `virtual` dosyasına virgülle ayrılmış iki hedef yazın: ```properties # info@'ya gelen her mail hem yerel kullanıcıya hem Gmail'e gönderilsin info@orneksite.com kullanici1, ornek@gmail.com ``` Hash'i güncelleyip reload edin: ```bash sudo postmap /etc/postfix/virtual sudo systemctl reload postfix ``` > **Neden bunu tercih ediyorum?** Birincisi, yerel kopya bir güvence: Gmail bir gün hesabı "olağandışı etkinlik" gerekçesiyle kilitlerse maillerim yerel kutuda duruyor. İkincisi arşiv: yıllar önceki bir maili kendi sunucumda aramak bana daha rahat geliyor. Log'da, SpamAssassin dönüşünden sonra iki ayrı teslimat satırı göreceksiniz: ```log postfix/smtp[...]: D4E5F6: to=, relay=gmail-smtp-in.l.google.com[...]:25, ..., status=sent postfix/local[...]: D4E5F6: to=, relay=local, ..., status=sent ``` İlki Gmail'e, ikincisi Dovecot LDA üzerinden yerel kutuya teslimat. ### Senaryo 3: Çoklu Harici Hedef (Dağıtım Listesi Mantığı) Küçük bir ekipte gelen müşteri maillerini herkesin görmesini istiyorsanız Mailman, Sympa gibi bir liste yazılımına gerek yok. `virtual` dosyasına virgülle birden fazla hedef yazabilirsiniz: ```properties # support@'a gelen mailler ekipteki herkese dağılsın support@orneksite.com ali@gmail.com, veli@hotmail.com, ayse@outlook.com ``` Sonra yine aynı iki komut: ```bash sudo postmap /etc/postfix/virtual sudo systemctl reload postfix ``` > **Cevaplar yine bireysel kalıyor:** Bu bir liste değil, yalnızca dağıtım. Ali müşteriye cevap yazdığında Veli ile Ayşe'nin haberi olmuyor. Ortak bir gelen kutusu istiyorsanız Mailman ya da Zammad, FreeScout gibi bir helpdesk yazılımı gerekiyor. Küçük ekipler ve basit ihtiyaçlar için bu yöntem yetiyor. > **Listeyi büyütmeyin:** Her gelen mail, listedeki her harici adrese ayrı bir gönderim demek. Liste büyüdükçe gelen spam'i de o kadar çoğaltıp dışarı gönderiyorsunuz ve bu IP itibarınızı etkiliyor. Ben bunu yalnızca birkaç kişilik ekipler için kullanıyorum; kalabalık gruplar için gerçek bir liste yazılımı daha doğru. ### Senaryo 4: Catch-all (Tüm Domain'i Tek Adrese Yönlendir) Domain'e gelen bütün mailleri (adres tanımlı olsun olmasın) tek bir yere toplamak için catch-all kullanılıyor: ```properties # Domain'in tüm gelen maillerini Gmail'e yönlendir @orneksite.com ornek@gmail.com ``` Yine postmap ve reload: ```bash sudo postmap /etc/postfix/virtual sudo systemctl reload postfix ``` Bu satır, `info@orneksite.com`, `ben@orneksite.com` gibi ayrıca tanımlı adresler dışında kalan her maili yakalıyor. Aynı dosyada hem tanımlı adres hem catch-all varsa Postfix önce tam adresi arıyor, bulamazsa catch-all'a düşüyor. > **Catch-all'ın bedeli:** İlk mail sunucumda heyecanla catch-all'ı açmıştım, "nereye yazsalar bana gelsin" diye. İlk hafta günde 200'ün üzerinde spam aldım. Botlar domain'leri tarayıp `admin@`, `webmaster@`, `john@` gibi rastgele adreslere mail atıyor; catch-all hepsini kabul edip Gmail'e iletiyordu. Kendi IP'mden Gmail'e her gün yüzlerce spam taşımak, gönderici itibarımı açıkça riske atıyordu. Sonunda catch-all'ı kapatıp yalnızca kullandığım adresleri tek tek tanımladım. Catch-all'ı yalnızca özel durumlarda düşünün: spam botlarını incelemek için ayrı bir tuzak hesap ya da kapatacağınız bir domain'in geçiş dönemi gibi. Günlük kullanım için önermem. ### Postmap ve Servis Yenileme: Pratik İpuçları Her senaryoda aynı iki komutu çalıştırdık: `postmap` ve `systemctl reload`. Unutmamak için kendime küçük bir alias yazdım: ```bash # ~/.bashrc veya ~/.zshrc'ye ekle alias virtual-reload='sudo postmap /etc/postfix/virtual && sudo systemctl reload postfix && echo "virtual map güncellendi"' ``` Artık her değişiklikten sonra `virtual-reload` yazıyorum. > **`postmap` neden gerekli?** Postfix `/etc/postfix/virtual` metin dosyasını doğrudan okumuyor, onun `postmap` ile üretilen `/etc/postfix/virtual.db` kopyasını okuyor. Bu hash dosyası sayesinde satır sayısı ne olursa olsun arama hızlı kalıyor. Metin dosyasını değiştirip `postmap`'i unutursanız Postfix eski kuralları kullanmaya devam ediyor. ### Forward SPF'i Kırıyor Buraya kadar iş kolay görünüyor: `virtual`'a satır ekle, postmap, reload. İlk forward kurulumumda da ben öyle sanmıştım. Senaryo 1'i kurduktan sonra Hotmail'den `info@orneksite.com`'a bir test maili attım. Mail Gmail'e geldi ama spam klasörüne düştü. "Orijinali göster" ile başlıklara baktığımda şunu gördüm: ```log-error Authentication-Results: mx.google.com; spf=fail (google.com: domain of test@hotmail.com does not designate 1.2.3.4 as permitted sender) smtp.mailfrom=test@hotmail.com; ``` Nedenini çözmem epey sürdü. Mantık şu: 1. `test@hotmail.com` bana mail attı. Zarf göndericisi (envelope sender) `test@hotmail.com`. 2. Postfix `info@orneksite.com`'un karşılığını `virtual`'da bulup maili `ornek@gmail.com`'a iletti. 3. İletirken zarf göndericisi değişmedi, hâlâ `test@hotmail.com`. 4. Gmail, maili getiren IP'yi (bizim sunucu) hotmail.com'un SPF kaydında aradı ve bulamadı. hotmail.com SPF kaydını `-all` ile bitirdiği için sonuç doğrudan `spf=fail`. 5. DMARC'ın geçmesi artık tamamen Hotmail'in DKIM imzasının yolda bozulmamasına kalıyor. İmza bozulursa ya da gönderen domain hiç imzalamıyorsa DMARC da düşüyor. Gönderen domain'in politikası `quarantine` veya `reject` ise mail spam'e gidiyor ya da hiç kabul edilmiyor. Yani forward, düzgün kurulmuş bir altyapıda bile SPF'i doğası gereği kırıyor. Bu Postfix'in hatası değil, SPF'in "maili kim getiriyor" sorusuna bakmasının sonucu. Çözümü SRS (Sender Rewriting Scheme). ### SRS Nedir, Ne Yapar? SRS, forward sırasında zarf göndericisini yeniden yazıyor. `test@hotmail.com`'u alıp şuna çeviriyor: ```plaintext SRS0=hash=tt=hotmail.com=test@orneksite.com ``` Zarf göndericisi artık bizim domain'imiz olduğu için Gmail SPF'e bizim kaydımızdan bakıyor ve `spf=pass` veriyor. DMARC tarafı biraz farklı: SPF'in DMARC'a sayılması için zarf domain'inin `From:` domain'iyle aynı olması gerekiyor. Burada biri `orneksite.com`, diğeri `hotmail.com`, yani hizalanmıyor. DMARC'ı geçiren, Hotmail'in yol boyunca bozulmadan gelen DKIM imzası. SRS'in işi SPF'i kurtarmak ve bounce'ların yolunu korumak. > **O garip adres ne?** `SRS0=hash=tt=hotmail.com=test` kısmı orijinal adresi saklıyor; mail geri dönerse (bounce) postsrsd bu kısımdan `test@hotmail.com`'u çıkarıp bounce'u ona yönlendiriyor. `tt` bir zaman damgası, `hash` ise sunucuya özel bir anahtarla üretilen imza. Bu imza sayesinde başkası kendi uydurduğu `SRS0=...@orneksite.com` adresleriyle sunucunuzu bounce aktarıcısı olarak kullanamıyor. ### postsrsd Kurulumu Ubuntu'da SRS için en kolay yol `postsrsd` paketi. Postfix'le TCP üzerinden konuşan küçük bir servis: ```bash sudo nala update sudo nala install -y postsrsd ``` Servis kurulunca başlıyor, yine de kontrol edin: ```bash sudo systemctl enable --now postsrsd sudo systemctl status postsrsd ``` Çıktıda `active (running)` görmelisiniz. > **Portlar hakkında:** Birazdan göreceğiniz 10001 ve 10002, Ubuntu 24.04'teki `postsrsd` 1.10 paketinin varsayılan portları, ben de onları kullanıyorum. Bu portlar sunucunuzda doluysa `/etc/default/postsrsd`'deki `SRS_FORWARD_PORT` ve `SRS_REVERSE_PORT` ile değiştirebilirsiniz. Tek şart, `main.cf`'e yazacağınız portların bunlarla aynı olması. ### postsrsd Yapılandırması `/etc/default/postsrsd` dosyasını açın: ```bash sudo vim /etc/default/postsrsd ``` Şunları ayarlayın: ```bash # Yeniden yazılan adresler bu domain altında oluşur SRS_DOMAIN=orneksite.com # Kendi domain'lerimiz: bunlardan çıkan mailin göndericisi yeniden yazılmaz SRS_EXCLUDE_DOMAINS=orneksite.com,ikincidomain.com ``` Servisi yeniden başlatın: ```bash sudo systemctl restart postsrsd ``` > **`SRS_EXCLUDE_DOMAINS` neden önemli?** `SRS_DOMAIN` zaten dokunulmadan geçiyor ama ikinci domain'iniz bu listede yoksa, o domain'den gönderdiğiniz her mailin zarf göndericisi de `SRS0=...@orneksite.com` oluyor. SPF hizası kayboluyor ve DMARC yalnızca DKIM'e kalıyor. Sunucudan mail gönderdiğiniz bütün domain'leri buraya yazın. ### Postfix'i SRS'e Bağlama `/etc/postfix/main.cf` dosyasının sonuna şu satırları ekleyin: ```ini # SRS: forward edilen mailin zarf göndericisi bizim domain'e yazılır, SPF geçer sender_canonical_maps = tcp:127.0.0.1:10001 sender_canonical_classes = envelope_sender recipient_canonical_maps = tcp:127.0.0.1:10002 recipient_canonical_classes = envelope_recipient,header_recipient ``` Sonra reload: ```bash sudo systemctl reload postfix ``` > **10001 ve 10002 ne yapıyor?** 10001 giden yön: çıkan mailin zarf göndericisini `test@hotmail.com`'dan `SRS0=...@orneksite.com`'a çeviriyor. 10002 dönüş yönü: `SRS0=...@orneksite.com`'a gelen bounce'un alıcısını tekrar `test@hotmail.com`'a çözüyor. Biri eksik kalırsa ya SPF ya da bounce yolu kırılıyor. ### Doğrulama: SRS Çalışıyor mu? Önce postsrsd'nin portları dinlediğinden emin olun: ```bash sudo ss -tlnp | grep -E '10001|10002' ``` Beklenen çıktı: ```log LISTEN 0 10 127.0.0.1:10001 0.0.0.0:* users:(("postsrsd",pid=...)) LISTEN 0 10 127.0.0.1:10002 0.0.0.0:* users:(("postsrsd",pid=...)) ``` Sonra bir test maili atıp log'u izleyin: ```bash sudo tail -f /var/log/mail.log | grep -E 'SRS0|ornek@gmail.com' ``` Forward edilen mail için şuna benzer satırlar görmelisiniz: ```log postfix/qmgr[...]: ...: from=, ... postfix/smtp[...]: ...: to=, ..., status=sent ``` `from=` görüyorsanız SRS çalışıyor. Son kontrol Gmail'de: maili açıp sağ üstteki üç noktadan "Orijinali göster"e tıklayın. Şuna benzer bir sonuç görmelisiniz: ```log-info Return-Path: Authentication-Results: mx.google.com; dkim=pass header.i=@hotmail.com; spf=pass (google.com: domain of SRS0=...@orneksite.com designates 1.2.3.4 as permitted sender) smtp.mailfrom=...; dmarc=pass (p=NONE sp=NONE dis=NONE) header.from=hotmail.com ``` `dkim=pass` satırında `hotmail.com` görmeniz normal ve doğru. Forward edilen mail bizim anahtarımızla yeniden imzalanmıyor, zaten imzalanmamalı da; o mail bizim değil. DMARC'ı Hotmail'in bozulmadan gelen imzası geçiriyor, SPF'i de SRS kurtarıyor. (`p=NONE` kısmı hotmail.com'un kendi DMARC politikası.) > **Bir istisna:** SpamAssassin maili spam bulup konusunu `[SPAM]` diye değiştirirse orijinal DKIM imzası bozuluyor. Spam'i forward etmemek için bir sebep daha. Bu rehberin ilk hâlinde sunucu dışarıdan gelen mailleri de kendi anahtarıyla imzalayıp iletiyordu, spoofing yapılmış bir mail bile imzamızla çıkabiliyordu. `non_smtpd_milters` satırını kaldırmam, forward'ı da güvenli hâle getirdi. ### Forward Sonrası: Adres Hâlâ Çalışıyor mu? Başlıklarda Ne Görünür? Forward'ı kurduktan sonra aklıma birkaç soru takıldı: `info@` adresi hâlâ çalışan bir adres mi, Gmail'e gelen mailde `info@` görünüyor mu, biri cevap yazarsa cevap nereye gidiyor? Cevapları aşağıya topladım, başlığa tıklayınca açılıyor:
1) Mail içeriğinde info@ adresi görünür mü? Evet, görünüyor. Postfix forward ederken yalnızca zarf alıcısını değiştiriyor. Mailin gövdesine ve mevcut başlıklarına dokunmuyor; yolda kendi `Received:` satırını, SpamAssassin de `X-Spam-*` başlıklarını ekliyor. Gmail'e ulaşan mailin başlıkları şöyle: | Header | Değer | Açıklama | |---|---|---| | `From:` | `test@hotmail.com` | Orijinal gönderici, değişmez | | `To:` | `info@orneksite.com` | Orijinal alıcı, değişmez | | `Subject:` | `[orijinal konu]` | Değişmez | | `Delivered-To:` | `ornek@gmail.com` | Gmail ekliyor: maili hangi kutuya teslim ettiği | Gmail'de mailin ayrıntılarını açtığınızda alıcı olarak `info@orneksite.com` görürsünüz. Postfix'in yerel teslimatta eklediği `X-Original-To:` başlığı ise forward edilen mailde olmuyor; o başlık yalnızca yerel kutuya düşen kopyada var. > **Pratik faydası:** Gmail'de `to:info@orneksite.com` filtresiyle forward edilen mailleri otomatik etiketleyebilir ya da ayrı bir klasöre alabilirsiniz. Hangi mailin nereden geldiği bir bakışta belli oluyor.
2) info@ hâlâ çalışan bir adres mi? Senaryoya göre değişiyor: | Senaryo | info@ adresinin durumu | |---|---| | **Senaryo 1** (yalnız harici): `info@ → ornek@gmail.com` | Adres mail kabul ediyor ama yerel kutuda saklamıyor, bir aktarma noktası gibi. Linux kullanıcısı ve Dovecot kaydı olmadığı için Roundcube'a `info@` ile girilemiyor | | **Senaryo 2** (yerel + harici): `info@ → kullanici1, ornek@gmail.com` | Mail hem Gmail'e gidiyor hem yerel kutuda kalıyor. `kullanici1` ile Roundcube'a girip `info@`'ya gelen her şeyi görebilirsiniz | | **Senaryo 3** (çoklu harici): `support@ → ali, veli, ayse` | Senaryo 1 gibi aktarma. Yerel kutu yok, yalnız dağıtım | | **Senaryo 4** (catch-all): `@orneksite.com → ornek@gmail.com` | Domain'deki her adres mail kabul ediyor gibi görünüyor ama hiçbiri yerelde saklanmıyor |
3) Reply (yanıt) davranışı: cevap nereye gider? Gmail'de forward edilen maile "Yanıtla" dediğinizde cevap `info@`'ya değil, orijinal göndericiye (`test@hotmail.com`) gidiyor. Mail istemcileri cevabı `From:` (varsa `Reply-To:`) başlığına göre yönlendiriyor. Çoğu zaman istenen de bu: `info@` yalnızca bir kanal, asıl muhatap gönderen kişi. Burada dikkat edilecek nokta cevabın hangi adresten çıktığı. Gmail'den yazdığınızda müşteri cevabı `ornek@gmail.com`'dan alıyor. Kurumsal adresten cevap vermek istiyorsanız Senaryo 2'yi kullanıp Roundcube'a `kullanici1` ile girin ve `info@orneksite.com` kimliğiyle yanıtlayın. Bunun için o kimliğin `login_maps.pcre`'de `kullanici1`'e tanımlı olması gerekiyor ("Yeni Mail Hesabı Ekleme" bölümündeki gibi).
4) info@'dan mail göndermek ayrı bir konu Bunu ayrıca yazıyorum çünkü sık karıştırılıyor: `virtual_alias_maps` yalnızca gelen maili yönlendiriyor. `info@`'dan mail göndermek için forward yetmiyor: - `info@` için bir Linux kullanıcısı olmalı (`useradd ...`) - Dovecot'un `users` dosyasında kaydı olmalı (kimlik doğrulama için) - `login_maps.pcre`'de bu kullanıcıya `info@orneksite.com` kimliği tanımlanmalı - Gönderim Roundcube'dan ya da bir mail istemcisinden, bu kullanıcıyla giriş yapılarak yapılmalı Kısacası forward gelen mailin nereye gideceğini, `login_maps` ise kimin hangi adresle gönderebileceğini belirliyor. İkisini birden istiyorsanız önce "Yeni Mail Hesabı Ekleme" akışıyla `info@` hesabını açın, sonra `virtual`'a `info@orneksite.com kullanici_info, ornek@gmail.com` yazın (Senaryo 2). Böylece `info@` hem yerel kutuya alıyor, hem Gmail'e kopyalıyor, hem de kendi adıyla gönderebiliyor.
### Tuzaklar ve Önemli Notlar Forward kurarken gözden kaçan ve sonradan can sıkan birkaç nokta: > **Mail döngüsü:** `info@orneksite.com → ornek@gmail.com` kuruluyken Gmail'de de "her şeyi `info@orneksite.com`'a ilet" kuralı açarsanız mail iki sunucu arasında gidip gelmeye başlıyor. Sonsuza kadar sürmüyor; Postfix, `Received:` başlığı sayısı `hopcount_limit` sınırını (varsayılan 50) geçince maili döngü sayıp geri çeviriyor, Gmail de benzer bir kontrol yapıyor. Ama o sınıra gelene kadar, özellikle Senaryo 2'de, yerel kutuya bir sürü kopya düşüyor. Kurduktan sonra bir test maili atıp birkaç dakika gelen kutusunu izleyin. > **Spam forward etmek itibarı yakıyor:** `info@`'ya gelen spam'i Gmail'e iletirseniz Gmail o spam'i sizin IP'nizden gelmiş gibi görüyor ve bu, sunucunuzun gönderici itibarına yazılıyor. SpamAssassin burada tek başına çözüm değil: spam bulduğu maile yalnızca etiket koyuyor, mail yine forward ediliyor. Bu yüzden çok spam alan adresleri forward etmek yerine yerel kutuda tutup IMAP ile okumayı tercih ediyorum. Ara sıra [MXToolbox Blacklist Check](https://mxtoolbox.com/blacklists.aspx) ile IP'nizin bir listeye girip girmediğine bakmak da iyi bir alışkanlık. > **Gmail'in alım sınırları:** Google'ın Workspace belgesine göre bir Gmail hesabı dakikada 60, saatte 3.600, günde 86.400 mail alabiliyor ([Gmail receiving limits](https://support.google.com/a/answer/1366776)). Normal bir `info@` adresi bu sınırlara yaklaşmaz. Ama bir spam dalgası ya da bir sistemden gelen otomatik bildirimler hesabı sınıra dayarsa, Google'ın belgesine göre hesap bir süre yeni mail alamıyor ve gelen mailler göndericiye geri dönüyor. Yoğun trafik alan adresler için yerel kutu artı IMAP istemcisi (Thunderbird, K-9 Mail) daha sağlıklı. > **`recipient_delimiter = +` ile birlikte:** main.cf'te `recipient_delimiter = +` tanımlı (Adım 5). `info+fatura@orneksite.com` için ayrı bir satır yoksa Postfix `info@orneksite.com` satırını kullanıyor ve uzantıyı hedefe taşıyor; mail `ornek+fatura@gmail.com`'a gidiyor. Gmail `+` uzantılarını desteklediği için sorun çıkmıyor, üstelik bu uzantıya göre filtre kurabiliyorsunuz. `info+fatura@` için ayrı bir satır yazarsanız o satır öncelikli oluyor. > **Yedek olarak forward:** Kritik adresleri (`domain-admin@` gibi) ikinci bir Gmail ya da Outlook hesabına da kopyalamak basit bir yedek yöntemi. Bunu otomatik yedekleme sanmayın: MX kaydı bizim sunucuyu gösterdiği için sunucu çökerse yeni mailler Gmail'e de ulaşmıyor. Gönderen sunucular maili birkaç gün boyunca yeniden deniyor, sunucu o sürede ayağa kalkarsa kayıp olmuyor. Kopyanın sağladığı şey, daha önce gelmiş maillerin Gmail'de de durması. > **Forward'ı geri almak:** `/etc/postfix/virtual`'dan satırı silin ya da başına `#` koyun, sonra `postmap` ve `reload`. `postmap`'i atlarsanız `virtual.db` eski kuralla çalışmaya devam ediyor. ## Son Sözüm ve Önerilerim İlk maili gönderip Gmail'de SPF, DKIM ve DMARC'ın üçünün de `PASS` olduğunu gördüğüm anki sevinci tarif etmek zor. Hosting'ten sunucuya geçtiğimden beri yıllarca ertelediğim, birkaç kez başlayıp yarıda bıraktığım, MailCow'la deneyip sevmediğim bu iş sonunda (epey emekle de olsa) çalışır hâle geldi. Artık tamamen benim kontrolümde olan bir mail sistemim var. :) **Bana kazandırdıkları:** - **Kontrol:** Mailler, hesaplar ve kurallar benim sunucumda. Bir hizmetin fiyatını, kotasını ya da kurallarını değiştirmesiyle uğraşmıyorum; bir hesabın bir sabah kilitlenip kapanması gibi bir riskim de yok. - **Bağımsızlık:** Ne hosting firmasının mail hizmetine ne de Gmail'e, Outlook'a bağlıyım. Yeni bir domain ya da adres gerektiğinde birkaç satırla açıyorum. **Hafife almamanız gerekenler:** - **Bakım:** Bu bir "kur ve unut" sistemi değil. Güncellemeleri takip edip log'lara düzenli bakıyorum. - **Yedek:** Mail kaybı telafisi zor bir kayıp. Maildir'leri ve config'leri düzenli yedekleyin. - **İzleme:** Sunucunun ayakta olduğunu ve mail kuyruğunun şişmediğini bir yerden görmeniz gerekiyor. **Sırada ne var:** - Her şey oturduktan sonra DMARC politikasını `quarantine`'den `reject`'e çekin ("Güvenlik Sıkılaştırma" bölümündeki kademeli geçişi izleyerek). - Ek domain'leri aynı adımlarla ekleyin. - Her proje için ayrı bir `no-reply@` hesabı açın ve `From:` domain'inin eşleşmesine dikkat edin. - Laravel ya da başka web uygulamalarınızı sunucunuza bağlayın (yukarıdaki entegrasyon bölümü). - Uptime Kuma gibi bir araçla sunucuyu, basit bir betikle de Postfix kuyruğunu izleyin. - Sunucuyu başkalarıyla paylaşıyorsanız `From:` başlığını zarfla karşılaştıran bir milter ekleyin ([milterfrom](https://github.com/magcks/milterfrom)) ya da SpamAssassin, OpenDKIM ve OpenDMARC'ın işini tek başına yapan [Rspamd](https://rspamd.com)'ye geçmeyi düşünün (ayrıntısı "Spoofing Savunması" bölümünde). - Kurulum bittiğinde ve her config değişikliğinden sonra "Spoofing Testleri"ni yeniden koşun. - Ara sıra `mail-tester.com` ile bir test maili gönderip puana bakın. - **Google Postmaster Tools:** Gmail'e düzenli olarak mail gönderiyorsanız domain'inizi [postmaster.google.com](https://postmaster.google.com)'da doğrulayın. Orada Gmail kullanıcılarının maillerinizi ne oranda spam olarak işaretlediğini ve domain'inizin Google'ın gönderici kurallarına (SPF, DKIM, DMARC) uyup uymadığını görebiliyorsunuz. Veriler ancak yeterli hacim olduğunda doluyor; az mail gönderen bir domain'de ekranlar boş kalabilir. Doğrulama bir DNS TXT kaydıyla yapılıyor ve her domain için ayrı ayrı gerekiyor. Bir gün mailleriniz Gmail'de spam'e düşmeye başlarsa ilk bakacağınız yer burası. Mail sunucusu kurmak kolay değil, bunu saklamayacağım. Ben de yıllarca erteledim, birkaç kez sunucuyu bozdum, MailCow'la boğuştum. Ama imkânsız da değil. Hosting günlerindeki hazır mail rahatlığını bu kez kendi sunucumda, kendi kurallarımla yaşıyorum ve harcadığım saatlere değdi. ## Tüm Yapılandırma Dosyaları Bütün yapılandırma dosyalarının son hâllerini burada topladım. Adımlarda bir şeyi atladıysanız ya da kendi dosyanızla karşılaştırmak isterseniz buraya bakın.
/etc/postfix/main.cf - Postfix Ana Yapılandırma ```ini smtpd_banner = $myhostname ESMTP $mail_name (Ubuntu) biff = no append_dot_mydomain = no readme_directory = no compatibility_level = 3.6 smtpd_tls_cert_file = /etc/letsencrypt/live/mail.orneksite.com/fullchain.pem smtpd_tls_key_file = /etc/letsencrypt/live/mail.orneksite.com/privkey.pem smtpd_tls_security_level = may smtp_tls_CApath=/etc/ssl/certs smtp_tls_security_level = may smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache smtpd_tls_auth_only = yes smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1 smtp_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1 smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1 smtp_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1 smtpd_sasl_auth_enable = yes smtpd_sasl_type = dovecot smtpd_sasl_path = private/auth smtpd_sasl_security_options = noanonymous, noplaintext smtpd_sasl_tls_security_options = noanonymous smtpd_relay_restrictions = permit_sasl_authenticated, reject_unauth_destination myhostname = mail.orneksite.com mydomain = orneksite.com myorigin = /etc/mailname mydestination = $myhostname, $mydomain, ikincidomain.com, mail, localhost.localdomain, localhost, localhost.$mydomain relayhost = mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128 mail_name = orneksite.com alias_maps = hash:/etc/aliases alias_database = hash:/etc/aliases mailbox_size_limit = 0 recipient_delimiter = + inet_interfaces = all inet_protocols = all home_mailbox = Mail/Inbox/ # Yalnız port 25'e uygulanır; sahiplik kontrolü master.cf'teki 465/587 override'larında smtpd_sender_login_maps = pcre:/etc/postfix/login_maps.pcre smtpd_sender_restrictions = reject_non_fqdn_sender, permit_mynetworks, reject_unknown_reverse_client_hostname, reject_unknown_sender_domain smtpd_recipient_restrictions = permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination, reject_unknown_recipient_domain, check_policy_service unix:private/policyd-spf policyd-spf_time_limit = 3600 smtpd_helo_required = yes smtpd_helo_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_invalid_helo_hostname, reject_non_fqdn_helo_hostname, reject_unknown_helo_hostname header_checks = regexp:/etc/postfix/header_checks # OpenDKIM + OpenDMARC; non_smtpd_milters bilerek yok (SpamAssassin dönüşü imzalanmasın) milter_default_action = accept milter_protocol = 6 smtpd_milters = inet:localhost:12301, inet:localhost:54321 mailbox_command = /usr/lib/dovecot/deliver smtpd_forbid_bare_newline = normalize smtpd_forbid_bare_newline_exclusions = $mynetworks virtual_alias_maps = hash:/etc/postfix/virtual # SRS - forward edilen mailler için envelope rewrite (SPF/DMARC korunur) sender_canonical_maps = tcp:127.0.0.1:10001 sender_canonical_classes = envelope_sender recipient_canonical_maps = tcp:127.0.0.1:10002 recipient_canonical_classes = envelope_recipient,header_recipient ```
/etc/postfix/master.cf - Postfix Servis Tanımları (özelleştirilmiş kısım) Dosyanın sonuna eklenen özel servis tanımları: ```properties spamassassin unix - n n - - pipe user=debian-spamd argv=/usr/bin/spamc -f -e /usr/sbin/sendmail -oi -f ${sender} ${recipient} policyd-spf unix - n n - 0 spawn user=policyd-spf argv=/usr/bin/policyd-spf smtp unix - - n - - smtp smtp inet n - y - - smtpd -o content_filter=spamassassin -o smtpd_sasl_auth_enable=no submission inet n - y - - smtpd -o syslog_name=postfix/submission -o smtpd_tls_security_level=encrypt -o smtpd_tls_auth_only=yes -o smtpd_enforce_tls=yes -o smtpd_client_restrictions=permit_sasl_authenticated,reject -o smtpd_sender_restrictions=reject_sender_login_mismatch -o smtpd_sender_login_maps=pcre:/etc/postfix/login_maps.pcre -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject_unauth_destination smtps inet n - y - - smtpd -o syslog_name=postfix/smtps -o smtpd_tls_wrappermode=yes -o smtpd_sasl_auth_enable=yes -o smtpd_client_restrictions=permit_sasl_authenticated,reject -o smtpd_sender_restrictions=reject_sender_login_mismatch -o smtpd_sender_login_maps=pcre:/etc/postfix/login_maps.pcre -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject_unauth_destination ```
/etc/dovecot/dovecot.conf - Dovecot Tam Yapılandırma ```ini # --- SSL / TLS bölümü --- ssl = required ssl_cert =
/etc/opendkim.conf - OpenDKIM Yapılandırma ```properties Syslog yes SyslogSuccess yes Canonicalization relaxed/simple Mode sv OversignHeaders From AuthservID mail.orneksite.com UserID opendkim UMask 007 PidFile /run/opendkim/opendkim.pid TrustAnchorFile /usr/share/dns/root.key KeyTable file:/etc/postfix/dkim/keytable SigningTable refile:/etc/postfix/dkim/signingtable InternalHosts refile:/etc/postfix/dkim/trustedhosts Socket inet:12301@localhost ```
/etc/opendmarc.conf - OpenDMARC Yapılandırma ```properties AuthservID mail.orneksite.com TrustedAuthservIDs mail.orneksite.com Syslog true UMask 0002 UserID opendmarc PidFile /run/opendmarc/opendmarc.pid PublicSuffixList /usr/share/publicsuffix/public_suffix_list.dat Socket inet:54321@localhost RejectFailures true RequiredHeaders true IgnoreAuthenticatedClients true IgnoreHosts /etc/opendmarc/ignore.hosts SPFIgnoreResults true SPFSelfValidate true FailureReports false ``` **/etc/opendmarc/ignore.hosts:** ```plaintext 127.0.0.1 ::1 ```
docker-compose.yml - Roundcube + PostgreSQL ```yaml networks: roundcube-net: driver: bridge services: app-roundcube-db: image: postgres:16-alpine container_name: db-postgresql-roundcube restart: unless-stopped volumes: - ./db-data:/var/lib/postgresql/data environment: POSTGRES_DB: roundcube POSTGRES_USER: roundcube POSTGRES_PASSWORD: BURAYA_GÜÇLÜ_BİR_ŞİFRE_YAZIN networks: - roundcube-net healthcheck: test: ["CMD-SHELL", "pg_isready -U roundcube"] interval: 10s timeout: 5s retries: 5 app-roundcube: image: roundcube/roundcubemail:latest-apache container_name: app-roundcube restart: unless-stopped depends_on: app-roundcube-db: condition: service_healthy ports: - "127.0.0.1:8080:80" volumes: - ./config:/var/roundcube/config - ./logs:/var/roundcube/logs extra_hosts: - "host.docker.internal:host-gateway" environment: ROUNDCUBEMAIL_DB_TYPE: pgsql ROUNDCUBEMAIL_DB_HOST: app-roundcube-db ROUNDCUBEMAIL_DB_PORT: "5432" ROUNDCUBEMAIL_DB_NAME: roundcube ROUNDCUBEMAIL_DB_USER: roundcube ROUNDCUBEMAIL_DB_PASSWORD: BURAYA_GÜÇLÜ_BİR_ŞİFRE_YAZIN ROUNDCUBEMAIL_DEFAULT_HOST: ssl://host.docker.internal ROUNDCUBEMAIL_DEFAULT_PORT: "993" ROUNDCUBEMAIL_SMTP_SERVER: ssl://host.docker.internal ROUNDCUBEMAIL_SMTP_PORT: "465" ROUNDCUBEMAIL_PLUGINS: archive,zipdownload,managesieve ROUNDCUBEMAIL_SKIN: elastic ROUNDCUBEMAIL_ASPELL_DICTS: tr networks: - roundcube-net ```
/etc/nginx/sites-available/mail.orneksite.com - Nginx Reverse Proxy ```nginx server { listen 80; listen [::]:80; server_name mail.orneksite.com; return 301 https://$host$request_uri; } server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name mail.orneksite.com; ssl_certificate /etc/letsencrypt/live/mail.orneksite.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/mail.orneksite.com/privkey.pem; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; client_max_body_size 25m; access_log /var/log/nginx/mail.orneksite.com.access.log; error_log /var/log/nginx/mail.orneksite.com.error.log; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } location ~ /\. { deny all; } location ~ ^/(config|temp|logs)/ { deny all; } } ```
/etc/fail2ban/ - Fail2ban Jail ve Filtre Dosyaları **filter.d/roundcube-auth-proxy.conf:** ```ini [Definition] failregex = IMAP Error: Login failed for \S+ against \S+ from \S+ \(X-Real-IP: [,)] ignoreregex = ``` **jail.d/zzz-roundcube.local:** ```ini [roundcube-auth] enabled = true port = http,https filter = roundcube-auth-proxy backend = polling logpath = /mnt/HC_Volume_XXXXXXXX/websites/mail.orneksite.com/logs/errors.log maxretry = 3 findtime = 600 bantime = 7200 ``` **jail.d/emailwiz.local:** ```ini [postfix] enabled = true backend = auto logpath = /var/log/mail.log ignoreip = 127.0.0.1/8 172.16.0.0/12 192.168.0.0/16 [postfix-sasl] enabled = true backend = auto logpath = /var/log/mail.log ignoreip = 127.0.0.1/8 172.16.0.0/12 192.168.0.0/16 [sieve] enabled = true ignoreip = 127.0.0.1/8 172.16.0.0/12 192.168.0.0/16 [dovecot] enabled = true ignoreip = 127.0.0.1/8 172.16.0.0/12 192.168.0.0/16 ```
Diğer Yardımcı Dosyalar **/etc/postfix/login_maps.pcre:** ```plaintext /^ben@orneksite\.com$/ ben@orneksite.com /^info@ikincidomain\.com$/ info@ikincidomain.com ``` **/etc/postfix/header_checks:** ```plaintext /^X-Originating-IP:/ IGNORE ``` > **Dikkat:** `Received` başlıklarını silmeyin; mailleriniz spam'e düşer. Nedenini Adım 5'teki "header_checks" bölümünde anlattım. **/etc/postfix/virtual:** ```properties ben@orneksite.com kullanici1 info@ikincidomain.com kullanici2 ``` Sonra hash'leyin: `sudo postmap /etc/postfix/virtual` **/etc/postfix/dkim/keytable:** ```properties mail._domainkey.orneksite.com orneksite.com:mail:/etc/postfix/dkim/orneksite.com/mail.private mail._domainkey.ikincidomain.com ikincidomain.com:mail:/etc/postfix/dkim/ikincidomain.com/mail.private ``` **/etc/postfix/dkim/signingtable:** ```properties *@orneksite.com mail._domainkey.orneksite.com *@ikincidomain.com mail._domainkey.ikincidomain.com ``` **/etc/postfix/dkim/trustedhosts:** ```plaintext 127.0.0.1 ::1 ``` > **Dikkat:** Buraya Docker aralığı (`172.16.0.0/12` vb.) eklemeyin. Roundcube şifreyle bağlandığı için zaten imzalanıyor; Docker aralığı yalnızca şifresiz container'lara imza yetkisi verir. Ayrıntı: Adım 6. **/var/lib/dovecot/sieve/spam.sieve** (derledikten sonra: `sudo sievec /var/lib/dovecot/sieve/spam.sieve`): ```plaintext require ["fileinto"]; if header :contains "X-Spam-Flag" "YES" { fileinto "Junk"; stop; } ``` **/etc/dovecot/users** (şifre hash'lerini kendi kullanıcılarınıza göre düzenleyin): ```plaintext ben@orneksite.com:{CRYPT}$y$j9T$HASH...:UID:GID::/home/kullanici1::userdb_mail=maildir:~/Mail:INBOX=~/Mail/Inbox:LAYOUT=fs info@ikincidomain.com:{CRYPT}$y$j9T$HASH...:UID:GID::/home/kullanici2::userdb_mail=maildir:~/Mail:INBOX=~/Mail/Inbox:LAYOUT=fs ``` **/mnt/.../config/custom.config.inc.php:** ```php ['verify_peer' => false, 'verify_peer_name' => false], ]; $config['smtp_conn_options'] = [ 'ssl' => ['verify_peer' => false, 'verify_peer_name' => false], ]; ```
---
Kaynakça **Postfix Resmi Dokümanları:** - https://postfix.org/documentation.html - https://github.com/roehling/postsrsd **Dovecot ve Sieve:** - https://doc.dovecot.org/2.3 - https://pigeonhole.dovecot.org **OpenDKIM, OpenDMARC, SPF ve SpamAssassin:** - https://github.com/trusteddomainproject/OpenDKIM - https://github.com/trusteddomainproject/OpenDMARC - https://launchpad.net/spf-engine - https://spamassassin.apache.org **Spoofing Savunması ve Test:** - https://github.com/magcks/milterfrom - https://rspamd.com - https://github.com/jetmore/swaks **Roundcube, Docker ve PostgreSQL:** - https://roundcube.net - https://github.com/roundcube/roundcubemail - https://github.com/roundcube/roundcubemail-docker - https://docs.docker.com - https://postgresql.org/docs/16 **Nginx, TLS ve Sertifika:** - https://nginx.org/en/docs - https://ssl-config.mozilla.org - https://owasp.github.io/www-project-secure-headers - https://certbot.eff.org/instructions - https://letsencrypt.org/docs **Sistem Yönetimi ve Güvenlik:** - https://manpages.ubuntu.com - https://help.ubuntu.com/community - https://github.com/fail2ban/fail2ban **Hetzner ve Cloudflare:** - https://docs.hetzner.com - https://console.hetzner.com/support - https://developers.cloudflare.com/dns/manage-dns-records **E-Posta Standartları (RFC):** - https://datatracker.ietf.org/doc/html/rfc5321 - https://datatracker.ietf.org/doc/html/rfc7208 - https://datatracker.ietf.org/doc/html/rfc6376 - https://datatracker.ietf.org/doc/html/rfc7489 - https://datatracker.ietf.org/doc/html/rfc8601 - https://datatracker.ietf.org/doc/html/rfc5228 **Teslimat ve Doğrulama Araçları:** - https://mail-tester.com - https://mxtoolbox.com/SuperTool.aspx - https://postmaster.google.com - https://support.google.com/mail/answer/81126 - https://support.google.com/a/answer/1366776 - https://senders.yahooinc.com/best-practices - https://dmarcian.com - https://dmarc.postmarkapp.com **Diğer Kaynaklar:** - https://laravel.com/docs/13.x/mail - https://documentation.ubuntu.com/release-notes/24.04 - https://mailcow.email - https://gitlab.com/volian/nala
--- *Bu rehber kendi kurulumumda yaşadıklarıma dayanıyor. Takıldığınız bir yer olursa ya da bir hata görürseniz bana yazın.* --- ## Ubuntu 24.04'ten 26.04 Resolute Raccoon'a Geçiş: Bir Geliştiricinin Yol Haritası - URL: https://erho.dev/post/058-ubuntu-2604-yukseltme-deneyimi - Markdown: https://erho.dev/post/058-ubuntu-2604-yukseltme-deneyimi.md - Tarih: 2026-04-14 - Kategori: linux - Etiketler: linux, devops, php Ubuntu 26.04 LTS **23 Nisan 2026**'da, yani dokuz gün sonra, GA olacak. Masaüstünde 24.04 Noble Numbat kullanıyorum ve uzun zamandır bu yeni LTS'i bekliyordum. Aslında bekleme sebebim biraz karışık: Hem geliştirme ortamımı modern bir PHP 8.5 + GNOME 50 dünyasına taşımak istiyorum, hem de Hetzner'daki sunucularımı düşünüyorum, hem de bir LTS yükseltmesinin ne kadar riskli olabileceğini bildiğimden iki kez düşünmeden hareket etmiyorum. Bu yazıda hem kendi yükseltme planımı hem de 24.04'ten 26.04'e geçerken bilmeniz gereken kritik değişiklikleri paylaşacağım. İçinde PHP geliştiricisi olarak benim için ne değiştiğini, hangi noktalarda "dur bir dakika" demem gerektiğini ve son olarak da yükseltmeyi adım adım nasıl yapacağımı bulacaksınız. ## Resmi sürüm takvimi 26.04'ün yaşam döngüsü Canonical'in [resmi sürüm notları sayfasındaki](https://documentation.ubuntu.com/release-notes/26.04/schedule) takvime göre ilerliyor. Önemli kilometre taşları: | Tarih | Kilometre taşı | |---|---| | 16 Eki 2025 | Araç zinciri yüklendi | | 27 Kas 2025 | Resolute anlık görüntüsü 1 | | 11 Ara 2025 | Resolute anlık görüntüsü 2 | | 29 Oca 2026 | Resolute anlık görüntüsü 3 | | 19 Şub 2026 | Özellik dondurma / Debian içe aktarma dondurması | | 26 Şub 2026 | Resolute anlık görüntüsü 4 | | 12 Mar 2026 | Arayüz dondurma | | 19 Mar 2026 | Doküman dizesi dondurması / Çekirdek özellik dondurması | | 23 Mar 2026 | Beta dondurma / ISO test haftası | | 26 Mar 2026 | Beta (zorunlu) | | 9 Nis 2026 | Çekirdek dondurma | | 16 Nis 2026 | Son dondurma / Sürüm adayı | | **23 Nis 2026** | **Kesin sürüm (GA)** | | **27 Ağu 2026** | **26.04.1 nokta sürümü** | Canonical ayrıca arşiv genelinde "Planned and potentially disruptive archive-wide activities" listesi yayımlıyor. PHP geliştiricisi olarak beni ilgilendiren üçü: - **8 Oca 2026**: Python 3.14 varsayılan sürüm oldu - **22 Oca 2026**: PHP 8.5 geçişi - **12 Şub 2026 (tahmini)**: GCC 16 opsiyonel, Rust 1.93 geçişi ## Neden bu yükseltme "sıradan" bir LTS geçişi değil 24.04'ü temiz kurulum olarak yükledim ve iki yıldır bu sistemi kullanıyorum. Şimdi karşımda olan 26.04 ise bambaşka bir tablo çiziyor. Aradaki iki yılda dört tane ara sürüm geçti (24.10 Oracular Oriole, 25.04 Plucky Puffin, 25.10 Questing Quokka ve şimdi de 26.04) ve her biri kendi içinde büyük değişiklikler getirdi. 26.04, bu dört ara sürümün kümülatif etkisini tek bir LTS'de önümüze koyuyor. Hızlıca birkaç başlık: - **Kernel 6.8'den 7.0'a atlıyor.** Evet, Linus Torvalds sürüm numarasını değiştirdi. 7.0 stabil sürümü daha bir hafta önce, 12 Nisan civarında çıktı. Canonical bunu "Day-0 SRU" ile gönderme taahhüdünde bulundu, yani GA'dan hemen sonra kernel güncellemesi gelecek. - **GNOME 46'dan 50'ye atlıyor.** Dört büyük sürüm. X11 oturumu tamamen kaldırıldı; sadece Wayland. Triple buffering, VRR, fractional scaling artık varsayılan. - **PHP 8.3'ten 8.5'e atlıyor.** Aradaki 8.4 ile property hooks ve asymmetric visibility; 8.5 ile pipe operator ve clone with geldi. - **Redis'in yerini Valkey aldı.** Lisans değişikliğinin doğal sonucu. Drop-in uyumlu ama yine de dikkat. - **Masaüstü için minimum RAM 4 GB'dan 6 GB'a çıktı.** Bu beni pek etkilemiyor ama eski makinelerde hâlâ 24.04 çalıştıranlar düşünmeli. - **sudo ve coreutils, Rust ile yeniden yazılan sürümlerle değişti.** Bellek güvenliği için güzel bir adım ama alıştığımız davranışlarda ince farklar var. Tek tek bakalım. ## Masaüstünde ne değişiyor: GNOME 50 ve Wayland-only Açık konuşayım: GNOME 50'yi 25.10'da kısa bir VM kurulumunda denemiştim ve triple buffering'in farkı gerçekten hissediliyor. Mutter 50 ile geliştirilen yeni render boru hattı, animasyonlarda ve pencere sürüklemelerinde o "Linux masaüstünde hep var olan" hafif titremeyi kaldırıyor. Fractional scaling da artık dconf'a elle dokunmadan, ayarlar panelinden seçilebiliyor. İkinci monitör olarak kullandığım 32" ekranda yüzde 125 ölçeklendirme kullanıyorum ve bunun sorunsuz çalışması benim için büyük bir rahatlık. X11 oturumunun kaldırılması ise iki tarafı keskin bıçak. XWayland sayesinde eski X11 uygulamaları çalışmaya devam ediyor, **ama** bazı senaryolarda Wayland'a özel davranışlara dikkat etmek gerekiyor: - **Ekran paylaşımı** PipeWire üzerinden çalışıyor. Discord, Google Meet, Zoom genelde artık sorunsuz ama Brave ve eski Electron uygulamalarında hâlâ garipliklere rastlayabiliyorum. - **Global kısayol ayarları** Wayland güvenlik modeli nedeniyle bazı eski uygulamalarda çalışmayabiliyor. - **Renk profili yönetimi** GNOME 50 ile belirgin biçimde iyileşti; Wayland color management protocol v2 sayesinde ekran kaydında HDR metadata korunuyor. Varsayılan uygulamalar da değişti. GNOME Terminal'in yerine **Ptyxis** geldi. Evince'in yerine **Papers**, Eye of GNOME'un yerine **Loupe**, Totem'in yerine **Showtime**. Ben varsayılan terminali ZSH + Powerlevel10k ile özelleştirerek kullanıyorum; Ptyxis'e geçiş benim için yeni bir alışma süreci olacak. Eski alışkanlıklarınız varsa siz de biraz süre ayırın. ## Geliştirici tarafında kritik değişiklikler İşin asıl ilgilendiğim kısmı burası. PHP ve Laravel geliştirirken değişecek şeyler. ### PHP 8.5 varsayılan Ubuntu 26.04'ün `php` meta paketi artık PHP 8.5'i kuruyor. Laravel 12.x PHP 8.2 ve üzerini desteklediği için uyumluluk açısından bir sorun yok, ancak üçüncü parti paketlerin PHP 8.5 desteğini Packagist'te tek tek kontrol etmek gerekiyor. Özellikle eski ama vazgeçemediğimiz paketler (örneğin bazı OAuth entegrasyon kütüphaneleri, eski PDF üreticiler) sorun çıkarabilir. Ben yükseltmeden önce `composer.json` üzerinde `"php": "^8.3"` kısıtını `"php": "^8.3 || ^8.5"` olarak güncelleyip `composer update --dry-run` ile durumu görmeyi planlıyorum. `./vendor/bin/rector` ile PHP 8.5'e özgü refactor'ları da otomatik uygulayacağım. Bir not: Ondřej Surý'nin PPA'sı henüz `resolute` paketlerini üretmedi. Eğer PHP'nin birden fazla sürümünü yan yana tutmanız gerekiyorsa (ben CI pipeline'larında bunu yapıyorum), bir süre daha Docker imajlarıyla idare etmek zorunda kalabiliriz. ### Redis'ten Valkey'e geçiş Bu benim için en merak ettiğim konuydu. Redis'in Mart 2024'teki lisans değişikliğinden sonra Linux Foundation çatısı altında Valkey forkunu başlatmıştı ve artık Ubuntu Valkey'i varsayılan olarak gönderiyor. İyi haber şu: Valkey **drop-in uyumlu**. Yani: - `predis/predis` değişmeden çalışıyor - `phpredis` native eklentisi değişmeden çalışıyor - Tüm `redis.conf` direktifleri aynı - `valkey-cli` ile eski `redis-cli` arasında fark yok (hatta `valkey-redis-compat` paketi redis-cli'ı symlink olarak sağlıyor) - Laravel'in `config/database.php` dosyanızdaki Redis bağlantı ayarları hiç değişmeden çalışıyor Yine de şu adımları atmayı planlıyorum: 1. Staging sunucumda Valkey'i önce yükleyip tüm cache/session/queue işlemlerini entegrasyon testleriyle geçirmek 2. Production yükseltmesinde önce veriyi `BGSAVE` ile diske almak 3. Valkey'e geçtikten sonra `INFO` çıktısında `server_name:valkey` görünmesini doğrulamak ### MySQL 8.0 → 8.4 LTS Ubuntu 26.04 artık MySQL 8.4.8 LTS'i varsayılan olarak gönderiyor. 8.0 sonu desteği yaklaşıyordu, bu güzel bir zamanlama. Ama **breaking change**'ler var: - `default_authentication_plugin` kaldırıldı; yerine `authentication_policy` geldi - `validate_password` bileşeni konfigürasyonunda değişiklikler var - Bazı eski SQL modu bayrakları kaldırıldı MySQL kullananların `/etc/mysql/mysql.conf.d/mysqld.cnf` dosyasını gözden geçirmesi gerekiyor. Ben kişisel projelerde çoğunlukla MariaDB tarafındayım; MariaDB 11.8.6 LTS artık **main** deposunda ve tam Canonical desteği var. ### Node.js 22, OpenSSH 10.2 ve diğer araçlar Node.js 18'den 22'ye atlıyor. Eğer projelerinizde hâlâ Node 18 kullanıyorsanız `fnm` ile izolasyon şart (ben fnm kullanıyorum, hız farkı nvm'e göre ciddi). Laravel projelerimin frontend tarafında Vite + Node 22 ile çalışıyorum, bu tarafta sorun beklemiyorum. **OpenSSH'de DSA desteği tamamen kaldırıldı.** Ed25519 anahtarlarım var ama hâlâ DSA anahtarı kullanan arkadaşlar biliyorum; yükseltmeden önce mutlaka migrate edin. Ayrıca post-kuantum hibrit anahtar değişimi (`mlkem768x25519-sha256`) varsayılan oldu. Eski SSH istemcileriyle bağlantı sorununa düşerseniz sebep bu olabilir. ## Sıkılaşan güvenlik: sudo-rs ve uutils Bu benim biraz temkinli yaklaştığım bir konu. `sudo-rs` ve `uutils/coreutils` artık varsayılan. Rust ile yazıldıkları için bellek güvenliği garantisi var, bu güzel. Ama: - `sudo` şifre girişinde artık yıldız karakterleri gösteriyor. Alıştığımız "sessiz" davranış kayboldu. - `uutils` hâlâ GNU coreutils ile yüzde yüz uyumlu değil; bazı egzotik `find`, `ls`, `cp` flag'lerinde sessiz farklılıklar var. Bash script'lerimi ve CI pipeline'larımı yükseltmeden önce `sudo apt install coreutils` ile klasik GNU sürümüne geri dönebileceğimi biliyorum ama önce yeni sürümle devam edip gerçek bir sorun çıkar mı görmek istiyorum. ## Kendi yükseltme planım İşte kendi masaüstümde uygulamayı planladığım adım adım plan. Bunu olduğu gibi kopyalamanızı önermem çünkü sizin kullandığınız yazılımlar farklı olabilir, ama iskelet olarak faydalı olacak. ### Aşama 0: Yükseltme öncesi hazırlık (bu hafta sonu) ```bash # Önce backup dizinini oluştur (ZSH'de yönlendirme hedefi var olmalı) mkdir -p ~/backup # Tam sistem yedeği (ben Timeshift kullanıyorum, harici SSD'ye) sudo timeshift --create --comments "24.04 pre-upgrade" # Kurulu paketlerin listesi dpkg --get-selections > ~/backup/packages-2404.list # PPA listesi ls /etc/apt/sources.list.d/ > ~/backup/ppa-list.txt # Home dizini önemli yapılandırmaları tar czf ~/backup/dotfiles-2404.tar.gz ~/.config ~/.zshrc ~/.gitconfig ~/.ssh # Docker imajları (Orizora projelerim için) docker image ls --format "{{.Repository}}:{{.Tag}}" > ~/backup/docker-images.txt ``` Ben ayrıca `/etc` dizininin tamamını [etckeeper](https://ubuntu.com/server/docs/how-to/backups/install-etckeeper/) ile versiyonluyorum, o yüzden sistem yapılandırmaları git üzerinde zaten güvende. ### Aşama 1: Mevcut 24.04'ü tamamen güncel yapmak ```bash sudo apt update sudo apt full-upgrade -y sudo apt autoremove --purge -y sudo snap refresh ``` Bu kritik. Eski paket kalıntılarıyla yükseltmeye girmek istemiyorum. ### Aşama 2: LTS-to-LTS yükseltmesinin açılmasını beklemek **Önemli:** Standart LTS-to-LTS yükseltmesi 26.04.1 nokta sürümüyle, yani **27 Ağustos 2026**'da açılacak. Bunu beklemenin mantıklı olduğunu düşünüyorum çünkü: - İlk GA sürümünde genelde küçük ama can sıkıcı buglar oluyor - 26.04.1 ile kritik SRU'lar gelmiş olacak - Topluluk deneyimi birikecek, benzer sorun yaşayanların çözümleri forumlarda olacak Eğer beklemek istemeyenler `sudo do-release-upgrade -d` ile GA sonrası deneysel yükseltme yapabilir, ama bu production makinede tavsiye etmem. ### Aşama 3: Yükseltme komutu ```bash # Ağustos'ta veya erken test için -d bayrağıyla sudo do-release-upgrade # Eğer SSH üzerinden uzak makinede yapıyorsanız (Hetzner sunucu gibi) # Ubuntu otomatik olarak 1022 portundan yedek SSH açıyor, firewall'da hazır tutun ``` Yükseltme sırasında sorulacak sorular: - **Üçüncü parti depoları devre dışı bırak**: Evet deyin, sonra tek tek yeniden etkinleştireceğiz - **Yapılandırma dosyası değişiklikleri**: Özel olarak değiştirmediğiniz dosyalar için "maintainer's version" seçin, kendi değiştirdikleriniz için "keep local version" - **Eski paketleri kaldır**: Yedeğiniz varsa evet, yoksa no deyip sonra elle temizleyin ### Aşama 4: Yükseltme sonrası kontrol listesi ```bash # Kernel sürümünü doğrula uname -r # 7.0.x beklenen # Önemli servisler systemctl status nginx php8.5-fpm mariadb valkey docker # PHP modülleri php -m # Composer ve Laravel projesini dene cd ~/projects/orizora-project composer install php artisan --version php artisan test # Snap'leri güncelle sudo snap refresh # Üçüncü parti depoları kontrol et ve gerektiğinde güncelle # (Docker, VS Code, Chrome gibi) ``` ### Aşama 5: Geri dönüş senaryosu Eğer işler çok ters giderse: 1. Timeshift snapshot'ından geri dön 2. Son çare: USB'den 24.04 kur ve `/home` yedeğini geri yükle 3. Çok son çare: 26.04'ü temiz kur (ben bu senaryoya hazırlanmak için `/home` ve `/` için ayrı partition kullanıyorum) ## Benim için öne çıkan iki iyileştirme İki şey var ki yükseltmeyi bekliyor olmamın asıl sebebi: **Birincisi, TPM destekli tam disk şifreleme.** 26.04'te deneysel etiketinden çıkıp GA'ya ulaştı. Yeni laptop'ımda zaten TPM 2.0 var ve LUKS yerine TPM-backed FDE'ye geçmek istiyorum. PIN ekleme/kaldırma gibi yetenekler Security Center üzerinden geliyor. **İkincisi, APT 3.1.** Yeni `apt why` ve `apt why-not` komutları ve Solver3 dependency çözücü, karmaşık paket çakışmalarında ömür uzatacak. Özellikle eski PPA'ları temizlerken `apt why` ile "bu paket neden kurulu" sorusuna anında cevap almak harika olacak. ## Önerim: Ağustos'u bekleyin 26.04 ciddi bir sıçrama. Kernel, GNOME, PHP, toolchain, güvenlik katmanı, paket yönetimi, hepsi birden modernleşiyor. Ama bir LTS'in asıl değeri stabilitesinde ve ben de o stabiliteye kavuşmak için **26.04.1**'i, yani 27 Ağustos 2026'yı bekleyeceğim. O zamana kadar test ortamımda erken sürümle denemeler yapıp Orizora projelerimi PHP 8.5 + Valkey ortamında bir bir doğrulayacağım. Eğer siz de 24.04 kullanıcısıysanız ve yükseltmeyi düşünüyorsanız benim önerim net: 1. **Hemen gereksiniyorsanız**: 23 Nisan GA sonrası `-d` ile erken geçiş, ama test ortamında 2. **Üretim/ana makine için**: 27 Ağustos 2026 nokta sürümünü bekleyin 3. **5 yıllık desteğinizi uzatmak istiyorsanız**: 24.04'te kalıp Nisan 2029'a kadar standart desteği kullanmak da tamamen meşru bir seçenek Ben geçişi yaptığımda hem masaüstü deneyimimi hem de Hetzner sunucu tarafındaki yükseltme notlarımı ayrı bir yazıda paylaşacağım. Bu arada sorularınız olursa altta yorumlarda tartışabiliriz. Sağlıcakla kalın. ---
Kaynakça - https://documentation.ubuntu.com/release-notes/26.04 - https://cdimage.ubuntu.com/releases/26.04 - https://9to5linux.com/ubuntu-26-04-lts-beta-is-out-now-with-linux-kernel-7-0-and-gnome-50 - https://ubuntuhandbook.org/index.php/tag/ubuntu-26-04
--- ## Laravel 12'den Laravel 13'e Geçiş Rehberim: Deneyimlerim, İpuçları ve Örneklerle - URL: https://erho.dev/post/060-laravel-12den-laravel-13ye-gecis-rehberim-deneyimlerim-ipuclari-ve-orneklerle - Markdown: https://erho.dev/post/060-laravel-12den-laravel-13ye-gecis-rehberim-deneyimlerim-ipuclari-ve-orneklerle.md - Tarih: 2026-03-24 - Kategori: laravel - Etiketler: laravel, php Selamlar, bu yazımda sizlerle, Laravel 12'den Laravel 13'e geçiş sürecimi paylaşmak istiyorum. Laravel 13 tam bir hafta önce, 17 Mart 2026'da Laracon EU sahnesinde duyuruldu ([yayınlanma yazım burada](https://erho.dev/p/026)). Resmi sürüm notları bu döngünün odağını "kırılma değişikliklerini en aza indirmek" olarak tanımlıyor ve yükseltmeyi "efor açısından görece küçük" diye niteliyor. Upgrade rehberi de tahmini süreyi 10 dakika yazıyor. Ben o 10 dakikayı yarım günde tamamladım. Rehberin kağıt üzerinde küçük görünen maddeleri, canlı veriyle karşılaşınca büyüyor. Bu yazıda resmi rehberin üzerinden geçerken kendi projelerimde patladığım yerleri ve çözümleri sizinle paylaşacağım. Resmi rehber için: [Laravel 13.x Upgrade](https://laravel.com/docs/13.x/upgrade). --- ## Laravel 13 İçin Sistem Gereksinimleri: Laravel 13'e geçmeden önce projenizin şu gereksinimleri karşıladığından emin olun: * **PHP sürümü:** Minimum PHP 8.3 gerekiyor. Laravel 12 ile gelen 8.2 desteği tamamen kaldırıldı ([PHP.net 8.3 sürüm notları](https://php.net/releases/8.3/en.php)). * **PHP uzantıları:** Ctype, cURL, DOM, Fileinfo, Filter, Hash, Mbstring, OpenSSL, PCRE, PDO, Session, Tokenizer ve XML aktif olmalı ([resmi sunucu gereksinimleri](https://laravel.com/docs/13.x/deployment#server-requirements)). * **Composer:** Resmi rehber bir alt sürüm sınırı vermiyor, ama güncel bir Composer kullanın; eski sürümler platform kısıtlarını doğru çözemiyor ([Composer Güncelleme Rehberi](https://getcomposer.org/doc/03-cli.md#self-update)). İlk denemede PHP 8.2 ile `composer update` çalıştırdım ve `requires php ^8.3` hatasıyla karşılaştım. CI hattınız hâlâ 8.2 üzerinde çalışıyorsa, composer güncellemesine başlamadan önce pipeline'ı da güncellemeniz gerekiyor; yoksa build anında yine aynı hatayı alırsınız. ## Laravel 13'e Geçişte Bilmeniz Gereken Kritik Değişiklikler: ### Bağımlılıkları Güncelleyin: Laravel 13 için `composer.json` dosyanızdaki ilgili satırları şu şekilde güncellemelisiniz: ```json { "require": { "php": "^8.3", "laravel/framework": "^13.0", "laravel/tinker": "^3.0" }, "require-dev": { "laravel/boost": "^2.0", "phpunit/phpunit": "^12.0", "pestphp/pest": "^4.0" } } ``` `laravel/boost: ^2.0` ile `phpunit/phpunit: ^12.0` ve `pestphp/pest: ^4.0` sürüm pimlerini atlamayın; kendi projelerimde boost'u ^1.x'te bırakıp composer update çalıştırdığımda Pest 4'ün test signature'ı PHPUnit 11 ile çakıştı ve saatlerce "method not found" hatası aldım. ### PreventRequestForgery: CSRF Middleware'i Yeniden Adlandırıldı: Bu, geçişte en çok dikkat etmeniz gereken değişiklik. Eski `VerifyCsrfToken` middleware'i artık `PreventRequestForgery` olarak adlandırıldı. Yeni sınıf ek olarak `Sec-Fetch-Site` başlığını doğrulayarak origin-aware CSRF kontrolü yapıyor. Eski isimler (`VerifyCsrfToken`, `ValidateCsrfToken`) deprecated alias olarak kalsa da doğrudan referanslarınızı yeni isimle güncellemeniz önerilir; özellikle testlerde `withoutMiddleware` ile dışlama yapıyorsanız. Laravel 11+ artık `app/Http/Kernel.php` yerine `bootstrap/app.php` üzerinden `withMiddleware` closure API'si kullanıyor. Yeni `preventRequestForgery()` metodunun tam imzası `preventRequestForgery(array $except = [], bool $originOnly = false, bool $allowSameSite = false)` şeklinde. Origin doğrulamasının davranışını belirleyen asıl iki anahtar burada. Middleware önce `Sec-Fetch-Site` başlığına bakıyor: `same-origin` her koşulda geçiyor, `same-site` yalnız `$allowSameSite` açıkken geçiyor, diğer değerlerde token doğrulamasına düşülüyor. `$originOnly` açıksa token karşılaştırması hiç yapılmıyor ve eşleşmeyen istek `OriginMismatchException` ile reddediliyor; bu modda yanıtlara `XSRF-TOKEN` çerezi de eklenmiyor. Dışlama listesini ve bu iki bayrağı şu şekilde tanımlayabilirsiniz: ```php // bootstrap/app.php içinde use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Middleware; return Application::configure(basePath: dirname(__DIR__)) ->withMiddleware(function (Middleware $middleware): void { $middleware->preventRequestForgery( except: ['webhook/*'], originOnly: false, allowSameSite: false, ); }); ``` Testlerde de aynı güncellemeyi yapmayı unutmayın: `->withoutMiddleware([VerifyCsrfToken::class])` çağrılarını `->withoutMiddleware([PreventRequestForgery::class])` ile değiştirin. Ben eski `app/Http/Middleware/VerifyCsrfToken.php` dosyamı sildim ama testlerdeki `use` satırlarını güncellemeyi atladım; autoload cache temizlendikten sonra sınıf bulunamadı hatası aldım. ### Cache `serializable_classes` Artık Varsayılan `false`: Laravel 13'ün varsayılan `config/cache.php` dosyası artık `serializable_classes` anahtarını içeriyor ve değeri `false`. Yani APP_KEY sızarsa gadget zinciri saldırısı kurulamasın diye cache'ten hiçbir PHP sınıfı unserialize edilmiyor. Uygulamanız cache'te bilerek nesne saklıyorsa, unserialize edilmesine izin verilen sınıfları kendiniz listelemeniz gerekiyor. Burada dikkat edilecek nokta anahtarın yeri. `serializable_classes` bir store ayarı değil, dosyanın kök seviyesinde duruyor. `CacheManager` değeri `cache.serializable_classes` yolundan okuyup her store'un constructor'ına geçiriyor. `stores.redis` altına yazarsanız değer sessizce yok sayılır ve sorununuz çözülmez: ```php // config/cache.php, kök seviyede (stores dizisinin İÇİNDE değil) return [ 'default' => env('CACHE_STORE', 'database'), 'stores' => [ // ... ], 'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'), 'serializable_classes' => [ App\Models\User::class, App\Models\Order::class, Spatie\LaravelData\Data::class, ], ]; ``` İlk deploy sonrası login akışım patladı. 12'de sorunsuz çalışan `auth()->user()` cache yapım 13'te cache'ten geri okunamadı, çünkü varsayılan `false` değeri o nesnenin unserialize edilmesini engelliyordu. Çözüm yukarıdaki allow-list'i eklemek oldu. Staging'de gözden kaçmıştı çünkü testlerimde cache driver `array` idi ve array store serileştirme yapmıyor. ### Cookie Prefix Sessizce Değişti: Framework'ün ürettiği varsayılan önekler tire ile ayrılmaya başladı. 12.x `Str::slug($appName, '_')` çağırıp `_cache_`, `_database_` ve `_session` üretiyordu; 13.x ayırıcı argümanını kaldırdı ve `Str::slug($appName)` varsayılan tiresiyle `-cache-`, `-database-` ve `-session` üretiyor. Bu maddenin resmi etki derecesi düşük ve nedeni önemli: çoğu uygulamanın kendi config dosyaları bu değerleri zaten açıkça tanımlıyor. Değişiklik yalnız framework fallback'ine güvenen, yani `CACHE_PREFIX`, `REDIS_PREFIX` ve `SESSION_COOKIE` ayarlanmamış projeleri vuruyor. Sizinki o gruptaysa deploy sonrası eski cache anahtarlarınız geçersiz kalır ve oturum çerezinin adı değişir: ```ini # .env dosyasında (eski davranışı korumak için) CACHE_PREFIX=laravel_cache REDIS_PREFIX=laravel_database SESSION_COOKIE=laravel_session ``` İlk prod deploy'umdan sonra destek e-postası yağmuruna tutuldum: herkes logout olmuş. Benim projemde bu değerler ayarlı değildi, o yüzden fallback değişikliği doğrudan üzerime geldi. Çözüm yukarıdaki env değerlerini eski isimle pinlemekti. ### Session `serialization` Varsayılanı `json` Oldu: Toplu logout yaşadıysanız suçlu cookie prefix olmayabilir. Laravel 13 iskeleti `config/session.php` içindeki `serialization` değerini `php` yerine `json` yapıyor ve bu, aynı zincirin ikinci halkası. Resmi rehber burada çok net konuşuyor: mevcut bir uygulamada bu değeri `php`'den `json`'a çekerseniz aktif oturumların TAMAMI geçersiz olur. İki seçeneğiniz var ve ikisi de savunulabilir: ```php // config/session.php // Oturumları kesintisiz korumak istiyorsanız eski değeri koruyun 'serialization' => 'php', // Güvenliği tercih ediyorsanız json'a geçin, kullanıcılar yeniden giriş yapar 'serialization' => 'json', ``` `json` tarafının kazandırdığı şey `php` tarafında serbest kalan deserialization gadget zinciri riskini kapatmak. Uygulamanız session'da PHP nesnesi saklamıyorsa geçiş bedava sayılır, tek maliyeti bir kerelik yeniden giriş. Nesne saklıyorsanız önce onu diziye çevirmeniz gerekir. Ben geçişi iki adıma böldüm: önce `php` ile deploy edip cache prefix işini oturttum, oturumlar stabil hale geldikten sonraki hafta `json`'a çektim. Aynı deploy'da iki oturum kırıcı değişikliği birden yapmak, hangisinin patladığını anlamanızı imkânsızlaştırıyor. ### `upsert` Artık Boş `uniqueBy` Kabul Etmiyor: MySQL ve MariaDB kullanıyorsanız bu maddeye dikkat. Laravel eskiden boş bir `uniqueBy` değeriyle geçersiz SQL üretiyordu; 13 artık `InvalidArgumentException` fırlatıyor. ```php // Laravel 13'te patlar DB::table('orders')->upsert($rows, [], ['total']); // Doğrusu: eşleştirme sütunlarını verin DB::table('orders')->upsert($rows, ['reference'], ['total']); ``` İşin ilginç yanı, MySQL ve MariaDB sürücüleri `uniqueBy` değerini zaten kullanmıyor, kayıt eşleşmesini tablonun primary ve unique index'lerinden çözüyorlar. Buna rağmen doğrulama uygulanıyor, yani sürücü değeri yok saysa bile boş bırakamıyorsunuz. ### Küçük Ama Sinir Bozan Diğer Değişiklikler: * `JobAttempted` event'inde `$exceptionOccurred` (bool) alanı kaldırıldı; yerini `$exception` (?Throwable) aldı. Custom listener yazdıysanız `if ($event->exceptionOccurred)` artık her zaman `false` döner. * `Manager::extend()` callback'leri artık manager instance'a bind ediliyor; `$this`'i eski provider'a bağlayan closure'lar sessizce bozulabilir. * Polymorphic pivot tablolarının varsayılan adı artık çoğul (`post_tag` yerine `post_tags`). Custom pivot model'inizde `$table` set etmediyseniz prod'da upsert patlayabilir. * "Reset Password Notification" varsayılan konusu İngilizce "Reset your password" olarak değişti; Pest snapshot testleriniz kırmızıya dönebilir. * `QueueBusy` event'inde `$connection` alanı `$connectionName` oldu; diğer kuyruk event'leriyle tutarlılık için. * `Container::call` artık nullable sınıf varsayılanlarına saygı duyuyor. `function (?Carbon $date = null)` imzası 12'de Carbon örneği alıyordu, 13'te `null` alıyor. * Domain tanımlı route'lar, domain'siz olanlardan önce eşleştiriliyor. Catch-all subdomain route'unuz varsa eşleşme sırası değişmiş olabilir. * MySQL'de `DELETE ... JOIN` sorgularına artık `ORDER BY` ve `LIMIT` de derleniyor. Önceden sessizce yok sayılan bu ifadeler, desteklemeyen motorda `QueryException` üretebilir. * Bootstrap 3 pagination view adları netleşti: `pagination::default` yerine `pagination::bootstrap-3`, `pagination::simple-default` yerine `pagination::simple-bootstrap-3`. * `Str` factory'leri (custom UUID, ULID, rastgele dize) test teardown'ında sıfırlanıyor. Testler arası kalıcı olmasına güveniyorsanız her testte yeniden set edin. * Eloquent koleksiyonları serileştirilip geri yüklendiğinde eager-loaded ilişkiler de geri geliyor. Kuyruk job'larında ilişkilerin yokluğuna güvenen kod bozulabilir. * Model boot edilirken aynı modelden yeni örnek oluşturmak `LogicException` fırlatıyor. `boot()` içindeki `(new static())->getTable()` gibi kullanımları dışarı taşıyın. * Laravel 13 `symfony/polyfill-php85` bağımlılığı getiriyor. PHP 8.5 altında bu paket global `array_first()` ve `array_last()` tanımlıyor ve `laravel/helpers` gibi eski yardımcı paketlerle çakışabiliyor. Çözüm: `Arr::first()` ve `Arr::last()` kullanın. * `Js::from` artık varsayılan olarak `JSON_UNESCAPED_UNICODE` kullanıyor. Kaçışlı unicode bekleyen test karşılaştırmalarınız kırılır. * HTTP Client'ta `Response::throw` ve `throwIf` callback parametreleri imzaya taşındı. Bu metodları override eden özel response sınıflarınız varsa imzaları uyumlu olmalı. * Kuyruğa alınan bildirimler artık `#[DeleteWhenMissingModels]` niteliğine ve `$deleteWhenMissingModels` özelliğine uyuyor. * Sözleşmelere yeni metotlar eklendi: `Dispatcher::dispatchAfterResponse`, `ResponseFactory::eventStream`, `MustVerifyEmail::markEmailAsUnverified` ve kuyruk tarafında `pendingSize`, `delayedSize`, `reservedSize`, `creationTimeOfOldestPendingJob`. Kendi implementasyonunuz varsa bunları eklemeniz gerekiyor. * `ApplicationBuilder::withScheduling()` ile kaydedilen zamanlamalar `Schedule` çözülene kadar erteleniyor. Bu listeyi tek satırlık tuttum; ayrıntısı için yine resmi upgrade rehberine göz atmanızı tavsiye ederim. ## Laravel 13'e Geçiş İçin Önerdiğim Adımlar: ### 1. Hazırlık ve Yedekleme: Mutlaka veritabanınızın, `.env` dosyanızın ve `composer.lock` dosyanızın yedeğini alın. Cookie prefix değişikliği yüzünden geri dönmek zorunda kalabilirsiniz. ### 2. Branch Açın: Geçişi ayrı bir feature branch'te yapın; `main` dalına doğrudan push etmeyin. ### 3. Composer Bağımlılıklarını Güncelleyin: ```bash composer update ``` ### 4. Kod Refaktörizasyonu: Yukarıdaki kritik değişiklikleri (CSRF yeniden adlandırması, cache `serializable_classes`, cookie prefix, session `serialization`, `upsert` doğrulaması, `JobAttempted`, polymorphic pivot) projenizde uygulayın. ### 5. Konfigürasyon Güncellemeleri: Laravel 13 varsayılanları ile `config/session.php`, `config/cache.php` ve `config/queue.php` dosyalarınızı kıyaslayarak güncelleyin. İki anahtara özellikle bakın: `config/session.php` içindeki `serialization` (oturumları geçersiz kılabilir) ve `config/cache.php` kökündeki `serializable_classes`. [Laravel 13 Upgrade Guide](https://laravel.com/docs/13.x/upgrade) bu noktada en başvuracağınız kaynak; ben de her seferinde oradan bakıp kendi config'imle diff çıkarıyorum. Resmi rehber ayrıca [12.x ile 13.x iskelet farkını](https://github.com/laravel/laravel/compare/12.x...13.x) karşılaştırmanızı öneriyor, config dosyalarını senkronlamanın en hızlı yolu bu. ### 6. Önbellek Temizliği: ```bash php artisan optimize:clear # veya ayrı ayrı php artisan config:clear php artisan cache:clear php artisan route:clear php artisan view:clear ``` ### 7. Test ve Doğrulama: ```bash ./vendor/bin/pest ``` CSRF, bildirim ve queue testlerini özellikle çalıştırın; bu üçü en çok etkilenen alanlar. ### 8. Staging'e Dağıtım: Gerçek kullanıcı trafiği olmadan staging'de bir kez çalıştırın. Cookie davranışı, queue event payload'ı ve session sürekliliği gözlemleyin. ### 9. Canlı Ortama Dağıtım: Staging'de sorun yoksa production'a güvenle dağıtabilirsiniz. ## Laravel Installer ve Boost'u Güncelleyin: Laravel 13 ile uyumlu installer CLI aracını ve Boost'u güncellemeyi unutmayın: ```bash composer global update laravel/installer composer require laravel/boost:^2.0 --dev ``` Boost 2 ile editörünüzde `/upgrade-laravel-v13` slash komutunu çalıştırırsanız, kontrat implementasyonlarınızı (Cache Store::touch, Queue::pendingSize vb.) otomatik tarayıp eksikleri size listeliyor. Ben Claude Code içinde bu komutu kullanarak kontrat kontrolünü iki günden yirmi dakikaya indirdim; eksik kalan iki event listener'ı kendi gözüm kaçırmıştım, Boost yakaladı. ## Laravel Shift ile Otomatik Geçiş Yapın: Geçiş sürecini hızlandırmak isterseniz [Laravel Shift](https://laravelshift.com/) ücretli ama otomatik bir alternatif sunuyor. Ben henüz denemedim; küçük projeler için Boost slash komutu yeterli, orta-büyük codebase'lerde Shift'in pull request'i ciddi zaman kazandırıyor. ## Hemen Yararlanmaya Başladığım Yenilikler: Laravel 13 ile gelen yeniliklerden hemen faydalandığım üçü şunlar: `Cache::touch()` metodu, bir cache key'inin TTL'ini geri çağırmadan uzatmanıza izin veriyor. Oturum uzatma ve rate-limit pencereleri için çok pratik: ```php Cache::touch('user:1:rate-limit', 300); // 5 dakika daha uzat ``` PHP Attributes ile route ve middleware tanımları artık daha temiz. `Illuminate\Routing\Attributes\Controllers\Middleware` namespace'i altında gelen attribute'lar sayesinde controller seviyesinde tek satırla middleware bağlayabilirsiniz: ```php use Illuminate\Routing\Attributes\Controllers\Middleware; #[Middleware('auth')] class DashboardController { #[Middleware('verified')] public function index() { /* ... */ } } ``` Yeni `Queue::route()` API'si de çok işime yaradı. Belirli job sınıfları için merkezi queue ve connection routing'i tek noktadan tanımlayabiliyorsunuz; her job sınıfında tek tek `->onQueue('...')->onConnection('...')` zinciri yazmaktan kurtuluyorsunuz: ```php Queue::route(ProcessPodcast::class, connection: 'redis', queue: 'podcasts'); Queue::route(SendNotification::class, connection: 'sqs', queue: 'emails'); ``` Bu konularda daha fazla bilgi için bir önceki yazıma bakabilirsiniz: [Laravel 13 Yayınlandı](https://erho.dev/p/026). ## Sonuç ve Önerilerim: Resmi rehber geçiş süresini 10 dakika olarak tahmin ediyor. Bu tahmin bağımlılıkları güncelleyip kodun derlendiğini görmek için doğru olabilir, ama benim ölçtüğüm gerçek farklı: composer update, kontrat imzaları ve CSRF yeniden adlandırması kendi projelerimde yarım gün sürdü, geri kalan kısım test düzeltmeleriydi. Aradaki farkın tamamı, 10 dakikada göremeyeceğiniz üç kalemden geliyor. Laravel 13'e geçiş, 11'den 12'ye göre biraz daha fazla dikkat istiyor. Özellikle session `serialization`, cache prefix ve `serializable_classes` değişiklikleri staging'de gözden kaçabiliyor; üçü de kodu derlemekten değil, canlı veriyle karşılaşmaktan patlıyor. Bu yüzden staging'i en az 24 saat açık tutmanızı ve gerçek kullanıcı trafiği olmasa bile cron job ile cache'i boşaltıp yeniden doldurmanızı öneriyorum. Sizin Laravel 12'den 13'e geçiş süreciniz nasıl geçti? Session `serialization`, cache prefix veya `serializable_classes` üçlüsünden hangisi sizi vurdu, yoksa hepsini es geçip 10 dakikada mı bitirdiniz? Yorumlarınızı merakla bekliyorum! ---
Kaynakça **Resmi Laravel Kaynakları:** - https://laravel.com/docs/13.x **GitHub Depoları:** - https://github.com/laravel/framework - https://github.com/laravel/laravel/compare/12.x...13.x - https://github.com/laravel/boost **Geçiş Araçları ve Bağımlılıklar:** - https://laravelshift.com - https://php.net/releases/8.3/en.php - https://getcomposer.org/doc/03-cli.md#self-update
--- ## Laravel 13 Yayınlandı: PHP Attributes, AI SDK ve Tüm Yenilikler - URL: https://erho.dev/post/026-laravel-13-yayinlandi - Markdown: https://erho.dev/post/026-laravel-13-yayinlandi.md - Tarih: 2026-03-17 - Kategori: laravel - Etiketler: laravel, php, architecture Bugün 17 Mart 2026 ve Laravel 13 yayınlandı. Taylor Otwell'in Laracon EU 2026'da duyurduğu tarih geldi. Sürümü haftalardır takip ediyorum: PR'lar, Laracon konuşmaları, resmi dokümantasyon ve farklı dillerdeki topluluk kaynakları. Bu yazıda yeni özellikleri, kırılma noktalarını, ekosistem güncellemelerini ve farklı topluluklardan ilginç bulguları topladım. --- ## Taylor Otwell sahneye çıktı ve tarih verdi Her şey **2-3 Mart 2026'da Amsterdam'da** düzenlenen Laracon EU 2026'da başladı. Taylor Otwell'ın "Laravel Update" başlıklı keynote konuşması saat 16:30 CET'te sahne aldı ve en önemli duyuruyu yaptı: **Laravel 13, 17 Mart 2026'da yayınlanacak.** Otwell, bu sürümü "Laravel tarihinin en sorunsuz yükseltmesi" olarak tanımladı; sıfır kırılma değişikliği vaadiyle. Ama sahne bununla sınırlı kalmadı. Taylor canlı bir demo yaptı: kasıtlı bir bug oluşturdu, Nightwatch hatayı yakaladı, bir AI agent (OpenClaw) hatayı analiz edip düzeltme PR'ı açtı, Laravel Cloud test ortamı ayağa kaldırdı ve OpenClaw Taylor'ı telefonla arayarak PR'ın merge edilmesini istedi. Bug, tek satır kod yazılmadan sesli komutla düzeltilip deploy edildi. Demo, Laravel'in AI öncelikli yöneliminin nereye geldiğini gösterdi. Laravel'in yıllık sürüm döngüsü artık oturmuş durumda. Laravel 9'dan itibaren büyük sürümler **yılda bir, genellikle Q1'de** çıkıyor: | Versiyon | PHP Gereksinimi | Yayın Tarihi | | --- | --- | --- | | Laravel 9 | 8.0-8.2 | 8 Şubat 2022 | | Laravel 10 | 8.1-8.3 | 14 Şubat 2023 | | Laravel 11 | 8.2-8.4 | 12 Mart 2024 | | Laravel 12 | 8.2-8.5 | 24 Şubat 2025 | | **Laravel 13** | **8.3-8.5** | **17 Mart 2026** | Destek politikası da aynı: 18 ay hata düzeltmesi, 2 yıl güvenlik yaması. Laravel 11'in güvenlik desteği 12 Mart 2026'da (5 gün önce) sona erdi. Laravel 12 ise Ağustos 2026'ya kadar hata düzeltmesi, Şubat 2027'ye kadar güvenlik yaması alacak. Yani acele etmenize gerek yok ama geçiş sürecini planlamaya bugünden başlamak akıllıca. ## PHP Attributes: Laravel 13'ün öne çıkan yeniliği Laravel 13'ün dikkat çeken kullanıcı tarafı özelliği PHP 8 Attributes desteği (PR [#58578](https://github.com/laravel/framework/pull/58578), bizzat Taylor Otwell tarafından). Artık class property'leri yerine native PHP attribute'ları kullanarak Laravel bileşenlerini yapılandırabilirsiniz, 15'ten fazla farklı lokasyonda. Önemli bir detay: Bu tamamen opsiyonel ve geriye dönük uyumlu. Mevcut property tabanlı sözdizimi çalışmaya devam ediyor. Yani mevcut kodunuzu değiştirmek zorunda değilsiniz. Eloquent Model attribute'ları ile başlayalım. Artık `$table`, `$fillable`, `$hidden`, `$guarded`, `$appends` gibi property'leri şu şekilde tanımlayabilirsiniz: ```php #[Table('users')] #[Fillable('name', 'email')] #[Hidden('password', 'remember_token')] #[Guarded('id')] #[Appends('full_name')] class User extends Model { } ``` `#[Connection]`, `#[Touches]`, `#[Unguarded]`, `#[Visible]` da dahil. Özellikle `#[Unguarded]` ilginç: sadece attribute olarak mevcut, property karşılığı yok. Bu, Laravel ekibinin gelecekte attribute-first yönelime gittiğinin sinyali. Queue Job attribute'ları da kapsamlı: `#[Connection('redis')]`, `#[Queue('emails')]`, `#[Tries(3)]`, `#[Timeout(120)]`, `#[Backoff(10, 30, 60)]`, `#[MaxExceptions(3)]`, `#[FailOnTimeout]`, `#[UniqueFor(3600)]`. Bir job sınıfı artık şöyle görünebilir: ```php #[Queue('notifications')] #[Tries(3)] #[Timeout(60)] #[Backoff(5, 15, 30)] class SendNotification implements ShouldQueue { } ``` - **Console komutları** için `#[Signature('users:sync {--force}')]` ve `#[Description('Sync users from external API')]`; - **Controller'lar** için `#[Middleware('auth')]` ve `#[Authorize('admin')]`; - **Form Request'ler** için `#[RedirectTo('/dashboard')]` ve `#[StopOnFirstFailure]`; - **API Resource'lar** için `#[Collects(UserResource::class)]` ve `#[PreserveKeys]`; - **Factory'ler** için `#[UseModel(User::class)]`; - **Test Seeder'ları** için `#[Seed]` ve `#[Seeder(UserSeeder::class)]`. Japon geliştirici topluluğundan değerli bir bulgu: no-hack-no.life sitesindeki analiz, attribute ile property aynı anda tanımlandığında öncelik davranışının hiçbir yerde belgelenmediğini ortaya koyuyor. Eloquent modellerinde property öncelikli, diğer her yerde attribute öncelikli. Ayrıca 13.x dokümanlarının sessizce property tabanlı yapılandırma belgelerini kaldırmaya başladığını tespit etmişler; attribute-only geleceğin işareti. Toplulukta bu özellik hakkında tartışma var. dev.to'daki yorumlarda bazı geliştiriciler 20+ field'lı büyük modellerde attribute'ların "okunaksız dev bir satıra dönüşeceğini" savunurken, diğerleri bunu "üç versiyondur gelen en büyük yaşam kalitesi iyileştirmesi" olarak nitelendiriyor. Benim görüşüm: küçük ve orta modellerde attribute'lar gerçekten temiz bir sözdizimi sunuyor, ama büyük modellerde property yaklaşımı hala daha okunabilir. İki yaklaşımın birlikte var olması doğru bir tasarım kararı. ## Cache::touch(), Reverb database driver ve diğer teknik yenilikler `Cache::touch()` metodu (PR #55954) basit ama kullanışlı bir ekleme. Bir cache öğesinin TTL'ini değerini okumadan veya yeniden yazmadan uzatmanıza olanak tanıyor. Redis'te tek bir `EXPIRE`, Memcached'te `TOUCH`, veritabanında `UPDATE` komutu kullanıyor. Session benzeri cache pattern'lerinde (kullanıcı aktivitesi takibi, rate limiting gibi) gerçek bir performans kazancı sağlayacak. Reverb Database Driver da önemli bir yenilik. Laravel Reverb (WebSocket sunucusu) artık Redis olmadan çalışabiliyor; doğrudan MySQL veya PostgreSQL kullanarak yatay ölçekleme yapabilirsiniz. Bu, özellikle küçük ve orta ölçekli projeler için altyapı karmaşıklığını azaltıyor. Redis kurulumu ve yönetimi derdi ortadan kalkıyor. PreventRequestForgery middleware (PR #58400) az dikkat çeken ama önemli bir yenilik. İspanyol geliştirici topluluğundan cursosdesarrolloweb.es'in vurguladığı gibi, bu middleware `Sec-Fetch-Site` header'ını kullanarak CSRF koruması sağlıyor; Ruby on Rails'in benzer yaklaşımı gibi. Bu header JavaScript ile sahtelenemiyor, geleneksel CSRF token'larından daha güvenilir bir sinyal. Türkçe kaynaklarda hiç bahsedilmemiş ama bence ciddi bir güvenlik iyileştirmesi. Diğer teknik iyileştirmeler: - **Symfony 7.4/8.0 desteği** (PR #56029): altta yatan Symfony bileşenleri güncellendi - **MySQL DELETE...JOIN** artık ORDER BY ve LIMIT'i doğru derliyor (PR #57196) - **HTTP pool eşzamanlılığı** varsayılan olarak 2'ye ayarlandı (PR #57972); önceden null'dı, yani pool istekleri aslında sıralı çalışıyordu - `Request::get()` Symfony davranışıyla hizalandı (PR #58081) - **Subdomain route'ları** artık domain olmayan route'lardan önce kaydediliyor (PR #55921) - **Model `boot()` kısıtlaması**: boot() sırasında yeni Eloquent model örneği oluşturmak artık yasak (PR #55685). Bir geliştirici, User::boot() içinde Role model sorgusu yapan prodüksiyon kodunun bu yüzden patladığını belgelemiş; dikkatli olun - `JobAttempted` event'i artık boolean yerine tam `Throwable` nesnesi gönderiyor (PR #56148) - **Polimorfik pivot tablolar** varsayılan olarak çoğul isim kullanıyor (PR #56832) - `__wakeup` yerine `__unserialize` kullanılıyor (PR #57032); PHP 8.5 uyumluluğu için Performans tarafında, Cloudways'in benchmark testleri API workload'larında ~445 req/s gösteriyor; PHP 8.3 ve temizlenmiş codebase ile %2-5 throughput artışı. Uzun süren process'lerde ve queue worker'larda bellek kullanımı da düşmüş. ## Laravel 12'den 13'e: Gerçekte ne değişiyor? Laravel 12, Şubat 2025'te bakım odaklı bir sürüm olarak çıktı. Ana yenilikleri yeni starter kit'ler (React, Vue, Livewire), WorkOS AuthKit entegrasyonu, Breeze ve Jetstream'in emekliye ayrılması, Carbon 3 zorunluluğu ve UUIDv7 varsayılanı idi. Laravel 13'e geçişte temel farklar şunlar: | Özellik | Laravel 12 | Laravel 13 | | --- | --- | --- | | **PHP minimum** | 8.2 | **8.3** | | **PHP Attributes** | Yok | **~15 lokasyonda** | | **Cache::touch()** | Yok | **Tüm driver'larda** | | **Reverb DB driver** | Yok | **Redis gereksiz** | | **Passkey auth** | WorkOS üzerinden | **Fortify'a native entegre** | | **Teams** | Sadece Jetstream (emekli) | **Starter kit'lerde geri döndü** | | **AI SDK** | Beta | **Stable** | | **CSRF** | VerifyCsrfToken | **PreventRequestForgery (Sec-Fetch-Site)** | | **HTTP pool** | Varsayılan null (sıralı) | **Varsayılan 2 (eşzamanlı)** | | **Yükseltme süresi** | ~5 dk (11->12) | **~10 dk (12->13)** | Taylor'ın "sıfır kırılma değişikliği" söylemi büyük ölçüde doğru ama küçük kırılma noktaları var: PHP 8.3 minimum, `JobAttempted` event imzası, Manager `extend()` callback'leri, cache serialization ve session cookie isim varsayılanları. Resmi upgrade guide çoğu uygulama için ~10 dakika tahmin ediyor. Laravel 12'nin yaşam döngüsü boyunca eklenen özellikler de var: `Arr::partition()`, `Context::scope()`, `shouldRun()` migration'ları, Memoized Cache Driver, Failover Queue ve Cache Driver'ları, Deferred Queue Driver, `fillAndInsert()`, Horizon `horizon:listen` komutu, Pint paralel çalıştırma. Laravel'in stratejisi büyük özellikleri major sürümlere saklamak yerine minor sürümlerde sürekli sunmak. ## AI-first ekosistem: SDK, Boost, MCP ve Nightwatch Laravel 13 ile birlikte dört AI odaklı paket tam olgunluğa ulaşıyor: Laravel AI SDK (`laravel/ai`), 17 Mart'ta beta'dan çıkıp stable oluyor. OpenAI, Anthropic, Google Gemini ve diğer sağlayıcılar için birleşik API sunuyor: metin üretimi, tool-calling agent'lar, embedding'ler, ses, görüntü ve vektör deposu iş akışları. Otomatik failover ile provider kesintilerinde geçiş yapıyor. Queue desteğiyle uzun süren AI görevlerini arka planda çalıştırabilirsiniz. Laravel Boost (`laravel/boost`), AI agent'lara uygulama bağlamı sağlayan bir dev dependency. 15'ten fazla MCP aracı, 16'dan fazla ekosistem paketi için versiyona duyarlı AI yönergeleri ve 17 binin üzerinde vektörleştirilmiş Laravel dokümanı içeriyor. Claude Code, Cursor, Copilot, Gemini CLI, Codex vb ile entegre çalışıyor. Laravel MCP (`laravel/mcp`), Laravel uygulamalarınızı ChatGPT, Claude, Cursor gibi AI client'lara açan bir MCP sunucu kütüphanesi. OAuth 2.1 (Passport) ve token auth (Sanctum) desteğiyle güvenli erişim sağlıyor. Laravel Nightwatch ise barındırılan uygulama izleme platformu: istekler, job'lar, sorgular, log'lar, komutlar, cache operasyonları için gerçek zamanlı görünürlük sunuyor. Nightwatch MCP ile AI agent'lar prodüksiyon sorunlarını konuşarak teşhis edebiliyor. ## Starter kit'lerde Teams ve Passkey desteği geri dönüyor Laravel 12'de Jetstream'in emekliye ayrılmasıyla birlikte Teams fonksiyonalitesi kaybolmuştu. Laravel 13 bunu starter kit'lere yeniden entegre ediyor; Jetstream'in eski implementasyonundan farklı bir biçimde. Yeni Teams, aynı anda farklı browser tab'larında farklı takımlarla çalışmayı destekliyor. Jetstream'in en büyük kullanılabilirlik sorunlarından biri sonunda çözüldü. Passkey (WebAuthn) desteği de Fortify üzerinden starter kit'lere entegre ediliyor. Face ID, Touch ID, donanım anahtarları kutusundan çıkar çıkmaz kullanılabilir hale geliyor. Daha önce WorkOS AuthKit üzerinden mümkündü, şimdi doğrudan Laravel'in kendi auth sistemiyle geliyor. ## Ekosistem paketlerinin durumu ve Inertia.js v3 beta Inertia.js v3, Laracon EU 2026'da Joe Tannenbaum tarafından tanıtıldı ve 5 Mart 2026'da beta'ya girdi. Artık resmi olarak Laravel'e ait (Laravel 12 döneminde satın alınmıştı). Büyük değişiklikler: dahili XHR client (Axios kaldırıldı), yeni Vite plugin'i, optimistic updates, instant visits, `useHttp` hook, layout props ve basitleştirilmiş SSR. React 19+, Svelte 5+ gerektiriyor. Laravel Cloud da olgunlaşıyor. API ve CLI, 3 Mart 2026'da GA oldu. Infrastructure-as-code desteği, AI agent entegrasyonu, auto-scaling, preview environments, MySQL GA, PostgreSQL, Valkey caching ve Private Cloud seçeneği mevcut. Diğer paketlerin durumu: - **Sanctum**: Aktif, API auth için kullanılmaya devam ediyor - **Reverb**: Aktif, yeni database driver ile genişledi - **Pint**: Paralel çalıştırma ve `--format agent` bayrağı eklendi - **Pennant**: Aktif, feature flag yönetimi - **Sail**: Aktif, Docker tabanlı lokal geliştirme - **Horizon**: `horizon:listen` komutu eklendi (dosya değişikliklerini izleyip otomatik yeniden başlatma) - **Telescope**: Vite'a geçti (Mix'ten) - **Pulse**: Livewire 4 uyumlu - **Herd**: v1.25.0 - PHP 8.5, Boost entegrasyonu, MCP sunucusu, Svelte starter kit desteği ## GitHub deposundaki branch'ler, PR'lar ve commit'ler `laravel/framework` deposunda 13.x branch'i aktif olarak mevcut, 41.458 commit ile. `laravel/laravel` skeleton deposunun 13.x branch'indeki `composer.json` açıkça `"php": "^8.3"` ve `"laravel/framework": "^13.0"` gerektiriyor. En önemli merge edilmiş PR'lar: - **#54763** - PHP 8.3 minimum gereksinimi - **#58578** - PHP Attributes desteği (@taylorotwell) - **#55954** - Cache::touch() metodu - **#56029** - Symfony 7.4/8.0 desteği - **#55921** - Subdomain route önceliği - **#55685** - Model boot() kısıtlaması - **#57972** - HTTP pool varsayılan eşzamanlılık - **#58081** - Request::get() Symfony hizalaması - **#57196** - MySQL DELETE...JOIN düzeltmesi - **#56148** - JobAttempted full Throwable - **#57032** - `__wakeup` -> `__unserialize` (PHP 8.5) - **#58400** - PreventRequestForgery middleware - **#54876** - Fonksiyon varlık kontrollerinin kaldırılması (PHP 8.3 garanti ediyor) Açık PR'lar arasında **#59204** (14 Mart 2026'da açıldı) dikkat çekici - Artisan komutları için ek attribute'lar (`usage`, `help`, `hidden`). Kapatılan PR'lar arasında **#58594** (`containsOneItem`/`containsManyItems` -> `hasSole`/`hasMany`) ve **#58970** (Casts attribute) var - bunlar 13.x'e dahil edilmedi. 12.x'ten 13.x'e backport örneği olarak **#58798** (`withMiddleware` değişikliklerinin 12.x'e backport'u) - bu, özelliklerin önce 13.x'te geliştirilip güvenli olanların geriye taşındığını doğruluyor. Mart 2026 itibarıyla laravel/framework'ün stable v13 tag'ı henüz yayınlanmadı; en son stable sürüm halen v12.54.1 (10 Mart 2026) ama tag'ın bugün atılması bekleniyor. ## Dünya genelinden topluluklardan... Bu araştırmanın en ilginç kısmı farklı dillerdeki topluluk kaynaklarıydı. İngilizce kaynaklar (Laravel News, Laravel Daily, Benjamin Crozat, dev.to, Medium) geniş içerik sunuyor; bazı dillerde özgün bulgular var. Japonca kaynaklar teknik derinlik açısından öne çıkıyor. no-hack-no.life sitesi, attribute ile property'nin aynı anda tanımlandığında öncelik davranışının belgelenmediğini keşfetmiş. Ayrıca 13.x dokümanlarının property tabanlı yapılandırmayı sessizce kaldırmaya başladığını tespit etmişler. Fivenine Design'dan bir makale Docker deployment script'leri ve aşamalı migration stratejisi sunuyor; ama aynı sitedeki başka bir makale "Laravel 13, Q4 2024'te çıkacak" gibi tamamen yanlış AI üretimi içerik barındırıyor. Dikkatli olun. İspanyolca kaynaklar güvenlik perspektifinden değerli. cursosdesarrolloweb.es, PreventRequestForgery middleware'ini ve `WorkerStopReason` queue gözlemlenebilirlik özelliğini İngilizce kaynaklardan daha detaylı analiz etmiş. angelcruz.dev ise kod örnekleriyle anlaşılır bir anlatım sunuyor. Türkçe kaynaklar konusunda hayal kırıklığı yaşadım. "Laravel 13 yenilikler", "Laravel 13 özellikleri", "Laravel 13 çıkış tarihi" aramaları sıfır sonuç döndürdü. Türk geliştirici topluluğu henüz Laravel 13'e özel içerik üretmemiş; muhtemelen stable sürümü bekliyorlar. Bu yazının bu boşluğu doldurabileceğini umuyorum. Reddit ve Laracasts forumlarında doğrudan Laravel 13 tartışmaları arama sonuçlarında belirgin değildi. dev.to en aktif topluluk platformu; en az 4 detaylı makale ve canlı yorum tartışmaları mevcut. PHP Attributes konusundaki kutuplaşma ilginç: "üç versiyondur en büyük iyileştirme" diyen de var, "tanıtılan en nefret edilen özellik" diyen de. ## Laracon etkinlikleri zaman çizelgesi ve gelecek duyurular Laravel 13 ile ilgili Laracon kronolojisi şöyle şekillendi: **Laracon EU 2025** (Şubat 2025, Amsterdam): Laravel 12, yeni starter kit'ler, Laravel Cloud, Nightwatch, Inertia.js satın alımı, VS Code extension duyuruldu. "Laravel tarihinin en büyük günü" olarak adlandırıldı. **Laracon US 2025** (Temmuz 2025, Denver): Cloud MySQL GA, basitleştirilmiş Cloud fiyatlandırma, akıllı queue autoscaling, preview environments, yeni nesil Forge duyuruldu. Laravel 13 burada duyurulmadı. **Laracon India 2026** (31 Ocak - 1 Şubat, Ahmedabad): AI entegrasyonunun altyapı olarak konumlandırılması, çoklu AI provider failover demosu. **Laracon EU 2026** (2-3 Mart, Amsterdam): Laravel 13 burada duyuruldu. PHP Attributes, Passkeys, Teams, AI SDK stable, Cloud API/CLI ve o AI demosu. Gelecek etkinlikler: Laracon JP 2026 (26-27 Mayıs, Tokyo), Laracon US 2026 (28-29 Temmuz, Boston), Laracon Online (Eylül, YouTube'da ücretsiz). Boston'da Laravel 13 deep-dive konuşmaları bekleniyor. ## Laravel 13 neyi değiştiriyor? Laravel 13 büyük özelliklerden çok altyapı modernizasyonuyla geliyor. Büyük yenilikler artık major sürümleri beklemek yerine minor sürümlerde geliyor; major sürümler PHP versiyon yükseltme ve developer experience düzenlemeleri için kullanılıyor. PHP Attributes desteği bir paradigma değişikliğinin başlangıcı. `#[Unguarded]`'ın sadece attribute olarak var olması ve 13.x dokümanlarının property tabanlı yapılandırmayı sessizce kaldırması, Laravel 14-15'te attribute-first (belki attribute-only) bir geleceğe işaret ediyor. Tavsiyem: yeni projelerde attribute sözdizimini benimseyin, mevcut projelerde acele etmeyin. AI entegrasyonu artık bir eklenti değil, ekosistemin merkezinde. AI SDK, Boost, MCP ve Nightwatch birlikte çalışarak bir geliştirici iş akışı oluşturuyor: Boost agent'lara kod bağlamı sağlıyor, MCP agent'ların uygulama ile etkileşimini kuruyor, Nightwatch prodüksiyonu izliyor, AI SDK provider seçimi yapıyor. Taylor'ın Laracon EU'daki sesli bug düzeltme demosu pazarlama gösterisi gibi durdu ama altında çalışan parçalar gerçek. Pratik öneri: `composer.json`'daki PHP versiyonunu 8.3 veya 8.4'e yükseltin, `composer update` çalıştırın, `boot()` metodlarında model instance oluşturmadığınızdan emin olun. 10 dakika içinde geçiş tamam. Sonra `Cache::touch()`, Reverb database driver ve attribute sözdizimini deneyin. ---
Kaynakça **Resmi Laravel Kaynakları:** - https://laravel.com/docs/13.x - https://laravel.com/docs/changelog **GitHub Pull Request'leri ve Depolar:** - https://github.com/laravel/framework - https://github.com/laravel/laravel/releases/tag/v13.0.0 **Laravel Topluluk Kaynakları:** - https://laravel-news.com/laravel-13 - https://laraveldaily.com/post/laravel-13-laracon-eu-taylor-otwell - https://benjamincrozat.com/laravel-13 **Geliştirici Blog Yazıları ve Teknik Analizler:** - https://dev.to/polash/laravel-13-drops-march-17-heres-every-new-feature-with-code-examples-3joa - https://dev.to/hafiz619/laravel-13-php-attributes-refactor-your-models-jobs-and-commands-in-10-minutes-291b - https://dev.to/mmurtuzah/laravel-13-deep-dive-every-new-feature-change-and-code-example-12al - https://nabilhassen.com/laravel-13-new-features-release-date-install-now - https://cloudways.com/blog/laravel-13
--- ## 6 Temel Deployment Stratejisi ve Karşılaştırması - URL: https://erho.dev/post/025-6-temel-deployment-stratejisi-ve-karsilastirmasi - Markdown: https://erho.dev/post/025-6-temel-deployment-stratejisi-ve-karsilastirmasi.md - Tarih: 2026-03-05 - Kategori: diger - Etiketler: devops, architecture Yıllar önce bir müşterinin sistemini 3 saatliğine çökerttim. O an ensemdeki soğuk teri ve monitöre boş boş baktığım saniyeleri hâlâ hatırlıyorum. Sebep basitti: ortada strateji yoktu. "Deploy et, çalışıyorsa devam et" mantığıyla, dualara ve şansa güvenerek ilerliyorduk. O 3 saat bana kariyerimin en değerli derslerinden birini öğretti: deployment teknik bir detay değil, risk yönetimi kararı. Bu yazıda sektörde kullanılan 6 temel deployment stratejisini ele alıyorum. Hangisi ne zaman hayat kurtarır, hangisi ne zaman ters teper. ### 1. Recreate (Basit Yaklaşım) En sade yaklaşımdır; eski ortam tamamen kapatılır ve yeni sürüm ayağa kaldırılır. - **Avantajı:** Kurulumu çok basittir. - **Dezavantajı:** Kesinti (downtime) yaşanması kaçınılmazdır ve geri alma (rollback) işlemi manuel yapılır. - **Ne Zaman Kullanmalı:** Kesintinin tolere edilebildiği küçük projelerde ve geliştirme ortamlarında. - **Ne Zaman Kaçınmalı:** Üretim (production) ortamında müşteriye bir erişilebilirlik (SLA) sözünüz varsa kesinlikle uzak durmalısınız. ### 2. Rolling Update (Dengeli Yaklaşım) Sunucular (instance'lar) birer birer güncellenir, böylece trafik kesintisiz bir şekilde akmaya devam eder. - **Avantajı:** Kesinti yaşanmaz. Kubernetes gibi sistemlerin varsayılan stratejisidir ve stateless (durumsuz) servisler için idealdir. - **Dezavantajı:** Herhangi bir kriz anında rollback işlemi oldukça yavaştır. - **Ne Zaman Kaçınmalı:** Eski ve yeni sürüm arasında veritabanı şeması uyumsuzluğu (migration vb.) varsa kullanmamalısınız. ### 3. Blue / Green (Güvenli Yaklaşım) İki özdeş ortam (Blue ve Green) paralel olarak çalışır ve Load Balancer üzerinden tek bir komutla trafik yeni ortama aktarılır. - **Avantajı:** Kesinti sıfırdır ve en önemlisi anında (anlık) rollback imkanı sunar. - **Ne Zaman Kullanmalı:** Anlık geri dönüşün kritik olduğu, veritabanı şemasının değişmediği senaryolarda. - **Ne Zaman Kaçınmalı:** Ortamı iki katına çıkardığı için ekstra maliyet yaratır. Bu yüzden bütçesi kısıtlı küçük ölçekli projelerde tercih edilmemelidir. ### 4. Canary (Benim Favorim - Risk Yönetimi) Yeni sürüm önce trafiğin sadece %5 veya %10'luk küçük bir dilimine açılır. Hata oranları (error rate) ve gecikme (latency) gibi metrikler izlenir. Sorun yoksa ağırlık kademeli olarak artırılır. - **Neden Favorim?** Çünkü bize hata yapma hakkı tanıyor ama bedelini tüm kullanıcılara ödetmiyor! Kesinti yok ve rollback çok hızlı. - **Ne Zaman Kullanmalı:** Yüksek riskli değişikliklerde, özellikle feature flag altyapınız da varsa mükemmeldir. - **Ne Zaman Kaçınmalı:** Az trafik alan servislerde istatistiksel bir anlam ifade etmeyeceği için mantıklı değildir. ### 5. A/B Testing (Ürün Odaklı) Kullanıcılar belirli segmentlere (cookie, User ID vb.) ayrılarak farklı sürümlere yönlendirilir ve karar tamamen veriye dayalı olarak alınır. - **Ne Zaman Kullanmalı:** Özellikle UI/UX değişikliklerinin kullanıcı davranışına veya iş metriklerine olan etkisini somut olarak ölçmek istediğinizde biçilmiş kaftandır. - **Ne Zaman Kaçınmalı:** Sadece arkaplanı (backend) ilgilendiren değişikliklerde kullanmak gereksizdir. ### 6. Feature Flag (Trunk-Based) Kod aslında canlıya (production) alınır ancak yeni özellik bir "bayrak" (flag) ile kontrol altında tutulur ve varsayılan olarak kapalıdır. - **Avantajı:** Geri dönüş anlıktır. Dark launch veya özellikleri kademeli açma senaryoları için harikadır. - **Dezavantajı:** Uzun süre kodun içinde unutulan flag'ler ileride başınıza ciddi bir teknik borç (technical debt) olarak dönebilir, dikkatli yönetilmelidir. --- "Deploy et" deyip geçemiyoruz. Seçtiğiniz strateji, o gece ne kadar rahat uyuyacağınızı doğrudan belirliyor. Benim favorim açık ara Canary. Peki sizde hangi strateji oturmuş durumda? "Deploy et, dua et" aşamasında olanlar veya kendi sistem çöküş anısı olanlar varsa yorumlarda paylaşalım; birbirimizin hatalarından öğreneceğimiz çok şey var. --- ## Filament v5 Yayınlandı: Livewire v4 Desteği ve Yapay Zeka Entegrasyonu - URL: https://erho.dev/post/024-filament-v5-yayinlandi-livewire-v4-destegi-ve-yapay-zeka-entegrasyonu - Markdown: https://erho.dev/post/024-filament-v5-yayinlandi-livewire-v4-destegi-ve-yapay-zeka-entegrasyonu.md - Tarih: 2026-01-17 - Kategori: laravel - Etiketler: laravel, php Filament ekibi yakın zamanda Filament v5'i duyurdu. Bu sürüm, Livewire v4 desteği ve yeni Filament Blueprint aracıyla geliyor. Bu yazıda neyin değiştiğine ve neyin değişmediğine bakıyorum. ## Filament v5 Nedir? Bilmeyenler için: Filament, Laravel tabanlı açık kaynaklı bir UI framework'üdür. Blade, Livewire ve Alpine.js kullanarak hızlı ve etkili admin paneller ve uygulamalar geliştirmenizi sağlar. Filament v5, yalnızca bir hafta öncesinde çıkan Livewire v4 desteğini sağlamak için geliştirilmiştir. Önemli detay: Filament v5, v4'e kıyasla yeni özellik eklemez. Sadece Livewire v4 ile uyumluluk için altyapı güncellemelerine odaklanır. Yeni özellikler hem v4 hem de v5'e eklenmeye devam edecek; geçiş sürecini esnek tutan bu yaklaşım. ## Neden Filament v5? Filament ekibi, Livewire v4 desteğini neden yeni bir major versiyon olarak çıkardığını şöyle açıklıyor: projeniz `livewire/livewire` paketine doğrudan bağımlı değilse v5'e geçerken beklenmedik breaking change'lerle karşılaşmayacaksınız. Bu tasarım, mevcut Filament v4 projelerinin istikrarlı kalmasını sağlıyor. ### Livewire v4 Desteği Filament v5'in en önemli özelliği Livewire v4 uyumluluğudur. Livewire v4, [Caleb Porzio](https://github.com/calebporzio) ve ekibinin önemli bir sürümü. Bu güncelleme daha hızlı reaktivite, iyileştirilmiş state yönetimi, modernleştirilmiş API ve performans optimizasyonları getiriyor. Laravel geliştirici olarak Filament v5'e geçerek bu iyileştirmelerden doğrudan yararlanabilirsiniz. ## Filament Blueprint: AI Destekli Planlama Filament v5 duyurusuyla birlikte yeni bir araç olan Filament Blueprint da tanıtıldı. Bu premium Laravel Boost uzantısı, AI coding agent'larının Filament projeleri için daha doğru uygulama planları üretmesini sağlıyor. ### Blueprint Neden Gerekli? Bir AI agent'a "Filament ile bir şey yap" dediğinizde genellikle karmaşık ve gereksiz token tüketen bir planlama yapar. Tipik sorunlar: yanlış component namespace'leri (v4 projesinde v3 önermesi gibi), eksik konfigürasyon detayları, mantıksız layout tasarımları, fazla manuel düzeltme ihtiyacı. Filament Blueprint bu sorunu çözmek için tasarlanmış. Agent, Filament'in component'leri, pattern'leri ve best practice'leri hakkında bilgi sağlayarak doğrudan uygulanabilir spesifikasyonlar üretiyor. ### Örnek Kullanım Blueprint kullanarak şöyle bir prompt verebilirsiniz: ```plaintext Filament Blueprint kullanarak, Filament v4 uygulaması için bir proje planı oluştur... Bu proje, aşağıdaki özelliklere sahip bir SaaS faturalama sistemidir: - Müşterileri yönetme - Ürünleri yönetme - Faturaları oluşturma ve düzenleme - Faturalara satır öğeleri ekleme - Müşterilere fatura gönderme - Ödemeleri kaydetme ve takip etme - gibi gibi... ``` **Örnek Blueprint çıktısı** şu tarz kodlar içerecektir: ```php ->relationship('customer', 'name') ->searchable() ->preload() ->createOptionForm([...]) ->live() ->afterStateUpdated() ``` Bu spesifikasyonlar doğrudan uygulanabilir, AI hallüsinasyonlarıyla saatlerce zaman harcamaktan kurtarır. ## Teknik Detaylar ve Upgrade Süreci ### Upgrade Nasıl Yapılır? Filament v5 için bir kısa upgrade rehberi vardır ve **hiçbir manuel adım gerekmez**. Ekip, uyumluluk sorunlarını kontrol etmek ve değişiklikleri otomatik olarak uygulamak için bir upgrade script sağlamıştır. Eğer custom Livewire component'leriniz varsa, Livewire upgrade rehberini de takip etmelisiniz. ### Kimlerin Upgrade Etmesi Gerekir? - Livewire v4'ün yeni özelliklerinden yararlanmak istiyorsanız: v5 zorunlu - Livewire v3 ve Filament v4 ile devam etmek istiyorsanız: zorunluluk yok - Yeni projeler başlatıyorsanız: Filament v5 önerilir ## Pratik Olarak Backend geliştirici olarak Filament v5'e geçişin somut karşılığı: Livewire v4'ün optimize state handling'i, kompleks formlarda daha hızlı yanıt süreleri sağlıyor. Modernleştirilmiş Livewire API'si hata oluşumunu azaltıyor ve debugging'i kolaylaştırıyor. [Filament Blueprint](https://packages.filamentphp.com/p/blueprint/buy) (20% indirimli promo kodu: `BPLAUNCH20`) sayesinde AI agent'lar daha doğru implementasyon planları üretiyor; SaaS fatura sistemi, müşteri yönetimi, veri grid'leri gibi karmaşık özellikler için hızlı çözümler veriyor. Hem v4 hem v5'e özellik eklenmeye devam ettiği için proje migrasyonunda baskı yok, kendi hızınızda geçebilirsiniz. ## Gerçek Hayat Kod Örneği Filament v5'te resource tanımı v4'e benzemeye devam eder: ```php Pages\ListBlogPosts::route('/'), 'create' => Pages\CreateBlogPost::route('/create'), 'edit' => Pages\EditBlogPost::route('/{record}/edit'), 'view' => Pages\ViewBlogPost::route('/{record}'), ]; } } ``` Mevcut Filament v4 kodunuz, Livewire versiyonu güncellendiğinde v5'te çalışmaya devam edecek. ## Neler Beklenmeli? Filament ekibi önümüzdeki aylarda diğer projelerden de bahsetmeyi planlıyor. v5'in stabil sürümü gelirken rich editor fonksiyonalitesi iyileştirilecek, filter ve tablo konfigürasyon seçenekleri genişleyecek, erişilebilirlik ve uluslararası çeviri desteği gelişecek. ## Son Söz Bana göre Filament v5 esasen [Filament Blueprint](https://packages.filamentphp.com/p/blueprint/buy)'i tanıtmak için bahane edilmiş bir major sürüm gibi duruyor. Filament v5 yeni özellik getirmiyor ama Livewire v4 desteği sayesinde modern altyapıyla uyumlu kalıyor; mevcut projeleriniz istikrarlı şekilde devam ediyor. Beta sürümüyle deneme yapmak, stable'a geçişi hazırlamak için iyi bir başlangıç. ---
Kaynakça - https://filamentphp.com/docs/5.x - https://github.com/filamentphp/filament/releases/tag/v5.0.0 - https://filamentphp.com/content/danharrin-filament-v5-blueprint - https://laravel-news.com/filament-5 - https://cube.nl/blog/filament-5
--- ## Cloudflare'in 5 Aralık Kesinti Felaketinden Ders Al: Laravel Pennant ile Benzer Senaryo'dan Nasıl Korunabiliriz? - URL: https://erho.dev/post/023-cloudflarein-5-aralik-kesinti-felaketinden-ders-al-laravel-pennant-ile-benzer-senayodan-nasil-korunabiliriz - Markdown: https://erho.dev/post/023-cloudflarein-5-aralik-kesinti-felaketinden-ders-al-laravel-pennant-ile-benzer-senayodan-nasil-korunabiliriz.md - Tarih: 2025-12-06 - Kategori: laravel - Etiketler: laravel, php Dün Cloudflare yine yaptı yapacağını. Ne oldu, ne ders çıkarabiliriz? Bu yazıda Laravel Pennant ile aynı senaryodan nasıl korunabileceğinizi anlatıyorum. Cloudflare, 5 Aralık 2025'te internet trafiğinin %28'ini etkileyen bir kesinti yaşadı. 25 dakika içinde milyon dolarlık firmalar, e-ticaret siteleri ve API'ler çöktü. Sebebi ne miydi? Güvenlik açığını kapatmak için yapılan küçük bir configuration değişikliği. Şu senaryoyu düşünün: PHP/Laravel ile yazılmış bir e-ticaret siteniz var. Bir güvenlik açığı fark ettiniz ve hızlıca patch uyguladınız. Configuration'ı güncellediniz, tüm sunuculara aynı anda deploy ettiniz ve ödeme sisteminiz çöktü. Müşteriler alışveriş yapamıyor, para kaybediyorsunuz, sosyal medyada şikayetler birikiyor. Cloudflare'in dün yaşadığı tam olarak böyle bir senaryoydu. Cloudflare'in sorununun temelinde şu vardı: yeni configuration kademeli (gradual) deployment olmadan tüm ağa aynı anda yayıldı. Bir "feature flag" sistemi olsaydı değişikliği önce %1 sunucuya açıp test edebilirlerdi; sorun çıktığında %99 etkilenmeyecekti. Burada Laravel Pennant devreye giriyor. Yazının devamında Cloudflare benzeri bir felaketten kaçınmak için Pennant'ı nasıl kullanabileceğinizi gerçek hayat örnekleriyle aktarıyorum. ## Cloudflare Nelerden Geçti? Cloudflare'in blog posunda anlattığı olayı özetleyelim: (Kaynak linki altta) * **08:47 UTC:** WAF (Web Application Firewall) buffer size'ını 128KB'dan 1MB'ye çıkarmak için bir configuration değişikliği yapıldı. Bu değişiklik **gradual deployment** ile başlamıştı (iyi bir karar). * **Ancak:** Aynı sırada, WAF testing tool'u kapatmak için başka bir configuration değişikliği yapıldı ve **bu bu global configuration system'i kullandı - hiç gradual deployment yok**. * **Sonuç:** Bu ikinci değişiklik, tüm ağa **saniyeler içinde** yayıldı ve **FL1 proxy**'de bir **Lua exception trigger** tetiklendi. Sonra da `attempt to index field 'execute' (a nil value)` hatası fırlatıldı… * Tüm istekler HTTP 500 döndürmeye başladı. %28'i etkilendi. * Kod 1 dakika sonra identify edilip, revert yapıldı ve 25 dakika sonra sorun çözüldü. Peki burada kritik sorun neydi? **Global configuration system hiç rollback plan olmadan, fail-safe bir mekanizması olmadan tüm network'ü etkiledi**. Eğer bir gradual rollout mekanizması olsaydı, ilk %1-5'te sorun tespit edilip, geriye kalan %95'i hiç etkilenmeyecekti. ## Laravel Pennant ile Farklı Bir Senaryo Hayali Kuralım Siz de bir e-ticaret firması yönettiğinizi vs düşünün. Yeni bir ödeme gateway'ine geçiyorsunuz (Ör: Stripe'dan Iyzico/PayTR gibi başka bir sağlayıcıya). Bu kritik bir değişiklik. Eğer hata yaparsanız, hiç kimse ödeme yapamayacak! ### Senaryo 1: Pennant OLMADAN (Cloudflare gibi) ```php PaymentGatewayEnum::NEW_PROVIDER, // Değiştirdiniz ]; // routes/api.php Route::post('/checkout', function (Request $request) { $provider = config('payment.gateway'); if ($provider === PaymentGatewayEnum::NEW_PROVIDER) { return (new NewPaymentProvider())->charge($request->amount); } else { return (new StripePaymentProvider())->charge($request->amount); } }); ``` Sonra config'i güncellediniz ve tüm sunucuları deploy ettiniz. Tam o anda, yeni provider'ın API'si down oldu (ya da yanlış entegrasyon vs vardı). **Tüm müşterileriniz artık ödeme yapamıyor**. Rollback yapmanız gerekiyor, yeni deployment, vb. 10 dakika boyunca ödeme alamayacaksınız vs... ### Senaryo 2: Pennant İle (Doğru Şekilde) ```php isInternalStaffMember() => true, $user->isBetaTester() => true, default => Lottery::odds(1 / 100), // Sonra %1'e aç }; } } // CheckoutController.php user()) ->active(NewPaymentProvider::class) ? new NewPaymentProviderService() : new StripePaymentProvider(); return $paymentService->charge($request->amount); } } ``` Şimdi, yeni provider'ın API'si problem çıkarırsa ne olur? * **İlk saat,** sadece %1 kullanıcı etkilenir (100 kişiden biri). * **Monitoring** tool’lar ile hata oranını izlersiniz ve sorunu görürsünüz. * Ve gerekirse Feature'ı anında kapatırsınız (sadece bir flag değişikliği, deployment yok): ```php use Laravel\Pennant\Feature; use App\Features\NewPaymentProvider; // Acil olayda çalıştırırsınız Feature::deactivate(NewPaymentProvider::class); ``` Problem çözüldü. %99 kullanıcı hiç etkilenmedi. ## Gerçek Hayat Örneğiyle, Aşamalı Uygulama Stratejisi Pennant ile nasıl bir başlangıç yapardınız? İşte bir deployment örneğ: ### Gün 1: Internal Testing ```php Feature::define(NewPaymentProvider::class, fn (User $user) => $user->isInternalStaffMember() ); ``` **Sadece 10 internal staff member test ediyor.** Hataları bulup fix ediyorsunuz. ### Gün 2: Beta Testers Dahil ```php Feature::define(NewPaymentProvider::class, fn (User $user) => match (true) { $user->isInternalStaffMember() => true, $user->isBetaTester() => true, default => false, }); ``` **100 beta tester** ile gerçek ortamda davranışını gözlemliyorsunuz. ### Gün 3: Canary Rollout %5 ```php Feature::define(NewPaymentProvider::class, fn (User $user) => match (true) { $user->isInternalStaffMember() => true, $user->isBetaTester() => true, default => Lottery::odds(5 / 100), }); ``` **%5 kullanıcı.** Monitoring dashboard'unuzu açıyorsunuz. Success rate, error rate, latency, hepsi izleniyorsunuz. Eğer hata oranı %0.1'den fazla olursa, flag'i kapatıyorsunuz. **DİPNOT:** Canary deployment, biri canlı trafiğe sahip orijinal kodu diğeri ise canlı trafik olmadan güncellenmiş kodu içeren iki ortamla başlayan bir dağıtım stratejisidir. ### Gün 4: %10'a Çık ```php default => Lottery::odds(10 / 100), ``` ### Gün 5: %25'e Çık ```php default => Lottery::odds(25 / 100), ``` ### Gün 6: %50'ye Çık ```php default => Lottery::odds(50 / 100), ``` ### Gün 7: %100'e Çık (Full Rollout) ```php Feature::activateForEveryone(NewPaymentProvider::class); ``` Bu şekilde, 7 gün içinde, milyonlarca kullanıcıya güvenli bir şekilde yeni sistemi sunarsınız. ## Scope Kullanarak SaaS'ta Team-Level Rollout Eğer SaaS uygulamanız varsa, feature'ları team bazında yaygınlaştırmak Pennat ile çok güçlüdür: ```php hemen aktif * Normal teams => %10 rastgele */ public function resolve(Team $team): mixed { return match (true) { $team->plan === 'premium' => true, $team->created_at->isAfter(new Carbon('1 Jan 2025')) => Lottery::odds(10 / 100), default => Lottery::odds(1 / 100), }; } } ``` Sonra controller'da: ```php if (Feature::for($user->team)->active(AdvancedReportingEngine::class)) { return view('reports.advanced'); } else { return view('reports.basic'); } ``` Bu yaklaşım, premium müşterilere yeni özellikleri daha hızlı, sıradan müşterilere daha yavaş açmanızı sağlıyor. ## Production'da Monitoring: Flag'ler Neler Yapıyor? Pennant, `FeatureRetrieved` event'i dispatch ediyor her kontrol edildiğinde. Bunu logging ile birleştirip, hangi feature'ların ne kadar kullanıldığını görebilirsiniz: ```php $event->feature, 'scope' => $event->scope, 'value' => $event->value, 'timestamp' => now(), ]); }); } } ``` Böylece, log dashboard'unuzda: * "NewPaymentProvider feature'ı kaç kez kontrol edildi?" * "Hangi users aktif olarak yeni provider'ı kullanıyor?" * "Error rate, eski vs. yeni provider'da ne durumda?" Tüm bu metrikleri ve daha fazlasını izleyebilirsiniz. ## Hook'tan Önce: Acil Durum Butonu Pennant'ın güçlü bir özelliği daha: class-based feature'da `before()` metodu yazıp before hook'u çalıştırabilirsiniz: ```php isInternalStaffMember(); } } public function resolve(User $user): mixed { return match (true) { $user->isInternalStaffMember() => true, $user->isBetaTester() => true, default => Lottery::odds(5 / 100), }; } } ``` Şimdi, eğer yeni payment provider API'si crash olursa: ```php // config/features.php dosyasında veya env'de 'payment' => [ 'emergency_mode' => true, // Feature'ı sadece internal team için aç ] ``` **1 saniye içinde, tüm müşterileriniz eski sisteme geri dönüyor!**. Storage'dan hiçbir şey okumaya bile gerek bile yok, önceden belirlenmiş rule işlyor. ## Eager Loading: Döngüde Performans Sorunu Eğer tüm aktif kullanıcıların feature status'unu bir batch işlemde kontrol etmek istiyorsanız, bu nokta da bi N+1 sorunu yaşayabilmeniz muhtemeldir: ```php // ❌ Hatalı: Her user için ayrı bir kontrol foreach ($users as $user) { if (Feature::for($user)->active(NewPaymentProvider::class)) { // Yeni provider'ı kullan } } // ✅ Doğru: Hepsini bir kere yükle Feature::for($users)->load([NewPaymentProvider::class]); foreach ($users as $user) { if (Feature::for($user)->active(NewPaymentProvider::class)) { // Yeni provider'ı kullan } } ``` Başta bir query ile tüm feature values'ları yüklenir. Sonra loop içinde, önceden yüklenen değerler kullanılır. ## Testing: Feature Flag'ler Nasıl Test Edilir? Unit testlerde feature'ları kontrol etmek basit (Pest'e uyarlayabilirsiniz): ```php create(['role' => 'internal_staff']); // Feature'ı tanımla Feature::define(NewPaymentProvider::class, fn (User $u) => $u->role === 'internal_staff' ); // Assert $this->assertTrue( Feature::for($user)->active(NewPaymentProvider::class) ); } public function test_regular_users_cannot_access_new_payment_provider() { $user = User::factory()->create(['role' => 'customer']); Feature::define(NewPaymentProvider::class, false); $this->assertFalse( Feature::for($user)->active(NewPaymentProvider::class) ); } } ``` Böylece, test ortamında `array` driver kullanıp, database'e yazmadan testleri çalıştırabilirsiniz. ## Peki Ya Flag'leri Temizlemek İstersek Yeni ödeme provider tamamen stabil hale geldi ve tüm kullanıcılar artık onu kullanıyor. Kodda "if feature active" kontrolleri olmasına gerek kalmadı. Şimdi ne yapacaksınız? **Flag'i temizleyin:** ```bash php artisan pennant:purge NewPaymentProvider ``` Sonra, codebase'den de eski provider'ı kullanma kodunu silin: ```php // Eski kod (sil): if (Feature::for($user)->active(NewPaymentProvider::class)) { $paymentService = new NewPaymentProviderService(); } else { $paymentService = new StripePaymentProvider(); } // Yeni kod: $paymentService = new NewPaymentProviderService(); // Varsayılan oldu ``` Bu şekilde kodunuz temiz ve sürdürülebilir kalır. ## Kurmuş Sistemlerde Pennant Deployment Strategy'si Eğer siz de **existing production system**'inize feature flag sistemi ekliyorsanız, aşamalı bir approach şöyle olabilir: ### Faz 1: Kritik Features (Week 1) Sadece en kritik, en riskli features için flag ekle: * Ödeme sistemi * Authentication * Yeni API endpoints ### Faz 2: Orta Öncelikli Feature (Week 2-3) * Yeni UI components * Database migrasyonları * External API integrations ### Faz 3: En Son Feature (Week 4+) * UI/UX improvements * Performance optimizations * Analytics features Bu sayede, kademeli bir şekilde, hiç sistemin çökmeden Pennant'ı entegre edebilirsiniz. ## Cloudflare'in Hatası, Sizin İçin Ders Cloudflare'in 25 dakikalık kesintisinin maliyeti milyon dolar oldu. Ancak sizin için bu kesinti değerli bir ders. Önemli bir sistem geliştiriyorsanız ve feature flag sisteminiz yoksa, her deployment Cloudflare'in dün yaşadığı türden bir riski yanında getiriyor. Laravel Pennant bu riski yönetiyor: kademeli rollout, monitoring, hızlı rollback, kill switch. Hepsi Laravel ekosisteminde hazır. Bir senaryo: yeni özelliği production'a koydunuz, sorun çıktı. Klasik akış: 1. Problemi debug etmek: 5 dakika 2. Yeni kod yazıp test etmek: 15 dakika 3. Tekrar deploy etmek: 5 dakika 4. Trafik sağlam mı kontrol etmek: 5 dakika Toplam: ~30 dakika downtime. Laravel Pennant ile: 1. Sorunu tespit etmek: 1 dakika 2. Feature flag'i kapatmak: 10 saniye Toplam: ~70 saniye downtime. Aradaki fark net. ---
Kaynakça - https://blog.cloudflare.com/5-december-2025-outage - https://laravel.com/docs/12.x/pennant
--- ## PHP 8.5 Sürümüne Dair Bilmeniz Gereken Her Şey - URL: https://erho.dev/post/022-php-85-surumune-dair-bilmeniz-gereken-her-sey - Markdown: https://erho.dev/post/022-php-85-surumune-dair-bilmeniz-gereken-her-sey.md - Tarih: 2025-11-21 - Kategori: php - Etiketler: php Selamlar. Bu yazıda PHP'nin yeni versiyonu 8.5'i inceliyoruz. 20 Kasım 2025'te (dün) yayınlandı; geliştiricilerin işini kolaylaştıracak özellikler getiriyor. Adım adım bakalım. ## Yayın Tarihi ve Sürüm Planlaması PHP 8.5'in yayın süreci 3 alfa, 3 beta ve 5 aday sürümden (Release Candidate) oluşuyor. Şu anda RC5 aşamasını geçti ve 20 Kasım 2025'te (GA, General Availability) resmi olarak yayınlandı. Şu ana kadarki tarihsel seyir şöyle gelişti: İlk alfa sürümü **3 Temmuz 2025**'te yayınlandı, ardından Temmuz ve Ağustos ayları boyunca alfa ve beta testleri sürdü. **12 Ağustos**'ta özellik dondurma (feature freeze) gerçekleşti. Bu tarihten sonra artık yeni özellik eklenmiyor, sadece hatalar düzeltiliyor. Eylül ve Ekim aylarında aday sürümlerle son testler yapılıyor ve nihayet Kasım ayında dünya PHP 8.5 ile tanışacak. | **Tarih** | **PHP 8.5 Sürümü** | |-----------------|-----------------------------------| | 03 Temmuz 2025 | Alpha 1 | | 17 Temmuz 2025 | Alpha 2 | | 31 Temmuz 2025 | Alpha 4 - (Alfa 3 atlandı) | | 12 Ağustos 2025 | Feature Freeze (Özellik Dondurma) | | 14 Ağustos 2025 | Beta 1 | | 28 Ağustos 2025 | Beta 2 | | 11 Eylül 2025 | Beta 3 | | 25 Eylül 2025 | RC1 | | 09 Ekim 2025 | RC2 | | 23 Ekim 2025 | RC3 | | 06 Kasım 2025 | RC4 | | 13 Kasım 2025 | RC5 | | 20 Kasım 2025 | GA (Genel Kullanıma Sunulma) | ## Pipe Operatörü (\|>) PHP 8.5'in en dikkat çeken özelliklerinden biri, belki en çok konuşulanı pipe operatörü (`|>`). Bu operatör, fonksiyon zincirleme işlemlerini soldan sağa aktarmanızı sağlıyor. Bir veri üzerinde art arda işlemler yapmak istediğinizde devreye giriyor. Önceki yöntemlerle bunu nasıl yapıyorduk? İki seçeneğimiz vardı: Ya **iç içe fonksiyon çağrıları** kullanıyorduk (ki bu okumayı zorlaştırıyordu ve içeriden dışarıya doğru okunması gerekiyordu), ya da **geçici bir değişken** tanımlayıp sürekli ona atama yapıyorduk. ```php // Eski yöntem 1: İç içe fonksiyon çağrıları - okumak çok zor $result = trim(str_shuffle(strtoupper("Hello World"))); // Eski yöntem 2: Geçici değişken kullanımı - namespace kirliliği $result = "Hello World"; $result = strtoupper($result); $result = str_shuffle($result); $result = trim($result); ``` Şimdi PHP 8.5 ile bu işlemi çok daha şık bir şekilde yapabiliyoruz: ```php $result = "Hello World" |> strtoupper(...) |> str_shuffle(...) |> trim(...); ``` Gördüğün gibi kod soldan sağa doğru akıyor ve ne yaptığını anlamak çok daha kolay. Buradaki `(...)` sözdizimi PHP 8.1'de tanıtılan **"first-class callable"** özelliği. Bu sayede fonksiyon ismini string olarak yazmak yerine direkt fonksiyon referansını kullanabiliyoruz. ### Gerçek Dünya Örneği: Slug Oluşturma Blog yazıları için URL-friendly slug oluşturma senaryosunu ele alalım: ```php class PostService { public function createSlug(string $title): string { return $title |> trim(...) |> strtolower(...) |> fn($s) => preg_replace('/[^a-z0-9\s]/i', '', $s) |> fn($s) => str_replace(' ', '-', $s) |> fn($s) => preg_replace('/-+/', '-', $s); } } $service = new PostService(); $slug = $service->createSlug(' Harika bir PHP yazısı! 2025 '); echo $slug; // "harika-bir-php-yazisi-2025" ``` ### API Response Processing Daha karmaşık bir örnek: API'den gelen verileri işlemek: ```php $response = $apiClient->get('/users') |> json_decode(...) |> fn($data) => array_filter($data, fn($user) => $user['active']) |> fn($users) => array_map(fn($user) => new User($user), $users) |> fn($users) => new UserCollection($users); ``` Bu kod çok daha okunabilir ve her adım kendi başına anlamlı! **Önemli kısıtlamalar:** Pipe operatörünün bazı sınırlamaları var ve bunları bilmek önemli: - Her fonksiyon **yalnızca bir zorunlu parametre** kabul edebilir - **Referans parametreli** fonksiyonlar kullanılamaz - İlk sınıf çağrılabilir (first-class callable) olarak belirtilmeli ```php // ✅ Doğru kullanım $result = $value |> strtoupper(...); // ❌ Yanlış kullanım - birden fazla zorunlu parametre $result = $value |> str_replace('a', 'b'); // Hata! // ✅ Closure ile çalışan alternatif $result = $value |> fn($v) => str_replace('a', 'b', $v); ``` ## Clone with: İmmutability için Zarif Bir Çözüm PHP 8.5'in bir diğer önemli özelliği `clone with` ifadesi. Özellikle `readonly` özelliklerle çalışırken bu özellik işi çok kolaylaştırıyor. Şu senaryoyu düşün: Elinde immutable (değiştirilemez) bir nesne var ve bu nesnenin sadece bir özelliğini değiştirerek yeni bir kopya oluşturmak istiyorsun. PHP 8.4 ve öncesinde bunun için ya tüm özellikleri yeni bir nesneye elle kopyalaman ya da `__clone()` method'unda özel bir mantık yazman gerekiyordu. ### Eski Yaklaşım vs Yeni Yaklaşım ```php readonly class User { public function __construct( public int $id, public string $name, public string $email ) {} } $user = new User(1, 'Ali', 'ali@erho.test'); // PHP 8.4 ve öncesi: Zahmetli, hata yapmaya açık yöntem $newUser = new User($user->id, 'Ayşe', $user->email); // PHP 8.5: Zarif, temiz ve açık yöntem $updatedUser = clone $user with ['name' => 'Ayşe']; ``` Bu özellik sayesinde orijinal nesne değişmeden kalırken, belirttiğin özelliklerin yeni değerlerle güncellendiği bir kopya elde ediyorsun. ### Domain Model Örneği İşletme uygulamalarında çok yaygın olan Order sınıfı: ```php class Order { public function __construct( public readonly string $id, public readonly OrderStatus $status, public readonly array $items, public readonly Money $total ) {} public function markAsShipped(): self { // Sadece status değişti, diğer özellikler aynı kaldı return clone $this with [ 'status' => OrderStatus::Shipped ]; } public function addItem(OrderItem $item): self { $newItems = [...$this->items, $item]; $newTotal = $this->total->add($item->price); return clone $this with [ 'items' => $newItems, 'total' => $newTotal ]; } } // Kullanım $order = new Order( 'ORD-123', OrderStatus::Pending, [], Money::zero() ); $order = $order->addItem($item1); $order = $order->addItem($item2); $order = $order->markAsShipped(); // Orijinal $order hala değişmemiş, her adımda yeni bir nesne yaratılmış ``` **Teknik detaylar:** - `__clone()` magic method'u önce çağrılıyor (varsa) - Yeni özellikler array'in iterasyon sırasına göre atanıyor - Tüm visibility (görünürlük) kuralları geçerli - Private bir özelliği sınıf dışından `clone with` ile değiştiremezsin - Property hook'lar da beklendiği gibi çalışıyor: setter'lar clone sırasında çağrılıyor ## #\[NoDiscard\] İle Dönüş Değerlerini Unutma Artık! Bazen bir fonksiyonun dönüş değeri o kadar önemlidir ki göz ardı edilmemesi gerekir… PHP 8.5'te tanıtılan `#[NoDiscard]` attribute'u tam da bu sorunu çözmek için tasarlanmış. Düşün ki bir batch işleme fonksiyonun var ve bu fonksiyon hatalar oluştuğunda exception fırlatmak yerine hata detaylarını döndürüyor. Eğer birisi bu fonksiyonu çağırıp dönüş değerini kontrol etmeyi unutursa, ciddi sorunlar yaşanabilir. ### Sorun ve Çözüm ```php #[NoDiscard] function processOrders(array $orders): OrderProcessingResult { $errors = []; $processedCount = 0; foreach ($orders as $order) { try { $this->validateOrder($order); $this->saveOrder($order); $processedCount++; } catch (Exception $e) { $errors[] = [ 'order_id' => $order->id, 'error' => $e->getMessage() ]; } } return new OrderProcessingResult($processedCount, $errors); } // Hatalı kullanım: PHP bir uyarı verecek // Uyarı: processOrders() işlevinin dönüş değeri kullanılmalı processOrders($orders); // Doğru kullanım $result = processOrders($orders); if (!$result->isSuccessful()) { $this->logger->error('Bazı siparişler başarısız oldu:', $result->getErrors()); } ``` Özelleştirilmiş uyarı mesajı da ekleyebilirsin: ```php #[NoDiscard("Bu bir batch işleme fonksiyonudur ve herhangi bir öğe başarısız olursa, \ exception fırlatmak yerine hata detaylarını bir array içinde döndürür. \ Sonuçları mutlaka kontrol edin!")] function processOrders(array $orders): OrderProcessingResult { // ... } ``` ### Uyarıyı Bastırma Yolları Eğer gerçekten dönüş değerini göz ardı etmek istiyorsan, bunu açıkça belirtmek için birkaç yöntem var: ```php // Yöntem 1: Explicit void cast (en tercih edilen) (void)processOrders($orders); // Yöntem 2: Error suppression operatörü (önerilmez - kötü uygulama) @processOrders($orders); // Yöntem 3: Go tarzı kullanım $_ = processOrders($orders); // Yöntem 4: Değeri kullan (en doğrusu) $result = processOrders($orders); if ($result->hasErrors()) { handleErrors($result->errors); } ``` Bu özellik özellikle **API fonksiyonları**, **batch işlemler** ve **dönüş değeri başarı/başarısızlık** durumunu gösteren methodlar için son derece değerli. ## Array'ler için Yeni Yardımcı Fonksiyonlar PHP 7.3'te `array_key_first()` ve `array_key_last()` fonksiyonları eklenmişti. Ancak bu anahtarlara karşılık gelen değerleri almak için hala zahmetli bir sözdizimi kullanman gerekiyordu: ```php // Eski yöntem - zahmetli $firstValue = $array[array_key_first($array)]; $lastValue = $array[array_key_last($array)]; ``` PHP 8.5 bu sorunu iki yeni fonksiyonla çözüyor: ### array_first() ve array_last() ```php $users = ['Ali', 'Ayşe', 'Fatma', 'Veli']; $firstUser = array_first($users); // 'Ali' $lastUser = array_last($users); // 'Veli' // Key'li array'lerle de çalışır $data = ['name' => 'Veli', 'age' => 30, 'city' => 'Istanbul']; echo array_first($data); // 'Veli' echo array_last($data); // 'Istanbul' // Boş array'ler için null döner $empty = []; var_dump(array_first($empty)); // NULL var_dump(array_last($empty)); // NULL ``` ### Pipe Operatörü ile Kombinasyon Bu fonksiyonlar basit görünse de, özellikle **pipeline'lar** ve **pipe operatörü** ile birlikte kullanıldığında çok güçlü hale geliyorlar: ```php $result = $users |> array_filter(fn($user) => $user->active) |> array_map(fn($user) => $user->email) |> array_first(...); // İlk aktif kullanıcının emaili // Gerçek dünya: Loglamak için ilk ve son order ID'si $firstOrderId = $orders |> array_first(...)?->id; $lastOrderId = $orders |> array_last(...)?->id; $this->logger->info("Siparişler işleniyor:", [ 'first_order' => $firstOrderId, 'last_order' => $lastOrderId, 'total' => count($orders) ]); ``` ## Yeni URI Extension İle Standartlara Uygun URL İşleme PHP'nin eski `parse_url()` fonksiyonu ne yazık ki modern internet standartlarına tam olarak uyumlu değildi. Bu sorun **güvenlik açıklarına** ve **tutarsız davranışlara** yol açabiliyordu. PHP 8.5 bu sorunu iki yeni sınıf olan `Uri\Rfc3986\Uri` ve `Uri\WhatWg\Url` ile çözüyor. ### parse_url()'in Problemleri ```php $url = 'https://api.erho.test:8080/users?id=123&sort=name#profile'; $parts = parse_url($url); // Associative array döner - nesne yönelimli değil echo $parts['scheme']; // 'https' echo $parts['host']; // 'api.erho.test' echo $parts['port']; // 8080 // Sorunlar: // - Yetersiz validasyon // - URL manipülasyonu için araç yok // - Modern standartları desteklemiyor // - Hata yönetimi zayıf ``` ### Yeni URI Sınıfları **RFC 3986 Implementasyonu:** Bu standart daha genel ve izin verici URI'ları (scheme olmadan da olabilir) kabul ediyor, geçersiz karakterlerde katı davranıyor ve "raw" (ham) ile "normalized-decoded" (normalleştirilmiş-decode edilmiş) görünümler sunuyor. **WHATWG URL Implementasyonu:** Bu standart tarayıcı davranışını takip ediyor, sadece URL'leri (scheme zorunlu) destekliyor, parse sırasında otomatik normalleştirme yapıyor. ```php use Uri\Rfc3986\Uri; use Uri\WhatWg\Url; // RFC 3986 kullanımı - daha esnek $uri = Uri::parse("https://erho.dev/path?x=1"); echo $uri->scheme; // 'https' echo $uri->host; // 'erho.dev' echo $uri->path; // '/path' // ERHO_TODO: WHATWG kullanımı - tarayıcı uyumlu $url = new Url("HTTPS://🐘.COM/foo/../bar/./baz?#fragment"); echo $url->toAsciiString(); // 'https://xn--go8h.com/bar/baz?#fragment' echo $url->toUnicodeString(); // 'https://🐘.com/bar/baz?#fragment' ``` ### Immutable Nesneler ve Fluent Interface URI sınıfları immutable (değişmez) olarak tasarlanmış ve fluent interface desteği sunuyor: ```php $baseUri = new Uri\Rfc3986\Uri("https://api.erho.test/v1/users"); // Yeni URI nesneleri oluştur - orijinal değişmez $updatedUri = $baseUri ->withScheme('https') ->withPort(443) ->withPath('/v2/users') ->withQuery('limit=50&offset=100'); echo $baseUri->toAsciiString(); // Orijinal değişmedi echo $updatedUri->toAsciiString(); // https://api.erho.test:443/v2/users?limit=50&offset=100 // URI karşılaştırma $uri1 = new Uri\Rfc3986\Uri("https://ERHO.dev"); $uri2 = new Uri\Rfc3986\Uri("https://erho.DEV"); echo $uri1->equals($uri2) ? 'Eşit' : 'Farklı'; // Eşit (normalleştirmeden sonra) ``` Bu yaklaşım, **thread-safety** ve **öngörülebilirlik** açısından büyük avantajlar sağlıyor. ## Fatal Error Backtraces ile Hata Ayıklama PHP 8.5'ten önce, fatal error'lar (hafıza sınırı aşımı, sonsuz döngü, syntax hatası vb.) oluştuğunda sadece hatanın oluştuğu satır numarasını görüyordun. **Stack trace olmadığı için** hataya nasıl ulaşıldığını anlamak çok zordu. PHP 8.5 ile yeni bir INI direktifi geliyor: `fatal_error_backtraces`. Bu özellik **varsayılan olarak aktif** ( `fatal_error_backtraces=1`) ve **fatal error'lara tam stack trace ekliyor**. ### Örnek: Maximum Execution Time ```php set_time_limit(1); function recurse(): void { usleep(100000); recurse(); } recurse(); ``` **PHP 8.4 ve öncesi çıktısı:** ```log-error Fatal error: Maximum execution time of 1 second exceeded in example.php on line 7 ``` **PHP 8.5 çıktısı:** ```log-error Fatal error: Maximum execution time of 1 second exceeded in example.php on line 6 Stack trace: #0 example.php(6): usleep(100000) #1 example.php(7): recurse() #2 example.php(7): recurse() #3 example.php(7): recurse() ... #11 {main} ``` Artık hatanın tam olarak nereden geldiğini ve hangi fonksiyon çağrı zincirine denk geldiğini görebiliyorsun. Bu özellik `#[\SensitiveParameter]` ve `zend.exception_ignore_args` ayarlarıyla da uyumlu çalışıyor, yani hassas bilgiler stack trace'den gizlenebiliyor. ## Final Property Promotion: Immutability'yi Daha da İyileştirme PHP 8.4'te property hooks ile birlikte `final` keyword'ü property'lerde kullanılabilir hale gelmişti. Ancak **constructor property promotion** ile bu mümkün değildi. PHP 8.5 bu kısıtlamayı kaldırıyor. ### Eski vs Yeni Yaklaşım ```php // Eski yöntem - PHP 8.4 class User { final public readonly string $id; public function __construct(string $id) { $this->id = $id; } } // Yeni yöntem - PHP 8.5 class User { public function __construct( final public readonly string $id, public string $name ) {} } ``` Daha da ilginç olanı, eğer `final` keyword'ünü kullanıyorsan, **visibility modifier'ı belirtmen gerekmiyor**; varsayılan olarak `public` oluyor: ```php class User { public function __construct( final string $id, // Bu public final olacak final int $age // Bu da public final olacak ) {} } ``` ### Boilerplate Kod Azalması Bu özellik **value object'ler** ve **DTO'lar** (Data Transfer Objects) için mükemmel: ```php // Birçok DTO'nun yapısı bu gibi class CreateUserRequest { public function __construct( final public string $email, final public string $password, final public string $firstName, final public string $lastName ) {} } // Immutability açık, değiştirilemez özellikler açık $request = new CreateUserRequest( 'user@erho.test', 'password123', 'Ali', 'Doe' ); // Değiştirmeye çalışma - hata verir $request->email = 'new@erho.test'; // Error: Cannot modify final property ``` ## Error ve Exception Handler'ları Almak Artık Çok Kolay PHP 8.5'ten önce, mevcut error veya exception handler'ı almak için garip bir hack kullanman gerekiyordu: ```php // Eski yöntem (PHP 8.4 ve öncesi) - Çirkin ve hassas $errorHandler = set_error_handler('some_valid_callback'); restore_error_handler(); $exceptionHandler = set_exception_handler('some_valid_callback'); restore_exception_handler(); ``` PHP 8.5 bunu iki yeni fonksiyonla basitleştiriyor: ```php // Yeni yöntem (PHP 8.5) - Temiz ve açık $errorHandler = get_error_handler(); $exceptionHandler = get_exception_handler(); // Eğer handler set edilmemişse null döner if (get_exception_handler() === null) { set_exception_handler('my_exception_handler'); } // Framework'lerle çalışırken çok önemli if ($exceptionHandler === null) { $this->registerDefaultExceptionHandler(); } ``` Bu fonksiyonlar **framework geliştiricileri** ve **handler zincirii yapan geliştiriciler** için özellikle kullanışlı. ## Grapheme Levenshtein: Unicode Desteğiyle Metin Karşılaştırma PHP'de uzun zamandır `levenshtein()` fonksiyonu vardı, ancak **UTF-8 desteği yoktu**. Bu fonksiyon iki string arasındaki Levenshtein mesafesini (kaç karakter düzenleme ile bir string diğerine dönüştürülebilir) hesaplıyordu, ama **byte seviyesinde** çalışıyordu. PHP 8.5 ile yeni `grapheme_levenshtein()` fonksiyonu geliyor ve bu fonksiyon **grapheme cluster'larla** (kullanıcının gördüğü karakterler) çalışıyor: ### Sorun ve Çözüm ```php // Eski fonksiyonla sorun - UTF-8 desteği yok var_dump(levenshtein('göthe', 'gothe')); // 2 (yanlış!) var_dump(levenshtein('café', 'cafe')); // 1 (yanlış!) // Yeni fonksiyonla doğru sonuç - UTF-8 desteği var var_dump(grapheme_levenshtein('göthe', 'gothe')); // 1 (doğru!) var_dump(grapheme_levenshtein('café', 'cafe')); // 1 (doğru!) // Normalizasyon farkları $string1 = "\u{00e9}"; // é (tek code point) $string2 = "\u{0065}\u{0301}"; // e + combining accent // Farklı encoding ama aynı grapheme var_dump(grapheme_levenshtein($string1, $string2)); // 0 (eşit!) ``` ### Gerçek Dünya Kullanımı ```php class SpellChecker { public function findSimilarWords(string $word, array $dictionary, int $maxDistance = 2): array { return array_filter( $dictionary, fn($dictWord) => grapheme_levenshtein($word, $dictWord) <= $maxDistance ); } } $checker = new SpellChecker(); $similar = $checker->findSimilarWords('yazı', ['yazı', 'yazıcı', 'yazılı', 'yargı']); // ['yazı', 'yazıcı', 'yazılı'] (yargı 2+ uzaklıkta) ``` Bu fonksiyon ayrıca **locale desteğine** de sahip, yani dil kurallarına göre karşılaştırma yapabiliyor: ```php // Türkçe için özel davranış var_dump(grapheme_stripos("i", "\u{0130}", 0, "tr_TR")); // 0 (eşleşme) var_dump(grapheme_stripos("i", "\u{0130}", 0, "en_US")); // false (eşleşme yok) ``` ## IntlListFormatter: Locale-Aware Liste Formatlaması Yeni `IntlListFormatter` sınıfı, array'leri **locale'e göre formatlanmış** string'lere dönüştürüyor. Yani farklı dillerde farklı ayraçlar ve bağlaçlar kullanabiliyor. ### Temel Kullanım ```php // İngilizce $formatter = new IntlListFormatter('en-US'); echo $formatter->format(['Zürich', 'Berlin', 'Amsterdam']); // Çıktı: "Zürich, Berlin, and Amsterdam" // Hollandaca $formatter = new IntlListFormatter('nl-NL'); echo $formatter->format(['Zürich', 'Berlin', 'Amsterdam']); // Çıktı: "Zürich, Berlin en Amsterdam" // Türkçe $formatter = new IntlListFormatter('tr-TR'); echo $formatter->format(['İstanbul', 'Ankara', 'İzmir']); // Çıktı: "İstanbul, Ankara ve İzmir" // Japonca $formatter = new IntlListFormatter('ja-JP'); echo $formatter->format([1, 2, 3]); // Çıktı: "1、2、3" ``` ### İngiltere vs ABD Farkı (Oxford Comma) ```php // İngilizce (ABD - Oxford comma var) $formatter = new IntlListFormatter('en-US'); echo $formatter->format(['tea', 'coffee', 'juice']); // Çıktı: "tea, coffee, and juice" // İngilizce (İngiltere - Oxford comma yok) $formatter = new IntlListFormatter('en-GB'); echo $formatter->format(['tea', 'coffee', 'juice']); // Çıktı: "tea, coffee and juice" ``` ### Farklı Liste Tipleri ```php // "and" listesi (varsayılan) $formatter = new IntlListFormatter('en', IntlListFormatter::TYPE_AND); echo $formatter->format(['tea', 'coffee', 'juice']); // "tea, coffee, and juice" // "or" listesi $formatter = new IntlListFormatter('en', IntlListFormatter::TYPE_OR); echo $formatter->format(['tea', 'coffee', 'juice']); // "tea, coffee, or juice" // Kısa format $formatter = new IntlListFormatter('en', IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_SHORT); echo $formatter->format(['tea', 'coffee', 'juice']); // "tea, coffee, & juice" // Birim listesi $formatter = new IntlListFormatter('en-US', IntlListFormatter::TYPE_UNITS); echo $formatter->format(['5 ft', '10 in']); // "5 ft, 10 in" ``` ## OPcache Artık Zorunlu PHP 5.5'ten beri OPcache PHP'nin ayrılmaz bir parçası olsa da, teknik olarak hala **opsiyoneldi**. Docker kullanırken OPcache'i unutmak kolaydı ve **OPcache'siz production'a çıkmak** büyük bir performans kaybı anlamına geliyordu. PHP 8.5 ile OPcache **artık PHP'nin zorunlu bir parçası** haline geliyor, tıpkı `ext/date`, `ext/hash` gibi. Ancak bu, OPcache'in **her zaman aktif olması** anlamına gelmiyor; INI ayarlarıyla (`opcache.enable`, `opcache.enable_cli`) kontrol edebilirsin. ### Yapı Değişiklikleri ```php # PHP 8.4 ve öncesi: OPcache'i aktif etmen gerekiyordu zend_extension=opcache # PHP 8.5: OPcache her zaman aktif # --disable-opcache opsiyonu kullanılırsa da artık çalışmayacak ``` ### Gerçek Dünya Avantajları Bu değişiklik birkaç **kritik avantaj** getiriyor: 1. Kod mantığı sadeleşecek ve PHP geliştiricileri artık "OPcache var" ve "OPcache yok" olmak üzere iki farklı kod yolu maintain etmek zorunda kalmayacak. Yani bakımı daha kolay olacak. 2. OPcache ve Zend Engine arasında daha iyi, daha sıkı entegrasyon mümkün olacak. 3. OPcache'in Optimizer'ı her zaman kullanılabilir olacak. ### Docker Örneği ```dockerfile # Öncesi - PHP 8.4 ve altı FROM php:8.4-fpm RUN docker-php-ext-install opcache RUN echo "opcache.enable=1" >> /usr/local/etc/php/conf.d/opcache.ini RUN echo "opcache.memory_consumption=256" >> /usr/local/etc/php/conf.d/opcache.ini # PHP 8.5 - OPcache zaten var FROM php:8.5-fpm RUN echo "opcache.enable=1" >> /usr/local/etc/php/conf.d/opcache.ini RUN echo "opcache.memory_consumption=256" >> /usr/local/etc/php/conf.d/opcache.ini ``` ## Asymmetric Visibility için Static Property Desteği PHP 8.4'te instance property'ler için **asymmetric visibility** tanıtılmıştı (okuma ve yazma için farklı visibility seviyeleri). PHP 8.5 bu özelliği **static property'lere** de getiriyor. ```php class Settings { // Public okunabilir, private yazılabilir public private(set) static string $mode = 'live'; // Protected yazılabilir protected(set) static int $counter = 0; public static function changeMode(string $newMode): void { // Private scope'tan, izin var self::$mode = $newMode; } } // Dışarıdan okuma - İzin var echo Settings::$mode; // Dışarıdan yazma - Hata! Settings::$mode = 'test'; // Error: Cannot modify protected(set) static property ``` Bu özellik dil tutarlılığını artırıyor ve static property'lerin de instance property'ler kadar esnek kontrol mekanizmalarına sahip olmasını sağlıyor. ## Closure'lar Constant Expression'larda PHP 8.5'in en etkileyici özelliklerinden biri de closure'ların **constant expression'larda** kullanılabilmesi. Bu, closure'ları artık şu yerlerde kullanabileceğin anlamına geliyor: - Default parametre değerleri - Constant tanımları - Property default değerleri - Attribute parametreleri ```php // Default parametre değeri olarak closure function validateUser( mixed $user, callable $validator = static function ($user) { return !empty($user->email); } ): bool { return $validator($user); } // Property default değeri olarak closure class UserRepository { private Closure $validator = static function (mixed $value): bool { return !empty($value); }; } // Class constant olarak closure class ValidationRules { public const EMAIL_VALIDATOR = static function($email) { return filter_var($email, FILTER_VALIDATE_EMAIL); }; } // Attribute parametresi olarak closure #[Serialize\Transform( static function (string $severity): string { return strtoupper($severity); } )] public string $severity; ``` **Önemli kısıtlamalar:** 1. Closure'ların `static` olması gerekiyor (ve `$this`'e erişemezler) 2. `use()` ile dış değişken yakalayamazlar 3. Bu yüzden arrow function'lar kullanılamazlar ## Yeni PHP_BUILD_DATE Constant PHP 8.5 `PHP_BUILD_DATE` adında yeni bir constant ekliyor... Bu constant, **PHP binary'sinin ne zaman build edildiğini** gösteriyor. Önceden bu bilgiye sadece `phpinfo()` çıktısını parse ederek erişebiliyordun, ki bu oldukça zahmetli bir işlemdi. ```php echo PHP_BUILD_DATE; // Çıktı: Nov 18 2025 11:19:23 // DateTimeImmutable'a çevirebilirsin $dt = DateTimeImmutable::createFromFormat('M j Y H:i:s', PHP_BUILD_DATE); echo $dt->format('Y-m-d'); // 2025-11-18 // Version tracking için if (strtotime(PHP_BUILD_DATE) > strtotime('2025-11-20')) { // PHP 8.5 öncesi bir RC versiyonu } ``` Bu constant özellikle **debugging**, **production environment tracking** ve **version control** için yararlı. ## Deprecation'lar: PHP 9'a Hazırlık Her yeni PHP versiyonu gibi, PHP 8.5 de bazı eski özellikleri **deprecated** olarak işaretliyor ve **PHP 9'da kaldırılmalarını** planlıyor. ### Dil/Sözdizimi Deprecation'ları **Backtick Operatörü:** Backtick'ler (``) `shell_exec()` için bir alias olarak kullanılıyordu, ancak bu karışıklığa yol açıyordu. ```php // ❌ Deprecated - PHP 8.5'te warning, PHP 9.0'da kaldırılacak $output = `ls -la`; // ✅ Alternatif - shell_exec kullan $output = shell_exec('ls -la'); ``` **Non-Standard Cast İsimleri:** Standardlaştırma adına: ```php // ❌ Deprecated $int = (integer)$value; $float = (double)$value; $bool = (boolean)$value; // ✅ Doğru kullanım $int = (int)$value; $float = (float)$value; $bool = (bool)$value; ``` **`__sleep()` ve `__wakeup()` Sihirli Method'lar:** ```php // ❌ Deprecated class OldWay { public function __sleep() { /* ... */ } public function __wakeup() { /* ... */ } } // ✅ Modern alternatif - __serialize() ve __unserialize() class ModernWay { public function __serialize(): array { /* ... */ } public function __unserialize(array $data): void { /* ... */ } } ``` **Null Array Offset:** ```php // ❌ Deprecated $array[null] = 'value'; // ✅ Alternatif - boş string kullan $array[''] = 'value'; ``` ### Switch Case'lerde Noktalı Virgül ```php // ❌ Deprecated - Her zaman iki nokta üst üste kullanılmalı switch ($value) { case 'foo'; // Deprecation uyarısı! echo 'foo'; break; case 'bar': // Doğru yöntem echo 'bar'; break; } ``` ### INI Directive Deprecation'ları - `report_memleaks`: Artık etkisiz - `register_argc_argv`: HTTP SAPI'lerde güvenlik riski - `disable_classes`: Runtime kararsızlığına yol açıyor ### Reflection API Deprecation'ları - `Reflection*::setAccessible()`: PHP 8.1'den beri işlevsiz (no-op) - `ReflectionParameter::allowsNull()`: `ReflectionType::allowsNull()` kullanılmalı ## cURL İyileştirmeleri: curl_multi_get_handles(), curl_share_init_persistent() **Paralel HTTP istekleri** yapanlar için harika bir ekleme: ```php $multiHandle = curl_multi_init(); // Handles ekle $ch1 = curl_init('https://api1.erho.test/data'); $ch2 = curl_init('https://api2.erho.test/data'); $ch3 = curl_init('https://api3.erho.test/data'); curl_multi_add_handle($multiHandle, $ch1); curl_multi_add_handle($multiHandle, $ch2); curl_multi_add_handle($multiHandle, $ch3); // PHP 8.5 ile - tüm handles'ları al $handles = curl_multi_get_handles($multiHandle); echo count($handles); // 3 // Her handle'ı kontrol et foreach ($handles as $handle) { $info = curl_getinfo($handle); echo "URL: {$info['url']}\n"; } // curl_multi_exec() döngüsü $running = 0; do { $mrc = curl_multi_exec($multiHandle, $running); // İstek bitmiş mi kontrol et $handles = curl_multi_get_handles($multiHandle); // ... } while ($running); // curl_share_init_persistent $sh = curl_share_init_persistent([ CURL_LOCK_DATA_DNS, CURL_LOCK_DATA_CONNECT, ]); $ch = curl_init('https://php.net/'); curl_setopt($ch, CURLOPT_SHARE, $sh); // Bu, önceki bir SAPI isteğinden gelen bağlantıyı yeniden kullanabilir. curl_exec($ch); ``` Önceden bu bilgiyi almak için **manuel olarak bir WeakMap** tutmamız gerekiyordu. ## max_memory_limit: Güvenli Memory Yönetimi **Sistem yöneticileri** için kritik bir güvenlik özelliği: ```ini ; php.ini max_memory_limit = 256M ; Üst sınır memory_limit = 128M ; Başlangıç değeri ``` ```php // Runtime'da ini_get('max_memory_limit'); // "256M" ini_get('memory_limit'); // "128M" // Yükseltmeye çalış - izin veriliyor ini_set('memory_limit', '200M'); // OK // Üst sınırı aşmaya çalış - engelleniyor ini_set('memory_limit', '300M'); // Warning: Failed to set memory_limit to 314572800 bytes. // Setting to max_memory_limit instead (currently: 268435456 bytes) ini_get('memory_limit'); // "256M" - max_memory_limit'e set edildi ``` Bu özellik, özellikle **shared hosting** ortamlarında kaynakların adil kullanımını garanti ediyor. ## Attribute'lar ve Constant'lar PHP 8.5 ile artık **constant'lara attribute ekleyebiliyorsun**. Yeni `Attribute::TARGET_CONSTANT` target'ı ve `ReflectionConstant::getAttributes()` method'u da bu özellikle birlikte geliyor. ```php #[Deprecated] const OLD_API_KEY = 'old_key'; #[Description("Maximum allowed connections")] const MAX_CONNECTIONS = 100; // Reflection ile kontrol et $rc = new ReflectionClass(__CLASS__); $const = $rc->getReflectionConstant('MAX_CONNECTIONS'); $attrs = $const->getAttributes(); foreach ($attrs as $attr) { echo $attr->getName(); // "Description" } ``` ## CLI İyileştirmeleri: php --ini=diff **Geliştirme ve debugging** için kullanışlı bir ekleme: ```bash # Sadece default olmayan INI ayarlarını göster php --ini=diff # Çıktı örneği: memory_limit: 256M (default: 128M) max_execution_time: 60 (default: 30) display_errors: On (default: Off) opcache.memory_consumption: 256 (default: 128) # Çıktıyı JSON olarak al php --ini=diff --json ``` Bu, özellikle **production ortamlarında** hangi ayarların custom olduğunu hızlıca görmek için harika. ## Performans Perspektifi PHP 8.5'in performans açısından 8.4'ten dramatik bir fark yaratması beklenmese de, belirli alanlarda **istikrarlı iyileştirmeler** var: - **SHA-NI desteği** (8.4'te geldi) SHA-256 için **2x-5x performans artışı** sağlıyor - **Pipe operatörü** PHP compiler tarafından optimize edilebildiği için bazı durumlarda performans kazancı sağlayabiliyor - **Lazy Objects** ve **sprintf compile-time optimizations** (8.4) kullanıldığında fark yaratıyor Ancak en önemli performans kazancı hala **uygulama mimarisi** ve **yazdığın koddan** geliyor. PHP versiyonu güncellemek tek başına magic pill değil. ## Önceki Versiyonlarla Karşılaştırma PHP 8.5'i önceki versiyonlarla kıyasladığımızda, her versiyon **belirli alanlara odaklanmış:** | **Versiyon** | **Temel Özellikler** | |--------------|----------------------------------------------------------------------------------------------------| | **PHP 8.0** | JIT compiler, Union types, Named arguments, Match expression, Attributes | | **PHP 8.1** | Enums, Readonly properties, Fibers, First-class callables | | **PHP 8.2** | Readonly classes, Disjunctive Normal Form (DNF) types | | **PHP 8.3** | Typed class constants, Granular DateTime exceptions, json_validate() | | **PHP 8.4** | Property hooks, Asymmetric visibility, HTML5 DOM, #[Deprecated] attribute | | **PHP 8.5** | **Pipe operator, Clone with, NoDiscard, URI extension, Fatal error backtraces, OPcache mandatory** | Her versiyon bir öncekinin temelini sağlamlaştırarak dili **daha iyi, güvenli ve developer-friendly** hale getiriyor. ## Gerçek Dünya Senaryoları Gelin bu özelliklerin gerçek dünyada nasıl kullanılabileceğine bakalım: ### Senaryo 1: E-Commerce Order Processing ```php class OrderProcessor { #[NoDiscard("Başarısız siparişlerin işlenmesi gerekir")] public function processBatch(array $orders): ProcessingResult { return $orders |> array_filter(fn($o) => $o->status === 'pending') |> array_map(fn($o) => $this->validate($o)) |> fn($validated) => $this->processOrders($validated) |> fn($result) => $this->logResults($result); } } $result = $processor->processBatch($orders); if (!$result->isSuccessful()) { foreach ($result->getFailures() as $failure) { $this->handleFailure($failure); } } ``` ### Senaryo 2: Immutable Domain Models ```php final readonly class Invoice { public function __construct( final public string $id, public Money $total, public array $items, public InvoiceStatus $status ) {} public function addItem(InvoiceItem $item): self { return clone $this with [ 'items' => [...$this->items, $item], 'total' => $this->total->add($item->price) ]; } public function markAsPaid(): self { return clone $this with ['status' => InvoiceStatus::Paid]; } } // Kullanım $invoice = new Invoice('INV-001', Money::zero(), []); $invoice = $invoice->addItem($item1); $invoice = $invoice->addItem($item2); $invoice = $invoice->markAsPaid(); // Başka hiçbir değişkene etki yok - fonksiyonel programlama! ``` ### Senaryo 3: Multilingual Application ```php class MultilingualService { public function formatUserList(array $users, string $locale): string { $formatter = new IntlListFormatter($locale); $names = array_map(fn($u) => $u->name, $users); return $formatter->format($names); // İngilizce: "Ali, Veli, and Ayşe" // Türkçe: "Ali, Veli ve Ayşe" } } ``` ## Migration ve Upgrade Stratejisi PHP 8.5'e geçiş yaparken dikkat etmen gereken noktalar: ### 1. Önce Test Ortamında Dene ```bash # 8.5 sürümünü test ortamında kur docker run -it --rm -v $(pwd):/app php:8.5-cli php -v # Uygulamanı test et php -d display_errors=1 run-tests.php ``` ### 2. Deprecation'ları Kontrol Et ```php // Kod tabanında deprecated özellikler kullanılıyor mu? error_reporting(E_ALL); ini_set('display_errors', 1); ``` ### 3. Dependency'leri Güncelle ```bash # Tüm dependency'lerin PHP 8.5 ile uyumlu olduğundan emin ol composer update --with-all-dependencies composer require-dev phpstan/phpstan ``` ### 4. Static Analysis Tool'larını Güncelle ```bash # PHPStan, Psalm gibi araçların son versiyonlarını kullan composer require-dev phpstan/phpstan:^1.10 composer require-dev vimeo/psalm:^5.0 ``` ### 5. CI/CD Pipeline'ını Güncelle ```yaml # .github/workflows/tests.yml - name: Test PHP 8.5 uses: shivammathur/setup-php@v2 with: php-version: '8.5' extensions: curl, intl, opcache ``` ### 6. Aşamalı Geçiş - Production'da aşamalı olarak geç - Önce düşük trafikli sunuculardan başla - Monitoring'i aktif tut - Sorun çıkarsa geri dönebilmeyi planla ## Sonuç PHP 8.5 ile öne çıkan birkaç pratik kazanım var: pipe operatörü okunabilirliği artırıyor, `clone with` immutability'yi kolaylaştırıyor, NoDiscard return değerlerinin yutulmasını engelliyor, yeni URI sınıfları RFC uyumlu URL işleme sağlıyor, fatal error backtraces hata ayıklamayı hızlandırıyor. Pratik açıdan bu özellikler maintainable kod, daha güvenli API'ler ve modern PHP paradigmalarına geçişi destekliyor. Test ortamında deneyin, küçük modüllerde adım adım uygulayın. Production geçişi için yukarıdaki "Aşamalı Geçiş" bölümünü uygulayın: düşük trafikli sunuculardan başlayıp monitoring'i aktif tutarak ilerleyin. ---
Kaynakça - https://wiki.php.net/todo/php85 - https://php.watch/versions/8.5 - https://php.net/releases/8.5 - https://github.com/php/php-src/releases/tag/php-8.5.0
--- ## Siz sordunuz ben araştırdım! Hangi Framework ve Neden? - URL: https://erho.dev/post/021-siz-sordunuz-ben-arastirdim-hangi-framework-ve-neden - Markdown: https://erho.dev/post/021-siz-sordunuz-ben-arastirdim-hangi-framework-ve-neden.md - Tarih: 2025-10-14 - Kategori: laravel - Etiketler: laravel, php, nodejs, adonisjs, nextjs, nestjs Bana çok soran oluyordu, piyasa da bir çok web frameworkü var ve hangi framework'ünü seçmeliyim, hangisine yatırım yapmalıyım, kafam çok karışık diye... Bu kafa karışıklığını bir nebze olsun dağıtmak ve karar süreçlerinize (kendimce) ışık tutmak amacıyla, piyasanın önde gelen web framework'lerini kendi bilgi ve deneyim süzgecimden geçirerek objektif kriterlere göre karşılaştırdım. Geliştirme hızından performansa, topluluk desteğinden iş piyasasındaki karşılığına kadar 10 farklı framwork'ü, 12 farklı kritik metriği baz aldığım bu analizin sonuçlarını ekteki özet grafikte bulabilirsiniz... Framework'lerin resmi dokümantasyonları: | Framework | Dil | Dokümantasyon | |-----------|-----|---------------| | Laravel | PHP | https://laravel.com/docs | | AdonisJS | Node.js | https://docs.adonisjs.com | | NestJS | Node.js | https://docs.nestjs.com | | NextJS | React | https://nextjs.org/docs | | Django | Python | https://docs.djangoproject.com | | Gin | Go | https://gin-gonic.com/tr/docs | | Rails | Ruby | https://rubyonrails.org/docs | | ASP.NET | C# | https://learn.microsoft.com/en-us/aspnet/core | | NuxtJS | Vue | https://nuxt.com/docs | | Angular | TypeScript | https://angular.dev/overview | Bana göre, "en iyi" diye tek bir cevap yok; projenizin hedeflerine, ekibinizin yetkinliklerine ve kariyer hedeflerinize en uygun olan "doğru" cevap var... Doğru web framework'ünü seçmek, sadece bir proje kararı değil, aynı zamanda bir kariyer yatırımıdır. Peki, teknoloji okyanusundaki bu kadar seçenek arasından kendi rotanızı nasıl çizeceksiniz? Şimdi karar sizin; Grafikteki değerlendirmelere katılıyor musunuz? Sizin tecrübelerinize göre, hangi framework hangi senaryoda daha uygun? Siz olsanız hangisini/hangilerini tercih edersiniz ve neden? --- ## #007: Yazılımda %100 Doğru Cevap Yok: Gereksinimlere Uygun Çözüm Var! - URL: https://erho.dev/post/020-yazilimda-100-dogru-cevap-yok-gereksinimlere-uygun-cozum-var - Markdown: https://erho.dev/post/020-yazilimda-100-dogru-cevap-yok-gereksinimlere-uygun-cozum-var.md - Tarih: 2025-09-29 - Kategori: laravel - Etiketler: laravel, php Yazılım dünyasında, genellikle %100 doğru veya yanlış bir cevap yoktur. Bunun yerine, mevcut koşullar ve gereksinimler göz önünde bulundurularak, belirli bir problem için en uygun çözümü bulmaya çalışırız. Her yazılım projesi, kendine özgü zorluklar ve gereksinimlerle gelir. Bu nedenle, bir çözümün uygunluğu, projenin kapsamı, hedefleri ve mevcut kaynakları gibi çeşitli faktörlere bağlıdır. Yazılım geliştiricileri olarak, bu faktörleri dikkatlice değerlendirerek, en iyi çözümü sunmak için çaba gösteririz. Bu süreç, sürekli öğrenme ve adaptasyon gerektirir, çünkü teknoloji ve gereksinimler zamanla değişebilir. Dolayısıyla, yazılım geliştirme sürecinde esneklik ve uyum sağlama yeteneği büyük önem taşır. ### SENARYO 1: CMS'inde 100.000 blog yazısı var. Kullanıcı "Teknoloji" kategorisindeki tüm yazıları istiyor. \*/ #### JSON Yaklaşımı: ```sql SELECT * FROM posts WHERE JSON_CONTAINS(meta, '{"category_id": 5}'); ``` * Süre: ~850ms * Index kullanılamaz, Full table scan #### İlişkisel Yaklaşım: ```sql SELECT posts.* FROM posts INNER JOIN post_category ON posts.id = post_category.post_id WHERE post_category.category_id = 5; ``` * Süre: ~15ms * Index kullanılır, çok hızlı ### SENARYO 2: Blog yazısında ara sıra kullanılan, filtrelemediğin özel alanlar var: ```php |-> author_notes (sadece gösterim için) |-> draft_history (sadece admin görür) |-> custom_css (çok nadir kullanılır) ``` **Bunlar için ayrı tablolar mı açacaksın? 3 tane JOIN mi yapacaksın?** #### Burada JSON Daha Mantıklı: ```php posts: id | title | content | meta (JSON) ``` ```php meta: { "author_notes": "...", "draft_history": [...], "custom_css": "..." } ``` --- Peki siz bu konuda ne düşünüyorsunuz... Deneyimlerinizi yorumlarda paylaşın, hep birlikte öğrenelim! --- ## Ubuntu 24.04 & Windows 11 Dual Boot Performans Sorunumu Nasıl Çözdüm - URL: https://erho.dev/post/019-ubuntu-2404-and-windows-11-dual-boot-performans-sorunumu-nasil-cozdum - Markdown: https://erho.dev/post/019-ubuntu-2404-and-windows-11-dual-boot-performans-sorunumu-nasil-cozdum.md - Tarih: 2025-09-19 - Kategori: linux - Etiketler: linux, performance Son günlerde Ubuntu 24.04 ve Windows 11 dual boot sistemimde karşılaştığım, açılıştan sonra 2-3 dk kadar süren ve uygulamaların geç yanıt vermesine neden olan bir performans sorununu çözmek için yoğun bir mesai harcadım... Başlangıçta Ubuntu'nun resmi forumlarında bir tartışma başlatsam da, sorunumun spesifik doğası nedeniyle standart çözümler yetersiz kaldı. Bunun üzerine konuyu derinlemesine araştırarak sorunun kök nedenini tespit ettim ve birkaç temel konfigürasyon ayarıyla sistemin açılış süresini saniyelere indirmeyi, programlarında anında açılmasını sağlamayı başardım. Bu tür sorunların ne kadar zaman alıcı ve moral bozucu olabildiğini bildiğim için, ulaştığım çözümün tüm adımlarını ve teknik detaylarını, açtığım orijinal forum başlığı altında detaylı bir şekilde paylaştım. Amacım, benzer bir durumla karşılaşabilecek diğer geliştirici ve kullanıcılara pratik bir kaynak sunmak... Çözümün tamamına ve teknik analizime aşağıdaki bağlantıdan erişebilirsiniz: [https://discourse.ubuntu.com/t/ubuntu-24-04-dual-boot-windows-11](https://discourse.ubuntu.com/t/ubuntu-24-04-dual-boot-windows-11-2-3-minute-delay-after-startup-programs-respond-slowly-after-booting/67547/4) DİPNOT: Nadiren de olsa forum 503 hatası fırlatmakta... Eğer linke tıklarken bu hatayla karşılaşırsanız 3-5dk sonra tekrar deneyin düzelmiş olacaktır :) --- Sizin de karşılaştığınız benzer performans optimizasyonu tecrübeleri varsa, yorumlarda belirtebilirsiniz... --- ## Hetzner'da 34GB Volume'ü 68GB'a Güvenli Genişletme Rehberim - URL: https://erho.dev/post/018-hetznerda-34gb-volumeu-68gba-guvenli-genisletme-rehberim - Markdown: https://erho.dev/post/018-hetznerda-34gb-volumeu-68gba-guvenli-genisletme-rehberim.md - Tarih: 2025-09-15 - Kategori: linux - Etiketler: linux Selamlar! Bu yazıda, Hetzner üzerinde sunucularınızın Volume’larını nasıl güvenli bir şekilde alan yükseltmesi yapabileceğinizi anlatacağım... Hetzner, uygun fiyatlı ve güçlü sunucu çözümleri sunan bir hizmet sağlayıcıdır. Başlangıçtan itibaren adım adım ilerleyerek, sunucumda Docker projeleri için ayırdığım alanı nasıl yükselttiğimden bahsedeceğim… Linux komutlarıyla mevcut olan diskinizi hatasız ve güvenli bir şekilde genişletmeye dair tüm süreci adım adım anlatıyorum... Eğer Linux tabanlı farklı bir firmada sunucunuz varsa, aynı şekilde güvenle genişletebilirsiniz! Ben burada Docker hizmeti için anlattım ama siz sadece Docker için değill diğer diskleriniz içinde (docker komutlarını atlayarak) aynısını yapabilirsiniz… Hazırsanız, başlayalım! --- ## Mevcut Durumun Detaylı Analizi ```bash # Mevcut Volume Durumu: # - Volume.....: /dev/sdb # - Mount Point: /mnt/HC_Volume_102406908 # - Boyut......: 34GB (32GB kullanılmış, 818MB boş) # - Filesystem.: ext4 ``` ## Adım 1: Kritik Backup İşlemleri İlk olarak, tüm verilerimizi güvence altına alıyorum: ```bash # Backup dizini oluşturuyorum mkdir -p /root/volume_backup_$(date +%Y%m%d_%H%M) # Hangi dizinlerde docker-compose.yml var kontrol ediyorum find /mnt/HC_Volume_102406908 -name "docker-compose.yml" -type f 2>/dev/null | head -20 # Docker volume listesini alıyorum ve kaydediyorum docker volume ls > /root/volume_backup_20250915_1314/docker_volumes_list.txt # Container listesini kaydediyorum docker ps -a > /root/volume_backup_20250915_1314/container_list.txt # Docker compose projelerini ve konfigürasyonları yedekliyorum cd /mnt/HC_Volume_102406908 tar -czf /root/volume_backup_20250915_1314/docker_configs.tar.gz \ --exclude='*.log' \ --exclude='*/logs/*' \ --exclude='*/cache/*' \ .env* docker-compose* */docker-compose.yml */docker-compose.yaml */.env 2>/dev/null || true ``` ## Adım 2: Docker Container'ları Güvenli Durdurma Container'larınızı tek tek kontrollü bir şekilde durduruyorum: ```bash # Önce container'ların durumunu kaydediyorum docker ps --format "table {{.Names}}\t{{.Status}}" > /root/volume_backup_20250915_1314/containers_status.txt # Container'ları güvenli bir şekilde durduruyorum (graceful shutdown) echo "Container'ları durduruyorum..." docker stop -t 30 $(docker ps -q) # Durduğundan emin oluyorum docker ps # Docker servisini durduruyorum systemctl stop docker.socket systemctl stop docker ``` ## Adım 3: Volume'ü Güvenli Unmount Etme ```bash # Hangi process'lerin volume'ü kullandığını kontrol ediyorum lsof +D /mnt/HC_Volume_102406908 2>/dev/null # Eğer hala kullanan process varsa, bunları listele fuser -v /mnt/HC_Volume_102406908 2>/dev/null # Volume'ü unmount ediyorum umount /mnt/HC_Volume_102406908 # Unmount başarılı mı kontrol ediyorum mount | grep HC_Volume_102406908 # Eğer hala mount edilmişse, force unmount deneyelim # umount -l /mnt/HC_Volume_102406908 # Sadece gerekirse! ``` ## Adım 4: Hetzner Cloud Panel'de Volume Genişletme Şimdi Hetzner Cloud Console'a geçiyorum: 1. [**Hetzner Cloud Console**](https://console.hetzner.cloud/) sayfasına giriş yapıyorum 2. Sol menüden **"Volumes"** sekmesine tıklıyorum 3. **HC\_Volume\_102406908** volume'ümü buluyorum 4. Volume'ün yanındaki **⋮ (üç nokta)** menüsünden **"Resize"** seçeneğini seçiyorum 5. Slider'ı veya input alanını kullanarak boyutu 68 **GB** olarak ayarlıyorum… (Toplam aylık 3$ karşılığında) 6. **"Resize Volume"** butonuna tıklayıp işlemi onaylıyorum **Önemli:** Bu işlem 2-5 dakika sürebilir. Panel'de "Resizing" durumunu göreceksiniz. İşlem tamamlanana kadar bekleyin! ## Adım 5: Filesystem'i Genişletme Hetzner panel işlemi tamamlandıktan sonra: ```bash # Volume'ü tekrar mount ediyorum mount /dev/sdb /mnt/HC_Volume_102406908 # Mount durumunu kontrol ediyorum df -h /mnt/HC_Volume_102406908 # Bu aşamada hala 34GB görebilirsiniz, normal! # Partition tablosunu kontrol ediyorum fdisk -l /dev/sdb # ext4 filesystem'i genişletiyorum (online resize) resize2fs /dev/sdb # İşlem tamamlandıktan sonra yeni boyutu kontrol ediyorum df -h /mnt/HC_Volume_102406908 # Şimdi 68GB görmelisiniz! ``` ## Adım 6: Docker'ı Yeniden Başlatma ```bash # Docker servisini başlatıyorum systemctl start docker systemctl start docker.socket # Docker'ın düzgün başladığını kontrol ediyorum systemctl status docker # Container'ları başlatıyorum docker start $(docker ps -aq) # Tüm container'ların çalıştığını kontrol ediyorum docker ps # Container'ların sağlık durumunu kontrol ediyorum docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" ``` ## Adım 7: Son Kontroller ve Doğrulama ```bash # Disk durumunu son kez kontrol ediyorum df -h /mnt/HC_Volume_102406908 # Docker'ın düzgün çalıştığını doğruluyorum docker system df # Birkaç container'ın logunu kontrol ediyorum docker logs --tail=10 app-portainer docker logs --tail=10 app-uptime-kuma # Mount durumunu kontrol ediyorum mount | grep HC_Volume_102406908 # IO performansını test ediyorum dd if=/dev/zero of=/mnt/HC_Volume_102406908/test.img bs=1M count=100 conv=fdatasync rm /mnt/HC_Volume_102406908/test.img ``` ## Kalıcı Mount Ayarı Kontrolü ```bash # fstab'da mount ayarını kontrol ediyorum grep -E "sdb|HC_Volume" /etc/fstab # Eğer yoksa, otomatik mount için ekliyorum # NOT: UUID'yi kullanmak daha güvenli blkid /dev/sdb # UUID'yi alıp fstab'a ekleyebilirsiniz: # echo 'UUID=YOUR-UUID-HERE /mnt/HC_Volume_102406908 ext4 defaults,nofail,discard 0 0' >> /etc/fstab ``` ## İşlem Sonrası Temizlik ve Öneriler ```bash # Docker'da kullanılmayan verileri temizleme (opsiyonel) docker system prune -a --volumes --force # Backup dosyalarını saklama süresi (7 gün sonra silebilirsiniz) echo "Backup files saved at: /root/volume_backup_20250915_1314" # Sistem durumunu genel kontrol htop # CPU ve RAM kullanımı normal mi? iotop # Disk I/O normal mi? ``` ## Sorun Giderme Eğer herhangi bir aşamada sorun yaşarsanız: 1. **Unmount edilemiyorsa:** ```bash systemctl stop docker docker.socket fuser -km /mnt/HC_Volume_102406908 umount -l /mnt/HC_Volume_102406908 ``` 2. **resize2fs hata verirse:** ```bash e2fsck -f /dev/sdb # Önce filesystem check resize2fs /dev/sdb # Sonra tekrar dene ``` 3. **Container'lar başlamazsa:** ```bash docker logs docker-compose up -d # İlgili dizinde ``` ## Sonuç İşte bu kadar! Volume'mümüzü başarıyla 34GB'dan 68GB'a yükselttik… Artık: * Tüm verilerimiz güvende * Container'larımız çalışıyor * Yeni alan kullanıma hazır * Backup'larımız `/root/volume_backup_*` dizininde **Pro İpucu:** Gelecekte alan sorunları yaşamamak için Docker'ı düzenli temizleyin: ```bash # Haftalık cron job ekleyebilirsiniz echo "0 3 * * 0 docker system prune -af --volumes" >> /etc/crontab ``` ### Bu Komutun Açıklaması: #### Crontab Formatı: ```php 0 3 * * 0 docker system prune -af --volumes ┬ ┬ ┬ ┬ ┬ │ │ │ │ │ │ │ │ │ └─── Haftanın günü (0 = Pazar) VEYA sun,mon,tue,wed,thu,fri,sat │ │ │ └───── Ay (1-12) │ │ └─────── Ayın günü (1-31) │ └───────── Saat (0-23) └─────────── Dakika (0-59) ``` **Bu ayar:** Her Pazar günü saat 03:00'te çalışır #### Docker Komutunun Anlamı: * `docker system prune`: Docker'da kullanılmayan kaynakları temizler * `-a`: Kullanılmayan TÜM image'ları siler (sadece dangling olanları değil) * `-f`: Force - onay istemeden direkt siler * `--volumes`: Kullanılmayan volume'leri de siler ### ÖNEMLİ UYARI: Bu komut **tehlikeli olabilir** çünkü: 1. **Tüm durmuş container'ları siler** 2. **Hiçbir container tarafından kullanılmayan TÜM image'ları siler** 3. **Kullanılmayan volume'leri siler** (veri kaybına yol açabilir!) 4. **Onay istemeden yapar** (-f flag'i yüzünden) ### Daha Güvenli Alternatifler: ```bash # Volume'leri SİLMEDEN temizlik (daha güvenli) echo "0 3 * * 0 docker system prune -f" >> /etc/crontab # Sadece 1 haftadan eski olanları sil echo "0 3 * * 0 docker system prune -f --filter 'until=168h'" >> /etc/crontab # Sadece dangling image'ları temizle (en güvenli) echo "0 3 * * 0 docker image prune -f" >> /etc/crontab # 1. tercihim # Eklenip eklenmediğini kontrol edin cat /etc/crontab ``` **Tavsiyem:** `--volumes` flag'ini kullanmayın, çünkü önemli verilerinizi kaybedebilirsiniz! **BONUS:** htop veya btop gibi bir araç ile mevcut tüm durumu kontrol etmeniz önemle rica olunur! ![Volume Panel](/images/posts/018-hetznerda-34gb-volumeu-68gba-guvenli-genisletme-rehberim/img/volume-panel.webp) Herhangi bir sorunla karşılaşırsanız, backup'larınız güvenle geri yükleyebilirsiniz... --- ## #006: Laravel Projelerinde Gizli Performans Katili N+1 Sorununa Dikkat! - URL: https://erho.dev/post/017-laravel-projelerinde-gizli-performans-katili-n1-sorununa-dikkat - Markdown: https://erho.dev/post/017-laravel-projelerinde-gizli-performans-katili-n1-sorununa-dikkat.md - Tarih: 2025-08-12 - Kategori: laravel - Etiketler: laravel, php, architecture Geçen hafta bir code review yaparken, benden bi önceki geliştiricilerde birinin yazdığı kodda klasik bir N+1 sorunu tespit ettim. Sayfa yükleme süresi 3 saniyeden 200ms'ye düştü sadece tek bir satır değişiklikle. N+1 sorunu nedir? Basitçe tanımlamam gerekirse; bir koleksiyon için 1 sorgu + her item için N ek sorgu demek. Laravel'de Eloquent ilişkilerini yanlış kullandığınızda karşınıza çıkan en yaygın performans sorunlarından biri... Neyse ki Laravel, bu sorunu "Eager Loading" (Ön Yükleme) ile şık bir şekilde çözüyor. with() metodunu kullanarak, ihtiyaç duyacağınız tüm ilişkisel verileri sadece iki sorgu ile önceden yükleyebilirsiniz. Bu basit dokunuş, uygulamanızın performansında devrim yaratabilir. Kötü Kullanım: ```php // Controller'da $posts = Post::all(); // 1 sorgu // View'da @foreach($posts as $post) {{ $post->user->name }} // Her post için +1 sorgu (N sorgu) @endforeach ``` 100 post varsa = 101 veritabanı sorgusu! Temiz, Performanslı Kod: ```php // Eager Loading ile $posts = Post::with('user')->get(); // Sadece 2 sorgu! // Birden fazla ilişki için $posts = Post::with(['user', 'comments', 'tags'])->get(); // İç içe ilişkiler için $posts = Post::with('comments.user')->get(); ``` Pro İpucu: [Laravel Debugbar](https://github.com/barryvdh/laravel-debugbar) veya [Telescope](https://laravel.com/docs/telescope) vb kullanarak N+1 sorunlarını kolayca tespit edebilirsiniz. Ben projelerimde ->preventLazyLoading() metodunu development ortamında aktif tutuyorum: ```php // AppServiceProvider.php public function boot() { Model::preventLazyLoading(! app()->isProduction()); } ``` Bu sayede lazy loading kullanıldığında Laravel exception fırlatıyor ve sorunlu yerleri anında görüyorum. Bu code review işlemi sonucunda, projemizde sadece eager loading optimizasyonları ile: - Veritabanı sorgu sayısı: 847'den 23'e - Sayfa yükleme süresi: 4.2 saniyeden 0.3 saniyeye - Sunucu RAM kullanımı: ort. %40 azaldı Peki siz projelerinizde N+1 sorunlarını nasıl tespit ediyorsunuz? Hangi araçları kullanıyorsunuz? Deneyimlerinizi yorumlarda paylaşın, hep birlikte öğrenelim! --- ## #005: Laravel’de Koşullu Sorgular İçin `when()` Metodunu Kullanın! - URL: https://erho.dev/post/016-laravelde-kosullu-sorgular-icin-when-metodunu-kullanin - Markdown: https://erho.dev/post/016-laravelde-kosullu-sorgular-icin-when-metodunu-kullanin.md - Tarih: 2025-07-10 - Kategori: laravel - Etiketler: laravel, php, architecture Eloquent sorgularında birden çok `if` bloğu ile şartları kontrol etmek, kodunuzu hem uzatır hem de bakım maliyetini artırır. `when()` metodu sayesinde sorgularınızı zincirleyerek hem daha temiz hem de esnek bir yapı elde edebilirsiniz... İşte örnek: ** Kötü Kullanım:** ```php // Path: app/Http/Controllers/Api/V1/PostController.php public function index(Request $request) { $query = Post::query(); if ($request->has('status')) { $query->where('status', $request->status); } if ($request->has('category_id')) { $query->where('category_id', $request->category_id); } if ($request->has('published_after')) { $query->where('published_at', '>', $request->published_after); } return $query->get(); } ``` ** Temiz, Okunabilir ve Sürdürülebilir Kod:** ```php // Path: app/Http/Controllers/Api/V1/PostController.php public function index(Request $request) { return Post::query() ->when($request->status, fn($q, $status) => $q->where('status', $status)) ->when($request->category_id, fn($q, $category) => $q->where('category_id', $category)) ->when($request->published_after, fn($q, $date) => $q->where('published_at', '>', $date)) ->get(); } ``` Bu sayede; * Sorgu zinciri kesintisiz ve akıcı kalır, * Koşullar parametre olarak geçildiğinde otomatik uygulanır, * Kod tekrarı ve fazladan `if` bloklarından kurtulursunuz. when() hakkında daha fazla bilgi için resmi dokümana bkz: https://laravel.com/docs/queries#conditional-clauses Peki siz projelerinizde when() metodunu kullanıyor musunuz? Veya kullanıyorsanız da nasıl kullanıyorsunuz? Yorumlarda deneyimlerinizi paylaşın, hep birlikte öğrenelim! --- ## #004: Laravel’da Query Scope Kullanımıyla Tekrardan Kurtulun ve Sürdürülebilir Kod Yazın! - URL: https://erho.dev/post/015-laravelda-query-scope-kullanimiyla-tekrardan-kurtulun-ve-surdurulebilir-kod-yazin - Markdown: https://erho.dev/post/015-laravelda-query-scope-kullanimiyla-tekrardan-kurtulun-ve-surdurulebilir-kod-yazin.md - Tarih: 2025-05-19 - Kategori: laravel - Etiketler: laravel, php, architecture Küçük görünen tekrarlar, büyüdükçe yönetilemez hale gelir. Sizin kodunuzda tekrar eden sorgular var mı, yoksa her şey kontrolünüz altında mı? Birçok geliştirici, Eloquent sorgularında benzer filtreleri defalarca yazmaktan bıkmıştır. Tekrarlayan koşullar, hem kodun okunabilirliğini hem de bakımını zorlaştırır. Üstelik ileride filtrelerde bir değişiklik olursa, projenin farklı yerlerinde aynı güncellemeleri tekrar tekrar yapmak zorunda kalırsınız. Bu kısır döngüden çıkmanın en etkili yolu **Query Scope**’ları kullanmaktır! Scope’lar sadece kod tekrarını önlemekle kalmaz; kodunuza **anlam katar, standartlaştırır** ve takım içi iş birliğini kolaylaştırır. Büyük ölçekli projelerde, ortak kullanılan filtrelerin tek noktadan yönetilmesi, hata oranını ciddi şekilde azaltır. Sadece sabit koşullar için değil, parametreli scope’lar ile çok daha esnek sorgular üretebilirsiniz. **Örnek: Yayınlanma tarihiyle filtreleme** ```php // Path: app/Models/Post.php public function scopePublishedAfter($query, $date) { return $query->where('published_at', '>', $date); } // Kullanımı: $recentPosts = Post::active()->publishedAfter(now()->subDays(7))->get(); ``` Scope’lar birbiriyle zincirlenebilir. Böylece birbirinden bağımsız, anlamlı filtreleri kolayca kombine edebilirsiniz. ```php // Path: app/Models/Post.php public function scopePopular($query) { return $query->where('views', '>', 1000); } // Kullanımı: $posts = Post::active()->popular()->publishedAfter('2025-01-01')->get(); ``` Bazı filtrelerin her zaman ve her yerde uygulanmasını istiyorsanız, **global scope** kullanarak modelinize otomatik davranışlar kazandırabilirsiniz. ```php // Path: app/Models/Post.php protected static function booted() { static::addGlobalScope('notDeleted', function ($query) { $query->whereNull('deleted_at'); }); } ``` Artık tüm Post sorgularınızda `deleted_at` otomatik olarak filtrelenir. Tekrar eden string değerler yerine enum kullanarak kodunuzu hem okunur hem hatasız hâle getirin. ```php // Path: app/Enums/StatusEnum.php class StatusEnum extends BaseEnum { public const DEFAULT = self::ACTIVE; public const ACTIVE = 'active'; public const PASSIVE = 'passive'; // ... } // Path: app/Models/Post.php public function scopeActive($query) { return $query->where('status', StatusEnum::ACTIVE)->whereNull('deleted_at'); } // Path: app/Http/Controllers/Api/PostController.php $activePosts = Post::active(); ``` Veya bu şekilde bir kullanım ile: ```php // Path: app/Enums/StatusEnum.php enum StatusEnum: string { case ACTIVE = 'active'; case PASSIVE = 'passive'; } // Path: app/Models/Post.php public function scopeStatus($query, StatusEnum $status) { return $query->where('status', $status->value); } // Kullanımı: $activePosts = Post::status(StatusEnum::ACTIVE)->get(); ``` ** Tekrarlı, okunmaz ve bakım maliyetli kod:** ```php // Path: app/Http/Controllers/Api/PostController.php $posts = Post::where('status', 'active') ->where('views', '>', 1000) ->whereNull('deleted_at') ->where('published_at', '>', now()->subMonth()) ->get(); ``` ** Temiz, okunabilir, sürdürülebilir kod:** ```php // Path: app/Http/Controllers/Api/PostController.php $posts = Post::active()->popular()->publishedAfter(now()->subMonth())->get(); ``` Siz projelerinizde scope’ları nasıl kullanıyorsunuz? Parametreli scope, global scope ya da enum ile birlikte scope kullanımında farklı yaklaşımlarınız var mı? Kodunuzun sürdürülebilirliğini artırmak için başka hangi yöntemleri tercih ediyorsunuz? Fikirlerinizi ve deneyimlerinizi paylaşarak topluluğa katkı sağlayın! --- ## #003: Laravel Controller’larınızda Sadelik Olsun! - URL: https://erho.dev/post/014-laravel-controllerlarinizda-sadelik-olsun - Markdown: https://erho.dev/post/014-laravel-controllerlarinizda-sadelik-olsun.md - Tarih: 2025-05-16 - Kategori: laravel - Etiketler: laravel, architecture Tüm validasyon işlemlerini controller içinde yapmak, kodunuzu hantallaştırır. Laravel’in Request Validation özelliğiyle controller’ı hafifletin, kodunuzu modernleştirin. ** Kötü Kullanım:** ```php // Path: app/Http/Controllers/Api/UserController.php public function store(Request $request) { if (!$request->has('email') || !filter_var($request->email, FILTER_VALIDATE_EMAIL)) { return response()->json(['error' => 'Geçersiz e-posta!']); } // Diğer işlemler... } ``` ** İyi ve Temiz Kullanım:** ```php // Path: app/Http/Requests/StoreUserRequest.php public function rules() { return [ 'email' => 'required|email', ]; } // Path: app/Http/Controllers/Api/V1/UserController.php public function store(StoreUserRequest $request) { // Validasyon otomatik yapılır, controller temiz kalır! } ``` Request sınıfları ile controller’larınıza nefes aldırın; kodunuzun kalitesini hissedin! Peki, sizin projelerinizde form validasyonu için nasıl bir yaklaşım benimsiyorsunuz? Sizin için en kritik “temiz kod” standardı nedir? Yorumlarda fikirlerinizi ve deneyimlerinizi paylaşmayı unutmayın! --- ## Laravel ile Modüler ve Ölçeklenebilir RESTful API Geliştirme Yolculuğum - URL: https://erho.dev/post/013-laravel-ile-moduler-ve-olceklenebilir-restful-api-gelistirme-yolculugum - Markdown: https://erho.dev/post/013-laravel-ile-moduler-ve-olceklenebilir-restful-api-gelistirme-yolculugum.md - Tarih: 2025-04-14 - Kategori: laravel - Etiketler: laravel, php, api Selamlar, ben Erhan. Bir süredir modüler ve ölçeklenebilir bir RESTful API geliştirme üzerinde çalışıyorum. Bu yazıda Laravel ile nasıl bir API inşa ettiğimi, hangi araçları seçtiğimi ve neden bu kararları aldığımı paylaşıyorum. Teknik tarafa girerken kendi karşılaştığım sorunları ve çözümleri de katacağım. --- ## Projenin Temelleri: Neden Laravel ve Neden Modüler Mimari? Öncelikle, projemde **Laravel**’i seçmemin en büyük sebeplerinden biri; Laravel, modern PHP dünyasının en popüler framework’lerinden biri olarak, hızlı geliştirme süreci, zengin ekosistemi ve güçlü topluluğu sayesinde bana büyük avantajlar sağladı. Projemde, özellikle B2B konteyner lojistik pazaryeri gibi dinamik ve çok yönlü bir sistemi hayata geçirmek istiyorum. Bunun için; * **Kimlik Doğrulama:** Laravel Passport ile OAuth2 tabanlı güvenli bir kimlik doğrulama sistemi kurmayı planladım. * **Ödeme İşlemleri:** Hem Iyzico hem de Stripe entegrasyonları ile abonelik (aylık/yıllık) ve tek seferlik ödeme seçeneklerini sunmayı hedefliyorum. * **Modüler Yapı:** Her özelliği (örneğin, kullanıcı yönetimi, ilanlar, ödeme, bildirimler, raporlama) ayrı bir modül olarak geliştirerek, ileride mikroservislere geçişi kolaylaştırmayı amaçlıyorum. * **Asenkron İşlemler:** Bildirimler, ödeme webhook’ları ve raporlama gibi zaman alıcı işlemleri, Laravel’in kuyruk sistemiyle (Queue) asenkron hale getirmeyi planlıyorum. Bu yapı sayesinde projem, başlangıçta hızlıca bir Minimum Viable Product (MVP) olarak hayata geçerken, zamanla ihtiyaç duyulduğunda ölçeklendirilebilecek, bakım ve güncellemeler de zahmetsizce yapılabilecek bir hal alacak. ## Proje Kurulumu: Temelleri Sağlam Atmak Proje başlangıcında ilk işim, Laravel projesini oluşturmak oldu. Modern geliştirme ortamına uygun olarak, Docker kullanarak geliştirme ortamımı oluşturmak, ekip içindeki herkesin aynı koşullarda çalışmasını sağladı. .env dosyasında veritabanı, kuyruk, ödeme servisleri ve diğer API anahtarlarını yapılandırarak projenin temellerini attım. Ayrıca, **modüler yapıyı** benimseyebilmek için [nwidart/laravel-modules](https://github.com/nWidart/laravel-modules) paketini projeme ekledim. Bu sayede, her modülü bağımsız olarak geliştirebiliyor, daha sonra bunları kolaylıkla ayrı mikroservislere dönüştürme şansı elde edebiliyordum. ## Güvenli Kimlik Doğrulama ve Yetkilendirme Güvenlik, API geliştirmede ilk sırada gelmeli. Benim projemde, kullanıcı türleri üç ana kategoriye ayrılıyor: süper admin, admin ve normal kullanıcı (alıcı ve satıcı). Laravel Passport kullanarak OAuth2 tabanlı, token temelli bir kimlik doğrulama sistemi inşa ettim. Böylece her API isteğinde, geçerli bir erişim token’ı kontrol ediliyor ve kullanıcıların rol bazlı yetkilendirmesi, Laravel’in Gate/Policy mekanizması veya [spatie/laravel-permission](https://github.com/spatie/laravel-permission) gibi paketlerle sağlanıyor. Bu yapı, kullanıcı kayıtları, giriş işlemleri ve her türlü güvenlik gereksinimi için esnek ve güçlü bir altyapı sunuyor. Özellikle admin onayı gerektiren işlemlerde (örneğin yeni ilanların veya kullanıcıların aktif hale getirilmesi) sistemin doğru yetkilendirme kontrolünü yapabilmesi, API’nin sağlamlığını garantiliyor. ## Ödeme İşlemleri: Iyzico ve Stripe Entegrasyonu Ödeme süreçlerini iki farklı sistem üzerinden yönetmek benim için önemli bir gereksinimdi. * [**Stripe**](https://stripe.com)**:** Global pazara hitap eden kullanıcılar için, [Laravel Cashier](https://laravel.com/docs/billing) kullanarak Stripe ile abonelik yönetimi ve tek seferlik ödemeler kolaylıkla entegre edildi. Cashier sayesinde, kullanıcı aboneliklerini oluşturmak, yenilemek ve iptal etmek çok daha sorunsuz hale geldi. * [**Iyzico**](https://iyzico.com)**:** Yerel pazara yönelik işlemler için ise [Iyzico’nun PHP SDK](https://github.com/iyzico/iyzipay-php)’sını kullanarak, ödeme form verilerinin toplanması, 3D Secure sürecinin yönetilmesi ve webhook’lar aracılığıyla ödeme sonuçlarının takip edilmesi sağlandı. Her iki ödeme sisteminde de, ödeme işlemleri güvenli bir şekilde yapılırken, webhooks sayesinde ödeme durumları API üzerinden güncelleniyor. Böylece, hem kullanıcı deneyimi kesintiye uğramıyor hem de sistem, anlık olarak ödeme sonuçlarını işleyebiliyor. ## Modüler API Yapısı: Servis ve Repository Katmanları Projemde, iş mantığını controller’lardan ayırarak **servis katmanları** oluşturmayı benimsedim. Bu sayede; * **Controller’lar** yalnızca gelen HTTP isteklerini alıp, servis katmanına yönlendiriyor. * **Service Katmanı**, tüm iş kurallarını, doğrulamaları ve mantığı içeriyor. * **Repository Katmanı**, veritabanı erişim işlemlerini merkezi bir noktada topluyor. Bu katmanlı mimari sayesinde, kod tekrarını en aza indiriyor, gelecekte yapılacak değişiklikleri kolaylaştırıyor ve test edilebilirliği artırıyorum. Her modül, örneğin “İlanlar” veya “Ödemeler” gibi, kendi içinde bu yapıya uygun olarak geliştiriliyor. ## Asenkron İşlemler ve Kuyruklama Kullanıcı deneyimini iyileştirmek için zaman alan işlemleri arkaplana almak şart. Laravel’in kuyruk sistemi ile; * **E-posta Bildirimleri:** Yeni kullanıcı kaydı, şifre sıfırlama gibi işlemlerde e-postalar asenkron olarak gönderiliyor. * **Ödeme Webhook’ları:** Hem Stripe hem de Iyzico’dan gelen ödeme bildirimleri kuyruklara alınarak, sistemin yoğunluk anlarında bile sorunsuz işleniyor. * **Raporlama İşlemleri:** Yoğun veri analizlerini ve rapor oluşturma süreçlerini arkaplanda çalıştırarak, API’nin anında cevap vermesini sağlıyorum. [Redis](https://redis.io)’i kuyruk bağlantısı olarak kullanıyor, [Laravel Horizon](http://laravel.com/docs/horizon) ile de kuyruk işlerini ve performansını izliyorum. Bu sayede, kullanıcılar hiçbir zaman yavaşlamış bir API ile karşılaşmıyor. ## Bildirim ve Raporlama Modülü: Kullanıcıyla Anında Etkileşim API’de, kullanıcıları anında bilgilendirmek benim için çok önemliydi. Laravel’in Notification sistemi ile e-posta, SMS (v2’de eklenecek) ve push bildirimlerini entegre ettim. Böylece, kullanıcılarım; kayıt olduklarında, onay bekleyen işlemler olduğunda veya kampanyalardan haberdar edilmek istediklerinde anında bilgilendiriliyor. Ayrıca, adminler için kapsamlı raporlar sunan bir modül geliştirerek, uygulamanın kullanım istatistiklerini, gelir raporlarını ve kullanıcı aktivitelerini gerçek zamanlı takip edebiliyorum. Bu sayede, stratejik kararlar alırken elimde güncel veriler oluyor. ## Dış Servis Entegrasyonları: Google ile Güçlenmek Güvenlikten performansa, pek çok alanda Google servislerini entegre ettim: * [reCAPTCHA](https://developers.google.com/recaptcha/intro)**:** Bot ve spam koruması için, kullanıcı formlarına Google reCAPTCHA ekledim. * [**Google Maps**](https://developers.google.com/maps)**:** İlanların konum bilgilerini göstermek ve adres doğrulama işlemleri için Maps API’den faydalandım. * [**Google Analytics**](https://developers.google.com/analytics) **ve** [**Search Console**](https://search.google.com/search-console/about)**:** API’ye bağlı web uygulamasının performansını ve SEO durumunu izlemek için bu araçları kullanıyorum. Bu entegrasyonlar, API’nin hem güvenliğini hem de kullanıcı deneyimini büyük ölçüde artırıyor. ## Test, Güvenlik ve Performans Optimizasyonu Geliştirdiğim API’nin güvenilir ve performanslı olmasını sağlamak için; * **PHPUnit/Pest ile Birim ve Entegrasyon Testleri:** Her modülün ve iş akışının doğru çalıştığından emin olmak için kapsamlı testler yazdım. * **API Rate Limiting:** Kötü niyetli isteklerin ve aşırı kullanımın önüne geçmek için istek hız sınırlandırması uyguladım. * **HTTPS ve Diğer Güvenlik Önlemleri:** Tüm trafiği HTTPS üzerinden yürüttüm, CORS ayarlarını ve girdi doğrulamalarını sıkılaştırdım. Ayrıca, performansı artırmak adına caching, veritabanı optimizasyonları ve sayfalama gibi yöntemleri devreye aldım. Her şeyin sorunsuz çalıştığından emin olmak için sürekli izleme ve profil çıkarma araçlarını kullanıyorum. ## API Dökümantasyonu: Scramble & Postman Projemde, API’nin hem anlaşılır hem de güncel olmasını sağlamak amacıyla iki farklı dokümantasyon aracından yararlandım: [**Scramble**](https://scramble.dedoc.co) **API Dökümantasyonu** ve [**Postman**](https://postman.com) **API Koleksiyonu**. Aşağıda bu iki aracın sunduğu imkanları ve entegrasyon sürecindeki katkılarını detaylandırıyorum: ### Scramble API Dökümantasyonu [Scramble](https://scramble.dedoc.co), [Swagger](https://swagger.io/docs) benzeri bir yapıya sahip olup, API’nizin görsel ve interaktif olarak sunulmasını sağlıyor. Bu aracı tercih etmemin nedenleri şu şekilde: * **Interaktif Arayüz:** Tüm endpoint’ler, HTTP metotları, parametreler ve örnek dönüşler, kullanıcı dostu bir arayüzde detaylandırılıyor. Bu sayede, API’nizi kullanan geliştiriciler isteklerin nasıl yapılandırıldığını ve hangi verilerin beklendiğini anında görebiliyor. * **Canlı Test İmkanı:** Dokümantasyon içerisinde yer alan “try-it-out / Send API Request” özelliği ile doğrudan API çağrıları yapabiliyorsunuz. Bu özellik, entegrasyon sürecinde anlık test ve hata ayıklama imkanı sağlayarak, API’nin işleyişine dair pratik bir deneyim sunuyor. * **Otomatik Güncelleme:** API’ye eklenen yeni endpoint veya mevcut endpoint’lerde yapılan değişiklikler, dokümantasyona da anında yansıtılıyor. Böylece, her zaman güncel ve doğru bilgi sunarak olası uyumsuzlukların önüne geçiyorum. Bu yapıyla, API’nin mimari yapısını, veri modellerini ve hata yönetimini net bir şekilde ortaya koyuyor, interaktif örneklerle geliştiricilerin sürece hızla adapte olmasını sağlıyorum. Swagger biraz daha uğraştırıcı olduğu için (bana göre) genelde bunu tercih ediyorum. ### Postman API Dokümantasyonu ve Koleksiyonu Ayrıca, API’nin kullanımını daha da kolaylaştırmak adına [Postman](https://postman.com) üzerinden hazırlanmış kapsamlı bir dokümantasyon ve koleksiyon da sunuyorum. Bu koleksiyonda: * **Önceden Tanımlanmış İstekler:** API’deki tüm endpoint’ler için yapılandırılmış örnek istekler mevcut. Bu örnekler, geliştiricilerin hızlıca test yapabilmesini ve entegrasyona başlamasını destekliyor. * **Çevresel Yapılandırma:** API anahtarları, tokenlar gibi gizli bilgilerin yönetimi için Postman environment (ortam) değişkenleri kullanılarak, test sürecinin hem güvenli hem de kolaylaştırılmış hale gelmesi sağlanıyor. * **Adım Adım Dokümantasyon:** Her API endpoint’i için detaylı açıklamalar, istek ve cevap örnekleri ile kullanım senaryoları dokümantasyona eklenmiş durumda. Bu sayede, API’nin nasıl çalıştığı ve hangi durumlarda nasıl davranacağı, geliştiricilere net olarak aktarılıyor. Postman koleksiyonu, API entegrasyon sürecinde karşılaşılabilecek sorunları minimuma indirerek, hata ayıklama ve test aşamalarını oldukça kolaylaştırıyor. ### Neden Bu Dokümantasyon Yöntemleri? API dokümantasyonu, projenizin sürdürülebilirliği ve geliştirici deneyimi açısından kritik bir öneme sahip. Scramble ile sağlanan interaktif ve otomatik güncellenen belge yapısı, API’nizin tüm gelişmelerini anında yansıtırken; Postman koleksiyonu, canlı test imkanları ve önceden tanımlanmış istek örnekleriyle entegrasyon sürecinde zaman kazandırıyor. Bu iki yöntem, API’nin kullanımının kolay, anlaşılır ve güncel kalmasını sağlarken, aynı zamanda geliştiricilerin hata ayıklama ve entegrasyon süreçlerini de hızlandırıyor. ## CI/CD ve Dağıtım: Koddan Canlıya Sorunsuz Geçiş Projemin geliştirme sürecinde, **GitHub Actions** ile **CI/CD** boru hattı oluşturmayı ihmal etmedim. Her push’ta otomatik testler çalışıyor, başarılı sonuçlar alınca [Docker](https://docker.com) tabanlı bir ortamda derlenip, staging ve ardından prod ortamına dağıtım gerçekleştiriliyor. Böylece kodun her an güncel, test edilmiş ve canlıya geçmeye hazır olduğundan emin oluyorum. ## Bonus: Laravel Octane Laravel Octane’i (FrankenPHP üzerinde) klasik PHP-FPM yerine kullandığınızda yanıt süreleri ciddi şekilde düşüyor. Yüksek trafikli projelerde sunucu başlatma maliyetinin sıfıra inmesi büyük fark yaratıyor. Ben Octane’i hem development tarafında hem de yüksek trafik beklediğim endpoint’lerde tercih ediyorum. ## Özet Bu projede Laravel’le modüler bir RESTful API kurguladım: Iyzico ve Stripe ödeme sistemleri, Passport ile OAuth2 kimlik doğrulama, Redis + Horizon ile asenkron işlemler, Scramble ve Postman ile dokümantasyon. Servis ve repository katmanlarını ayırarak controller’ları ince tuttum. Benzer bir projeye başlayacaksanız: önce modülleri net ayırın, ödeme webhook’larını mutlaka kuyruğa alın ve dokümantasyonu kod ile birlikte güncel tutmayı bir disiplin haline getirin. > Not: Bu yazı zamanla görsellerle pekiştirilebilir veya kapsam değişebilir. --- ## Zsh ile Linux (Ubuntu) Terminalinin Görünümünü ve İşlevselliğini Artırma - URL: https://erho.dev/post/012-zsh-ile-linux-ubuntu-terminalinin-gorunumunu-ve-islevselligini-artirma - Markdown: https://erho.dev/post/012-zsh-ile-linux-ubuntu-terminalinin-gorunumunu-ve-islevselligini-artirma.md - Tarih: 2025-04-13 - Kategori: linux - Etiketler: linux Selamlar! Yazılım dünyasında her gün kullandığım Linux (Ubuntu) terminalimi hem daha şık hem de daha işlevsel hâle getirmek için ufak bir maceraya atıldım. İşin aslı, kullandığım terminalin sade görünüşü beni bir süre sonra sıkmaya başladı. Üstelik tekrar tekrar aynı komutları yazmaktan, uzun klasör isimlerini yanlış yazmaktan da bıkkınlık geldi. Derken, **Zsh (Z Shell)** ve üzerine inşa edilen popüler araçlar radarımda belirdi. Terminalime farklı bir hava ve ekstra kolaylıklar kazandıran bu araçları ve deneyimlerimi gelin birlikte keşfedelim. --- ## Zsh Nedir, Neden Tercih Ettim? Zsh, Bash’e alternatif bir kabuk (shell) olarak tanımlanabilir. Pek çok kişinin “Bir shell ne kadar farklı olabilir ki?” dediğini duyar gibiyim. Ama Zsh gerçekten çok daha esnek, özelleştirilebilir ve geliştirici dostu bir deneyim sunuyor. Benim için bir numaralı sebep, eklenti sistemi ve temalar. Özellikle kendi yazdığım komutları yeniden kullanmak istediğimde, Zsh’in otomatik tamamlama ve tahmin özelliği beni kendine çekti diyebilirim. **Kısaca kurulum:** 1. `sudo apt update && sudo apt install zsh -y` komutuyla Zsh’i indirip kurdum. 2. `chsh -s $(which zsh)` diyerek varsayılan shell olarak ayarladım. 3. Çıkış yapıp tekrar girdiğimde artık Zsh karşıma gelmeye başlamıştı. ## Oh My Zsh: Zsh ile Tanışmanın En Keyifli Yolu Zsh kurduktan sonra, onu bir çerçeve (framework) yardımıyla zenginleştirmek istedim. Burada karşıma **“Oh My Zsh”** çıktı. Oh My Zsh; * Yüzlerce **tema**, * Bir sürü **eklenti**, * Kolay **güncelleme** yapısı sunan bir çerçeve. **Nasıl kurdum?** ```bash sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" ``` Bu komutla Oh My Zsh tek seferde sistemime yüklendi. Kurulum bitince `~/.zshrc` dosyam otomatik olarak Oh My Zsh ile entegre hale geldi. ## Powerlevel10k: Hızlı ve Gösterişli Tema Kabuğumu dönüştürme yolunda, sıradaki durağım **Powerlevel10k** oldu. Bu tema hem hızlı çalışıyor hem de görsel olarak oldukça etkileyici bir arayüz sunuyor. 1. GitHub’dan Powerlevel10k deposunu indirip, Oh My Zsh’in tema klasörüne klonladım: ```bash git clone --depth=1 https://github.com/romkatv/powerlevel10k.git \ $ZSH_CUSTOM/themes/powerlevel10k ``` 2. Sonrasında `~/.zshrc` dosyamda aşağıdaki satırı bulup değiştirdim: ```bash ZSH_THEME="powerlevel10k/powerlevel10k" ``` 3. Terminali yeniden başlatınca Powerlevel10k devreye girdi. Adeta bambaşka bir terminale dönüştüm! Powerlevel10k bana kullanıcı adım, klasör yolum, Git dalı, tarih/saat gibi bilgileri renkli ve simgelerle beraber gösteriyor. Ayrıca ilk kurduğumda interaktif bir kurulum sihirbazı çıktı. Bu sihirbaz sayesinde terminal'i tam olarak kendi zevkime göre ayarladım. > **Önemli Not:** Powerlevel10k temasının daha da güzel gözükmesi için özel fontlar (Nerd Fonts) gerekiyor. Ör: [MesloLGS NF](https://github.com/romkatv/powerlevel10k/blob/master/font.md) ‘u kullanabilirsiniz! ## Zsh Eklentileri: Otomatik Tamamlama ve Daha Fazlası ### zsh-autosuggestions En sevdiğim eklentilerden biri **zsh-autosuggestions**. Önceden yazdığım komutları tahmin ederek, metnin kalan kısmını silik bir yazı stiliyle gösteriyor. Örneğin, daha önce `pnpm dev` yazmışsam, tekrar `pnpm d` yazmaya başlarken `pnpm dev` şeklinde bir öneri görebiliyorum. Sağ ok tuşuna, `TAB`'a veya `CTRL + F` ile tamamlarsam direkt kullanabiliyorum. **Kurulumu oldukça basit:** ```bash git clone https://github.com/zsh-users/zsh-autosuggestions \ ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions ``` Sonra `~/.zshrc` içinde şu satırı ekliyorum: ```bash # vim ~/.zshrc plugins=(git zsh-autosuggestions) ``` ve `source ~/.zshrc` diyerek ayarları yüklediğimde, terminalim artık komutlarımı tahmin etmeye başlıyor. ### zsh-syntax-highlighting İkinci favori eklentim de **zsh-syntax-highlighting**. Bu eklenti, terminalde yazdığım komutları sözdizimine göre renklendiriyor. Hatalı veya geçersiz bir komut varsa kırmızı renkte uyarıyor. Doğru olanlar yeşil renkte gösteriliyor. Böylece gözümden kaçan ufak yazım hatalarını daha kolay fark edebiliyorum. **Kurulumu:** ```bash git clone https://github.com/zsh-users/zsh-syntax-highlighting.git \ ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting ``` `~/.zshrc` dosyasında plugini etkinleştiriyorum: ```bash # vim ~/.zshrc plugins=(git zsh-autosuggestions zsh-syntax-highlighting) ``` Ardından `source ~/.zshrc` diyerek aktif hale getiriyorum. ## Birkaç Faydalı Terminal Aracı İşlevselliğimi arttıran bazı araçlardan da bahsetmek isterim: * **bat**: `cat` komutuna benzer şekilde dosya içeriklerini gösterir, ancak sözdizimi vurgulaması yapar ve satır numaraları ekler. * **exa**: `ls` komutuna renkli, ikonlu ve daha okunaklı bir alternatif. * **fzf**: Komut geçmişi, klasör ve dosya araması gibi durumlarda fuzz (bulanık) arama imkânı sunan interaktif bir araç. * **ripgrep (rg)**: Metin aramalarını çok hızlı yapar, `grep`e kıyasla modern ve pratik. Bu araçları, `apt` veya `snap` aracılığıyla kolayca yükleyebilirsiniz. Örneğin: ```bash sudo apt install bat exa fzf ripgrep ``` > **Önemli Not:** Ubuntu sürümünüze göre `bat` yerine `batcat` olarak geçiyor olabilir, o yüzden paket adlarına dikkat etmenizi öneririm. ## exa Kurulurken Aldığım Hata ve Çözüm Yöntemleri Terminali renklendirmek ve gelişmiş özellikler katmak deyince, `exa` epey popüler. `ls`’in modern, renkli ve ikon desteği olan bir alternatifi. Fakat Ubuntu’nun bazı sürümlerinde `exa` yerine `eza` paketi bulunabiliyor veya `exa` tamamen kaldırılmış olabiliyor. Ben de şu şekilde bir hata aldım: ```powershell Package exa is not available, but is referred to by another package. This may mean that the package is missing, has been obsoleted, or is only available from another source However the following packages replace it: eza ``` İki farklı çözüm yolum oldu: ### 1. eza Kurup alias Oluşturmak `eza`, exa’nın aktif geliştirilen bir fork’u. Yani exa’yı aratmıyor, hatta yeni özellikler eklenmiş bir sürümü diyebilirim. ```bash sudo apt update sudo apt install eza ``` Bittikten sonra, exa komutuna alıştığım için bir alias ekledim: ```bash echo 'alias exa="eza"' >> ~/.zshrc source ~/.zshrc ``` Böylece terminalde `exa` yazdığımda aslında `eza` çalışmaya başladı. ### 2. exa’yı Cargo Üzerinden Kurmak Orijinal exa’yı kullanmakta ısrarcıysanız, Rust’ın paket yöneticisi Cargo ile derleyip kurabilirsiniz. Önce Rust’ı kurmalısınız: ```bash curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh # Oturum kapatıp açın veya: source $HOME/.cargo/env ``` Daha sonra: ```bash cargo install exa ``` Bitince `~/.cargo/bin` klasörünü PATH’e eklemek gerekiyor: ```bash echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> ~/.zshrc source ~/.zshrc ``` Böylece orijinal exa da emirlerinize amade oluyor. Ben şahsen eza ile devam etmeyi tercih ettim, çünkü yüklemesi kolay ve aktif geliştirme alması hoşuma gitti. ## Terminal Maceramın Bana Kazandırdıkları Artık Ubuntu terminalime girdiğimde, sadece komut çalıştırdığım bir ekran yerine adeta kişisel bir asistana dönüştüğünü hissediyorum. Komutlarım otomatik tamamlanıyor, göz alıcı renklerle vurgulanıyor ve Git depo bilgilerini tek bakışta görebiliyorum. Geliştirici deneyimim belirgin şekilde hızlandı. * **Zsh** ile Bash’in klasik yapısından kurtulup daha esnek bir kabuğa geçtim. * **Oh My Zsh** ve **Powerlevel10k** ile görünümü ve işlevselliği eş zamanlı artırdım. * **zsh-autosuggestions** ve **zsh-syntax-highlighting** sayesinde metin yazmak çok daha keyifli ve hatasız hale geldi. * **eza/alias exa** ile klasör içeriklerini modern bir arabirimde görüntüleyebiliyorum. * **ripgrep (rg)** ile aradığım şeyi `grep`'e kıyasla çok daha hızlı bir şekilde bulabiliyorum. Eğer siz de terminalde uzun vakit geçiriyorsanız, bu adımları mutlaka deneyin derim. Hem eğlenceli hem de oldukça verimli bir deneyim elde ediyorsunuz. Karşınıza çıkan hatalar veya farklılıklar için de bu yazıda paylaştığım alternatif kurulum yöntemlerini göz önünde bulundurabilirsiniz. Umarım deneyimlerimi paylaştığım bu yazı, sizin de kendi terminalinizi özelleştirme yolculuğunuzda rehber niteliğinde olur. Bir sonraki yazıda görüşmek dileğiyle… --- ## Laravel 11'den Laravel 12'ye Geçiş Rehberim: Deneyimlerim, İpuçları ve Örneklerle - URL: https://erho.dev/post/011-laravel-11den-laravel-12ye-gecis-rehberim-deneyimlerim-ipuclari-ve-orneklerle - Markdown: https://erho.dev/post/011-laravel-11den-laravel-12ye-gecis-rehberim-deneyimlerim-ipuclari-ve-orneklerle.md - Tarih: 2025-03-24 - Kategori: laravel - Etiketler: laravel, php Selamlar, Laravel'in yeni sürümleri her zaman beni heyecanlandırır. Laravel 12'nin yayınlanmasıyla birlikte bu heyecanım yeniden tazelendi. Laravel 11'den Laravel 12'ye geçerken resmi dokümantasyondan ve kendi deneyimlerimden edindiğim kritik bilgileri ve süreci sizlerle paylaşmak istiyorum. Umarım sizin için de yol gösterici olur! Resmi upgrade rehberine buradan ulaşabilirsiniz: [Laravel 12.x Upgrade](https://laravel.com/docs/12.x/upgrade). --- ## Laravel 12 İçin Sistem Gereksinimleri: Laravel 12'ye geçmeden önce projenizin şu gereksinimleri karşıladığından emin olun: * **PHP sürümü:** Minimum PHP 8.2 gerekiyor ([PHP.net](https://www.php.net/releases/8.2/en.php)). * **PHP uzantıları:** cURL, DOM, Fileinfo, Mbstring, OpenSSL, PDO, Tokenizer ve XML aktif olmalı. * **Composer:** En az 2.2 sürümüne sahip olmanız gerekiyor ([Composer Güncelleme Rehberi](https://getcomposer.org/doc/03-cli.md#self-update)). ## Laravel 12'ye Geçişte Bilmeniz Gereken Kritik Değişiklikler: ### Bağımlılıkları Güncelleyin: Laravel 12 için aşağıdaki bağımlılıkları güncellemelisiniz: ```json "laravel/framework": "^12.0", "phpunit/phpunit": "^11.0", "pestphp/pest": "^3.0" ``` ### Carbon 3: Laravel 12 artık Carbon 3.x kullanıyor. Carbon 2 desteği kaldırıldı: ```bash composer require nesbot/carbon:^3.0 ``` ### UUIDv7 Kullanımı: Laravel 12'de varsayılan olarak UUIDv7 kullanılıyor. Örneğin: ```php use Illuminate\Database\Eloquent\Concerns\HasUuids; class User extends Model { use HasUuids; } ``` UUIDv4 kullanmaya devam etmek için: ```php use Illuminate\Database\Eloquent\Concerns\HasVersion4Uuids; class User extends Model { use HasVersion4Uuids; } ``` ### SVG Dosyaları ve Validasyon: Laravel 12’de SVG dosyaları varsayılan olarak devre dışı bırakıldı. SVG kullanmak isterseniz: ```php $request->validate([ 'logo' => 'required|image:allow_svg', ]); ``` ### Concurrency: Sonuçlar İlişkisel Dizi Olarak Dönüyor: Laravel 12 ile `Concurrency::run()` metodunun sonuçları ilişkisel bir dizi olarak dönüyor: ```php $results = Concurrency::run([ 'task1' => fn() => 'Sonuç 1', 'task2' => fn() => 'Sonuç 2', ]); // Çıktı: ['task1' => 'Sonuç 1', 'task2' => 'Sonuç 2'] ``` ## Laravel 12'ye Geçiş İçin Önerdiğim Adımlar: ### 1. Hazırlık ve Yedekleme: Mutlaka dosyalarınızın ve veritabanınızın yedeğini alın! ### 2. Composer Bağımlılıklarını Güncelleyin: ```bash composer update ``` ### 3. Kod Refaktörizasyonu: UUID, SVG ve diğer yukarıda belirtilen noktalara özenle dikkat gösterin. ### 4. Konfigürasyon Güncellemeleri: Laravel 12 varsayılanları ile konfigürasyonlarınızı kıyaslayarak güncelleyin. [Laravel 12 Upgrade Guide](https://laravel.com/docs/12.x/upgrade) faydalı olacaktır. ### 5. Önbellek Temizliği: ```bash php artisan optimize:clear # veya php artisan config:clear php artisan cache:clear php artisan route:clear php artisan view:clear ``` ### 6. Test ve Doğrulama: Otomatik testlerinizi çalıştırın ve uygulamanızı kapsamlıca test edin: ```bash ./vendor/bin/pest ``` ### 7. Canlı Ortama Dağıtım: Son testlerden sonra güncellemelerinizi canlı ortama güvenle dağıtabilirsiniz. ## Laravel Installer'ı Güncelleme: Laravel 12 ile uyumlu Laravel installer CLI aracını güncellemeyi unutmayın: ```bash composer global update laravel/installer ``` ## Laravel Shift ile Otomatik Geçiş Yapın: Geçiş sürecini hızlandırmak için [Laravel Shift](https://laravelshift.com/) gibi araçlardan faydalanabilirsiniz. ## Laravel 12 ile Gelen Yenilikler: Laravel 12’nin yeni Starter Kit’leri ve WorkOS AuthKit entegrasyonu gibi özelliklerinden hemen yararlandım. Örneğin, AuthKit entegrasyonunu kullanmak: ```bash php artisan breeze:install react --authkit ``` Bu konu hakkında daha fazla bilgi için bir önceki yazıma buradan ulaşabilirsiniz: [https://erho.dev/laravel-12-yayinlandi](https://erho.dev/laravel-12-yayinlandi) ## Ek Olarak Farklılıkları görebilmeniz adına laravel'in orijinal (1x.x) sürüm branch'lerinden bir repo oluşturdum... Buradan sürümler arasındaki farkları kabaca görebilirsiniz! Repo: https://icin.tr/laravel-version-changes Laravel Branch'lerine Göre Sürüm Karşılaştırma: - v11.x vs v12.x: https://icin.tr/lara-11vs12 - v10.x vs v12.x: https://icin.tr/lara-10vs12 - v10.x vs v11.x: https://icin.tr/lara-10vs11 ## Sonuç ve Önerilerim: Laravel 12, önceki sürümlere göre daha sorunsuz ve hızlı bir geçiş sağlıyor. Düzenli güncelleme yaparak projelerinizi güvende ve güncel tutmanız çok önemli. Sizin Laravel 12 geçiş süreciniz nasıl geçti? Paylaşımlarınızı ve yorumlarınızı merakla bekliyorum! --- ## Laravel 12 Yayınlandı: İşte Tüm Yenilikler, Özellikler ve Geliştiricilere Sağladığı Avantajlar - URL: https://erho.dev/post/010-laravel-12-yayinlandi - Markdown: https://erho.dev/post/010-laravel-12-yayinlandi.md - Tarih: 2025-02-27 - Kategori: laravel - Etiketler: laravel Laravel topluluğu olarak heyecanla beklediğimiz **Laravel 12**, resmi olarak yayınlandı. Bu sürümle birlikte pek çok yeni özellik, performans iyileştirmesi ve geliştirici deneyimini artıran yenilikler hayatımıza girdi. Bir Laravel tutkunu ve profesyonel geliştirici olarak Laravel 12'nin tüm detaylarını sizin için derledim... --- ## Laravel 12'nin Yayınlanma Süreci Laravel 12, 24 Şubat 2025 tarihinde resmi olarak yayınlandı. Geçmiş sürümlerin aksine, Laravel 12 büyük kırıcı değişiklikler getirmiyor. Laravel ekibi, bu sürümde geriye dönük uyumluluğa özen göstererek sürüm yükseltmeyi daha basit ve sorunsuz hale getirdi. ## Öne Çıkan Yeni Özellikler ### 1. Starter Kit'lerle Hızlı Başlangıç Laravel 12 ile React, Vue ve Livewire için yeni proje başlangıç kitleri duyuruldu. Bu kitler sayesinde kullanıcı kimlik doğrulama, kayıt, sosyal medya entegrasyonu gibi temel özellikler birkaç komutla projeye entegre edilebiliyor. > **Daha fazlası için bkz:** > - https://laravel.com/docs/12.x/starter-kits > - https://laravel.com/starter-kits ### 2. WorkOS AuthKit Entegrasyonu Yeni sürümde WorkOS AuthKit entegrasyonu ile kurumsal seviyede kimlik doğrulama özellikleri varsayılan olarak destekleniyor. Sosyal medya girişleri, SSO ve passkey gibi gelişmiş yöntemlerle uygulamalar daha güvenli hale geliyor. ### 3. Dahili GraphQL Desteği Laravel 12, modern uygulamaların ihtiyacı olan GraphQL desteğini artık resmi olarak sağlıyor. Bu yenilikle REST API'lerin yanı sıra daha esnek veri yönetimi sağlanıyor. ### 4. Performans İyileştirmeleri Yeni asenkron önbelleğe alma ve optimize edilmiş sorgu motoru sayesinde Laravel 12 uygulamaları önceki sürümlere göre belirgin bir hız artışı sunuyor. Ayrıca, PHP 8'in modern özellikleri maksimum düzeyde kullanılarak uygulama hızları artırılmış durumda. ## Güvenlik ve Doğrulama İyileştirmeleri Laravel 12, kullanıcı girişlerinde güçlü parola politikalarını varsayılan hale getirdi. Yeni `secureValidate()` fonksiyonu ile şifre karmaşıklığı otomatik kontrol ediliyor ve uygulamaların güvenliği artırılıyor. ## Geliştiriciler İçin Yeni Araçlar - **Birleşik Scaffold Komutu:** Model, migration ve kontrolcü tek komutta oluşturulabiliyor. - **AI Destekli Hata Ayıklama:** `debug()->suggest()` metoduyla yapay zekâ destekli hata ayıklama önerileri alabiliyorsunuz. - **Güncel Ön Yüz Araçları:** Tailwind CSS, Vite ve popüler UI bileşenleri Laravel 12 projelerinde varsayılan hale geldi. ## Framework İçi Önemli Değişiklikler Bazı eski fonksiyonlar kaldırılarak daha tutarlı ve temiz kod yazımı teşvik ediliyor. Örneğin: - `route()` fonksiyonu artık yalnızca string parametre kabul ediyor. - Eloquent ilişki tanımlamalarında yalnızca metod kullanımı destekleniyor. ## Topluluk ve Ekosistem Üzerindeki Etkisi Laravel 12’nin piyasaya çıkışıyla birlikte, Laravel ekosistemi bu sürümü hızlıca benimsedi. Topluluk geri bildirimleri genel olarak çok olumlu; performans, kolay yükseltme ve yeni özellikler geliştiriciler arasında büyük beğeni topladı denebilir... ## Laravel 12'ye Geçmeli Miyim? Kesinlikle! Laravel 12’nin getirdiği yenilikler, kolay yükseltme süreci ve performans kazanımları, bu sürümü tüm Laravel geliştiricileri için cazip kılıyor. Yükseltme rehberini dikkatlice takip ederek, siz de sorunsuz bir geçiş yapabilirsiniz. (Bu konuda ilerleyen zamanlarda bir yazı paylaşacağım :)) > **Daha fazlası için bkz:** https://laravel.com/docs/12.x/upgrade ## Son Olarak Laravel 12, sunduğu performans, güvenlik ve geliştirici deneyimi yenilikleriyle web uygulama geliştirme dünyasında çıtayı yükseltiyor. Laravel’in bu istikrarlı ve yenilikçi yaklaşımı, geliştirici olarak bizlere daha verimli ve keyifli bir geliştirme ortamı sağlıyor. Peki siz Laravel 12 hakkında ne düşünüyorsunuz? Deneyimlerinizi yorumlarda paylaşabilirsiniz! > **Laravel 12’ye dair daha fazla bilgi için resmi dokümantasyona göz atabilirsiniz:** https://laravel.com/docs/12.x --- ## Laravel 10’dan Laravel 11’e Geçiş: Adım Adım Rehber - URL: https://erho.dev/post/009-laravel-10dan-laravel-11e-gecis-adim-adim-rehber - Markdown: https://erho.dev/post/009-laravel-10dan-laravel-11e-gecis-adim-adim-rehber.md - Tarih: 2025-02-19 - Kategori: laravel - Etiketler: laravel, php Selamlar, bu yazımda sizlerle, Laravel 10’dan Laravel 11’e geçiş sürecimi paylaşmak istiyorum. Bu yazıyı hazırlarken Laravel’in resmi yükseltme dökümanını ([https://laravel.com/docs/11.x/upgrade](https://laravel.com/docs/11.x/upgrade)) detaylıca inceledim ve kendi projelerimde uyguladığım adımları not aldım… Artık 24 Şubat’ta Laravel v12 çıkıyor; ancak ben hâlâ birçok projemi Laravel v10 ile çalıştırıyordum. İşte bu durum, bu yükseltme sürecine başlamam için son nokta oldu. Artık artık eski sürümlere veda etme zamanı! --- ## 1. Ortam Hazırlığı ve Bağımlılık Güncellemeleri Laravel 11’e geçişte ilk adım, ortamımızı güncel hale getirmek oldu. Öncelikle, sunucumda ve yerel geliştirme ortamımda PHP sürümünü kontrol ettim; çünkü **Laravel 11** için minimum **PHP 8.2** gerekliliğini yerine getirmem gerekiyordu. Aynı şekilde, **curl** sürümümü de **7.34** veya üzeri olacak şekilde güncelledim. **Composer** bağımlılıklarına gelince, projemdeki `composer.json` dosyasını açıp, Laravel framework’ünü `^11.0` sürümüne ve `nunomaduro/collision` paketini `^8.1` sürümüne güncelledim. Benim projemde ayrıca Laravel **Breeze, Cashier, Dusk, Jetstream, Octane, Passport, Sanctum, Scout, Spark Stripe, Telescope, Livewire** ve **InertiaJS** gibi ek paketlerden de bazıları vardı. Bu paketleri de uyumlu sürümlere çekmek gerekiyordu. Resmi dokümandan yola çıkarak, şöyle bir güncelleme yaptım: ```powershell composer require laravel/framework:^11.0 composer require nunomaduro/collision:^8.1 ``` Ayrıca, **Cashier Stripe, Passport, Sanctum, Spark Stripe** ve **Telescope** gibi paketlerin artık migration’larını otomatik yüklemediğini fark ettim. Bu yüzden **vendor publish** komutlarını kullanarak migration dosyalarını projeme aktardım: ```pgsql php artisan vendor:publish --tag=passport-migrations php artisan vendor:publish --tag=spark-migrations php artisan vendor:publish --tag=telescope-migrations ``` Global Laravel installer’ımı da güncellemem gerekiyordu (yeni projeler için): ```powershell # Resmi dokümanda önerilen composer global require laravel/installer:^5.6 # Benim önerim son sürümü kullanmanız yönünde composer global require laravel/installer:^* ``` ## 2. Uygulama Yapısındaki Değişiklikler Laravel 11, yeni projelerde varsayılan olarak daha sade bir uygulama yapısı sunuyor; daha az sayıda servis sağlayıcı, middleware ve konfigürasyon dosyası bulunuyor. Yine de, mevcut Laravel 10 projemi yeniden yapılandırmaya (tüm yapıyı v11’e benzetmeye) çalışmadan, Laravel 11’e geçiş yapabildiğimi görmek beni gerçekten rahatlattı. Çünkü bu yükseltme sürecinde, resmi dökümandan edindiğim bilgiler ışığında, eski yapıyı korurken yeni özellikleri de projeme dahil edebiliyordum. ## 3. Kimlik Doğrulama (Authentication) Yenilikleri Laravel 11, kullanıcı parolalarını otomatik olarak yeniden hash’leyebiliyor; yani hash algoritmamın “work factor”’ı güncellendiğinde, kullanıcı parolalarının yeniden oluşturulması sağlanıyor. Ancak, projemizde parolanın saklandığı sütun adı `password` dışında bir isimdeyse, modelime şu şekilde bir özellik eklememiz gerektiğini öğrendim: ```php protected $authPasswordName = 'custom_password_field'; ``` Tabii, eğer bu davranışı kapatmak isterseniz `config/hashing.php` dosyasında: ```php 'rehash_on_login' => false, ``` şeklinde ayarlamayı da unutmamanız gerekiyor. Ayrıca, `UserProvider` ve `Authenticatable` sözleşmelerine eklenen yeni metotları (örneğin, `rehashPasswordIfRequired` ve `getAuthPasswordName`) kendi implementasyonlarıma ekleyerek, süreci tamamladım. Böylece, kimlik doğrulama ile ilgili hiçbir aksaklık yaşamadım. ## 4. Veritabanı Güncellemeleri Veritabanı tarafında da pek çok ince detay dikkatimi çekti. Projemde, örneğin `members` tablosunda oluşturduğum `points` sütununu ilk etapta şöyle tanımlamıştım: ```php Schema::create('members', function (Blueprint $table) { $table->integer('points')->unsigned()->default(10)->comment('Üyelerin puanları'); }); ``` Daha sonra bu sütunu nullable yapmak istediğimde, Laravel 10’da önceki özellikler otomatik korunurken, Laravel 11’de tüm özellikleri yeniden belirtmem gerekti. Ben de şu şekilde bir güncelleme yaptım: ```php Schema::table('members', function (Blueprint $table) { $table->integer('points') ->unsigned() ->default(10) ->comment('Üyelerin puanları (opsiyonel)') ->nullable() ->change(); }); ``` Değiştirme `change` yöntemi sütunun dizinlerini değiştirmez. Bu nedenle, sütunu değiştirirken bir dizini açıkça eklemek veya bırakmak için dizin değiştiricileri kullanabilirsiniz: ```php // Bir dizin ekleyin... $table->uuid('id')->primary()->change(); // Bir dizin kaldırın... $table->char('postal_code', 10)->unique(false)->change(); ``` Bu arada, eğer migration’larınızı güncellemekte zorlanıyorsanız, migration’ları **squash** edip tek bir schema dosyası oluşturmak da iyi bir yöntem. Ben bu yöntemi tercih ettim: ```powershell php artisan schema:dump ``` Ayrıca, floating-point türlerinde de küçük değişiklikler vardı. **Double** ve **float** sütunlarını artık farklı şekilde tanımlamak gerekiyor. Örneğin: ```php $table->double('price_value'); $table->float('price_value', precision: 66); ``` ## 5. Doctrine DBAL’ın Kaldırılması Laravel 11, artık Doctrine DBAL’a bağlı değil. Ben de projemde önceden eklediğim `doctrine/dbal` paketini kaldırarak, Laravel’in native schema metotlarını kullanmaya başladım. Artık: ```php Schema::getTables(); Schema::getViews(); ``` gibi metotlar ile işlerimi hallediyorum. Bu benim için fazlasıyla temiz ve hızlı bir geçiş sağladı. ## 6. Tarih İşlemleri ve Carbon 3 Laravel’in tarih işlemlerinde **Carbon 3** desteği, önemli yenilikler getiriyor. **Carbon 3** ile `diffIn*` metotları artık ondalıklı sayı döndürebiliyor ve negatif değerler verebiliyor. Bu değişiklikleri kendi kodlarıma entegre ederken, **Carbon**’un **changelog** ve dokümantasyonunu dikkatlice inceledim. Böylece, tarih hesaplamalarında hiçbir aksaklık yaşamadan projemi güncelledim. ## 7. Mail ve Diğer Küçük Güncellemeler `Illuminate\Contracts\Mail\Mailer` sözleşmesi yeni bir `sendNow` yöntemi eklendi. Uygulamanız veya paketiniz bu sözleşmeyi manuel olarak uyguluyorsa, uygulamanıza yeni **sendNow** yöntemini eklemelisiniz: ```php public function sendNow($mailable, array $data = [], $callback = null); ``` Ayrıca, servis sağlayıcılar artık `config/app.php` yerine yeni `bootstrap/providers.php` dosyasında yönetiliyor. Kendi paketlerimi geliştirirken, servis sağlayıcımı eklemek için şu metodu kullandım: ```php use Illuminate\Support\ServiceProvider; ServiceProvider::addProviderToBootstrapFile(MyCustomProvider::class); ``` Eski yapı ile yenisi arasında da farklılıklar, değişiklikler bulunmakta… Örneğin: ```php // Eski: bootstrap/app.php (v10.x) $app = new Illuminate\Foundation\Application( $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__) ); $app->singleton( Illuminate\Contracts\Http\Kernel::class, App\Http\Kernel::class ); $app->singleton( Illuminate\Contracts\Console\Kernel::class, App\Console\Kernel::class ); $app->singleton( Illuminate\Contracts\Debug\ExceptionHandler::class, App\Exceptions\Handler::class ); return $app; --- // Yeni: bootstrap/app.php (v11.x) use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Middleware; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( web: __DIR__.'/../routes/web.php', commands: __DIR__.'/../routes/console.php', health: '/up', ) ->withMiddleware(function (Middleware $middleware) { // }) ->withExceptions(function (Exceptions $exceptions) { // })->create(); // bootstrap/providers.php (v11.x) return [ App\Providers\AppServiceProvider::class, // App\Providers\CustomServiceProvider::class, // ... ]; ``` * Bu, değişen dosyalardan sadece birkaçı... Ayrıca, `.env` ve `artisan` gibi dosyaların içeriğinin v11'e uygun olduğundan ve `app/Http/Kernel.php` dosyasının silindiğinden emin olmanız da fayda var... Daha fazla bilgi için [resmi dokümana](https://laravel.com/docs/11.x/releases) göz atın! ## 8. Kuyruklar, Rate Limiting ve Paket Güncellemeleri Laravel 11, kuyruk işlemlerinde **“after commit”** davranışını daha doğru şekilde uyguluyor ve **rate limiting**’i saniye bazında yapabiliyor. Projemde bu alanlarda da bazı ayarlamalar yapmam gerekti… Ayrıca, Cashier Stripe, Spark Stripe, Passport, Sanctum ve Telescope gibi paketlerin artık eski sürümleri desteklenmiyor. Ben de sırasıyla: * **Cashier Stripe:** `^15.0` * **Spark Stripe:** `^5.0` * **Passport:** `^12.0` (Ayrıca, password grant tipi için `AppServiceProvider`’ımda `Passport::enablePasswordGrant();` çağırdım.) * **Sanctum:** `^4.0` * **Telescope:** `^5.0` gibi güncellemeleri yaparak, her paketin migration dosyalarını **vendor publish** komutları ile projeme ekledim… Ayrıca, bütün işlemler sonunda **composer**’ı **dump** etmeyi, **cache**’i temizlemeyi ve **migrate**’i **fresh**’lemeyi unutmayın! ```powershell composer dump-autoload ; php artisan optimize:clear ; php artisan migrate:fresh ``` ## BONUS: Laravel Shift’in "Can I Upgrade Laravel Yet?" Aracı Upgrade sürecinde en büyük korkulardan biri, **bağımlılık karmaşası**’na girmekti. İşte tam bu noktada, Laravel Shift’in [Can I Upgrade Laravel Yet?](https://laravelshift.com/can-i-upgrade-laravel) aracını keşfettim. Bu araç sayesinde, projemdeki tüm bağımlılıkların en güncel Laravel sürümüyle uyumlu olup olmadığını hızlıca kontrol edebildim. **Packagist**’teki paket isimlerini analiz eden bu araç, hangi paketlerin yeni sürümle uyumlu olduğunu bana bildiriyor. Böylece, hangi alanlarda ekstra dikkat etmem gerektiğini önceden öğrenmiş oldum. Bu bonus özellik, benim **upgrade** sürecimde adeta bir rehber görevi gördü… `composer.json` dosyamdaki bağımlılıkları buraya yükleyerek (json formatında `require` ve `require-dev`’i) desteklenenleri geliştirme sürecinden önce ss’te gördüğünüz üzere anında görebildim: [![Laravel Shift](/images/posts/009-laravel-10dan-laravel-11e-gecis-adim-adim-rehber/img/laravel-shift.webp)](https://laravelshift.com/can-i-upgrade-laravel) ## Yani Özetle 24 Şubat’ta Laravel v12 çıkacak (bu konu hakkındaki yazım için [buraya](https://erho.dev/laravel-12-populer-php-frameworkunun-en-yeni-surumunu-kesfedin) tıkla). Bu fırsatı değerlendirerek artık projelerimi Laravel 10’da bırakmamaya karar verdim. Laravel 11’e geçiş sürecimde, resmi dokümana bağlı kalarak adım adım ilerledim ve kodlarımı titizlikle güncelledim. Laravel 12’ye yükseltmeden önce, eğer elimizde daha eski bir Laravel sürümü (örneğin Laravel 9) varsa, doğrudan en son sürüme geçmek yerine, önce Laravel 10’a, ardından Laravel 11’e ve son olarak Laravel 12’ye yükseltmek daha sağlıklı bir yöntem, aksi durumda proje yüksek ihtimalle elinizde patlayacaktır! Bu yazıda, ortam ve bağımlılık güncellemelerinden veritabanı migration’larına, kimlik doğrulama yeniliklerinden kuyruk ve rate limiting ayarlarına kadar birçok detayı, kendi deneyimlerimle harmanlayarak paylaşamaya çalıştım... Eğer siz de Laravel 10’dan Laravel 11’e geçiş yapmayı düşünüyorsanız, adımları dikkatlice uygulamanızı ve her adımda test ederek ilerlemenizi tavsiye ediyorum. Laravel Shift gibi araçlardan yararlanmak, süreçte karşılaşabileceğiniz sorunları önceden görmenize yardımcı olabilir. Daha fazlası için Laravel’in resmi dökümanına [buraya](https://laravel.com/docs/11.x/upgrade) tıklayarak bi göz atabilirsiniz… Umarım bu yazı, sizin için de faydalı olur :) --- ## PHP'de ??, ??=, ?: Operatörleri ve "Falsy" Değerler - URL: https://erho.dev/post/008-phpde-operatorleri-ve-falsy-degerler - Markdown: https://erho.dev/post/008-phpde-operatorleri-ve-falsy-degerler.md - Tarih: 2025-01-31 - Kategori: php - Etiketler: php, architecture PHP/Laravel ile geliştirdiğim projelerde her zaman daha temiz, daha okunabilir ve daha verimli kodlar yazmanın yollarını ararım. Siz de benim gibi mi düşünüyorsunuz? O zaman doğru yerdesiniz! Bugün sizlerle PHP'nin belki de en kullanışlı operatörlerinden bazılarını ve "falsy" değerler kavramını paylaşmak istiyorum. Bu operatörler sayesinde kodlarınızı hem sadeleştirebilir hem de performansını artırabilirsiniz. Hazırsanız, hemen başlayalım! --- ### **(**`??`): **Null Coalescing Operatörü** PHP'de `??` operatörü, değişkenin var olup olmadığını ve `null` değeri taşıyıp taşımadığını kontrol etmek için mükemmel bir araçtır. Özellikle form verileri veya dizi anahtarlarıyla çalışırken, bu operatör sayesinde uzun `if-else` bloklarına veda edebilirsiniz. **Örnek:** ```php ``` Bu kod parçasında, `$_GET['username']` tanımlı değilse veya `null` ise, `$username` değişkenine `'Misafir'` değeri atanır. Kısa ve öz, değil mi? ### **(**`??=`): **Null Coalescing Assignment Operatörü** `??=` operatörü, bir değişkenin tanımlı olup olmadığını ve `null` değer taşıyıp taşımadığını kontrol ederek, eğer öyleyse ona bir değer atar. Bu, varsayılan değer atamalarını daha da kolaylaştırır. **Örnek:** ```php ``` Bu operatör, konfigürasyon dosyalarında varsayılan değerler belirlemek için harika bir yöntemdir. ### **(**`?:`) **Ternary Operatörü** ve Elvis Operatörü PHP'deki ternary operatörü, basit koşul ifadeleri için ideal bir araçtır. Kısa ve anlaşılır yapısıyla, `if-else` bloklarına alternatif sunar. Ayrıca, PHP'de "Elvis Operatörü" olarak da bilinir. **Örnek:** ```php = 18) ? 'Yetişkin' : 'Çocuk'; // (? :) Ternary Operatörü // Gelen veride 'username' var mı kontrol et, yoksa 'Misafir' değerini ata $username = $_GET['username'] ?: 'Misafir'; // (?:) Elvis Operatörü ?> ``` Hem okunabilir hem de kısa bir çözüm için oldukça kullanışlıdır! ### `??` ve `?:` Operatörlerinin Arasındaki İnce Farklar Belki de en çok merak edilen soru: `??` ile `?:` operatörleri arasındaki farklar nelerdir? Gelin, bu iki operatörü detaylıca karşılaştıralım. | **Özellik** | `??` Operatörü | `?:` Operatörü | | --- | --- | --- | | **Kontrol Edilen Durum** | Sadece `null` | Herhangi bir "falsy" değer (`false`, `0`, `''`, vs.) | | **Kullanım Alanı** | Varsayılan değer atamaları, null kontrolleri | Genel koşul kontrolleri, "falsy" değerlerin ele alınması | | **Kısa Yazım** | `$a = $b ?? 'varsayılan';` | `$a = $b ?: 'varsayılan';` | | **Zincirleme Kullanım** | Evet | Evet | **Ne Zaman Hangisini Kullanmalıyız?** * `??` Operatörü: Değişkenin tanımlı olup olmadığını ve `null` olup olmadığını kontrol etmek istediğiniz durumlarda kullanın. Özellikle form verilerinde veya dizi anahtarlarında varsayılan değerler belirlemek için idealdir. * `?:` Operatörü: Değişkenin "falsy" değerlerini de dikkate almak istediğiniz genel koşul kontrolleri için tercih edin. Kullanıcı girdisinin boş olup olmadığını kontrol etmek gibi durumlarda mükemmel bir seçimdir. ### **“Falsy” Değerler Nedir?** Programlamada "falsy" değerler, boolean bağlamlarda `false` olarak değerlendirilen değerlerdir. PHP'de bu değerler şunlardır: 1. `false` 2. `0` (integer) 3. `0.0` (float) 4. `""` (boş string) 5. `"0"` (string) 6. `[]` (boş dizi) 7. `NULL` **NOT:** `" "` (boşluk içeren string) PHP'de "falsy" olarak kabul edilmez; bu nedenle, boşluklu stringler "truthy" kabul edilir. **Neden Önemli?** "Falsy" değerler, koşul ifadelerinde doğrudan değişken kullanarak kodunuzu sadeleştirmenize yardımcı olur. Gereksiz karşılaştırmalardan kaçınarak daha temiz ve okunabilir kod yazabilirsiniz. **Örnek:** ```php ``` ### **Pratik Örneklerle Öğrenelim** #### **1. Kullanıcı Girişi Kontrolü** ```php ``` Bu örnekte, eğer kullanıcı adı gönderilmemişse `'Misafir'` atanır ve duruma göre mesaj görüntülenir. #### **2. Ayar Dosyasındaki Varsayılan Değer Ataması** ```php ``` Bu kod parçasında, konfigürasyon ayarları için varsayılan değerler belirlenir. #### **3. Arama Fonksiyonunda "Falsy" Değer Kullanımı** ```php ``` Burada, eğer arama terimi boş veya "falsy" bir değer taşıyorsa, varsayılan olarak `'Tüm Kayıtlar'` atanır. ## Daha Akıllı ve Verimli PHP Kodları Yazmak Sizin Elinizde PHP'deki `??`, `??=` ve `?:` operatörlerini etkin bir şekilde kullanmak, kodlarınızı daha okunabilir, kısa ve verimli hale getirir. Ayrıca, "falsy" değerler kavramını anlamak, koşul ifadelerinizde daha akıllı ve esnek kontroller yapmanıza olanak tanır. ### **Öneriler:** * **Doğru Operatörü Seçin:** `??` operatörünü yalnızca `null` kontrolleri için, `?:` operatörünü ise genel koşul kontrolleri için kullanın. * **Kod Okunabilirliğine Önem Verin:** Operatörleri kullanırken kodunuzun anlaşılır ve sürdürülebilir olmasına dikkat edin. * **Test Etmeyi Unutmayın:** Kullanacağınız operatörlerin beklenen davranışı sergilediğinden emin olmak için kodunuzu test edin. Bu operatörleri projelerinizde kullanarak kod kalitenizi artırabilir ve daha profesyonel PHP çözümleri geliştirebilirsiniz. Siz de bu operatörleri deneyin ve farkı kendiniz görün! --- ## PHP 8’de Nullsafe Operatörü ile Daha Temiz Kod Yazın! - URL: https://erho.dev/post/007-php-8de-nullsafe-operatoru-ile-daha-temiz-kod-yazin - Markdown: https://erho.dev/post/007-php-8de-nullsafe-operatoru-ile-daha-temiz-kod-yazin.md - Tarih: 2025-01-30 - Kategori: php - Etiketler: php, architecture PHP sürümlerini güncel tutmak, projelerimizde hem performans hem de okunabilirlik açısından bize büyük avantaj sağlar. Eskiden bir değerin var olup olmadığını kontrol etmek için sürekli `if` blokları içinde boğuluyorduk. Örneğin: ```php if ($user !== null) { $profile = $user->profile; // Eloquent ilişkisi if ($profile !== null) { $address = $profile->address; if ($address !== null) { $city = $address->city; } } } ``` PHP 8 ile gelen **Nullsafe Operatörü** (`?->`) ise bize şu şekilde daha temiz ve anlaşılır bir kod yazma imkânı sunuyor: ```php $city = $user?->profile?->address?->city; ``` Artık nesneleri tek satırda kontrol edebiliyor ve her adımda `null` kontrolü yapmanızı sağlayan bu özelliği kullanabiliyoruz. Kodunuzdaki iç içe if’lerden kurtulmak ve daha iyi bir görünüm elde etmek için mutlaka deneyin. **Neden kullanmalı?** - Daha az satır, daha anlaşılır kod - Hızlı bakım ve geliştirme - Null değerleri otomatik kontrol edebilme Siz de kendi projelerinizde `?->` operatörünü kullanarak **daha temiz, bakımı kolay ve modern** bir PHP kodu elde edebilirsiniz! --- ## Docker İmajları ile Çalışmak: Docker Hub ve GitHub Packages Rehberi - URL: https://erho.dev/post/006-docker-imajlari-ile-calismak-docker-hub-ve-github-packages-rehberi - Markdown: https://erho.dev/post/006-docker-imajlari-ile-calismak-docker-hub-ve-github-packages-rehberi.md - Tarih: 2025-01-05 - Kategori: linux - Etiketler: docker, github, devops Selamlar! Ben Erhan. Bugün sizlerle Docker dünyasının en önemli konularından biri olan imaj yönetimi hakkında kapsamlı bir rehber hazırladım. Çayınızı kahvenizi alın, beraber keyifli bir yolculuğa çıkıyoruz… ## Neden Bu Rehberi Hazırladım? Son zamanlarda bana en çok sorulan sorulardan biri: "**Docker Hub mu, GitHub Packages mı kullanmalıyım?**" oldu. Ben de düşündüm ki, gelin size kendi deneyimlerimden yola çıkarak detaylı bir karşılaştırma yapayım ve hangi platformun sizin için daha uygun olabileceğini birlikte keşfedelim... Hazırsanız başlayalım! ## Docker Hub ve GitHub Packages: Detaylı Karşılaştırma ### Fiyatlandırma ve Özellik Karşılaştırması | Özellik | Docker Hub | GitHub Packages | |----------------------|-------------------------------------|----------------------------| | **Ücretsiz Plan** | | | | Public Repo | Sınırsız | Sınırsız | | Private Repo | 1 adet (2GB depolama) | 500MB | | Depolama | 2GB / 5GB / 50GB / 500GB | 500MB / 2GB | | Bandwidth | 40 pull/sa | 1GB/ay | | Kullanıcı | 1 kullanıcı | Sınırsız | | **Ücretli Planlar** | | | | Pro | $11/ay | $4/ay | | Team | $16/kullanıcı/ay | $4/kullanıcı/ay | | Business | $24/kullanıcı/ay | $21/Özel fiyatlandırma | | **Teknik Özellikler**| | | | Build Otomasyonu | Docker Build Cloud | GitHub Actions ile | | Multi-arch Desteği | Var | Var | | Vulnerability Tarama | Docker Scout ile | Advanced Security ile | | CI/CD Entegrasyonu | Docker Build Cloud ve Testcontainers Cloud | Native GitHub Actions | | **Güvenlik Özellikleri**| | | | Kimlik Doğrulama | Basic Auth | GitHub Auth | | Access Tokens | Var | Var | | SSO Desteği | Docker Business'da | GitHub Enterprise Cloud'da | | Güvenlik Taraması | Docker Scout ile | Advanced Security ile | | **Ekstra Özellikler**| | | | Community Desteği | Çok güçlü | Güçlü | | Official Images | Var | Yok | | Paket Çeşitliliği | Sadece Docker | Çoklu paket desteği | | Desktop Entegrasyonu | Docker Desktop | GitHub Desktop ile | | **Enterprise Özellikleri**| | | | Team Yönetimi | Docker Team ve Business planlarında | Organization yapısı | | Audit Logging | Docker Team ve Business planlarında | GitHub Enterprise Cloud'da | | Support | Premium | Premium | | Custom Domain | Docker Business'da | GitHub Enterprise Cloud'da | Bu bilgiler ilerleyen zamanlarda değişiklik gösterebilir. Güncel bilgiler ve daha fazla detay için bkz: - Docker Hub: - GitHub Packages: ### Performans ve Kullanım Senaryoları | Senaryo | Docker Hub | GitHub Packages | Önerilen Platform | |---------|------------|-----------------|-------------------| | Bireysel Geliştirici | Ücretsiz plan yeterli | Storage limiti dezavantaj | Docker Hub | | Küçük Takım | Team plan gerekebilir | Ekonomik seçenek | GitHub Packages | | Kurumsal Kullanım | Enterprise gerekli | GitHub Enterprise ile tam entegrasyon | Duruma göre değişir | | Açık Kaynak | İdeal | GitHub ile entegre çalışır | İkisi de uygun | | Özel Projeler | Private repo sınırlı | Daha esnek | GitHub Packages | | CI/CD Odaklı | Manual config gerekir | Native entegrasyon | GitHub Packages | ### Güvenlik Özellikleri #### Docker Hub'da Güvenlik - Temel kimlik doğrulama - Access token desteği - Webhook entegrasyonu - Otomatik build özellikleri - İmaj tarama (ücretli planlarda) - Docker Content Trust (DCT) ile imaj imzalama - Otomatik güvenlik güncellemeleri - Vulnerability Database entegrasyonu #### GitHub Packages'ta Güvenlik - GitHub'un güçlü güvenlik altyapısı - Detaylı erişim kontrolü - GHCR (GitHub Container Registry) desteği - Bağımlılık grafiği ve güvenlik uyarıları - Gelişmiş güvenlik özellikleri (CodeQL, gizli tarama) - SBOM (Software Bill of Materials) desteği - Cosign ile imaj imzalama (opsiyonel) - Supply chain güvenliği için Sigstore entegrasyonu **Gözlemim**: Kurumsal projelerde GitHub Packages'ın gelişmiş güvenlik özellikleri size çok daha fazla kontrol ve görünürlük sağlıyor. ## Pratik Uygulama: Base Image'dan Özel İmaj Oluşturma Hadi şimdi teoriden pratiğe geçelim! Size gerçek bir örnek üzerinden hem Docker Hub hem de GitHub Packages kullanımını göstereceğim. Bu örnekte, Vito'nun resmi imajını ([base image](https://hub.docker.com/r/vitodeploy/vito)) kullanarak kendi özelleştirilmiş deployment imajımızı ([erho-deploy](ghcr.io/erhanurgun/erho-deploy)) oluşturacağız... **DİPNOT:** Kişisel imajlarınızı da bu şekilde oluşturabilirsiniz. İmajın varolan bir imajdan türetilerek oluşturulması zorunlu değildir, bu sadece bir örnek! ### 1. Dockerfile Hazırlama İlk adım olarak, projemizin kök dizininde bir `Dockerfile` oluşturalım. Base image olarak Vito'nun resmi imajını kullanacağız: ```dockerfile # Base image olarak Vito'nun resmi imajını kullanıyoruz FROM vitodeploy/vito:1.x # Kendi özelleştirmelerimizi ekliyoruz WORKDIR /var/www/html COPY . . # Gerekli kurulumları ve ayarları yapıyoruz RUN composer install --no-dev --optimize-autoloader RUN php artisan config:cache # İmaj meta bilgilerini ekliyoruz LABEL org.opencontainers.image.maintainer="Erhan ÜRGÜN " LABEL org.opencontainers.image.description="Özelleştirilmiş Vito deployment imajı" ``` ### 2. Docker Hub'a İmaj Gönderme ```bash # Kendi özelleştirilmiş imajımızı oluştur docker build -t erhanurgun/erho-deploy:1.0.1 . # Docker Hub'a giriş yap docker login # Özelleştirilmiş imajı gönder docker push erhanurgun/erho-deploy:1.0.1 ``` Bütün adımlar başarılı ise [Docker Hub](https://hub.docker.com)'da bu şekilde görüntülenecek: ![Docker Hub](https://raw.githubusercontent.com/erhanurgun/erho-deploy/refs/heads/main/img/ss_docker-hub.png align="left") ### 3. GitHub Packages'a İmaj Gönderme #### Personal Access Token (PAT) Oluşturma GitHub Packages'ı kullanabilmek için öncelikle bir Personal Access Token oluşturmanız gerekiyor: 1. GitHub hesabınızda sağ üst köşedeki profil fotoğrafınıza tıklayın. 2. `Settings > Developer settings > Personal access tokens > Tokens (classic)` yolunu izleyin veya direkt olarak [buraya](https://github.com/settings/tokens/new) tıklayın. 3. "Generate new token (classic)" butonuna tıklayın. 4. Token için bir not ekleyin (örn: "Docker Package Access") 5. Aşağıdaki izinleri seçin: * `write:packages` * `read:packages` * `delete:packages` * `repo` (opsiyonel - private repo'lar için) 6. "Generate token" butonuna tıklayın. 7. Oluşturulan token'ı güvenli bir yere kaydedin (**bu token bir daha gösterilmeyecek!**) ![GitHub TOKEN](https://raw.githubusercontent.com/erhanurgun/erho-deploy/refs/heads/main/img/ss_gh-token.png align="left") #### GitHub Secrets Oluşturma CI/CD süreçleri için token'ı GitHub Secrets'a eklemeniz gerekiyor: 1. GitHub'da projenizin sayfasına gidin. 2. `Settings > Secrets and variables > Actions` yolunu izleyin. Ör: `https://github.com/{kullanici-adi}/{repo-adi}/settings/secrets/actions` 3. "**New repository secret**" butonuna tıklayın. 4. **Name**: `GH_PAT` olarak girin. 5. **Value**: Yukarıda oluşturduğunuz `Personal Access Token (PAT)` değerini girin. 6. "**Add secret**" butonuna tıklayın. Daha açıklayıcı olması adına süreci görselleştirelim: ![GitHub Secrets](https://raw.githubusercontent.com/erhanurgun/erho-deploy/refs/heads/main/img/ss_gh-secrets.png align="left") #### İmaj Gönderme İşlemi ```bash # Önce imajı build edelim docker build -t erhanurgun/erho-deploy:dev . # GitHub'a giriş yap (PowerShell) $env:GH_PAT="" $env:GH_PAT | docker login ghcr.io -u erhanurgun --password-stdin # Özelleştirilmiş imajı etiketle # docker tag {username}/{repo}:{tag} ghcr.io/{username}/{repo}:{tag} docker tag erhanurgun/erho-deploy:dev ghcr.io/erhanurgun/erho-deploy:dev # Özelleştirilmiş imajı gönder docker push ghcr.io/erhanurgun/erho-deploy:dev ``` **Önemli Güvenlik Notları**: - Token'ınızı **asla** doğrudan _kodunuzda_ veya _Dockerfile_'da saklamayın. - Token'ı güvenli bir şekilde yönetin ve düzenli olarak yenileyin. - Minimum gerekli izinlerle token oluşturun! - Production ortamında environment variable veya secrets kullanın. - PowerShell komut geçmişinden `GH_PAT` değişkeninin bilgilerini silmek için: (opsiyonel) ```powershell # Tüm GH_PAT içeren komutları silmek için: (PowerShell) Get-History | Where-Object {$_.CommandLine -like "*GH_PAT*"} | ForEach-Object { Clear-History -Id $_.Id } ``` ### Repository Bağlama GitHub Packages'ta bir Docker imajını bir repository'e bağlamak, imajınızın dökümantasyonunu ve yönetimini kolaylaştırır. Bu işlem iki şekilde yapılabilir: #### 1. Dockerfile ile Bağlama Dockerfile'ınıza aşağıdaki satırı ekleyin: ```dockerfile LABEL org.opencontainers.image.source https://github.com/KULLANICI_ADI/REPO_ADI ``` Örnek: ```dockerfile LABEL org.opencontainers.image.source https://github.com/erhanurgun/erho-deploy ``` #### 2. Web Arayüzü ile Bağlama 1. GitHub Packages'ta **imajınızın sayfasına gidin**. 2. "**Connect Repository**" butonuna tıklayın. 3. Bağlamak istediğiniz **repository'i seçin**. **Önemli Not**: Repository ve container imajının namespace'i (kullanıcı/organizasyon adı) aynı olmalıdır. Örneğin, imajınız `ghcr.io/erhanurgun/erho-deploy` ise, repository de `erhanurgun/erho-deploy` olmalıdır. **Bağlama Avantajları**: - README dosyası otomatik olarak görüntülenir. - Issue ve tartışmalar imaj sayfasında görünür. - Katkıda bulunanlar listelenir. - Repository ile imaj arasında otomatik link oluşur. ### 4. GitHub Actions ile Otomatik Deploy Hamal gibi her defasında manuel olarak imajı göndermek yerine, GitHub Actions ile otomatik olarak deploy edebiliriz. * Öncelikle Dockerfile'ımızı da buna uygun olarak güncelleyelim: ```dockerfile # Base image olarak Vito'nun belirli bir versiyonunu kullanıyoruz ARG BASE_IMAGE_VERSION=1.x FROM vitodeploy/vito:${BASE_IMAGE_VERSION} # Kendi özelleştirmelerimizi ekliyoruz WORKDIR /var/www/html COPY . . # Gerekli kurulumları ve ayarları yapıyoruz RUN composer install --no-dev --optimize-autoloader RUN php artisan config:cache # İmaj meta bilgilerini ekliyoruz LABEL org.opencontainers.image.maintainer="Erhan ÜRGÜN " LABEL org.opencontainers.image.description="Özelleştirilmiş Vito deployment imajı" LABEL org.opencontainers.image.source https://github.com/erhanurgun/erho-deploy LABEL org.opencontainers.image.base.name="vitodeploy/vito:${BASE_IMAGE_VERSION}" ``` * Daha sonra `.github/workflows/docker-publish.yml` dosyasını oluşturalım: ```yaml # Workflow'un adı - GitHub Actions sekmesinde görünecek isim name: Publish Erho Deploy Docker Image # Workflow'u tetikleyecek olaylar on: push: branches: ["main"] # Semantic versioning için tag'leri de dinle tags: ["v*.*.*"] pull_request: branches: ["main"] # Tüm job'larda kullanılacak environment değişkenleri env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} # Çalıştırılacak iş tanımları jobs: build-and-push: runs-on: ubuntu-latest # Security hardening permissions: contents: read packages: write id-token: write steps: - name: Repository'yi kontrol et uses: actions/checkout@v4 with: fetch-depth: 0 # Tüm history'yi al (semantic versioning için) - name: QEMU kurulumu uses: docker/setup-qemu-action@v3 - name: Docker Buildx kurulumu uses: docker/setup-buildx-action@v3 with: buildkitd-flags: --debug # Build sorunlarını daha iyi tespit etmek için - name: GitHub Container Registry'e giriş uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GH_PAT }} - name: Docker için meta verileri ayıklama id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | # Latest tag type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }} # Branch bazlı tag type=ref,event=branch # PR bazlı tag type=ref,event=pr # Semantic versioning type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} # Kısa SHA type=sha,prefix=sha-,format=short # Tarih bazlı tag type=raw,value={{date 'YYYY.MM.DD'}} type=schedule,pattern={{date 'YYYYMMDD'}} - name: Docker image build ve push id: build uses: docker/build-push-action@v5 with: context: . platforms: linux/amd64,linux/arm64 push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max ``` **Bu workflow şunları sağlar:** - Base image versiyonu (1.x) merkezi olarak yönetilir. - Build argümanları ile versiyon kontrolü sağlanır. - GitHub Container Registry ile güvenli entegrasyon sağlanır. - Otomatik metadata ve etiket yönetimi ile imajınız daha iyi yönetilebilir. - Artık her **push** işleminde otomatik olarak çalışacaktır. - Semantic versiyonlama desteğiyle (v1.0, v1.1 gibi tag'ler için) **tag** oluşturulduğunda otomatik olarak çalışacaktır. **EK BİLGİ:** Ayrıca, **Docker Scout'u GitHub Actions ile entegre etme** için bkz: Workflow çalıştırıldığında eğer başarılı ise **ilgili repo'nun GitHub Actions** sekmesinde bu şekilde görüntülenecek: ![GitHub Actions](https://raw.githubusercontent.com/erhanurgun/erho-deploy/refs/heads/main/img/ss_gh-actions.png align="left") Bütün adımlar başarılı ise **GitHub Packages**'da bu şekilde görüntülenecek: ![GitHub Packages](https://raw.githubusercontent.com/erhanurgun/erho-deploy/refs/heads/main/img/ss_gh-packages.png align="left") ### Multi-Stage Build Örneği (Opsiyonel) Şimdi `Dockerfile` dosyamını biraz daha geliştirelim... Daha optimize bir _Dockerfile_ için _multi-stage build_ kullanabilirsiniz: - Bu işlemler bir [Laravel](https://laravel.com) projesi olan [vitodeploy/vito](https://hub.docker.com/r/vitodeploy/vito) imajı referans alınarak yapılmıştır! ```dockerfile # Composer kurulum aşaması FROM composer:2.6 AS composer # Çalışma dizinini ayarla WORKDIR /app # Sadece composer dosyalarını kopyala COPY composer.* ./ # Composer bağımlılıklarını kopyala ve kur RUN composer install \ --no-interaction \ --no-plugins \ --no-scripts \ --no-dev \ --prefer-dist \ --optimize-autoloader # Ana imaj aşaması FROM vitodeploy/vito:1.x AS app # Çalışma dizinini ayarla WORKDIR /var/www/html # Tüm proje dosyalarını kopyala COPY . . # Composer vendor klasörünü kopyala COPY --from=composer /app/vendor ./vendor # Storage ve cache dizinleri için izinleri ayarla RUN chown -R www-data:www-data /var/www/html \ && chmod -R 755 /var/www/html/storage \ && mkdir -p /var/www/html/storage/framework/{sessions,views,cache} \ && chmod -R 775 /var/www/html/storage/framework # İmaj meta bilgilerini ekle LABEL org.opencontainers.image.maintainer="Erhan ÜRGÜN " LABEL org.opencontainers.image.description="Özelleştirilmiş Vito deployment imajı" LABEL org.opencontainers.image.source="https://github.com/erhanurgun/erho-deploy" LABEL org.opencontainers.image.title="Erho Deploy" LABEL org.opencontainers.image.version="1.0" LABEL org.opencontainers.image.vendor="Erhan ÜRGÜN" LABEL org.opencontainers.image.licenses="MIT" # Sağlık kontrolü HEALTHCHECK --interval=30s --timeout=3s --start-period=30s --retries=3 \ CMD curl -f http://localhost/ || exit 1 # Port ayarı EXPOSE 80 # Çalıştırma komutu CMD ["php-fpm"] ``` _Bu multi-stage build yaklaşımı şu avantajları sağlar:_ * Daha küçük ana imaj boyutu. * Build araçlarının ana imajda yer almaması. * Daha güvenli bir production imajı. ## Hangi Platform Sizin İçin Doğru Seçim? ### Docker Hub'ı Tercih Edin, Eğer * Sadece Docker imajları ile çalışıyorsanız. * Community imajlarına sık erişiyorsanız. * Docker Desktop kullanıyorsanız. * Basit ve hızlı bir çözüm arıyorsanız. ### GitHub Packages'ı Tercih Edin, Eğer * CI/CD pipeline'larınız GitHub'daysa... * Farklı paket türleriyle çalışıyorsanız. * GitHub'da host edilen projeleriniz varsa... * Gelişmiş güvenlik özelliklerine ihtiyacınız varsa... ## Önerilerim 1. **Hybrid Kullanım**: Her iki platformu da kullanabilirsiniz! Ben genellikle açık kaynak projelerim için Docker Hub, kurumsal projelerim için GitHub Packages kullanıyorum. 2. **Cache Stratejisi**: GitHub Packages'ın GitHub Actions ile native entegrasyonu, build sürelerini önemli ölçüde düşürüyor. 3. **Güvenlik**: Her zaman en güncel güvenlik pratiklerini takip edin ve API tokenlarınızın güvenliğinden emin olun. ## Son Olarak Umarım bu yazı, hangi platformun sizin ihtiyaçlarınıza daha uygun olduğunu belirlemenize yardımcı olmuştur. Ve unutmayın ki, en iyi öğrenme yöntemi deneyimlemektir... O yüzden sizlere naçizane tavsiyem; hemen bir proje oluşturun ve bu platformları deneyimleyerek anlatılanları pekiştirin! --- ## Docker İmajları'nın Güvenilirliği Nasıl Kontrol Edilir? - URL: https://erho.dev/post/005-docker-imajlarinin-guvenilirligi-nasil-kontrol-edilir - Markdown: https://erho.dev/post/005-docker-imajlarinin-guvenilirligi-nasil-kontrol-edilir.md - Tarih: 2024-12-19 - Kategori: linux - Etiketler: linux, docker, security, devops Bilgisayarınızda veya sunucunuzda çalışan Docker tabanlı uygulamalar ne kadar güvenli? Bilmediğiniz açıklar nedeniyle hack'lenmek an meselesi olabilir. Aralık 2025'te paylaşılan bir araştırma, Docker Hub'da 10 binden fazla imajın içinde public olarak erişilebilen veritabanı şifreleri, API anahtarları ve bulut servis kimlik bilgileri barındırdığını ortaya koydu. Saldırganların doğrudan kullanabileceği bilgilerden bahsediyoruz; rakamı görünce ben de oldukça şaşırdım. Backend geliştirici olarak Docker ile her gün çalışıyorum: API geliştirirken, CI/CD pipeline'ları kurarken. Container teknolojisi artık vazgeçilmez. Ama bu gücün yanında gelen sorumluluk, konuştuğum geliştiricilerin önemli kısmında karşılığını bulmuyor. Bu yazıda Docker imajlarını gerçek anlamda güvenli hale getirmek için kullandığım pratik adımları derliyorum. ## Docker İçin İlk Adımlar: Sistem ve Konteyner Analizi Docker güvenliğinden bahsetmek kolaydır, ancak uygulamak başka. Başlangıç noktalarından biri, sisteminizde çalışan konteynerlerin detaylı analizidir. Çünkü **ne güvence altına alırız, bunu başlangıçta görmemiz lazım.** ### 1. Konteynerlerin Aktif Durumunu Kontrol Edin Docker imajının ve ona bağlı konteynerlerin güvenliğinden emin olmak için ilk adım, çalışan konteynerlerin durumunu ve davranışlarını analizlemektir. Bunun için basit fakat güçlü komutlar var: ```bash # Tüm konteynerlerin durumunu listeleyin (aktif ve durdurulmuş) docker ps -a # Belirli bir konteynerde çalışan prosesleri inceleyin docker top # Konteynerde yapılan değişiklikleri kontrol edin docker diff ``` **Senaryolar - Bunlar Size Ne Söylüyor?** **Aktif Durumu Görüntüleyin:** Bir Docker imajı kullanarak çalıştırdığınız uygulamada beklenmeyen bir davranış varsa, ilk yapmanız gereken şey `docker ps -a` komutudur. Çıktı şuna benzer olabilir: ```plaintext CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES d1e8f5c6e987 nginx:latest "/docker-entrypoint.…" Up 2 hours 0.0.0.0:80->80/tcp web_server a3f6c9e4a231 mysql:8.0 "docker-entrypoint.s…" Exited (1) 2 days ago db_server ``` Burada ne görmeli ki? `web_server` konteyneri aktif durumda çalışıyor. Ancak `db_server` neden durdurulmuş? Eğer siz bu durdurmayı yapmadıysanız, bu bir sorun işareti olabilir. Çöküş mü yaşadı? Kaynak yetersizliği mi var? Zafiyet istismarı mı oldu? **Prosesleri Analiz Edin:** İmajın çalışan proseslerinde beklenmeyen bir işlem var mı? `docker top` ile bu durumu analiz edin: ```plaintext UID PID PPID C STIME TTY TIME CMD root 1234 0 0 10:00 ? 00:00:05 nginx: master process www-data 1240 1234 0 10:00 ? 00:00:02 nginx: worker process ``` Burada dikkat etmeniz gereken nokta: **Neden** `root` kullanıcısı olarak çalışıyor? Buna biraz sonra detaylı çözüm sunacağım, fakat şimdilik bilmeniz gereken şey, bu **kritik bir güvenlik sorunu** olduğudur. **Konteyner Değişikliklerini Tespit Edin:** `docker diff` komutu, konteynerin dosya sisteminde yapılan tüm değişiklikleri gösterir. Bu, imajınızın temiz olup olmadığını anlamanıza yardımcı olur: ```plaintext C /etc A /etc/my.cnf D /var/lib/mysql/tmp ``` Burada: * **C /etc**: `/etc` dizini değiştirilmiş * **A /etc/my.cnf**: Yeni bir dosya eklenmiş * **D /var/lib/mysql/tmp**: Bir dosya veya dizin silinmiş **Uyarı:** Eğer bu değişiklikler imajın temel alındığı duruma uymuyorsa, anormal bir etkinlik söz konusu olabilir. Bir saldırgan veya yanlış yapılandırma, konteynerı değiş tokuş etmiş olabilir. ### 2. Proses Ağacına Derinlemesine Bakarak Anormallikleri Keşfedin Belki konteyner içindeki basit prosesler yeterli değil. Daha derin bir analiz için **pstree** aracını kullanabilirsiniz: ```bash # Proses ağacını görüntülemek için (command -v pstree || sudo apt install -y psmisc) && pstree -p ``` Bu komut, tüm proseslerin bağımlılık ağacını gösterir. Saldırgan bir proses çalıştırıyor mu? Şüpheli bir bağlantı var mı? Bu komut ortaya çıkarır. (**btop** gibi araçlar ile de kontrol sağlayabilirsiniz) ## Docker Konteynerlerinin Taraması: Güvenlik Zafiyetlerini Bulma Bu noktada sorunun gerçek boyutu ortaya çıkıyor. Docker imajları, çoğu zaman üçüncü taraf kaynaklardan gelir ve bu kaynakların güvenilir olduğunu varsaymak **tehlikeli bir hata** olabilir. Zafiyet tespiti için ise **Docker Scout, Trivy** gibi yeni nesil araçlar kullanmalısınız. ### 1. **Docker Scout - Native Docker Çözümü** **Docker Scout**, Docker'ın resmi güvenlik taraması aracıdır ve 2023'ten beri Docker Desktop ve CLI'nin içine entegre edilmiştir. Bu araç, **Docker Hub hesabınız** ile direkt çalışır ve ek kurulum gerektirmez. (Ama olurda gerekirse bkz: [https://docs.docker.com/scout/install](https://docs.docker.com/scout/install/)) **Neden Docker Scout Özel?** * Docker CLI ve Desktop'a direkt entegredir * Uygulamanızın tüm bileşenlerinin detaylı listesini sunar (**SBOM**) * Yeni CVE'ler ortaya çıktığında otomatik olarak raporlarınız güncellenir * Güvenlik açıklarını kapatmak için doğru sürüm önerileri sunar * Vulnerability Exploitability eXchange - açıkların gerçekten kullanılabilir olup olmadığını gösterir (**VEX**) **Kurulum ve Kullanım:** **Docker Scout**, en son **Docker** sürümlerinde varsayılan olarak kurulu gelir. Kullanmak için yalnızca Docker Hub'a giriş yapmanız gerekir: ```bash # Docker Hub hesabınıza giriş yapın docker login # Ardından imaj taraması yapabilirsiniz # ör: docker scout cves {taranacak_imaj} docker scout cves vitodeploy/vito:latest ``` **Docker Scout Komutları:** ```bash # Hızlı genel bakış (5-10 saniye) docker scout quickview vitodeploy/vito:latest # Detaylı CVE raporu docker scout cves vitodeploy/vito:latest # Sadece kritik açıkları göster docker scout cves --only-severity critical vitodeploy/vito:latest # EPSS skor ile sıralama (Exploit Prediction Scoring System) docker scout cves --epss vitodeploy/vito:latest # JSON formatında çıktı (CI/CD'ye entegre etmek için) docker scout cves --format json vitodeploy/vito:latest # Sadece base imajdan gelen açıkları göster docker scout cves --only-base vitodeploy/vito:latest # Sadece düzeltilebilir açıkları göster docker scout cves --only-fixed vitodeploy/vito:latest # CISA KEV kataloğu içindeki tehlikeli açıkları göster docker scout cves --only-cisa-kev vitodeploy/vito:latest # SARIF formatında (güvenlik araçlarına entegre etmek için) docker scout cves --format sarif --output scan_report.sarif vitodeploy/vito:latest # Temel imaj güncelleme önerilerini görüntülek için docker scout recommendations vitodeploy/vito:latest ``` **Docker Scout'un Avantajları:** * **Kurulum yok** - Docker zaten kurlu ise hemen kullanılabilir * **Docker Desktop UI** - Güzel görsel arayüz (Win/Mac vb) * **Otomatik güncelleme** - Yeni CVE'ler bulunduğunda otomatik raporunuz güncellenir * **VEX desteği** - Açıkların gerçekten exploitable olup olmadığını gösterir **Docker Scout'un Sınırlamaları:** * Docker Hub hesabı gerekli * Remote imajları taramak için giriş gerekli * Ücretsiz versiyon sadece açık kaynak projeler için tam özellikleri sunuyor * Secret scanning ve IaC taraması desteklemez ### 2. **Trivy - Açık Kaynaklı Çok Amaçlı Çözüm** **Trivy**, Docker Scout'tan daha fazla özellik sunan açık kaynaklı bir araçtır. Secret detection, IaC (Infrastructure as Code) taraması ve daha esnek kullanım seçenekleri sunar. ([https://trivy.dev](https://www.trivy.dev/)) Ben paket yöneticisi olarak **[nala](https://gitlab.com/volian/nala)**'yı kullanıyorum. Nala, APT'nin üzerine inşa edilmiş daha modern ve okunabilir bir arayüz. Kurulumları daha temiz gösteriyor, paralel indirme yapıyor ve işlem geçmişi tutuyor. Ama endişelenmeyin - eğer siz klasik APT kullanıyorsanız, komutlardaki `nala` yerine `apt` yazmanız yeterli. Birebir aynı şekilde çalışır. **Kurulum:** ```bash sudo nala install wget apt-transport-https gnupg lsb-release wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add - echo deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main | sudo tee -a /etc/apt/sources.list.d/trivy.list sudo nala update sudo nala install trivy -y ``` **Kullanım:** ```bash # Temel tarama trivy image vitodeploy/vito:latest # Sadece kritik açıkları göster trivy image --severity CRITICAL vitodeploy/vito:latest # JSON çıktısı trivy image --format json vitodeploy/vito:latest # Secret scanning de yap trivy image --scan-secret vitodeploy/vito:latest # IaC taraması (Dockerfile, YAML vb.) trivy config ./ # SBOM oluştur trivy image --format cyclonedx --output sbom.json vitodeploy/vito:latest # Belirli bir CVE'yi ara trivy image --severity HIGH,CRITICAL vitodeploy/vito:latest ``` **Trivy'nin Avantajları:** * **Açık kaynaklı** - Tamamen özgür ve şeffaf * **Çok amaçlı** - CVE, secret, IaC, Kubernetes taraması * **Hızlı** - Docker Scout'dan hızlı çalışır * **Giriş gerektirmez** - Kurulu ve kullanılmaya hazır * **Esnek** - Birçok filtreleme seçeneği * **SBOM desteği** - Software Bill of Material oluşturabilir **Trivy'nin Sınırlamaları:** * CLI odaklı - Grafik arayüzü yok * Docker Scout kadar entegreli değil ### 3. **Grype - SBOM Odaklı Tarayıcı** **Grype**, Chainguard tarafından geliştirilmiş, SBOM (Software Bill of Material) üzerinden tarama yapan bir araçtır. Hızlı, doğru ve hafif bir alternatif. ([https://github.com/anchore/grype](https://github.com/anchore/grype)) **Kurulum:** ```bash curl -sSfL https://get.anchore.io/grype | sudo sh -s -- -b /usr/local/bin ``` **Kullanım:** ```bash # Temel tarama grype vitodeploy/vito:latest # Table formatında grype vitodeploy/vito:latest -o table # JSON çıktısı grype vitodeploy/vito:latest -o json # Sadece kritik açıkları göster grype vitodeploy/vito:latest --fail-on critical ``` **Grype'nin Avantajları:** * **Çok hızlı** - En hızlı tarayıcılardan biri * **Temiz çıktı** - Anlaması kolay raporlar * **SBOM merkezli** - DevOps iş akışına uygun * **Hafif** - Docker Scout'tan daha az kaynak kullanır **Grype'nin Sınırlamaları:** * Secret scanning yok * IaC taraması yok ### 4. **Snyk - Kurumsal Çözüm** **Snyk**, özellikle başarılı DevOps/DevSecOps ekipleri tarafından kullanılan, daha güçlü ve ticari bir araçtır. Dependency updates ve vulnerability remediation gibi advanced özellikleri sunar. ([https://snyk.io](https://snyk.io/)) **Kurulum:** ```bash npm install -g snyk snyk auth ``` **Kullanım:** ```bash snyk container test vitodeploy/vito:latest snyk container test --severity-threshold=high vitodeploy/vito:latest ``` ### **Hangi Aracı Seçmeliyim?** | Araç | Avantajı | Dezavantajı | Kurulum | | --- | --- | --- | --- | | **Docker Scout** | Native, entegre, otomatik update | Giriş gerekli, sınırlı özellik | Hazır (Docker içinde) | | **Trivy** | Açık kaynaklı, çok amaçlı, hızlı | CLI odaklı | `apt install trivy` | | **Grype** | Çok hızlı, hafif, temiz | Sınırlı özellik | `curl install` | | **Snyk** | Kurumsal, otomatik fix | Ücretli, giriş gerekli | `npm install` | **Başlangıç için tavsiye:** Docker Scout ile başlayın, daha sonra automation için Trivy'yi ekleyin. ## Tehdit Yüzeyi Anlamak: Docker'da Neler Yanlış Gidebilir? Docker imajlarının güvenliğini sağlamadan önce, hangi risklere karşı korunmamız gerektiğini anlamak önemli. **Üç ana tehdidi kategorize edebilirim:** **1. Bilinmeyen Açıklar (CVE - Common Vulnerabilities and Exposures)** Base imajınızdan başlayarak, uygulamanızın bağımlılıkları arasında defalarca kez paket ve kütüphaneler kullanıyorsunuz. Ubuntu, Debian, Alpine Linux gibi base imajlar, hatta siz bile, bu paketlerin en son sürümünü kullanmıyor olabilirsiniz. Sonuç? **Binlerce bilinen açık, üretim ortamınızda canlı halde bekliyor.** Ama dikkat edin - her CVE kritik değildir. Bazı açıklar sizin kullanım durumunuzda hiç işlev görmeyebilir. **2. Sırlar ve Kimlik Bilgileri** Belki siz de yaptınız - build aşamasında bir .env dosyasını yanlışlıkla Docker imajınıza gömüştünüz. Ya da API anahtarını Dockerfile'a doğrudan yazdınız. Bu bilgiler imaja bir kez girdikten sonra, imajı çalıştırdığınız her container'da erişilebilir durumda kalıyor. Araştırmaya göre, kimlik bilgileri sızdırılan projelerin **%75'i**, kimlik bilgilerini revoke etmemiş - yani yıllarca hack edilebilir durumdalar. **3. Kötü Niyetli İmajlar** Docker Hub'da `mysqldatabase` veya `nginxserver` gibi, popüler araçlara benzeyen **sahte imajlar** bulunuyor. Siz yanlışlıkla bunları çektiğinizde, malware ve backdoor'lar doğrudan üretim ortamınıza giriyor. ## Adım Adım Çözümler: Docker İmajlarınızı Koruma Altına Almak ### 1. Non-Root Kullanıcı ile Çalıştırma - En Kritik Adım En kritik güvenlik önlemi, konteynerı root olarak çalıştırmamaktır. Varsayılan olarak, Docker container'ları `root` kullanıcısı olarak çalıştırır. Eğer bir saldırgan container'a giriş yapabilirse, sistem üzerinde tam kontrol kazanır. **Docker-compose.yml'de uygulanması çok basittir:** ```yaml services: vito: image: vitodeploy/vito:latest container_name: vito-deploy restart: unless-stopped user: "1000:1000" security_opt: - no-new-privileges:true ports: - "3000:3000" volumes: - anchor_data:/data volumes: anchor_data: ``` `user: "1000:1000"` parametresi, konteynerı UID 1000 ve GID 1000 ile çalıştırır. Bu UID ve GID değerleri imajda tanımlanmış olmalıdır. Eğer imaj varsayılan olarak root ile çalışıyorsa, imajı kendi UID/GID'nizle özelleştirmeniz gerekir. `security_opt: no-new-privileges:true` de önemli - bu, konteyner içindeki bir işlemin ayrıcalık yükseltme yapmasını engeller. **Gerçek Dünya Senaryosu:** Bir Node.js uygulaması geliştirirken, root kullanıcı olarak çalışmıştı. Bir zafiyet nedeniyle saldırgan container'a giriş yapabildi. Eğer non-root olarak çalışsaydı, yapabileceği zarar çok daha az olurdu. ### 2. Sırları Runtime'da Yönetin - Asla İmajda Değil Eğer uygulamanız API anahtarına veya veritabanı şifresine ihtiyaç duyuyorsa, **asla** Dockerfile'a koymayın. Bunları runtime'da yönetin: ```yaml services: vito: image: vitodeploy/vito:latest container_name: vito-deploy secrets: - db_password environment: DB_PASSWORD_FILE: /run/secrets/db_password secrets: db_password: file: ./secrets/db_password.txt ``` Ya da daha profesyonel ortamlar için, **Vault**, **AWS Secrets Manager** gibi çözümler kullanın. Bu, gizli anahtarlarınızı merkezi olarak yönetmenizi ve hemen revoke etmenizi sağlar. ### 3. Rootless Mode - İleri Seviye Güvenlik Eğer maksimum güvenlik istiyorsanız, **Rootless Docker** devreye girebilir. Bu, Docker daemon'ın kendisinin de root olmadan çalışmasını sağlar. **Neden önemli?** Varsayılan Docker'da, **daemon root** olarak çalışır. Eğer bir saldırgan container'dan **breakout** yapabilirse, doğrudan host sistemine erişim elde eder. **Rootless mode**'da, daemon bile **non-root** çalışır, böylece **privilege escalation** neredeyse imkansız hale gelir. **Rootless Mode Aktivasyonu:** ```bash # Rootless Docker'ı kurun: https://docs.docker.com/engine/security/rootless dockerd-rootless-setuptool.sh install # Rootless ortamında çalıştığınızı onaylayın docker run --rm busybox whoami # Çıktı: root (ama bu, container içinde namespaced bir "root"tür) # Host'ta aslında hangi user olduğunu görmek için ps aux | grep dockerd # rootless user'ın adı görünür ``` **DİKKAT:** Rootless mode bazı özellikler (GPU erişimi, bazı network modları) desteklemez/sınırlı, ama çoğu üretim uygulaması için yeterlidir! ### 4. Kapasite Sınırlandırması ve Privilege Restriction Bir imaj ne kadar uygun görünse de, container'a sınırlı **resource** ve **capability** vermek, başarılı bir saldırı durumunda hasarı sınırlandırır: ```yaml services: vito: image: vitodeploy/vito:latest container_name: vito-depoloy user: "1000:1000" security_opt: - no-new-privileges:true cap_drop: - ALL cap_add: - NET_BIND_SERVICE deploy: resources: limits: cpus: "0.5" memory: 512M reservations: cpus: "0.25" memory: 256M read_only: true tmpfs: - /tmp - /run ports: - "127.0.0.1:3000:3000" ``` **Ne Yaptı?** * `cap_drop: ALL` - Tüm Linux capabilities'i çıkardık * `cap_add: NET_BIND_SERVICE` - Sadece port binding için gerekli olanı ekledik * `read_only: true` - Dosya sistemi salt okunur * `tmpfs` - Geçici dosyalar için bellek-tabanlı volumes * `127.0.0.1:3000:3000` - Sadece localhost'tan erişim * CPU ve memory sınırları - DoS saldırılarına karşı koruma Bu ayarlar, konteynerı neredeyse "kilitli" bir ortamda çalıştırıyor. Saldırgan, **privilege escalation** yapamazsa ve sistem kaynaklarına da erişemezse, yapabileceği zararın çok sınırlı olduğu anlamına geliyor. ### 5. Minimal Base İmajları Seçin "**Neden bu kadar önemli?**" diye soruyorsunuz. Çünkü bir base imaj ne kadar büyükse, o kadar fazla paket içeriyor ve o kadar fazla potansiyel açık bulunuyor. * **Alpine Linux**: 5MB * **Debian**: 100MB+ * **Ubuntu**: 300MB+ Fark büyük... Dockerfile'ınızda bunu yazıyor olabilirsiniz: ```dockerfile FROM ubuntu:latest ``` Bunu bu şekilde değiştirin: ```dockerfile FROM alpine:3.20 ``` **Multi-Stage Build ile Daha Da İyisi:** Geliştirme sırasında birçok araç ve **dependency**'ye ihtiyacınız olabilir (compiler, test framework'ler). Fakat üretimde bunlara hiç ihtiyaç yok. **Multi-stage build** bunu yapabilir: ```dockerfile # Build stage FROM golang:1.21 AS builder WORKDIR /app COPY . . RUN go build -o myapp . # Runtime stage FROM alpine:3.20 COPY --from=builder /app/myapp /usr/local/bin/ CMD ["myapp"] ``` İlk imaj 2GB, final imaj 50MB. Güvenlik açısından, 50MB'da 2GB'dan çok daha az açık vardır. ### 6. Ağ Segmentasyonu - Container'ları İzole Edin Eğer birden fazla konteyner kullanıyorsanız, varsayılan Docker bridge ağı tüm container'ları birbiriyle iletişim kurması için açık hale getirir. Bu **büyük bir güvenlik açığı** olabilir. ```yaml services: web: image: nginx:latest networks: - web_network ports: - "80:80" db: image: postgres:15 networks: - db_network api: image: myapi:latest networks: - web_network - api_db_network networks: web_network: driver: bridge db_network: driver: bridge api_db_network: driver: bridge ``` Bu yapılandırmada: * **Web** ve **API** birbiriyle konuşabilir * **API** ve **DB** birbiriyle konuşabilir * Fakat **Web** direkt **DB**'ye erişemez Eğer web container'ı hack edilse bile, saldırgan veritabanına direkt erişemez. Bu, **lateral movement'i engeller.** ### 7. Port Güvenliği - Harici Erişimi Kısıtlayın Yalnızca gerekli portları açın ve localhost'a bağlayın: ```yaml ports: - "127.0.0.1:3000:3000" # Sadece localhost'tan erişim - server ip ``` Eğer harici erişim gerekiyorsa, **reverse proxy** (Nginx, Traefik) arkasına koyun ve TLS kullanın. ### 8. Dosya ve Proses Takibi - Anormallikleri Keşfedin Konteyner çalışırken içinde gerçekleşen olayları izlemek, anormal davranışın tespiti için **kritik önem** taşır. **Lsof ve Strace Kullanımı:** ```bash # Dosya bağlantılarını izleme sudo nala install -y lsof lsof | grep -i IPv4 # Sistem çağrılarını kayıt alma (yavaş, dikkat edin) sudo nala install -y strace strace -c -p ``` Bu araçlar, konteynerde ne olup bittiğini görmenizi sağlar. ### 9. İmaj Kaynak Kontrolü - Nereden Geliyorsa Oradan Bilin Docker Hub'da bulduğunuz her imaj, iyi niyetli birisi tarafından oluşturulmamış olabilir. Kontrol etmelisiniz: * **İmaj kimin tarafından yayınlandı?** Resmi kuruluşları (Docker, Github, Red Hat, Canonical vb.) tercih edin * **Kaç kişi bu imajı kullanıyor?** Milyonlar mı, yoksa yüzlerce mi? * **Son güncelleme tarihi nedir?** Eğer 2 yıldır güncellenmediyse, şüpheyle yaklaşmanızda fayda var! * **GitHub repository'si var mı?** Dockerfile ne kadar basit ve şeffaf? ### 10. İmaj Güncellemelerini İzleyin ve Otomatize Edin Düzenli olarak güncellemeleri kontrol edin. İdeal olarak CI/CD pipeline'ınızda Docker Scout veya Trivy'yi otomatize edin: ```bash # GitHub Actions örneği name: Container Security Scan on: [push] jobs: scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: docker/setup-buildx-action@v2 - run: | docker build -t myapp:latest . docker scout cves myapp:latest ``` ## Runtime Security - Üretimde Gerçek Zamanlı Koruma Üretim ortamınızda container'lar çalışırken, **gerçek zamanlı güvenlik izlemesi** yapmanız gerekir. Bu, sadece tarama yapmaktan ötesine geçer. **Sysdig**, **Falco**, **AccuKnox** gibi araçlar, container davranışlarını izler ve anormal aktiviteleri tespit eder. Örneğin: * Container'da açılmayan bir shell session'ı tespit et * Beklenmayan bir ağ bağlantısı engelle * Dosya sistemi değişikliklerini kayıt altına al * Credential sızıntısını canlı olarak tespit et **Runtime Security Araçları:** * [Sysdig](https://sysdig.com) - CNAPP platformu * [Falco](https://falco.org) - CNCF açık kaynaklı runtime detection * [AccuKnox](https://accuknox.com) - Zero Trust CNAPP Bu araçlar, **zero-day exploitler** bile karşısında koruma sağlayabilir. ## Tam Güvenli Yapılandırma Örneği Şu ana kadar öğrendiğiniz her şeyi bir araya getirin: ```yaml version: '3.8' services: vito: image: vitodeploy/vito:latest container_name: vito-deploy restart: unless-stopped # Güvenlik user: "1000:1000" security_opt: - no-new-privileges:true cap_drop: - ALL cap_add: - NET_BIND_SERVICE read_only: true # Gizli Anahtarlar secrets: - api_key environment: API_KEY_FILE: /run/secrets/api_key # Kaynak Sınırlandırması deploy: resources: limits: cpus: "0.5" memory: 512M reservations: cpus: "0.25" memory: 256M # Ağ ve Port ports: - "127.0.0.1:3000:3000" networks: - internal # Geçici Dosyalar tmpfs: - /tmp - /run volumes: - anchor_data:/data volumes: anchor_data: networks: internal: driver: bridge secrets: api_key: file: ./secrets/api_key.txt ``` ## CI/CD'de Docker Scout Entegrasyonu Modern DevSecOps iş akışında, tarama işlemi otomatik olmalıdır. Docker Scout'u CI/CD pipeline'ınıza örnek olarak bu şekilde ekleye bilirsiniz: **GitHub Actions:** ```yaml name: Docker Scout Scan on: push: branches: [main] pull_request: branches: [main] jobs: scout: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: docker/setup-buildx-action@v2 - name: Build Docker image run: docker build -t myapp:${{ github.sha }} . - name: Run Docker Scout run: | docker scout cves myapp:${{ github.sha }} \ --exit-code \ --only-severity critical,high ``` **GitLab CI:** ```yaml security:scan: stage: security script: - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA . - docker scout cves $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA allow_failure: false ``` ## Deneyimlerim **Bir PHP uygulamasında key tespit ettim.** Dockerfile'da `ENV DATABASE_PASSWORD=mysecret123` yazılmıştı. Git history'de de vardı. Bulut sistemine geçtim ve bu şifreyi revoke ettim. Sonrasında, her commit'te secret scanning yapan pre-commit hook'ları kurdum. `git-secrets` veya `truffleHog` harika araçlar. **En sevdiğim hikaye:** Docker Scout ile bir imajda 45 açık buldum. Hızlı bir şekilde `docker scout cves --only-fixed` komutu ile sadece düzeltilebilir olanları filtreledim ve 2 saat içinde hepsini kapattım. **Docker Scout**'un otomatik **remediation önerileri** sayesinde, hangi paket versiyonunu yüklemem gerektiğini tam olarak biliyordum. ## Dikkat: Her CVE Sizi Endişelendiremez Eğer **Docker Scout** veya **Trivy** gibi araçlar kullanıyorsanız, "critical" yazısını gördüğünüzde paniğe kapılmayın. Her critical CVE sizin için kritik değildir. Kendinize şu soruları sorup cevap verin: * Açık hangi kütüphanede? * Siz o kütüphaneyi kullanıyor musunuz? * Kodunuzu nasıl tetikleyebilir? * Sizin mimari bu saldırıya karşı açık mı? Geçtiğimiz ay, Docker Scout'ta "critical" ile işaretlenmiş bir açık gördüm. Ama açık, yalnızca `/etc/passwd` dosyasını yazma iznine sahip olmayan kullanıcılar tarafından tetiklenebiliyordu. Biz her zaman non-root çalıştırıyor olduğumuz için, bu açık bizim için hiç önem taşımıyordu. ## Güvenlik sürekli bir süreç Docker imajı güvenliği bir kez yapılıp biten iş değil; sürekli bir süreç. Her gün yeni CVE'ler bulunuyor, yeni imaj versiyonları çıkıyor, yeni saldırı teknikleri ortaya çıkıyor. Yapmanız gereken: 1. **Taramayı otomatize edin** - Docker Scout veya Trivy'yi CI/CD'ye entegre edin 2. **Minimal imajlar kullanın** - Attack surface'i azaltın 3. **Non-root user'lar çalıştırın** - Privilege escalation'ı engelle 4. **Gizli key’lerinizi ayrı yönetin** - Asla imajın içine koyma 5. **Runtime security kullanın** - Sadece tarama yeterli değil 6. **Ağ segmentasyonu yapın** - Container'ları izole et 7. **Registry'yi kilitleyin** - Sadece güvenilir imajları çek Bu adımları takip ettiğinizde uygulamalarınız çok daha güvende olur. En güzeli, gecenin bir yarısı sunucu açıklarıyla paniğe kapılmazsınız çünkü tarama otomatik çalışıyor ve durumu kontrol altında tutuyorsunuz. Başlamak için ne yapmalısınız? Hemen Docker Hub'daki mevcut imajlarınızı Docker Scout ile tarayın. Tek komut uzağınızda: ```bash docker login # Docker hub hesabınıza giriş gerekli docker scout cves vitodeploy/vito:latest ``` Buna alternatif olarak Trivy'yi de kurup kullanmaya başlayabilirsiniz: ```bash trivy image vitodeploy/vito:latest ``` --- ## Node.js Projelerini Sunucuda Kesintisiz Çalıştırma Yöntemleri - URL: https://erho.dev/post/004-nodejs-projelerini-sunucuda-kesintisiz-calistirma-yontemleri - Markdown: https://erho.dev/post/004-nodejs-projelerini-sunucuda-kesintisiz-calistirma-yontemleri.md - Tarih: 2024-10-29 - Kategori: linux - Etiketler: nodejs, devops Projelerinizin sunucuda kesintiye uğramadan çalışmasını ister misiniz? **Node.js** **tabanlı**, çalışır durumda olan ve terminali kapattığınızda aniden duran projeleriniz size de tanıdık geliyorsa, işte kalıcı çözüm! Geliştirdiğim projeleri canlıya aldığımda en büyük ihtiyaçlarımdan biri, uygulamaların **kesintisiz çalışması** oluyor. Hele ki bir web analiz aracı olan **Umami** gibi kritik bir yazılımı sunucumda (docker olmadan) çalıştırıyorsam, bunun aralıksız hizmet vermesi önemli. Ancak fark ettim ki, `yarn start` komutuyla başlattığımda terminali kapattığım anda uygulama duruyordu… NOT: Buradaki komut `npm`, `pnpm`, `bun`, `yarn` vb olması fark etmez! ![www.erho.dev - ss_01.png](/images/posts/004-nodejs-projelerini-sunucuda-kesintisiz-calistirma-yontemleri/img/ss_01.webp) ![www.erho.dev - status-code-502_cf-bad-gateway.png](/images/posts/004-nodejs-projelerini-sunucuda-kesintisiz-calistirma-yontemleri/img/status-code-502_cf-bad-gateway.webp) **"Peki, bir uygulamayı nasıl kesintisiz çalışır hale getirebilirim?"** sorusuna yanıt ararken karşılaştığım iki etkili yöntem var: [**PM2**](https://pm2.keymetrics.io) ve **systemd**. Bu yazıda, her iki yöntemi de nasıl uyguladığımı adım adım paylaşıyorum. Node.js tabanlı uygulamaların arka planda sorunsuz çalışmasını sağlamak için PM2 ve systemd harika çözümler sunuyor. Bu araçlar sayesinde uygulamam her durumda çalışmaya devam ediyor. Eğer siz de terminal kapandığında uygulamanızın durmasından şikayetçiyseniz, bu yöntemleri mutlaka denemenizi öneririm… **Not:** Örnek olarak "Umami Analitik" projesini kullandım, ancak terminale bağımlı herhangi bir projede bu yöntemleri kendi ihtiyaçlarınıza uyarlayarak benzer sorunları çözebilirsiniz. --- **NOT:** Anlaşılı olabilmesi adına örnek olarak [Umami](https://umami.is) projesi üzerinden anlatım yapacağım... ## **Yöntem 1: pm2 ile Kolay Arkaplan Yönetimi** Bir Node.js uygulamasını arka planda çalıştırmak için tercih edilebilecek en pratik çözüm, **pm2**. pm2, yalnızca arka planda çalıştırmakla kalmaz, aynı zamanda uygulamanız çökerse otomatik yeniden başlatma da sağlar. Ayrıca, sunucu yeniden başlasa bile uygulamanızı otomatik olarak devreye alır. ### **pm2 Kurulumu** Öncelikle pm2’yi kurmak için şu komutu çalıştırıyorum: ```bash npm install -g pm2 ``` ### **Uygulamayı pm2 ile Başlatma** Kurulum tamamlandıktan sonra Umami’yi pm2 ile çalıştırıyorum: ```bash cd umami.domain-adi.com pm2 start "node .next/standalone/server.js" --name umami-tracker ``` Artık uygulamam arka planda çalışıyor. ### **Sunucu Yeniden Başlatıldığında Uygulamayı Otomatik Başlatmak** ```bash pm2 startup pm2 save ``` Bu iki komut, pm2’nin her sistem açılışında otomatik çalışmasını sağlıyor. Sunucum yeniden başlasa bile artık Umami’nin durması diye bir problemim olmuyor! Detaylı kullanım için [bu bağlantı](https://pm2.keymetrics.io/docs/usage/quick-start/)’dan wesitesini ziyaret edebilirsiniz. Ayrıca [https://pm2.io](https://pm2.io) üzerinden de projeniz için daha fazla hakimiyet kurabiliyoruz... [![erho.dev - pm2.io img](https://pm2.io/images/home.gif)](https://pm2.io) --- ## **Yöntem 2: systemd ile Servis Tanımlamak** Eğer pm2 yerine daha sistem odaklı bir çözüm arıyorsanız, **systemd** ile bir servis tanımlayarak Umami’yi kalıcı olarak çalıştırmak mümkün. Bu yöntem, özellikle Linux sunucularda oldukça güçlü ve stabil bir çözüm sağlıyor. İşte bu işlemi nasıl yaptığımı adım adım anlatıyorum: ### **Servis Dosyası Oluşturmak** İlk olarak, systemd için bir servis dosyası oluşturmam gerekiyor: ```bash sudo vim /etc/systemd/system/umami.service ``` ### **Servis Dosyasının İçeriği** Aşağıdaki kodları dosyaya yapıştırıyorum: ```ini [Unit] Description=Umami Tracker Service After=network.target [Service] ExecStart=/usr/bin/node /home/kadi/domain-adi.com/.next/standalone/server.js WorkingDirectory=/home/kadi/domain-adi.com Restart=always User=root Environment=PATH=/usr/bin:/usr/local/bin Environment=NODE_ENV=production [Install] WantedBy=multi-user.target ``` ### **Servisi Etkinleştirmek ve Çalıştırmak** ```bash sudo systemctl daemon-reload sudo systemctl enable umami.service sudo systemctl start umami.service ``` ### **Servisin Durumunu Kontrol Etmek** Her şeyin sorunsuz çalıştığından emin olmak için şu komutu kullanıyorum: ```bash sudo systemctl status umami.service ``` --- ## **Hangi Yöntemi Tercih Etmeliyim?** İki yöntemin de kendine göre avantajları var. **pm2**, kurulumu ve yönetimi kolay bir araç olarak öne çıkıyor. Eğer birden fazla Node.js uygulamanız varsa ve kolayca kontrol etmek istiyorsanız, pm2 sizin için ideal. Öte yandan, **systemd** daha düşük seviyeli, ancak daha güçlü bir çözüm. Sistem açılış süreçlerine doğrudan entegre olması, daha fazla kontrol sağlıyor. Artık bu çözümleri hayata geçirdiğim için, uygulamalarımın stabil çalışacağından eminim. Terminal kapandı mı, sunucu yeniden mi başlatıldı? Hiç önemli değil... Peki siz bu tarz bir durumda nasıl bir çözüm kullanıyorsunuz? Yorumlarda belirtebilirsiniz! --- ## Docker Nedir, Nasıl ve Neden Kullanılır? - URL: https://erho.dev/post/003-docker-nedir-nasil-ve-neden-kullanilir - Markdown: https://erho.dev/post/003-docker-nedir-nasil-ve-neden-kullanilir.md - Tarih: 2024-10-22 - Kategori: linux - Etiketler: docker, github, devops Geliştirdiğiniz uygulamanın bir makinede mükemmel çalışırken, başka bir ortamda sorun çıkardığına şahit oldunuz mu? Veya bir sunucuda sorunsuz çalışan bir yazılımın, güncellemelerden sonra beklenmedik hatalar verdiği anlarla karşılaştınız mı? İşte Docker, tam da bu noktada size hayat kurtaran bir çözüm sunuyor. Bu yazıda, Docker’ın ne olduğundan, neden bu kadar popüler hale geldiğinden ve nasıl işinizi ciddi anlamda kolaylaştırabileceğinden bahsedeceğim. Üstelik size birkaç pratik ipucu vererek, Docker’ı hızla kullanmaya başlamanızı sağlayacağım. **Eğer bir yazılımcı ya da DevOps mühendisiyseniz ve hala Docker kullanmıyorsanız, eksik bir parça olduğunu hissedeceksiniz.** Hazırsanız, hadi başlayalım! --- ## Docker Nedir? Docker, yazılımlarınızı her yerde, her zaman, **aynı sorunsuzlukta** çalıştırmanızı sağlayan bir konteyner platformudur. Bu, bir geliştiricinin en sık yaşadığı “çalışıyor ama… başka bir ortamda değil” problemini ortadan kaldırır. Docker sayesinde uygulamalar, bağımlılıklarıyla birlikte izole bir ortamda çalışır, böylece yazılım geliştirme ve dağıtım süreciniz çok daha tutarlı hale gelir. **Artık “Benim bilgisayarımda çalışıyor!” cümlesini tarihe gömebiliriz.** ### Docker’ın Temel Bileşenleri 1. **Konteyner**: Uygulamanızın izole bir ortamda çalışmasını sağlar. Bu sayede, sistem kaynaklarını verimli kullanarak uygulamalar arasında bir çakışma yaşamazsınız. 2. **İmaj**: Konteyner oluşturmak için gerekli tüm yapılandırmayı içeren şablondur. Geliştirdiğiniz uygulamanın her ortama aynı şekilde dağıtılmasını sağlar. 3. **Docker Daemon**: İmajlarınızı ve konteynerlerinizi yöneten “arka plandaki beyin”dir. Siz komut verirsiniz, o işini yapar. 4. **Docker Hub**: İmajlarınızı sakladığınız, paylaştığınız ve başkalarından ilham aldığınız bulut platformudur. Kendi projelerinizi daha geniş bir kitleye duyurmanın da harika bir yolu. ## Neden Docker Kullanmalıyız? Eğer projelerinizi daha hızlı dağıtmak, kaynaklarınızı daha verimli kullanmak ve sürekli başınızı ağrıtan “çalışmıyor” problemini ortadan kaldırmak istiyorsanız, Docker sizin için biçilmiş kaftan. İşte Docker’ı neden kullanmanız gerektiğine dair en kritik nedenler: ### 1. Taşınabilirlik ve Tutarlılık Docker ile uygulamanız bir makinede nasıl çalışıyorsa, **her yerde aynı şekilde çalışır**. Geliştirme, test ve üretim ortamları arasında “acaba çalışacak mı?” stresi yaşamazsınız. Uygulamanız, her yerde aynı Docker imajıyla çalışır. **Bir kez yapılandırın, her yerde sorunsuz kullanın.** ### 2. Kaynak Verimliliği Geleneksel sanal makineler, büyük bir kaynak israfına sebep olabilir. Docker ise işletim sistemi çekirdeğini paylaştığı için çok daha hafif ve hızlıdır. Bir sunucuda çok sayıda Docker konteyneri çalıştırarak **aynı donanımla daha fazla iş yapabilirsiniz.** ### 3. Hızlı Dağıtım Yavaş dağıtım süreçlerinden sıkıldınız mı? Docker ile uygulamanızı dakikalar içinde başlatabilir, **değişiklikleri hızla hayata geçirebilirsiniz**. CI/CD süreçlerinde Docker kullanmak, ekibinizin hızını ve verimliliğini artıracaktır. ### 4. İzolasyon Docker, uygulamalarınızı izole bir ortamda çalıştırdığı için, bir uygulamanın diğerini bozması gibi bir problem yaşamazsınız. Her şey **güvenli, izole ve kontrol altında**. ### 5. Versiyon Kontrolü Docker imajlarıyla sürüm kontrolü yapmak çok kolaydır. Uygulamanızın her versiyonunu Docker ile takip edebilir, **hangi versiyonun ne zaman dağıtıldığını her zaman bilirsiniz.** Geri dönüş yapmak gerekirse, sadece eski imajınızı çalıştırmanız yeterli. ## Docker ile Başlarken Docker’ın gücünden yararlanmaya başlamak için temel birkaç komutu öğrenmeniz yeterli. İşte Docker ile en çok kullanacağınız komutlar: ### Oluşturma ve Çalıştırma * **İmaj İndirme**: ```bash docker pull {image} # Belirtilen imajı indirir docker pull {image}:{version} # Belirtilen sürümdeki imajı indirir ``` * **Konteyner Oluşturma ve Çalıştırma**: ```bash docker run {image} # Yeni bir konteyner oluşturur ve çalıştırır docker run -d {image} # Arka planda çalıştırır docker run -it {image} # Terminal etkileşimi ile çalıştırır docker run --name {container_name} {image} # Belirli bir isimle çalıştırır ``` * **Çalışanları Listeleme**: ```bash docker ps -a # Tüm konteynerleri listeler docker images # Mevcut imajları listeler ``` ### Silme İşlemleri * **Konteyner Silme**: ```bash docker rm {container_id} # Belirtilen konteyneri siler docker container rm $(docker container ls -aq) # Tüm konteynerleri siler ``` * **İmaj Silme**: ```bash docker rmi {image_id} # Belirtilen imajı siler ``` ### Durdurma ve Yeniden Başlatma * **Durdurma**: ```bash docker stop {container_id} # Konteyneri durdurur docker kill {container_id} # Konteyneri zorla durdurur ``` * **Yeniden Başlatma**: ```bash docker start {container_id} # Durdurulmuş bir konteyneri başlatır ``` ## Docker ile Veritabanı Yönetimi Docker, veritabanlarıyla çalışmayı da kolaylaştırır. Örneğin, bir PostgreSQL veritabanını hızlıca başlatmak için şu adımları izleyebilirsiniz: ```bash docker run --name postgres -e POSTGRES_PASSWORD={db_sifreniz} -d postgres ``` Bu komut, `postgres` adında bir konteyner oluşturur ve `db_sifreniz` şifresiyle PostgreSQL veritabanını başlatır. Veritabanı bağlantıları için gerekli olan port yönlendirmelerini ayarlamayı unutmayın! ### Kalıcı Veri Tutma Docker konteynerleri varsayılan olarak geçici (stateless) veri saklar. Ancak, kalıcı veri tutmak için volume (hacim) oluşturmak gerekir. Aşağıda bir örnek verilmiştir: ```bash docker run -d -v /doya/yolu/data:/data --name mydata myimage ``` Bu komut, yerel dosya sistemindeki `/doya/yolu/data` dizinini konteyner içindeki `/data` dizinine bağlar. Böylece konteyner silinse bile veriler kaybolmaz. ## Docker Compose Kullanımı Docker Compose, çoklu konteyner uygulamalarını yönetmek için kullanılır. Projenizdeki tüm hizmetleri tanımlamak için bir `docker-compose.yml` dosyası oluşturabilirsiniz. Aşağıda basit bir örnek verilmiştir: ```yaml version: "3.8" services: linkstack: hostname: 'linkstack' image: 'linkstackorg/linkstack:latest' environment: TZ: 'Europe/Istanbul' SERVER_ADMIN: 'admin@erho.dev' HTTP_SERVER_NAME: 'erho.dev' HTTPS_SERVER_NAME: 'erho.dev' LOG_LEVEL: 'info' PHP_MEMORY_LIMIT: '256M' UPLOAD_MAX_FILESIZE: '8M' volumes: - '/htdocs' ports: - '8080:80' - '8081:443' restart: unless-stopped user: apache:apache # read_only: true depends_on: - mysql links: - mysql mysql: image: mysql:8 environment: MYSQL_ROOT_PASSWORD: "db_sifreniz" ports: - 3306:3306 ``` Komut satırında `docker-compose up` yazarak bu hizmetleri başlatabilirsiniz. ### Konteynerleri Güncelleme Aşağıdaki komutları kullanarak yeni yapılandırmaya göre imajları güncelleyip çalıştırabilirsiniz: ```bash # Yalnızca yapılandırmayı yeniden oluşturun, ancak volume'leri (veri) silmeyin. docker-compose up -d --force-recreate --build ``` Bu komut, `docker-compose.yml` dosyasını temel alarak yeni imajları oluşturur ve mevcut volume'leri koruyarak konteynerleri yeniden başlatır. Bu sayede verileriniz kaybolmaz. ```bash docker-compose up -d --force-recreate --remove-orphans ``` Bu komut aşağıdaki işlemleri yapar: 1. `up -d`: Docker Compose'u arka planda (`-d`) başlatır ve `docker-compose.yml` dosyasına göre servisleri çalıştırır. 2. `--force-recreate`: Mevcut tüm konteynerleri yeniden oluşturur, yani yapılandırma değişmiş olsa da olmasa da mevcut konteynerleri durdurur ve tamamen yeniden yaratır. Bu sırada volume’leri (veri) silmez, yani veri kaybı olmaz. 3. `--remove-orphans`: `docker-compose.yml` dosyasında tanımlı olmayan ancak aynı ağ üzerinde çalışan diğer konteynerleri (yetim konteynerleri) durdurur ve siler. Bu, yalnızca ilgili `docker-compose.yml` yapılandırmasında olmayan, gereksiz konteynerlerin ortadan kaldırılmasını sağlar. Bu komut, `docker-compose.yml` dosyasındaki tüm servisleri yeniden oluşturur ve güncel olmayan konteynerlerden (yetim) kurtulurken verilerinizi silmeden yeni yapılandırmayı uygular. ### 3. Güncelleme Sonrası Durumu Kontrol Etme Güncellemelerin başarıyla uygulandığından emin olmak için aşağıdaki komutları kullanarak konteynerlerin durumunu ve loglarını kontrol edebilirsiniz: ```bash docker-compose ps # Konteyner durumunu kontrol eder docker-compose logs -f # Günlükleri takip eder ``` ### Ağ Yönetimi Docker, konteynerler arası iletişimi sağlamak için ağ yapılandırmalarına olanak tanır. Kullanıcı tanımlı özel ağlar oluşturabilir ve konteynerlerinizi bu ağlar üzerinde çalıştırabilirsiniz. * **Ağ Oluşturma**: ```bash docker network create --driver bridge mynetwork # Yeni bir ağ oluşturur ``` * **Ağ Listeleme**: ```bash docker network ls # Mevcut ağları listeler ``` * **Ağ Silme**: ```bash docker network rm {network_name} # Belirtilen ağı siler ``` ### Docker Hub’a Pushlama Kendi oluşturduğunuz imajları paylaşmak için Docker Hub’a yükleyebilirsiniz. Bu, işbirlikçi projelerde yararlıdır. ```bash docker login # Docker Hub’a giriş yapar docker tag {image_adi} {dockerhub_kullanici_adiniz}/{yeni_imaj_adi} # İmajı Docker Hub’a yüklemek için etiketler docker push {your_dockerhub_username}/{new_image_name} # İmajı Docker Hub’a yükler ``` ### Toplu Temizlik Docker kullanırken biriken kullanılmayan konteyner ve imajları temizlemek önemlidir. Aşağıdaki komutlar kullanılmayan tüm konteyner ve imajları temizler: ```bash docker container prune # Kullanılmayan konteynerleri siler docker image prune # Kullanılmayan imajları siler ``` Bu yazı, [Docker Resmi Dokümantasyonu](https://docs.docker.com), [KablosuzKedi'nin Docker Videoları](https://youtube.com/watch?v=4XVfmGE1F_w&list=PL_f2F0Oyaj4_xkCDqnRWp4p5ypjDeC0kO) ve kişisel deneyimlerim doğrultusunda hazırlanmıştır. Hala Docker kullanmıyorsanız, emin olun eksik bir şeyler var! Docker kullanmaya başlamak sadece birkaç komut uzağınızda. Üstelik öğrendiğinizde, projelerinizi daha tutarlı, güvenilir ve verimli bir şekilde yönetebileceksiniz. Docker’ın sunduğu bu gücü kendi projelerinizde denemekten çekinmeyin. **Bu yazı, Docker’a ilk adımı atmanız için küçük bir rehber niteliğinde hazırlandı.** Docker hakkında daha fazla bilgi edinmek için resmi dokümantasyonu incelemenizi ve KablosuzKedi'nin binbir emekle hazırladığı Docker videolarını da ayrıca izlemenizi şiddetle tavsiye ederim! Sorularınız ya da daha fazla bilgi almak isterseniz, yorumlarda belirtin... Docker yolculuğunuzda size yardımcı olmaktan memnuniyet duyarım! **Dipnot**: Bu yazı zaman zaman güncellenebilir, bu yüzden takipte kalın! --- ## Linux'te Terminal Üzerinden Dosya İçeriği Arama Yöntemleri - URL: https://erho.dev/post/002-linuxte-terminalden-dosya-icerigi-arama-kapsamli-rehber - Markdown: https://erho.dev/post/002-linuxte-terminalden-dosya-icerigi-arama-kapsamli-rehber.md - Tarih: 2024-10-11 - Kategori: linux - Etiketler: linux Hiç binlerce dosya arasında önemli bir bilgiyi aramak zorunda kaldınız mı? Ya da belki de büyük log dosyalarında kritik bir hata mesajını bulmaya çalıştınız? Eğer öyleyse, bu rehber tam size göre! Linux sunucularında çalışırken, dosyalar içinde belirli bir metni veya deseni aramak sık karşılaşılan ve bazen de kafa karıştırıcı olabilir... Ama endişelenmeyin! Bu rehberde, sistem yöneticileri, geliştiriciler ve Linux tutkunları için terminal üzerinden etkili arama tekniklerini keşfedeceğiz. Temel komutlardan ileri düzey kullanıma kadar, Linux sunucunuzda dosya içeriği aramanın tüm yönlerini ele alacağız... ## 1. Temel Grep Kullanımı `grep` komutu, Linux sunucularında metin aramak için kullanılan temel ve vazgeçilmez bir araçtır. Adeta bir dedektif gibi, dosyalarınızın içinde istediğiniz bilgiyi bulmanıza yardımcı olur. İşte bazı temel kullanım örnekleri: ```bash # Tek bir dosyada arama grep "404 Not Found" /var/log/nginx/error.log ``` Bu komut, Nginx'in hata log dosyasında "404 Not Found" ifadesini arar. Web sunucunuzda eksik sayfaları tespit etmek için harika bir başlangıç! ```bash # Büyük/küçük harf duyarsız arama grep -i "error" /var/log/syslog ``` `-i` ile grep'e "Hey, büyük küçük harf farketmez, sen sadece 'error' kelimesini bul!" demiş oluyoruz. Bu, farklı şekillerde yazılmış hata mesajlarını yakalamak için çok kullanışlıdır. ```bash # Eşleşen satır sayısını gösterme grep -c "Failed password" /var/log/auth.log ``` Bu komutla, başarısız giriş denemelerinin sayısını öğrenebilirsiniz. Eğer bu sayı beklenenden yüksekse, belki de bir güvenlik incelemesi yapmanın zamanı gelmiştir! **Pratik Uygulama**: Kendi sunucunuzda en sık karşılaştığınız hata mesajını grep ile arayın. Sonuçlar sizi şaşırttı mı? ## 2. Rekürsif (Recursive) Arama Bazen aradığımız iğnenin hangi samanlıkta olduğunu bilmeyiz. İşte burada rekürsif arama devreye girer: ```bash # Tüm alt dizinlerde arama grep -r "database connection failed" /var/www/ ``` Bu komut, web uygulamanızın tüm dosyalarında veritabanı bağlantı hatalarını arar. Performans sorunlarının kaynağını bulmak için birebir! ```bash # Sembolik bağlantıları takip ederek arama grep -R "config" /etc/ ``` `-R` ile grep'e "Tüm kapıları aç, tüm köşelere bak!" der. Sembolik bağlantıları da takip ederek, gizlenmiş yapılandırma dosyalarını bile bulabilirsiniz. **Dipnot**: Büyük dizinlerde arama yaparken sabırlı olmanızda fayda var. `grep` bazen biraz yavaş çalışabilir! ## 3. Aranan Değeri Değiştirme Bu işlemi terminalde yapabilmek için `sed` veya `perl` gibi araçları kullanabilirsiniz. ### Yöntem 1: `sed` Kullanarak Değiştirme Bu yöntem, dosyalar içindeki tüm eşleşen değerleri başka bir değerle değiştirecektir. #### Örnek: Tüm dosyalarda `eski_deger` ifadesini `yeni_deger` ile değiştirmek için: ```bash find /home/kadi -type f -exec sed -i 's/eski_deger/yeni_deger/g' {} + ``` * `find` komutu, belirtilen dizindeki tüm dosyaları bulur. * `sed -i 's/.../.../g'` ifadesi, her dosyada tüm eşleşmeleri değiştirir. ### Yöntem 2: `perl` ile Gelişmiş Değişiklik Eğer karmaşık bir arama veya yedekleme yapmak istiyorsanız, `perl` kullanabilirsiniz. #### Örnek: ```bash perl -pi.bak -e 's/eski_deger/yeni_deger/g' $(grep -rl "eski_deger" /home/kadi/) ``` * `perl -pi.bak` her dosyanın yedeğini `.bak` uzantısıyla alır. * `grep -rl` komutu, belirtilen dizindeki eşleşen dosyaların listesini döndürür. ## 4. Çıktı Formatını Özelleştirme Arama sonuçlarınızı daha okunabilir ve göz alıcı hale getirmek ister misiniz? İşte size birkaç ipucu: ```bash # Satır numaralarını gösterme grep -n "ssl_certificate" /etc/nginx/sites-available/* ``` Bu komut, SSL sertifikalarınızın tam olarak hangi satırda tanımlandığını gösterir. Yapılandırma dosyalarınızda hızlı değişiklikler yapmak için süper! ```bash # Eşleşen metni renklendir grep --color=auto "WARNING" /var/log/syslog ``` Renkli çıktılar sadece güzel görünmekle kalmaz, aynı zamanda önemli bilgileri hızlıca farketmenizi sağlar. Bu komutla uyarılar artık gözünüzden kaçmayacak! ```bash # Sadece dosya adlarını listele grep -l "server {" /etc/nginx/sites-available/* ``` Bazen sadece hangi dosyaların eşleştiğini bilmek isteriz. Bu komut, Nginx sunucu bloklarını içeren tüm dosyaları listeleyecektir. **Renklendir**: Kendi renk şemanızı oluşturun! `export GREP_COLORS="ms=01;31:mc=01;31:sl=:cx=:fn=35:ln=32:bn=32:se=36"` gibi bir komutla grep çıktılarınızı kişiselleştirin. ## 5. Regex ile Gelişmiş Arama Düzenli ifadeler (regex), arama yeteneklerinizi süper güçlere dönüştürür. İşte size birkaç örnek: ```bash # IP adreslerini bul grep -E "\b([0-9]{1,3}\.){3}[0-9]{1,3}\b" /var/log/nginx/access.log ``` Sitenizi kimlerin ziyaret ettiğini merak mı ediyor musunuz? Bu komut ile Nginx erişim loglarındaki tüm IP adreslerini bulabilirsiniz. ```bash # E-posta adreslerini bul grep -E "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}" /aradiginiz/dosya/yolu ``` Bu komut, belirli bir dosyadaki tüm e-posta adreslerini bulur. Müşteri verilerinizi temizlemeniz veya spam filtrelerinizi güçlendirmeniz gerekiyorsa işte size bir fırsat! **Beyin Jimnastiği**: Kendi regex deseninizi oluşturmaya ne dersiniz? Örneğin, geçerli bir kredi kartı numarasını bulacak bir regex yazabilir misiniz? ## 6. Belirli Dosya Türlerinde Arama Bazen sadece belirli dosya türlerinde arama yapmak isteriz. İşte size birkaç örnek: ```bash # Sadece PHP dosyalarında arama grep -r --include="*.php" "mysqli_connect" /var/www/ ``` Bu komut, web uygulamanızdaki tüm veritabanı bağlantılarını bulmanıza yardımcı olur. Güvenlik denetimi için birebir! ```bash # Sadece yapılandırma dosyalarında arama grep -r --include="*.conf" "listen 80" /etc/ ``` 80 portunu dinleyen tüm servisleri bulmak için harika bir yöntem. Açık portları kontrol etmek için kullanabilirsiniz. **Görev**: Sisteminizdeki tüm `.env` dosyalarını bulun ve içlerindeki hassas bilgileri kontrol edin. Hangi grep komutlarını kullanırdınız? ## 7. Hariç Tutma ve Filtreleme Bazen ne aradığımızdan çok, neyi aramadığımızı bilmek önemlidir: ```bash # Belirli bir dizini hariç tutma grep -r "error" /var/log/ --exclude-dir=apache2 ``` Bu komut, Apache logları hariç tüm log dosyalarında hata arar. Diğer servislerde sorun olup olmadığını kontrol etmek için harika bir yöntem! ```bash # Belirli dosya türlerini hariç tutma grep -r "TODO" . --exclude="*.min.js" ``` Projenizde kalan TODO'ları ararken minify edilmiş JavaScript dosyalarını atlayın. Kod kalitenizi artırmak için mükemmel bir fırsat! **Temizlik Görevi**: Sisteminizde gereksiz log dosyalarını bulun ve temizleyin. Hangi grep komutlarını kullanırdınız? ## 8. Performans İyileştirmeleri Büyük dosyalarda veya geniş dizinlerde arama yaparken performans kritiktir. İşte size fikir verecek birkaç kullanım örneği: ```bash # Paralel arama için GNU Parallel kullanımı find . -type f | parallel -k grep -H "pattern" {} ``` Bu komut, çok çekirdekli işlemcinizin tüm gücünü kullanarak aramanızı hızlandırır. Sanki süper bilgisayarınız varmış gibi! ```bash # Önbelleğe alınmış dosyalarda arama grep "pattern" $(find . -type f -print0 | xargs -0 stat -c '%X %n' | sort -rnk1 | head -1000 | cut -d' ' -f2-) ``` Bu karmaşık görünen komut, en son değiştirilen 1000 dosyada arama yapar. Sistem kaynaklarını verimli kullanmak için harika bir yöntem! ⏱ **Hız Testi**: Büyük bir log dosyasında normal grep ile paralel grep'in performansını karşılaştırın. Fark sizi şaşırtabilir! ## 9. Alternatif Arama Araçları Grep harika bir araç, ama bazen farklı yeteneklere ihtiyaç duyarız: ```bash # ack-grep ile arama (daha hızlı ve programcı dostu) sudo apt install ack-grep ack "function" --php ``` ack, özellikle kod tabanlarında arama yapmak için optimize edilmiştir. Bu örnekte, PHP dosyalarında fonksiyonları arar. ```bash # The Silver Searcher (ag) kullanımı sudo apt install silversearcher-ag ag "TODO" --ignore-dir=vendor ``` ag, grep'ten çok daha hızlıdır ve varsayılan olarak .gitignore dosyanızı dikkate alır. Bu örnek ile projenizdeki TODO'ları rahatça bulabilirsiniz. **Araç Karşılaştırması**: grep, ack ve ag'yi kendi projenizde test edin. Hangisi sizin iş akışınıza en uygun olduğunu yorumlarda tartışalım! ## 10. Güvenlik ve İzinler: Güvenli Arama Güvenlik her zaman ön planda olmalıdır, özellikle de hassas dosyalarda arama yaparken: ```bash # Sadece okunabilir dosyalarda arama grep -r "password" /etc/ 2>/dev/null ``` Bu komut, sadece okuma izniniz olan dosyalarda şifre araması yapar. Hata mesajlarını görmezden gelerek temiz bir çıktı alırsınız. ```bash # Belirli bir kullanıcıya ait dosyalarda arama sudo -u www-data grep -r "error" /var/www/ ``` Bu örnek ile web sunucusu kullanıcısı olarak arama yaparak, sadece web uygulamanızın erişebileceği dosyalarda hata ararsınız. **Güvenlik Denetimi**: Sisteminizdeki tüm "600" izinli dosyaları bulmanızı ve içlerinde hassas bilgi olup olmadığını kontrol etmenizi isteselerdi, nasıl bir komut kullanırdınız? ## 11. Pratik Senaryolar ve Örnekler: Gerçek Hayat Problemleri Teorik bilgiler güzel, ama gerçek hayatta nasıl kullanacağınızı bilmek daha önemli! İşte size birkaç senaryo: ### Senaryo 1: Güvenlik Denetimi Şüpheli IP adreslerini nginx loglarında arama: ```bash grep -E "192.168.1.[0-9]{1,3}.*400" /var/log/nginx/access.log ``` Bu komut, iç ağınızdan gelen ve 400 hata kodu üreten tüm istekleri bulur. İç tehditler konusunda sizi uyarır! ### Senaryo 2: Yapılandırma Yönetimi Tüm Nginx sanal host yapılandırmalarında SSL sertifikalarını kontrol etme: ```bash grep -r "SSLCertificateFile" /etc/nginx/sites-available/ | cut -d: -f2 | sort | uniq ``` Bu komut, tüm SSL sertifikalarınızın bir listesini çıkarır. Süresi dolan sertifika kalmadığından emin olunuz! ### Senaryo 3: Hata Ayıklama PHP hata loglarında belirli bir tarihte meydana gelen hataları bulma: ```bash grep "$(date +'%d-%b-%Y')" /var/log/php/error.log | grep -i "fatal error" ``` Bu komut, bugünkü tarihte meydana gelen tüm PHP ölümcül hatalarını listeler. Uygulamanızın sağlığını kontrol etmek için mükemmel! ### Senaryo 4: Sistem Performans Analizi Yüksek CPU kullanımına neden olan işlemleri bulma: ```bash grep "CPU" /var/log/syslog | sort -k6 -rn | head -n 10 ``` Bu komut, en çok CPU tüketen 10 işlemi listeler. Sisteminizi yavaşlatan şeyleri keşfetmek için harika bir başlangıç noktası! ### Senaryo 5: Veritabanı Sorgu Optimizasyonu MySQL yavaş sorgu günlüğünde uzun süren sorguları bulma: ```bash grep -E "Query_time: [0-9.]{4,}" /var/log/mysql/mysql-slow.log | sort -k3 -rn ``` Bu komut, en uzun süren MySQL sorgularını bulur ve sıralar. Veritabanı performansınızı artırmak için optimize edilecek sorguları belirlemede çok yardımcı olur! **Dedektif Görevi**: Kendi sisteminizde son 24 saat içinde meydana gelen en ilginç olayı bulmaya çalışın. Hangi grep komutlarını kullanırdınız? Yorumlarda paylaşın! ## 12. İleri Düzey Grep Teknikleri: Güçlü Arama Şimdi biraz daha derine inelim ve grep'in gerçek gücünü ortaya çıkaralım! ### 12.1. Context Lines: Hikayenin Tamamını Görmek Bazen sadece eşleşen satırı görmek yetmez, öncesini ve sonrasını da görmek isteriz: ```bash # Eşleşen satırın önceki ve sonraki 3 satırını göster grep -C 3 "error" /var/log/syslog ``` Bu komut, hata mesajının öncesinde ve sonrasında ne olduğunu görmenizi sağlar. Hata bağlamını anlamak için kullanılan yaygın bir tekniktir. ```bash # Sadece önceki 2 satırı göster grep -B 2 "warning" /var/log/apache2/error.log ``` Uyarıya neyin yol açtığını görmek için bu komutu kullanabilirsiniz. Sorunları tespit etmek için harika bir yol! ```bash # Sadece sonraki 2 satırı göster grep -A 2 "critical" /var/log/application.log ``` Kritik bir hatanın sonuçlarını anlamak için bu komutu kullanabilirsiniz. Hızlı bir hata ayıklama için mükemmel! **Senaryo Oyunu**: Bir web uygulamasında 500 hatası aldınız. Bu hatanın nedenini ve sonuçlarını araştırmak için hangi grep komutlarını kullanırdınız? Yorumlarda belirtin! ### 12.2. Çoklu Desen Arama: Birden Fazla İğneyi Bulmak Bazen birden fazla şeyi aynı anda aramak isteyebiliriz: ```bash grep -E "error|warning|critical" /var/log/syslog ``` Bu komut, tek bir hamlede tüm hata seviyelerini yakalar. Sistem durumunu hızlıca değerlendirmek için harika bir yöntem! ### 13.3. Negatif Arama: Olmayan Şeyleri Bulmak Bazen bir şeyin olmaması da önemli bir bilgi olabilir: ```bash grep -v "success" /var/log/application.log ``` Bu komut, başarılı olmayan tüm işlemleri gösterir. Sorunları tespit etmek için oldukça kullanışlıdır. **Bulmaca Zamanı**: Sisteminizdeki tüm yapılandırma dosyalarını bulun, ancak yorum satırlarını ve boş satırları hariç tutun. Bu görevi nasıl gerçekleştirirdiniz? Yorumlarda tartışalım! ## 13. Grep Sonuçlarını İşleme: Veri Madenciliğinin Temelleri Grep'in gerçek gücü, diğer komutlarla birleştirildiğinde ortaya çıkar: ```bash # En sık karşılaşılan hata mesajlarını bulma grep "error" /var/log/application.log | sort | uniq -c | sort -nr | head -n 10 ``` Bu komut zinciri, en sık karşılaşılan 10 hata mesajını sıralar. Hangi sorunlara öncelik vermeniz gerektiğini anlamak için mükemmel bir kullanım örneği! ```bash # Belirli bir zaman aralığındaki logları filtreleme grep "2024-10-11" /var/log/syslog | grep -E "error|warning" ``` Bu komut, belirli bir günde meydana gelen tüm hata ve uyarıları gösterir. Günlük sistem sağlığı kontrolü için ideal! ```bash # IP adreslerine göre başarısız giriş denemelerini gruplama grep "Failed password" /var/log/auth.log | awk '{print $11}' | sort | uniq -c | sort -nr ``` Bu komut, en çok başarısız giriş denemesi yapan IP adreslerini listeler. Potansiyel saldırıları tespit etmek için harika! **Veri Analizi Görevi**: Kendi log dosyalarınızdan ilginç bir istatistik çıkarın. Örneğin, gün içinde en çok ziyaret edilen web sayfalarını bulun. Bunu nasıl yapardınız? Yorumlarda belirtin! ## 14. Grep Alternatiflerini Derinlemesine İnceleme Grep harika bir araç, ama bazen farklı yeteneklere sahip araçlar işimizi daha da kolaylaştırabilir. İşte size iki popüler alternatif: ### 14.1. ripgrep (rg): Hızlı ve Modern ripgrep, hız ve kullanım kolaylığı açısından öne çıkan modern bir arama aracıdır: ```bash # ripgrep kurulumu sudo apt install -y ripgrep # Tüm alt dizinlerde arama rg "pattern" /aradiginiz/klasor/yolu # Sadece belirli dosya türlerinde arama rg -t php "function" /var/www/ # Gizli dosyalarda ve dizinlerde arama rg -uu "secret" /home/user/ ``` ripgrep, özellikle büyük kod tabanlarında grep'ten çok daha hızlı çalışır ve varsayılan olarak `.gitignore` dosyalarını dikkate alır. ### 14.2. fzf (Fuzzy Finder): Etkileşimli Arama fzf, interaktif arama ve filtreleme için kullanışlı bir araçtır: ```bash # fzf kurulumu sudo apt install fzf # Dosya içeriğinde interaktif arama grep -r "pattern" /aradiginiz/klasor/yolu | fzf # Dosya adlarında interaktif arama find /aradiginiz/klasor/yolu -type f | fzf ``` fzf, özellikle büyük sonuç kümelerinde gezinmek için mükemmeldir. Arama yaparken sonuçları anında filtreleyebilirsiniz. **Deney Zamanı**: grep, ripgrep ve fzf'i kendi projenizde karşılaştırın. Hangi durumda hangi aracı tercih ederdiniz ve neden? Yorumlarda paylaşın! ## 15. Grep ve Düzenli İfadeler: İleri Seviye Örnekler Düzenli ifadelerin (regex) gücünü kullanarak daha karmaşık aramalar yapabilirsiniz: ```bash # Geçerli bir IPv4 adresi arama grep -E "\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b" /var/log/auth.log ``` Bu komut, log dosyasındaki tüm geçerli IPv4 adreslerini bulur. Şüpheli IP'leri tespit etmek için kullanışlı! ```bash # 10 haneli telefon numaralarını bulma grep -E "\b\d{3}[-.]?\d{3}[-.]?\d{4}\b" /aradiginiz/dosya/yolu ``` Bu komut, belirli bir dosyadaki tüm telefon numaralarını bulur. Müşteri verilerinizde telefon numaralarını standartlaştırmak için harika! ```bash # Kredi kartı numaralarını maskeleme sed -E 's/\b([0-9]{4})[0-9]{8}([0-9]{4})/\1********\2/g' /aradiginiz/dosya/yolu ``` Bu komut, kredi kartı numaralarını maskeleyerek güvenliği artırır. GDPR uyumluluğu için önemli! **Beyin Jimnastiği**: Kendi regex deseninizi oluşturun. Örneğin, bir log dosyasında tüm URL'leri bulacak bir regex yazabilir misiniz? Deneyin ve sonucu yorumlarda paylaş! ## 16. Grep Performans İpuçları: Hızlı ve Verimli Arama Büyük dosyalarda veya çok sayıda dosyada arama yaparken grep performansını artırmak önemlidir: 1. **LC\_ALL=C** kullanın: Karakter seti işlemlerini hızlandırır. ```bash LC_ALL=C grep "pattern" buyuk_dosya.log ``` 2. **\--mmap** kullanın: Büyük dosyalar için bellek eşlemesi kullanır. ```bash grep --mmap "pattern" devasa_dosya.log ``` 3. **Paralel işleme** kullanın: ```bash find . -type f -print0 | xargs -0 -P $(nproc) grep "pattern" ``` ⏱ **Hız Testi**: Bu performans ipuçlarını kendi sisteminizde test edin. Ne kadar hız artışı sağladığını ölçün ve sonuçları yorumlarda bizimle paylaşın! --- **Final Görevi**: Öğrendiklerinizi kullanarak, kendi sisteminizde bir "sistem sağlığı raporu" oluşturan bir bash script yazın. Bu script, önemli log dosyalarını tarayan, kritik hataları bulan ve günlük bir özet rapor üreten bir araç olsun. * Tebrikler! Bu kapsamlı rehberi tamamladınız. Artık Linux sunucularında dosya içeriği aramanın inceliklerini biliyorsunuz. grep ve alternatiflerini kullanarak, sistem yönetimi, güvenlik denetimi, hata ayıklama ve performans optimizasyonu gibi çeşitli alanlarda ustalaştınız. * Unutmayın, her komut ve teknik farklı senaryolarda farklı avantajlar sağlar. Kendi ihtiyaçlarınıza ve kullanım senaryolarınıza en uygun yöntemleri seçerek, Linux sunucunuzda dosya içeriği arama işlemlerini optimize edebilirsiniz. * Linux dosya arama macerası burada bitmiyor! Sürekli öğrenmeye ve yeni teknikler keşfetmeye devam edin. Kim bilir, belki de bir gün kendi arama aracınızı geliştirirsiniz! * Sorularınız veya paylaşmak istediğiniz ilginç grep kullanım örnekleri varsa, lütfen yorum bırakın. Birlikte öğrenmeye ve keşfetmeye devam edelim! --- ## Bir GitHub Projesine Nasıl Katkı Sağlanır? - URL: https://erho.dev/post/001-bir-github-projesine-nasil-katki-saglanir - Markdown: https://erho.dev/post/001-bir-github-projesine-nasil-katki-saglanir.md - Tarih: 2024-10-09 - Kategori: diger - Etiketler: github, git GitHub, dünyanın en popüler kod paylaşım platformlarından biridir. GitHub üzerinde milyonlarca açık kaynaklı proje bulunmaktadır. Bu projelere katkı sağlamak, hem projenin gelişmesine katkıda bulunmanızı sağlar hem de diğer kullanıcılarla etkileşime geçmenize yardımcı olur. Bu yazıda, bir projeye nasıl katkıda bulunabileceğinizi adım adım ele alacağız... --- ## 1. Projeyi Keşfetme ### Depoyu İnceleyin Projeyi incelemek için GitHub üzerindeki depo sayfasını ziyaret edin. Projenin `README.md` dosyasını, katkı kılavuzlarını (`CONTRIBUTING.md`), ve varsa yol haritasını (`ROADMAP.md`) dikkatlice okuyun. Bu belgeler, projenin amaçlarını, nasıl katkıda bulunabileceğinizi ve projenin gelecekte nereye gitmeyi planladığını anlamanıza yardımcı olur. ### Sorunları ve Talepleri İnceleyin Proje üzerindeki `Issues` ve `Pull Requests` (PR) sekmelerini kontrol edin. Mevcut hatalar, iyileştirmeler ve yeni özellik taleplerini gözden geçirin. Katkıda bulunabileceğiniz konuları tespit edin. Yeni bir `Issue` açmadan önce, benzer bir konunun daha önce tartışılıp tartışılmadığını kontrol edin. ## 2. Depoyu Forklama ve Klonlama ### Depoyu Forklayın Kendi GitHub hesabınıza projeyi fork'lamak için projenin ana sayfasındaki `Fork` butonuna tıklayın. Bu, projeyi kendi hesabınıza kopyalar ve üzerinde çalışmanıza olanak tanır. ### Klonlama Kendi lokal bilgisayarınıza projeyi klonlayın. Terminal veya komut istemcisinde aşağıdaki komutu kullanarak projeyi indirin: ```bash git clone https://github.com/{kullanici-adi}/{repo-adi}.git cd repo-adi ``` ## 3. Yeni Bir Branch Oluşturma Katkıda bulunacağınız değişiklikler için yeni bir dal oluşturun. Bu, ana dalda (main branch) çalışmanızı engeller ve kolay geri dönüş sağlar. ```bash git checkout -b my-feature-branch ``` ## 4. Değişiklikleri Yapma ### Kodunuzu Yazın Değişikliklerinizi yapın. Bu, hata düzeltmeleri, yeni özellikler ekleme veya belge güncellemeleri olabilir. ### Testleri Çalıştırın Eğer proje testler içeriyorsa, değişikliklerinizin mevcut testleri bozmadığından emin olun ve gerekirse yeni testler yazın. Testler, kodunuzun beklendiği gibi çalıştığını doğrulamanın en iyi yoludur. ## 5. Değişiklikleri Commitleme Her bir mantıklı değişiklik setini commit edin. Anlamlı ve açıklayıcı commit mesajları kullanın. İyi bir commit mesajı, ne yaptığınızı ve neden yaptığınızı açıklar. ```bash git add . git commit -m "Açıklayıcı commit mesajı" ``` ## 6. Branch'i GitHub'a Pushlama Değişikliklerinizi GitHub üzerindeki kendi fork'unuza gönderin. ```bash git push origin my-feature-branch ``` ## 7. Pull Request (PR) Açma ### PR Açın GitHub üzerinde fork'unuzun sayfasına gidin ve yeni bir `Pull Request` açmak için `New Pull Request` butonuna tıklayın. Açıklayıcı bir başlık ve detaylı bir açıklama ekleyin. Yaptığınız değişiklikleri ve nedenlerini açıklayan bir PR açıklaması yazın. ### PR Şablonu Kullan Eğer projede herhangi bir PR şablonu mevcutsa, bu PR şablonlarını kullanın ve talimatları izleyin. Bu şablonlar, PR'ınızın kabul edilme olasılığını artırır. ## 8. Geri Bildirim ve İnceleme ### Geri Bildirimi Bekleyin Proje sahipleri veya diğer katkıda bulunanlar tarafından inceleme ve geri bildirimleri bekleyin. Gelen geri bildirimleri dikkate alarak gerekli değişiklikleri yapın. ### İncelemeleri Yanıtlayın PR incelemelerine yanıt verin ve gerektiğinde ek commitler yaparak değişikliklerinizi güncelleyin. Geri bildirimler, kodunuzun kalitesini artırmanıza yardımcı olabilir. ## 9. PR'ın Birleştirilmesi PR'ınız onaylandıktan sonra, proje sahibi tarafından ana dala birleştirilecektir. Bu süreci takip edin ve eğer varsa son bir inceleme yapın. PR'ınızın birleştirilmesi, katkınızın projeye dahil edildiği anlamına gelir. ## Ek İpuçları * **Katkı Kılavuzlarını Takip Edin:** Her projenin kendi katkı kılavuzları ve kodlama standartları olabilir. Bu kılavuzları dikkatlice takip edin. * **Dokümantasyonu İhmal Etmeyin:** Kod değişikliklerinizin yanı sıra, dokümantasyonu da güncel tutun. * **Nazik ve Yapıcı Olun:** Açık kaynak topluluğunda nazik ve yapıcı geri bildirimler vermek önemlidir. Her zaman profesyonel ve saygılı bir şekilde iletişim kurun. ## Sıkça Sorulan Sorular **1. Bir projeye katkıda bulunmak için ne kadar deneyim gerekir?** > Her seviyede geliştirici, açık kaynak projelere katkıda bulunabilir. Küçük hata düzeltmeleri, dokümantasyon güncellemeleri gibi basit değişikliklerle başlayabilirsiniz. **2. Hangi projeye katkıda bulunmalıyım?** > İlgi alanlarınıza ve yeteneklerinize uygun projeleri seçin. İlginizi çeken bir projede çalışmak, motivasyonunuzu yüksek tutar. **3. Katkıda bulunmak için onay almam gerekir mi?** > Genelde hayır. Projelerin çoğu, katkılarınızı incelemek ve birleştirmek için bekleyen bir topluluk yöneticisine sahiptir. Ancak, büyük değişiklikler yapmadan önce `Issues` sekmesinde bir konu açarak veya mevcut bir konuyu tartışarak proje yöneticilerinin onayını alabilirsiniz. **4. Bir hata buldum ama nasıl düzelteceğimi bilmiyorum, ne yapmalıyım?** > Hata bildiriminde bulunarak topluluğun dikkatini çekebilirsiniz. Belki de başka bir geliştirici hatayı düzeltmek için gerekli bilgiye sahiptir. **5. PR'ım reddedilirse ne yapmalıyım?** > Reddedilen PR'lar normaldir. Proje yöneticilerinin geri bildirimlerini dikkatlice okuyun ve gerekli düzenlemeleri yaparak PR'ınızı güncelleyin. Örnek PR: [msamgan/laravel-env-keys-checker/pull/1](https://github.com/msamgan/laravel-env-keys-checker/pull/1) Bu adımları izleyerek, GitHub projelerine katkıda bulunma sürecini daha anlaşılır ve etkili hale getirebilirsiniz. Unutmayın, açık kaynak topluluğuna katkıda bulunmak, hem öğrenmek hem de paylaşmak için harika bir yoldur. Kendinizi geliştirmek ve başkalarına yardımcı olmak için bu fırsatı mutlaka değerlendirmenizi tavsiye ederim. ---