Yazılım

Yedek Almak Yetmez: Coolify ve R2 Yedeklerimi Geri Yükleyerek Test Ettim

Medium’da oku ↗
9 Ekim 2026Aleyna

Coolify ve Cloudflare R2’deki Directus yedeklerimi ayrı bir ortamda geri yükledim. PostgreSQL, dosya bütünlüğü ve panel kontrollerini kullandığım komutlarla anlattım.

Yedek Almak Yetmez: Coolify ve R2 Yedeklerimi Geri Yükleyerek Test Ettim

Web sitemi Vercel’den Hetzner’e taşıdıktan sonra, sunucu tarafında ilgilenmem gereken işler de arttı. Bunlardan biri yedeklemeydi.

Coolify üzerinden yedekleri ayarladım, Cloudflare R2’ye gönderildiklerini gördüm. İlk bakışta her şey tamam gibi görünüyordu. Ama aklımda bir soru vardı:

Bir sorun yaşarsam bu yedeklerle gerçekten geri dönebilecek miyim?

Bunu öğrenmek için bir sorun çıkmasını beklemek yerine, yedekleri ayrı bir test ortamında geri yükledim. Bu yazıda hem süreci hem de kontrollerde kullandığım komutları paylaşıyorum.

Veritabanı yedeği tek başına yeterli mi?

Studioae’de içerik yönetimi için Directus, veritabanı olarak PostgreSQL kullanıyorum. Yazılar ve içerik kayıtları veritabanında, yüklediğim görseller ise ayrı bir Docker volume’ünde tutuluyor.

Bu yüzden iki parçayı birlikte kontrol etmem gerekiyordu:

- PostgreSQL veritabanı yedeği.

- Directus’a yüklenen dosyaların yedeği.

Veritabanında bir görselin kaydı bulunması, görsel dosyasının da yedeklendiği anlamına gelmiyor.

Aşağıdaki komutlarda gerçek sunucu adresi ve kaynak adları yerine örnek değerler kullandım. Tablo adları ve dosya yolları kendi kurulumunuza göre değişebilir.

SSH tüneli ve Mac’teki hash kontrolü dışındaki komutları sunucuda çalıştırdım.

1. Test için kaynakları belirledim

Önce çalışan container’ları ve Directus’un bağlı olduğu volume’leri listeledim:

docker ps --format 'table {{.Names}}\t{{.Image}}'

docker inspect DIRECTUS_CONTAINER_ADI \
  --format '{{range .Mounts}}{{println .Source "->" .Destination}}{{end}}'

Ardından aynı terminal oturumunda kullanacağım değişkenleri tanımladım:

PG_CONTAINER="POSTGRES_CONTAINER_ADI"
DIRECTUS_CONTAINER="DIRECTUS_CONTAINER_ADI"

DB_USER="directus"
TEST_DB="directus_restore_test_20261009"

DB_BACKUP="/YEDEK_KLASORU/veritabani.dmp"
UPLOADS_BACKUP="/YEDEK_KLASORU/uploads.tar.gz"

TEST_DIR="/root/studioae-restore-test-20261009"
TEST_CONTAINER="studioae-directus-restore-test"
DOCKER_NETWORK="coolify"

Buradaki büyük harfli örnek değerlerin kendi ortamınızdaki karşılıklarıyla değiştirilmesi gerekiyor. Test veritabanının adını da canlı veritabanından açıkça farklı seçtim.

2. Veritabanı yedeğinin içeriğine baktım

Elimdeki yedek, PostgreSQL’in custom arşiv biçimindeydi. İçeriğini `pg_restore` ile listeledim:

docker exec -i "$PG_CONTAINER" pg_restore --list \
  < "$DB_BACKUP" | head -n 25

Çıktıda yedeğin oluşturulma zamanı, PostgreSQL sürümü ve tablo kayıtları görünüyordu.

Bu kontrol arşivin okunabildiğini gösterdi. Tam olarak geri yüklenebildiğini anlamak için bir sonraki adıma geçtim.

3. Ayrı bir test veritabanına geri yükledim

Önce boş bir test veritabanı oluşturdum:

docker exec "$PG_CONTAINER" \
  createdb -U "$DB_USER" -T template0 "$TEST_DB"

Sonra yedeği bu veritabanına geri yükledim:

