Docker'da pg_restore: Django Postgres Yedeğini Geri Yükleme
Django deploy rehberlerinin çoğu yedekleme bölümünü aynı şekilde bitirir — kendi Lightsail rehberim dahil: her gece pg_dump ile gzip dosyası, bir cron satırı ve "geri yükleyebildiğini test et" cümlesi. Sonra orada durur. Geri yükleme okura ödev olarak kalır ve çoğu kişi bunu ilk kez gerçekten ihtiyaç duyduğu gün dener.
Ben de ödevi yaptım. PostgreSQL 16 üzerinde Django'nunkine benzeyen küçük bir şema kurdum (django_migrations tablosu, yabancı anahtarla bağlı iki model, bigserial birincil anahtarlar), yedeğini aldım ve aklıma gelen her yanlış yoldan geri yükledim. Bu yazı o denemelerin sonucu: karşına çıkacak hatalar, hangilerinin zararsız olduğu ve hiç hata gibi görünmeyen — asıl tehlikeli olan — bir durum.
Önce elindeki yedeğin türünü bil
pg_dump'ın pratikte karşılaşacağın iki biçimi var ve farklı araçlarla geri yüklenirler:
| Biçim | Nasıl alınır | Geri yükleme aracı |
|---|---|---|
| Düz SQL (varsayılan) | pg_dump mydb | psql |
| Custom arşiv | pg_dump -Fc mydb | pg_restore |
İlk hata genelde bunları karıştırmak. Lightsail rehberindeki yedek betiği düz SQL dosyası üretiyor (db-2026-09-25.sql.gz). Bunu pg_restore'a verirsen:
pg_restore: error: input file appears to be a text format dump. Please use psql.Dosyanın ne olduğunu bilmiyorsan ilk baytlarına bak. Custom arşiv PGDMP imzasıyla başlar; düz dump bir SQL yorumuyla:
head -c 5 backup.dump # PGDMP → pg_restore
gunzip -c db.sql.gz | head -3 # -- PostgreSQL database dump → psqlBaşarısız olup "başarılı" diyen geri yükleme
Ciddiye alınması gereken kısım bu. Düz bir dump'ı, tabloları zaten var olan bir veritabanına psql ile yükledim — "çalışan veritabanının üstüne geri yükle" hatasının gerçekçi hâli:
gunzip -c db.sql.gz | psql -U app -d appdb
echo $?Çıktıda 13 hata vardı: relation "blog_post" already exists, duplicate key value violates unique constraint "blog_post_pkey", multiple primary keys for table "blog_post" are not allowed ve benzerleri. Çıkış kodu ise 0'dı.
Bu belgelenmiş davranış: psql "normal biçimde bittiyse" 0 döner ve betik içindeki başarısız bir SQL komutu anormal sayılmaz — yalnız kendi ölümcül hataları (bellek yetmemesi, dosya bulunamaması) ya da kopan bağlantı sayılır. Bu komut bir cron işinde ya da CI adımında duruyorsa, hiçbir şey geri yüklemeden başarı bildirir.
İki bayrak bunu düzeltir:
gunzip -c db.sql.gz | psql -U app -d appdb \
-v ON_ERROR_STOP=1 --single-transactionON_ERROR_STOP=1,psql'i ilk hatada durdurur ve 3 koduyla çıkar (benim denememde de öyle oldu).--single-transactiontüm betiği tek bir transaction'a sarar; yarıda durursa her şey geri alınır. Bu olmadan tabloların yarısı oluşturulmuş hâlde kalabilirsin.
pg_restore da varsayılan olarak aynı şekilde davranır. Dokümanı bunu açıkça söylüyor: varsayılan davranış "devam etmek ve geri yüklemenin sonunda hata sayısını göstermek". Fark şu: pg_restore hiç değilse hataları yok saydığında 1 koduyla çıkar ve pg_restore: warning: errors ignored on restore: 6 gibi bir satır basar.
pg_restore'da da aynı iki koruma var: --exit-on-error ve --single-transaction (ikincisi birincisini de kapsar). İkisi eşdeğer değil, farkı ölçtüm. İlk ALTER TABLE'da hata veren bir arşivi geri yüklerken:
- yalnız
--exit-on-errorile hedef veritabanında 3 tablodan 1'i kaldı; --single-transactionile 0 kaldı — hiçbir şey uygulanmadı.
Yarım geri yüklenmiş veritabanı boş olandan kötüdür, çünkü Django onun üstünde gönül rahatlığıyla ayağa kalkar. --single-transaction kullan.
Yeni sunucuya taşırken: "role does not exist"
İkinci olası hata, yeni bir makinede geri yüklerken çıkar. Dump varsayılan olarak her tablonun sahibini kaydeder:
ALTER TABLE public.blog_post OWNER TO app;Yeni sunucudaki veritabanı kullanıcısının adı farklıysa — eski .env'de POSTGRES_USER=app, yenisinde django diyelim — bu satırların hepsi başarısız olur:
pg_restore: error: could not execute query: ERROR: role "app" does not exist
Command was: ALTER TABLE public.blog_author OWNER TO app;
...
pg_restore: warning: errors ignored on restore: 6Veri yine de yüklenir, o yüzden zararsız görünür. Her zaman değil. Tablolar artık geri yüklemeyi kim çalıştırdıysa ona ait. Ben postgres süper kullanıcısıyla geri yükleyip django olarak bağlandığımda, Django'nun kullanıcısı kendi tablolarını okuyamadı:
ERROR: permission denied for table blog_postResmî postgres Docker imajında bu daha az başa gelir, çünkü POSTGRES_USER süper kullanıcı olarak oluşturulur ve insanlar genelde aynı kullanıcıyla geri yükler. Ama uygulaman ayrı, süper kullanıcı olmayan bir rolle bağlanıyorsa buna takılırsın.
Çözüm, sahipliği denklemden çıkarmak ve uygulamanın bağlandığı kullanıcıyla geri yüklemek:
pg_restore -U django -d appdb --no-owner --single-transaction backup.dump--no-owner ile her nesne bağlanan kullanıcıya ait olur. Denememde çıkış kodu 0'dı, üç tablo da django'ya aitti ve 50 satırın hepsi geri geldi. Ayarı yedeğin içine gömmek istersen pg_dump da aynı --no-owner bayrağını kabul eder — geri yükleme anında seçim şansı olmayan düz SQL dump'lar için kullanışlı.
Bu arada sekanslar sorunsuz taşınıyor. Dump her tablo için bir SEQUENCE SET girdisi içeriyor; geri yüklemeden sonraki ilk kayıt id olarak 51 aldı, yani yüklenen 50 satırın hemen ardından. Elle setval gerekmedi.
Var olan veritabanının üstüne geri yükleme
Bir veritabanının içeriğini değiştirmenin iki yolu var ve ikisinin de bir tuzağı var.
1. yol: pg_restore --clean. Her nesneyi yeniden oluşturmadan önce siler. Ama boş ya da yarı boş bir veritabanında silme komutlarının kendisi hata verir:
pg_restore: error: could not execute query: ERROR: relation "public.blog_post" does not exist
Command was: ALTER TABLE ONLY public.blog_post DROP CONSTRAINT blog_post_author_id_fkey;
...
pg_restore: warning: errors ignored on restore: 13Çıkış kodu 1 — ve --single-transaction ile birlikte kullanılırsa bu "hatalar" gayet sağlam bir geri yüklemeyi iptal ettirir. --if-exists ekle: pg_restore --clean --if-exists hem boş veritabanında hem de verinin zaten durduğu veritabanında 0 döndü, satır sayısı da 50'de kaldı (çift kayıt yok).
2. yol: veritabanını silip yeniden oluşturmak. Daha temiz, çünkü eski durumdan hiçbir şey kalamaz. Ama Django hâlâ çalışıyorsa başarısız olur:
ERROR: database "appdb" is being accessed by other users
DETAIL: There is 1 other session using the database.Önce uygulama container'ını durdur (docker compose stop backend). PostgreSQL 13 ve sonrası DROP DATABASE appdb WITH (FORCE) da kabul ediyor; açık bağlantıları senin yerine sonlandırır — denememde çalıştı, ama o oturumlar ne yapıyorsa yarıda keseceğini bilerek kullan.
Hepsini Docker Compose içinde yapmak
Postgres bir Compose servisinde çalışıyorsa (aşağıda Lightsail rehberindeki gibi adı postgres), geri yükleme o container'ın içinde çalışır. Üç ayrıntı önemli.
-T kullan. docker compose exec varsayılan olarak TTY ayırır. Dosyayı pipe ile vereceksen kapat:
docker compose exec -T postgres \
sh -c 'pg_restore -U "$POSTGRES_USER" -d "$POSTGRES_DB" --no-owner --single-transaction' \
< backup.dumppg_restore dosya adı verilmezse standart girdiden okur; dump'ı container'ın içine kopyalamak gerekmez.
Değişkenleri hangi kabuğun açtığına dikkat et. sh -c '...' sarmalayıcısı olmadan pg_restore -U "$POSTGRES_USER" yazarsan, değişkeni Docker komutu görmeden önce host'taki senin kabuğun açar. Etkileşimli oturumda bu tesadüfen çalışabilir. Cron'da ise .env yüklenmediği için değişken boştur ve set -u kullanan bir betik POSTGRES_USER: unbound variable diyerek durur. sh -c'nin etrafındaki tek tırnak, değişkeni Postgres imajında zaten tanımlı olduğu container'a bırakır.
Paralel geri yükleme gerçek bir dosya ister. Büyük veritabanında pg_restore -j 4 işi hızlandırır, ama pipe'tan değil:
pg_restore: error: parallel restore from standard input is not supportedÖnce dosyayı docker compose cp backup.dump postgres:/tmp/backup.dump ile içeri kopyala ve o yolu ver. -j ayrıca --single-transaction ile birlikte kullanılamaz — hız için bütünlükten vazgeçmiş olursun, o yüzden geri yükleme süresi gerçekten sorun olduğunda başvur.
Sürüm uyumsuzluğu: yeni \restrict satırı
Güncel bir pg_dump ile alınmış düz dump'ı açarsan, en üstlerde bir yıl önce olmayan bir satır görürsün:
\restrict <her dump için üretilen rastgele anahtar>Bu, Ağustos 2025 ara sürümleriyle (16.10, 17.6, 15.14, 14.19, 13.22) CVE-2025-8714'ün düzeltmesi olarak geldi. Sürüm notları gerekçeyi anlatıyor: ele geçirilmiş bir kaynak sunucu, psql'in meta-komut olarak yorumlayacağı metin üretip geri yüklemeyi yapan makinede kabuk erişimi elde edebilirdi. \restrict, dosyanın geri kalanı için meta-komutları kapatıyor.
Pratik sonucu: bu sürümlerden eski bir psql'de bu komut yok ve psql tanımadığı her meta-komuta invalid command diye yanıt veriyor. Eski bir istemciyi burada test edemedim, o yüzden böyle bir geri yüklemenin ne kadar ilerleyeceğini tahmin etmeyeceğim — mesele, bunu öğrenmeye gerek olmaması. Buna pg_dump dokümanının genel kuralını da ekle: bir dump'ın daha eski bir ana sürüme yüklenebileceği garanti değil, "dump o sürümden alınmış olsa bile". Buradan basit bir alışkanlık çıkıyor: hedef Postgres container'ının içindeki psql/pg_restore ile geri yükle, host'ta ne kuruluysa onunla değil. Yukarıdaki Compose komutları zaten bunu yapıyor.
Geri yüklemeden sonra
Trafiği yönlendirmeden önce iki kontrol:
docker compose exec -T postgres sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "ANALYZE"'
docker compose run --rm backend python manage.py migrate --checkpg_dumpdokümanı, sorgu planlayıcının güncel istatistiklere sahip olması için geri yüklemeden sonraANALYZEçalıştırmayı öneriyor.migrate --check, uygulanmamış migration varsa sıfırdan farklı kodla çıkar. Geri yüklenendjango_migrationstablosu yedeğin alındığı günü yansıtır; o günden beri migration deploy ettiysen, bir istek olmayan bir sütuna çarpmadan önce bunu öğrenirsin.
Kendi kendine çalışan geri yükleme testi
"Yedeklerini test et" ancak otomatikse yapılır. Önce yedeği custom biçime çevir (varsayılan olarak sıkıştırılmış olduğu için gzip adımı da gider):
docker compose exec -T postgres \
sh -c 'pg_dump -U "$POSTGRES_USER" -Fc "$POSTGRES_DB"' \
> "$HOME/backups/db-$(date +%F).dump"Ardından, yedek alındıktan sonra en yeni dosyayı geçici bir veritabanına geri yükle ve içinde bir şey olduğunu kontrol et:
#!/bin/bash
# ~/restore-test.sh
set -euo pipefail
COMPOSE="docker compose -f $HOME/app/docker-compose.yml"
LATEST=$(ls -1t "$HOME"/backups/db-*.dump | head -n 1)
$COMPOSE exec -T postgres sh -c \
'dropdb -U "$POSTGRES_USER" --if-exists restore_test && createdb -U "$POSTGRES_USER" restore_test'
$COMPOSE exec -T postgres sh -c \
'pg_restore -U "$POSTGRES_USER" -d restore_test --no-owner --single-transaction' \
< "$LATEST"
ROWS=$($COMPOSE exec -T postgres sh -c \
'psql -U "$POSTGRES_USER" -d restore_test -tAc "SELECT count(*) FROM django_migrations"')
[ "$ROWS" -gt 0 ] || { echo "restore test FAILED: django_migrations is empty"; exit 1; }
echo "restore test OK: $LATEST ($ROWS migrations)"Aynı mantığı (Docker sarmalayıcısı olmadan) test kümemde çalıştırdım: geçerli dump'ta geçti, tekrar çalıştırmada yine geçti, bozuk bir dosyada ise 1 koduyla başarısız oldu (input file does not appear to be a valid archive). Başarısızlığı seni zaten uyaran neye bağlıysa ona bağla — cron'un MAILTO'su, bir health-check ping'i — çünkü kimsenin okumadığı bir geri yükleme testi, test olmamasıyla aynı.
Bunun kapsamadığı iki şey var. Yalnız veritabanını kontrol ediyor; yüklenen medya dosyalarının kendi yedeği gerekir. Ve bir Docker Compose kurulumundaki pgdata volume'ü container'dan uzun yaşar ama sunucudan değil; dump dosyalarının da makineden dışarı kopyalanması gerekir.
Özet
- Düz dump'lar
psqlile, custom arşivlerpg_restoreile yüklenir. Hangisi olduğunuhead -c 5söyler. - Denememde
psql13 hatadan sonra 0 ile çıktı. Her zaman-v ON_ERROR_STOP=1 --single-transactionkullan. pg_restore'da--exit-on-erroryerine--single-transactiontercih et: ilki şemanın üçte birini geride bıraktı.- Yeni sunucuda
--no-ownerve uygulamanın kendi kullanıcısıyla geri yükleme, hemrole does not existhempermission deniedhatasını önler. --cleaniçin--if-existsşart; veritabanını silmek için uygulamanın durması (ya daWITH (FORCE)) gerekir.- Compose'da:
exec -T, değişkenleri container açsın diye tek tırnaklısh -cve host'unki değil container'ın kendi istemcisi. - Geri yükleme testini otomatikleştir. Başarıyla bir kez çalışana kadar yedeğin, işe yaramasını umduğun bir dosyadan ibaret.