Teknoloji

Docker Compose ile WordPress + Nginx + MariaDB + Redis Nasıl Tatlı Tatlı Akıyor? Kalıcı Hacimler, Otomatik Yedek ve Güncelleme Akışı

Hikaye Gibi Bir Giriş: Küçük Bir Tıkanıklık, Büyük Bir Akış

Hiç canlı sitede minik bir düzenlemeyi yayınlayıp, sonra “Bir şeyler yavaşladı ama nereden?” diye ekran karşısında iç çektiniz mi? Ben bir sabah tam da bu tabloyla karşılaştım. Editör bir görseli değiştirmiş, eklentilerden biri de hem veritabanını hem disk yazmalarını biraz sıkıştırmış. Sayfalar açılıyor ama bir ağırlık var. O an düşündüm: Şu kurduğumuz WordPress’i biraz daha sağlam bir altyapının üstüne koymanın zamanı gelmedi mi?

İşte bu yazı tam oradan doğdu. Docker Compose ile WordPress + Nginx + MariaDB + Redis kurulumunu tek bir nefeste anlatmak istiyorum. Üstelik sadece çalıştırıp bırakmak değil; kalıcı hacimler ile verinin güvenini, otomatik yedek akışıyla iç huzuru ve güncelleme akışı ile “hadi bakalım, korkmadan bastım güncelle” özgüvenini beraber kuracağız. Mesela şöyle düşünün: Bir kez düzenini kurdunuz mu, sonrası rutin bir sabah kahvesi gibi. Kokusu tanıdık, tadı dengeli.

Yol boyunca basit örnekler, küçük notlar ve “bunu böyle yapınca rahat ediyorsun” diyeceğim anlar olacak. Teknik terimleri zorlamadan, ama işin püf noktalarını saklamadan anlatacağım. Hazırsanız başlayalım.

Resmi Netleştirelim: WordPress, Nginx, MariaDB ve Redis Nasıl Konuşur?

Gözünüzde şöyle canlandırın: Sahnenin ortasında WordPress var. Kulağında bir yanda MariaDB’nin verileri, diğer yanda Redis’in hızlı fısıltıları. Ön tarafta ise sahnenin ışıklarını ayarlayan Nginx. Ziyaretçi geldi mi Nginx karşılar, WordPress’e doğru iletir, WordPress de “veriyi soralım” der ve MariaDB’ye döner. Aynı sorular tekrar tekrar sorulmasın diye Redis devreye girer, “Bunun cevabı bende var” der ve site birden ferahlar.

Docker Compose burada bütün bu oyuncuları tek bir yerde toplar. “Şu konteyner Nginx, şu WordPress, şu MariaDB, şu da Redis” diye dosyada anlatırsınız, hepsini aynı ağda buluşturursunuz. En güzel tarafı, her biri kendi kutusunda. Yani Nginx’i güncellersiniz, MariaDB’ye dokunmaz. Redis’i ayarlarsınız, WordPress huzurla işine devam eder. Küçük bir tiyatro ekibi gibi, herkes rolünü bilir.

Bu kompozisyonun gücü, parçaların net ayrılmasından geliyor. Siz sadece akışı yönetirsiniz. Bir dosyada (Compose) “şu portlar, şu hacimler, şu değişkenler” dersiniz ve tekrar edilebilir bir düzen kurarsınız. Tam da bu yüzden harekete geçiyoruz.

Kalıcı Hacimler: Dosyalar Nerede, Neden Bu Kadar Önemli?

WordPress’te verinin kalbi iki yerde atar: veritabanı ve wp-content. Biri yazılarınız, ayarlarınız, yorumlarınız; diğeri temalar, eklentiler, görseller. Konteynerler gelip geçicidir; bugün var, yarın güncellenir. Ama bu iki set, yani db_data ve wp_data, kalıcı olmalı. İşte bu yüzden volume kullanırız.

Redis için de kalıcılık isteyebilirsiniz. Bazı kurulumlarda Redis sadece geçici bellektir; kapandığında silinse de sorun olmaz. Ama bazen “AOF” denilen günlük tutma ayarıyla kalıcılaştırırsınız. Bu, özellikle nesne önbelleğini uzun süreli saklamak istediğinizde faydalı olur. Ben genelde “işimi görsün, ama kapansın da dünyanın sonu olmasın” yaklaşımındayım; Redis’i taktiksel, veritabanını stratejik düşünürüm.

