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 15Sonra 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-ownerVeritabanı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: 0Bu 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ı.")
PYBu ö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/healthSağ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_ADRESITünel açıkken tarayıcıdan şu adrese girdim:
http://localhost:18055/adminPanele 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"
fiSon 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