docker exec -i "$PG_CONTAINER" \
  pg_restore \
    -U "$DB_USER" \
    -d "$TEST_DB" \
    --no-owner \
    --no-privileges \
    --exit-on-error \
    --single-transaction \
  < "$DB_BACKUP" &&
echo "Geri yükleme başarılı"

Bu testte sahiplik ve yetki kayıtlarını aktarmadım. `--exit-on-error` ile hata durumunda işlemin durmasını, `--single-transaction` ile geri yüklemenin tek işlem olarak yürütülmesini sağladım.

Terminalde beklediğim mesajı gördüm:

Geri yükleme başarılı

4. İçerik kayıtlarını kontrol ettim

Geri yüklenen veritabanındaki birkaç tablonun kayıt sayısına baktım:

docker exec "$PG_CONTAINER" \
  psql -U "$DB_USER" -d "$TEST_DB" -c "
    SELECT 'About' AS tablo, count(*) AS kayit
    FROM public.\"About\"
    UNION ALL
    SELECT 'Blog', count(*) FROM public.\"Blog\"
    UNION ALL
    SELECT 'Blog_Category', count(*)
    FROM public.\"Blog_Category\"
    UNION ALL
    SELECT 'directus_files', count(*)
    FROM public.directus_files;
  "

Sonuçta bir About kaydı, üç blog yazısı, iki blog kategorisi ve 26 dosya kaydı vardı.

Bu sayılar, yedekten gelen kayıtları gösteriyordu. Uygulamada doğru görüntülenip görüntülenmediklerini daha sonra panelden kontrol ettim.

5. Dosya yedeğini açıp veritabanıyla eşleştirdim

Önce dosyaların hangi depolamada tutulduğuna baktım:

docker exec "$PG_CONTAINER" \
  psql -U "$DB_USER" -d "$TEST_DB" -c "
    SELECT storage, count(*) AS dosya_sayisi
    FROM public.directus_files
    GROUP BY storage;
  "

26 dosyanın tamamı `local` depolama kullanıyordu.

Arşivin gzip bütünlüğünü kontrol edip içeriğine baktım:

gzip -t "$UPLOADS_BACKUP" &&
echo "Arşiv sağlam"

tar -tzf "$UPLOADS_BACKUP" | head -n 15

Sonra kendi oluşturduğum yedek arşivini ayrı bir test klasörüne açtım:

mkdir -p "$TEST_DIR/uploads"

tar -xzf "$UPLOADS_BACKUP" \
  -C "$TEST_DIR/uploads" \
  --no-same-owner

Veritabanındaki fiziksel dosya adlarını bir listeye aktardım:

docker exec "$PG_CONTAINER" \
  psql -U "$DB_USER" -d "$TEST_DB" -At -c "
    SELECT filename_disk
    FROM public.directus_files
    WHERE storage = 'local';
  " > "$TEST_DIR/files.txt"

Ardından listedeki her dosyanın test klasöründe bulunup bulunmadığını kontrol ettim:

(
  bulunan=0
  eksik=0

  while IFS= read -r dosya; do
    if [ -f "$TEST_DIR/uploads/$dosya" ]; then
      bulunan=$((bulunan + 1))
    else
      printf 'Eksik: %s\n' "$dosya"
      eksik=$((eksik + 1))
    fi
  done < "$TEST_DIR/files.txt"

  printf 'Bulunan: %s\nEksik: %s\n' "$bulunan" "$eksik"
)

Sonuç:

Bulunan: 26
Eksik: 0

Bu kontrol, veritabanının referans verdiği 26 yerel dosyanın da yedekte bulunduğunu gösterdi. Dosyaların görüntülenmesini ayrıca Directus üzerinden test ettim.

6. R2’deki kopyaları karşılaştırdım

Sunucudaki yedekleri test ettikten sonra Cloudflare R2’deki kopyalarını bilgisayarıma indirdim.

Sunucuda:

sha256sum "$DB_BACKUP"
sha256sum "$UPLOADS_BACKUP"

Mac bilgisayarımda:

shasum -a 256 "$HOME/Downloads/VERITABANI_YEDEGI.dmp"

shasum -a 256 "$HOME/Downloads/DOSYA_YEDEGI.tar.gz"

İki yedeğin de sunucudaki ve bilgisayarımdaki SHA-256 değerleri eşleşti.