Kalıcı hacimler, sadece veri güveni değil, yedek akışının basitleşmesi demek. Çünkü “al şu klasörü ve şu veritabanını yedekle” talimatı birkaç satırlık bir script’e dönüşür. Mesela şöyle düşünün: wp-content’i sıkıştır, veritabanını dışa aktar, ikisini birlikte sakla. Geri yüklemek de aynı kadar düz, bu kadar.

docker-compose.yml: Çalışır Bir Örnek ve Küçük Sırlar

Compose Dosyası

Aşağıdaki örnek, tek bir klasörden ayağa kalkacak temel bir düzen sunuyor. Nginx, WordPress (PHP-FPM), MariaDB ve Redis bir ağda buluşuyor; kalıcı hacimler bağlanıyor; ufak sağlık kontrolleri konuluyor. Üstüne bir de otomatik yedek için küçük bir servis hazırlıyoruz.

version: "3.9"

services:
  db:
    image: mariadb:10.11
    container_name: wp_db
    command: ["--skip-log-bin", "--innodb-buffer-pool-size=256M"]
    environment:
      - MARIADB_DATABASE=${DB_NAME}
      - MARIADB_USER=${DB_USER}
      - MARIADB_PASSWORD=${DB_PASSWORD}
      - MARIADB_ROOT_PASSWORD=${DB_ROOT_PASSWORD}
    volumes:
      - db_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    container_name: wp_redis
    command: ["redis-server", "--appendonly", "yes", "--save", "60", "1000"]
    volumes:
      - redis_data:/data
    restart: unless-stopped

  wordpress:
    image: wordpress:php8.2-fpm
    container_name: wp_php
    environment:
      - WORDPRESS_DB_HOST=db:3306
      - WORDPRESS_DB_NAME=${DB_NAME}
      - WORDPRESS_DB_USER=${DB_USER}
      - WORDPRESS_DB_PASSWORD=${DB_PASSWORD}
      - WORDPRESS_CONFIG_EXTRA=define('WP_REDIS_HOST','redis');n define('WP_MEMORY_LIMIT','256M');n define('AUTOMATIC_UPDATER_DISABLED', false);
    volumes:
      - wp_data:/var/www/html
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    restart: unless-stopped

  nginx:
    image: nginx:alpine
    container_name: wp_nginx
    ports:
      - "80:80"
    volumes:
      - wp_data:/var/www/html:ro
      - ./nginx/conf.d:/etc/nginx/conf.d
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - nginx_logs:/var/log/nginx
    depends_on:
      - wordpress
    restart: unless-stopped

  backup:
    image: alpine:3.20
    container_name: wp_backup
    entrypoint: ["/bin/sh","-c"]
    command: |
      apk add --no-cache bash mariadb-client restic tzdata ca-certificates tar pigz && 
      mkdir -p /scripts && 
      echo "0 3 * * * /bin/bash /scripts/backup.sh >> /var/log/backup.log 2>&1" > /etc/crontabs/root && 
      crond -f -L /var/log/cron.log
    environment:
      - RESTIC_REPOSITORY=${RESTIC_REPOSITORY}
      - RESTIC_PASSWORD=${RESTIC_PASSWORD}
      - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
      - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
      - TZ=Europe/Istanbul
      - DB_HOST=db
      - DB_USER=${DB_USER}
      - DB_PASSWORD=${DB_PASSWORD}
      - DB_NAME=${DB_NAME}
    volumes:
      - wp_data:/var/www/html:ro
      - ./backup:/scripts
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

volumes:
  wp_data:
  db_data:
  redis_data:
  nginx_logs:

.env Dosyası

Aynı klasörde basit bir .env dosyası, parolaları ve isimleri ortam değişkeni olarak verir. Kodun gövdesinde gizlemekten iyidir. İsterseniz Docker secrets veya kasada saklama yöntemleriyle daha da sertleştirebilirsiniz.

DB_NAME=wordpress
DB_USER=wpuser
DB_PASSWORD=degistir-lutfen
DB_ROOT_PASSWORD=degistir-ama-sakince

RESTIC_REPOSITORY=s3:s3.amazonaws.com/sizin-bucketiniz/wordpress-yedek
RESTIC_PASSWORD=uzun-bir-sifre-secin
AWS_ACCESS_KEY_ID=AKIAXXXXX
AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxx

Yedek Script’i

Geceleri saat 03:00’te çalışacak minik bir script. Veritabanını dışa aktarır, wp-content’i bir arşive alır, ikisini restic ile uzak depoya yollar. Geri dönüş yolunu da hazırladık, birazdan anlatacağım.

# backup/backup.sh
#!/usr/bin/env bash
set -euo pipefail

STAMP=$(date +"%Y%m%d-%H%M%S")
WORK=/tmp/backup-${STAMP}
mkdir -p "$WORK"

# 1) Veritabanı dump
mysqldump -h "$DB_HOST" -u "$DB_USER" -p"$DB_PASSWORD" "$DB_NAME" 
  --single-transaction --routines --events --triggers 
  | pigz -9 > "$WORK/db-${STAMP}.sql.gz"

# 2) wp-content arşivi (çekirdek ve eklentiler aynı volume'da, isterseniz tümünü alın)
cd /var/www/html
if [ -d wp-content ]; then
  tar -I "pigz -9" -cf "$WORK/wp-content-${STAMP}.tar.gz" wp-content
else
  echo "wp-content yok gibi, yine de devam" >&2
fi

# 3) restic init (ilkse) ve yedekleme
restic snapshots || restic init
restic backup --host wordpress 
  "$WORK/db-${STAMP}.sql.gz" 
  "$WORK/wp-content-${STAMP}.tar.gz" || true

# 4) Eski yedekleri budama: 7 günlük, 4 haftalık, 6 aylık
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune

rm -rf "$WORK"
echo "Yedek tamam: ${STAMP}"

İlk Çalıştırma

Komutlar sade. Önce klasör yapısını hazırlayın: nginx dizini, backup dizini, .env dosyası. Sonra çalıştırın ve çayı koyun.

docker compose pull
docker compose up -d

İlk kurulumda WordPress sizden site adı ve yönetici hesabı isteyecek. Nginx ayarlarını da ekleyelim ki vitrinimiz derli toplu olsun.

Nginx ve Redis: Vitrini Parlatmak, İçeriği Hızlandırmak

Nginx Yapılandırması

Basit ama iş gören bir Nginx, trafiği WordPress’in PHP-FPM’ine aktarır, statik dosyaları hızlı yeniler ve gereksiz yükü azaltır. Aşağıdaki iki dosya ile başlamak rahatlatır.

# nginx/nginx.conf
user  nginx;
worker_processes  auto;
error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
  worker_connections  1024;
}