Böylece R2’den indirdiğim dosyaların, sunucuda test ettiğim yedeklerle birebir aynı olduğunu doğruladım.

7. Geri yüklenen verilerle Directus’u başlattım

Sırada içerikleri uygulama üzerinden görmek vardı.

Canlı sistemde kullanılan Docker image kimliğini aldım:

DIRECTUS_IMAGE=$(docker inspect "$DIRECTUS_CONTAINER" \
  --format '{{.Image}}')

Test ayar dosyasını oluştururken veritabanı şifresini terminale yazdırmadan mevcut container’dan okudum. Dosyaya yalnızca sahibinin erişebilmesi için `600` izni verdim.

Bu Python komutu, yukarıdaki değişkenlerle aynı sunucu terminalinde çalıştırılabilir:

export DIRECTUS_CONTAINER PG_CONTAINER DB_USER TEST_DB TEST_DIR

python3 - <<'PY'
import json
import os
import secrets
import subprocess

info = json.loads(subprocess.check_output([
    "docker", "inspect", os.environ["DIRECTUS_CONTAINER"]
]))[0]

env = dict(
    item.split("=", 1)
    for item in info["Config"]["Env"]
    if "=" in item
)

password = env.get("DB_PASSWORD")
if not password:
    raise SystemExit("DB_PASSWORD bulunamadı.")

settings = {
    "DB_CLIENT": "pg",
    "DB_HOST": os.environ["PG_CONTAINER"],
    "DB_PORT": "5432",
    "DB_USER": os.environ["DB_USER"],
    "DB_PASSWORD": password,
    "DB_DATABASE": os.environ["TEST_DB"],
    "SECRET": secrets.token_hex(32),
    "PUBLIC_URL": "http://localhost:18055",
    "STORAGE_LOCATIONS": "local",
    "STORAGE_LOCAL_DRIVER": "local",
    "STORAGE_LOCAL_ROOT": "/directus/uploads",
}

for key, value in settings.items():
    if "\n" in value or "\r" in value:
        raise SystemExit(f"{key} değeri env dosyası için uygun değil.")

path = os.path.join(
    os.environ["TEST_DIR"], "directus-test.env"
)

fd = os.open(
    path,
    os.O_WRONLY | os.O_CREAT | os.O_EXCL,
    0o600,
)

with os.fdopen(fd, "w") as file:
    for key, value in settings.items():
        file.write(f"{key}={value}\n")

print("Test ayarları hazır. Şifre ekrana yazdırılmadı.")
PY

Bu örnek, benim kurulumum gibi `DB_PASSWORD` ortam değişkeni kullanan ve PostgreSQL container’ına aynı Docker ağı üzerinden erişebilen bir yapı için hazırlanmıştır.

Test uygulamasını açmadan önce geri yüklenen veritabanındaki Directus Flow’larını devre dışı bırakmak da önemlidir. Böylece test ortamındaki işlemler e-posta veya webhook gibi dış etkiler oluşturmaz. Aşağıdaki komutun hedefi yalnızca test veritabanıdır:

docker exec "$PG_CONTAINER" \
  psql -U "$DB_USER" -d "$TEST_DB" -c "
    UPDATE public.directus_flows
    SET status = 'inactive'
    WHERE status = 'active';
  "

Test dosyalarının sahipliğini image içindeki `node` kullanıcısına ayarladım:

docker run --rm --user 0 --entrypoint sh \
  -v "$TEST_DIR/uploads:/restore-uploads" \
  "$DIRECTUS_IMAGE" \
  -c 'chown -R node:node /restore-uploads'

Ardından test container’ını başlattım:

docker run -d \
  --name "$TEST_CONTAINER" \
  --network "$DOCKER_NETWORK" \
  -p 127.0.0.1:18055:8055 \
  --env-file "$TEST_DIR/directus-test.env" \
  -v "$TEST_DIR/uploads:/directus/uploads" \
  "$DIRECTUS_IMAGE"

Bu testte veritabanı ve uploads yedeğini kullandım. Özel extension ve template volume’lerini test container’ına bağlamadım.

Logları ve sağlık durumunu kontrol ettim:

docker logs --tail 60 "$TEST_CONTAINER"

curl -sS --max-time 10 \
  http://127.0.0.1:18055/server/health

Sağlık kontrolünün sonucu:

{"status":"ok"}

8. SSH tüneliyle test paneline girdim

Test portunu sunucuda yalnızca `127.0.0.1` adresine bağlamıştım. Paneli bilgisayarımdan açmak için Mac terminalinde SSH tüneli oluşturdum:

ssh \
  -i ~/.ssh/SUNUCU_SSH_ANAHTARI \
  -o IdentitiesOnly=yes \
  -o ExitOnForwardFailure=yes \
  -N \
  -L 18055:127.0.0.1:18055 \
  root@SUNUCU_IP_ADRESI

Tünel açıkken tarayıcıdan şu adrese girdim:

http://localhost:18055/admin

Panele giriş yapıp blog yazılarını, içerikleri ve görselleri açtım. Kontrol ettiğim içerikler görüntüleniyordu.

Benim için testin en rahatlatıcı kısmı buydu: Yedek dosyaları, tekrar kullanılabilen bir uygulamaya dönüşmüştü.

9. Test ortamını temizledim

Kontroller tamamlanınca yalnızca oluşturduğum test kaynaklarını kaldırdım:

docker rm -f "$TEST_CONTAINER"

docker exec "$PG_CONTAINER" \
  dropdb -U "$DB_USER" "$TEST_DB"

Geçici klasörü silerken hedefi açıkça kontrol eden bir koşul kullandım:

if [ "$TEST_DIR" = "/root/studioae-restore-test-20261009" ]; then
  rm -rf -- "$TEST_DIR"
else
  printf 'Beklenmeyen klasör; silinmedi: %s\n' "$TEST_DIR"
fi

Son olarak Mac’te SSH tünelinin açık olduğu terminalde `Control + C` ile bağlantıyı kapattım.

Bildirim tarafında da küçük bir sorun çıktı

Yedekleme başarısız olursa haberdar olmak için Coolify’de e-posta bildirimlerini ayarladım.

Test maili gelmeyince, sunucudan Gmail’e bağlantıyı kontrol ettim. 465 portu zaman aşımına uğrarken 587 portuna bağlantı kurulabildi. Port ve şifreleme ayarını düzelttikten sonra test maili ulaştı.

Bu adımda e-posta gönderimini doğruladım ve yedekleme hatası bildirimini etkin bıraktım. Gerçek bir yedekleme hatası oluşturarak ayrıca bildirim testi yapmadım.

Bu testten ne öğrendim?

Yedeklerin listede görünmesi iyi bir başlangıç. Ancak geri yükleme denemesi yapmak, neyi yedeklediğimi ve nasıl geri dönebileceğimi çok daha net anlamamı sağladı.

Bu çalışmada:

  • Veritabanını ayrı bir test veritabanına geri yükledim.
  • Veritabanındaki 26 dosya kaydını arşivle eşleştirdim.
  • R2’den indirdiğim kopyaların SHA-256 değerlerini karşılaştırdım.
  • Directus panelinden içerikleri ve görselleri kontrol ettim.

Bu, bütün sunucuyu sıfırdan kurduğum kapsamlı bir felaket kurtarma provası değildi. Directus’un veritabanı ve yüklenen dosyaları için yaptığım bir geri yükleme testiydi.

Ayrıca veritabanı ve dosya yedekleri farklı saatlerde alındığı için, dosyaların eşleştiğini doğrulamak özellikle değerliydi. Bu testte eksik dosya çıkmadı; aynı sonucun gelecekteki her yedek çifti için geçerli olduğunu varsaymamak gerekiyor.

Başlangıçtaki soruma ise somut bir cevap alabildim:

Test ettiğim yedeklerle içeriklerimi ve dosyalarımı geri getirebildim.

Bundan sonra yedeklemeyi yalnızca “dosya oluştu mu?” diye kontrol etmeyeceğim. Önemli değişikliklerden sonra geri yükleme sürecini de yeniden deneyeceğim.

Etiketler

  • Coolify
  • Cloudflare R2
  • Directus
  • PostgreSQL
  • Docker
  • Yedekleme
  • Hetzner

Birlikte üretmek için bağlantıda kalalım.

Yeni iş fırsatları, yazılım projeleri veya iş birlikleri hakkında konuşmak için bana ulaşabilirsiniz.

Lokasyon

Kırklareli, Türkiye

Rol

Software Developer

© 2026 Aleyna Ertin . Tüm hakları saklıdır.