http {
  include       /etc/nginx/mime.types;
  default_type  application/octet-stream;
  sendfile        on;
  tcp_nopush      on;
  tcp_nodelay     on;
  keepalive_timeout  65;
  client_max_body_size 64m;

  log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                     '$status $body_bytes_sent "$http_referer" '
                     '"$http_user_agent" "$http_x_forwarded_for"';
  access_log  /var/log/nginx/access.log  main;

  include /etc/nginx/conf.d/*.conf;
}
# nginx/conf.d/site.conf
upstream php {
  server wordpress:9000;
  keepalive 16;
}

server {
  listen 80;
  server_name _;
  root /var/www/html;
  index index.php index.html;

  location ~* .(png|jpg|jpeg|gif|svg|webp|css|js|ico|woff|woff2)$ {
    expires 7d;
    add_header Cache-Control "public, max-age=604800";
    try_files $uri $uri/ =404;
  }

  location /wp-content/cache/ { # plugin cache klasörleri olursa
    expires 10m;
    add_header Cache-Control "public, max-age=600";
  }

  location / {
    try_files $uri $uri/ /index.php?$args;
  }

  location ~ .php$ {
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_pass php;
    fastcgi_read_timeout 120s;
  }

  location ~* /(.git|.env|wp-config.php|readme.html|license.txt) {
    deny all;
  }
}

Üstteki yapı, yeni başlayanlar için yeterli bir zemin. Zamanla mikro önbellekleme ya da ek güvenlik başlıkları eklemek isteyebilirsiniz. Daha derine inmek isterseniz, sitemizdeki Docker ile WordPress’i VPS’te yaşatmanın sıcak hikayesi bu düzenin akrabası gibi; pratik dokunuşlarla çok şey netleşiyor.

Redis’i WordPress’e Tanıtmak

WordPress ile Redis konuşsun diye genelde küçük bir eklenti yeter. “Redis Object Cache” gibi popüler olanları kurup etkinleştirin. Ayarlarda “host” olarak redis konteyner adını yazmanız genelde yetiyor. Biz zaten WORDPRESS_CONFIG_EXTRA ile ‘WP_REDIS_HOST’ tanımladık. Aktivasyon sonrası yönetim panelinde “Connected” görürseniz işler yolunda demektir. Bazen sayfa oluşturucular veya ağır eklentiler ilk yüklemede üşengeç davranır, o anlarda önbelleği bir kere temizlemek rahatlatır.

Otomatik Yedek ve Geri Yükleme ile Güncelleme Akışı: Önce Emniyet, Sonra Cesaret

Yedek Akışı

Hazırladığımız backup servisi her gece 03:00’te çalışıyor. Veritabanı dump alınıyor, wp-content arşivleniyor, restic ile uzak depoya gönderiliyor. Bu uzak depo bir S3 uyumlu nesne depolama olabilir. Restic’in incelikleri için restic ile yedekleri S3’e göndermek belgesine göz atmak iyi gelir. Benim sahada en sevdiğim detay, “forget/prune” ile arşivi yaşlandırabilmek; dünün, geçen haftanın ve aylık fotoğraflarınız birlikte duruyor.

Geri yükleme provası baştan yapılınca, kriz anında titreme olmuyor. Provayı şöyle düşünebilirsiniz: Temiz bir klasöre aynı Compose’u kurup, yedekten çekiyorsunuz. İlk geri yüklemeyi görmeden “tamam yedekliyiz” demek havada kalır. Ben provaları küçük bir alt ortamda yapmayı seviyorum; verinin bir kısmıyla bile olsa kas hafızası kazanılıyor.

Geri Yükleme Komutları

Prova zamanı geldiğinde, backup konteynerine girip restic ile dosyaları indirirsiniz. Ardından veritabanını içe aktarır, wp-content’i geri açarsınız.

# son snapshot'ı listeleyin
docker exec -it wp_backup restic snapshots

# istediğiniz anın arşivini bir klasöre çıkarın
RESTORE=/tmp/restore-$(date +%s)
docker exec -e RESTORE=${RESTORE} -it wp_backup 
  sh -lc "mkdir -p $RESTORE && restic restore latest --target $RESTORE"

# wp-content'i geri koymak (nginx/wordpress durdurmayı unutmayın)
docker compose stop nginx wordpress
HOST_RESTORE=$(docker exec -it wp_backup sh -lc "echo $RESTORE" | tr -d 'r')
# Host'a kopyalamak için (Linux/Mac):
# docker cp wp_backup:${HOST_RESTORE}/tmp/backup-*/wp-content-*.tar.gz ./
# sonra uygun klasöre açıp wp_data volume'a yerleştirin

docker compose start db
# Veritabanı geri yükleme (dosyayı host'a alıp içe aktarın)
gzip -dc db-YYYYMMDD-HHMMSS.sql.gz | docker exec -i wp_db sh -lc 
  "mysql -u$DB_USER -p$DB_PASSWORD $DB_NAME"

docker compose start wordpress nginx

Buradaki adımların bir kez işlediğini görmek, akşamları çok daha rahat uyutuyor. Küçük bir not: İçerik canlıyken geri yükleme yapılacaksa, “önce bakım sayfası, sonra işlem” sırasını bozmamak lazım.

Güncelleme Akışı

Güncelleme en çok yanlış anlaşılan konu. Aslında üç parçadan oluşur: “önce yedek”, “sonra imajları güncelle”, “en sonda WordPress çekirdeği/eklenti/tema”. Benim pratik akışım şöyle akıyor: Önce bir manuel yedek tetikleyip tamamlandığını görüyorum. Sonra Compose imajlarını çekiyorum. Ardından WordPress’i WP-CLI ile güncelliyorum. Her adım küçük, ama bir araya gelince sağlam.

WP-CLI ile Güncelleme

WP-CLI, WordPress yönetiminde komut satırını sevmek için güçlü bir bahane. Kurulumla uğraşmadan bir konteyner olarak çağırabiliriz. Resmi dökümana göz atmak isterseniz WP-CLI komutları başlığını seversiniz.

# Tek seferlik WP-CLI konteyneri ile komut örnekleri
# Çekirdeği minör güncelle: (temkinli)
docker run --rm -it 
  --network $(docker network ls --filter name=_default --format {{.Name}}) 
  -v $(pwd)/html:/var/www/html 
  -e WORDPRESS_DB_HOST=db:3306 
  -e WORDPRESS_DB_NAME=${DB_NAME} 
  -e WORDPRESS_DB_USER=${DB_USER} 
  -e WORDPRESS_DB_PASSWORD=${DB_PASSWORD} 
  wordpress:cli-php8.2 
  wp core update --minor --path=/var/www/html

# Eklentileri güncelle
docker run --rm -it 
  --network $(docker network ls --filter name=_default --format {{.Name}}) 
  -v $(pwd)/html:/var/www/html 
  -e WORDPRESS_DB_HOST=db:3306 
  -e WORDPRESS_DB_NAME=${DB_NAME} 
  -e WORDPRESS_DB_USER=${DB_USER} 
  -e WORDPRESS_DB_PASSWORD=${DB_PASSWORD} 
  wordpress:cli-php8.2 
  wp plugin update --all --path=/var/www/html

Üstteki örnekler, WordPress dosyalarının host’ta ./html altında olduğu bir düzeni varsayıyor. Biz named volume kullandık. Bu durumda en kolayı, kısa süreliğine volume’ı bir yardımcı konteynere mount edip /var/www/html’i o konteynerden kullanmak. Alternatif olarak WordPress servisinde WP-CLI çalıştırmak da mümkün. Yani yöntem çok; önemli olan, önce yedek, sonra güncelle prensibini bozmamak.

Uygulama İmajlarını Güncellemek

Compose tarafında güncelleme genellikle iki komutla biter. Önce yeni imajları çekersiniz, sonra konteynerleri yeniden yaratırsınız. Her şey named volume’larda durduğu için veri yerinden kıpırdamaz.

docker compose pull
docker compose up -d

Nginx, PHP-FPM veya Redis imajlarındaki güncellemeleri böylece almış olursunuz. Büyük versiyon geçişlerinde küçük bir test ortamı kurmak iç rahatlatır. Aynı Compose dosyasını başka bir ağ ve port ile ayağa kaldırıp göz atmak, sürprizleri azaltır.

İnce Ayarlar ve Küçük Taktikler

Güncelleme öncesi “bakım modunu” kısa bir eklentiyle ya da Nginx’te basit bir kural ile açıp, işlem biter bitmez kapatmak işi pürüzsüz yapar. Redis önbelleğini güncellemelerden sonra temizlemek çoğu sitede “eski parça kalmış sanki” duygusunu yok eder. Ve tabii ki otomatik yedek akışını bir healthcheck gibi düşünün; yedeklerin boyutu, sayısı ve geri dönüş testi takvimini bir not defterine yazıp, ay sonunda bir bakış atın.

Compose Referanslarına Bakmak İyi Gelir

İşin tabanı Compose olduğu için, yeni bir özellik veya ufak bir püf aradığınızda Docker Compose referansları hayat kurtarıyor. Orada gördüğünüz bir ayarı küçük küçük denemek, bu yapıyı tam size göre hale getiriyor.

Kapanış: Bir Kez Kur, Hep Güven

Toparlayalım. WordPress’i Docker Compose ile Nginx, MariaDB ve Redis’in arasına yerleştirdiğimizde, aslında bir düzen kurduk. Parçalar ayrı; dokunurken birbirini rahatsız etmiyorlar. Kalıcı hacimler sayesinde verinin omurgası sağlam. Otomatik yedek akışı her gece bir fotoğraf çekiyor, siz de rahat uyuyorsunuz. Güncelleme akışı ise cesaret veriyor; önce emniyet kemeri, sonra yolculuk.

Pratik bir öneri seti bırakayım: İlk haftayı gözlem haftası yapın. Log’lara arada bir bakın, Redis bağlantısının gerçekten aktif olduğundan emin olun, yedeklerin gerçekten uzak depoya aktığını görün. Haftanın sonunda küçük bir geri yükleme provası yapın; birkaç adımı yeniden yaşamak kaslarınızı ısıtıyor. Ve unutmayın, küçük iterasyonlar büyük düzenlerin anahtarı. Bugün bir ayarı düzeltirsiniz, yarın imajı güncellersiniz, öbür gün yedeklerin saklama politikasını güzelleştirirsiniz.

Umarım bu rehber elinizi rahatlattı. Bir güncelleme sabahında yine buluşur, “bak bunu da ekledim, hayat kolaylaştı” deriz. Sorularınız olursa, üzerinde konuşmak her zaman keyif. Bir dahaki yazıda görüşmek üzere.

Sıkça Sorulan Sorular

Aynı sunucuda tutmak tek başına yetmez. Uzak bir depoda ikinci bir kopya olsun. S3 uyumlu bir nesne depolama iyi çalışır. Arada bir geri yükleme provası yapmayı da unutma.

Kritik güvenlik güncellemelerini geciktirmeden, diğerlerini haftalık bir pencerede yap. Öncesinde otomatik yedeğin başarıyla alındığını kontrol et, bittiğinde Redis önbelleğini temizle.

Şart değil, site yine çalışır. Ama Redis özellikle yoğun temalarda ve eklentilerde nefes aldırıyor. Basit bir eklentiyle kurup dene; farkı hissetmezsen kapatması bir dakika sürmüyor.