Docker Compose'ta bir servisin ana container'ı başlamadan önce veritabanı migration'ı çalıştırmak, volume izinlerini düzeltmek gibi hazırlık işleri için pre_start kullanılır. Compose bu adımları init container olarak modeller: her adım kendi kısa ömürlü container'ında, yazıldığı sırayla çalışır ve servis ancak tüm adımlar 0 koduyla çıkarsa başlar. Container çalıştıktan sonra bir komut çalıştırmak için post_start, durdurulmadan hemen önce bir komut çalıştırmak için pre_stop lifecycle hook'ları vardır.

Bu yazı üç hook'un nasıl çalıştığını, ne zaman yeniden çalıştırıldığını, eski "tek seferlik servis" kalıbıyla farkını ve başlangıç sırasını belirleyen depends_on koşullarını anlatır. Sağlık kontrolü ile depends_on kullanımının temelleri Docker Compose rehberinde var; burada init container ve hook ayrıntılarına odaklanıyoruz. Örneklerdeki Compose dosyaları Docker Compose v5.5.1 ile docker compose config komutundan hatasız geçti. Serinin tamamı için Docker öğrenme rehberi.

Üç hook'a genel bakış

Üç hook'a genel bakış: pre_start, post_start, pre_stop
HookNerede çalışırNe zamanTipik kullanım
pre_startAyrı, kısa ömürlü bir containerServis container'ı oluşturulduktan sonra, başlamadan önceMigration, izin düzeltme, yapılandırma üretme
post_startÇalışan servis container'ının içindeContainer başladıktan sonra, zamanı garanti değilHarici bir kayıt sistemine bildirim
pre_stopÇalışan servis container'ının içindeAçık bir durdurma komutunda, durdurma sinyalinden önceSon yedek, kapanış işlemleri

Belgeler lifecycle hook'ların önemli bir avantajına dikkat çeker: hook'lar özel ayrıcalıklarla, örneğin root kullanıcısıyla çalışabilir; container'ın kendisi ise güvenlik için düşük ayrıcalıklı bir kullanıcıyla çalışmaya devam eder. Böylece yüksek izin gerektiren tek bir iş için bütün servisi root'a çevirmek zorunda kalmazsınız.

pre_start nasıl çalışır?

pre_start listesindeki her adım için şunlar geçerlidir:

  • Adım, servis container'ı oluşturulduktan sonra ama başlatılmadan önce yaratılan kendi kısa ömürlü container'ında çalışır.
  • Varsayılan olarak servisin imajını kullanır; image ile başka bir imaj verebilirsiniz. Hook imajı, yenileme aralığı dahil, ana servisin pull_policy ayarına uyar.
  • Servisle aynı network'lere katılır; bu sayede depends_on ile belirtilen servislere ulaşabilir.
  • Servisin volume bağlamalarını paylaşır; paylaşılan bir volume'a yazdığı dosyalar servis tarafından hemen görülür.
  • Bir sonraki adımın ve servisin başlaması için 0 koduyla çıkmalıdır. Sıfır dışındaki bir çıkış kodu servisin ve ona bağımlı her şeyin başlatılmasını durdurur.

Adımlar ancak servisin depends_on koşulları sağlandıktan sonra çalışır. Yani veritabanının sağlıklı olmasını bekleyen bir servisin migration adımı da aynı beklemeden yararlanır. Kullanılabilen alanlar şunlardır: command (seçilen imajın entrypoint'i zaten istenen işi yapıyorsa isteğe bağlı), image, user (verilmezse imajdaki kullanıcı; image yoksa ana servis komutunun kullanıcısı), privileged, working_dir, environment (servis ortamını devralır, ekler veya ezer) ve per_replica.

Adım ne zaman yeniden çalışır?

Bu kural sunucu kullanımı için önemlidir, çünkü migration'ın her up komutunda tekrar çalışıp çalışmayacağını belirler. Mevcut tanımıyla daha önce başarıyla çalışmış bir adım, sonraki docker compose up komutlarında atlanır; servis container'ı restart politikası nedeniyle yeniden başladığında da çalışmaz. Adım şu durumlarda yeniden çalışır: tanımı değiştiyse, önceki çalıştırma başarısız olduysa ya da servis yeniden oluşturulduysa, örneğin servis yapılandırması değiştiğinde veya --force-recreate ile. Restart politikalarının kendisi Docker restart policy yazısında anlatılıyor.

bash
docker compose up -d

# Servisi ve pre_start adımlarını yeniden çalıştırmak için
docker compose up -d --force-recreate app

Örnek: veritabanı hazır olunca migration ve seed

Aşağıdaki dosyada app servisi önce db servisinin sağlıklı olmasını bekler, ardından migration ve başlangıç verisi yükleme adımlarını sırayla çalıştırır. Veritabanı parolası bir secret olarak verilir:

yaml
services:
  app:
    image: myapp:latest
    depends_on:
      db:
        condition: service_healthy
    pre_start:
      - command: ["./manage.py", "migrate"]
      - command: ["./manage.py", "loaddata", "fixtures.json"]
    restart: unless-stopped

  db:
    image: postgres:18
    environment:
      POSTGRES_USER: app
      POSTGRES_DB: app
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 10s
      retries: 5
      start_period: 30s
      timeout: 10s

secrets:
  db_password:
    file: ./db_password.txt

İki adım da image belirtmediği için myapp:latest imajından oluşturulan geçici container'larda çalışır. Migration başarısız olursa app başlamaz ve hata docker compose up çıktısında raporlanır. İkinci adım başarısız olursa birinci adım geri alınmaz ama app yine başlamaz; yani migration'larınızın tekrar çalıştırılmaya dayanıklı olması iyi bir alışkanlıktır. Sağlık kontrolündeki $$ kullanımı, değişkenin Compose tarafından değil container içindeki kabuk tarafından çözülmesi içindir (bkz. Compose değişken interpolasyonu). Secret kullanımı Compose secrets ve configs yazısında anlatılıyor.

Örnek: root olmayan servis için volume sahipliğini düzeltmek

Örnek: root olmayan servis için volume sahipliğini düzeltmek

Belgelerin belirttiği gibi named volume'lar root sahipliğiyle oluşturulur. Servis root olmayan bir kullanıcıyla çalışıyorsa volume'a yazamayabilir. Bunu servisin kendisini root yapmadan, küçük bir imajla root olarak çalışan tek bir adımla düzeltebilirsiniz:

yaml
services:
  app:
    image: myapp:latest
    user: "1000:1000"
    volumes:
      - data:/data
    pre_start:
      - image: busybox
        user: root
        command: sh -c 'chown -R 1000:1000 /data'

volumes:
  data:

Adım busybox imajını kullanır ve root olarak çalışır, servis ise 1000 numaralı kullanıcıyla kalır. Adım servisin volume bağlamalarını paylaştığı için /data aynı volume'dur. Volume'ların kendisi için Docker volume kullanımı yazısına bakın. İlginç bir ayrıntı: docker compose config çıktısında shell biçiminde yazdığımız komut sh, -c ve chown -R 1000:1000 /data olmak üzere üç öğeli bir listeye dönüştü; Compose komutları normalize ederken bu biçimi kullanır.

Tek seferlik servis kalıbı yerine pre_start

pre_start gelmeden önce "Y başlamadan X'i çalıştır" ihtiyacı genellikle ayrı bir servisle çözülürdü: hazırlık işi restart: "no" olan bir servis olarak yazılır, ana servis de ona condition: service_completed_successfully ile bağlanırdı.

yaml
services:
  migrate:
    image: myapp:latest
    command: ["./manage.py", "migrate"]
    restart: "no"

  app:
    image: myapp:latest
    depends_on:
      migrate:
        condition: service_completed_successfully

Aynı iş pre_start ile tek satırlık bir adıma iner. Belgeler bu yaklaşımın neden tercih edilebilir olduğunu şöyle sıralar: hazırlık işi hemen çıkan eşit düzeyde bir servis değil, servisin alt adımı olarak modellenir; tamamlanan adımlar docker compose ps çıktısında çıkmış servisler olarak görünmez; birden fazla adımı zincirlemek için tek seferlik servisler arasında depends_on ağı kurmak gerekmez; geçici container servisin imajını devraldığı için image: satırını tekrar yazmazsınız.

Tek seferlik servis kalıbı yine de geçerliliğini korur: hazırlık işi birden fazla servisin bağlı olduğu ortak bir işse ya da tek bir servisten bağımsız olarak ele alınması gerekiyorsa ayrı servis daha uygundur.

Ne zaman init container kullanılmaz?

Belgeler iki durumu açıkça ayırır. Statik dosyalar ve sırlar için init container gerekmez; Compose'un configs ve secrets öğeleri bunları hedef yol, izin, UID ve GID ayarlanabilir biçimde doğrudan bağlar. Zamanlanmış yedekler, çıkış sonrası temizlik veya periyodik bakım gibi kendi yaşam döngüsü olan arka plan işleri için de init container yanlış araçtır, çünkü bu işler servisin başlamasından önce değil, ondan bağımsız çalışır.

Bilinen sınırlamalar da şunlardır. pre_start her replika için değil, servis için bir kez çalışır (per_replica: false); belgelere göre replika başına çalıştırma (per_replica: true) henüz desteklenmiyor. Bizim denememizde per_replica: true yazılmış bir dosya docker compose config doğrulamasından hatasız geçti; doğrulamanın geçmesi özelliğin desteklendiği anlamına gelmez. Replikalar arasında paylaşılan bağlamalar (named volume, bind mount) adımdan erişilebilir; tmpfs veya anonim volume gibi her container'a özel bağlamalara tek bir ortak çalıştırma erişemez. Bu bir hata değildir, adım bu bağlamalar olmadan çalışır. Son olarak servisi ölçeklendirmek adımları yeniden tetiklemez.

post_start: container başladıktan sonra

post_start, container başladıktan sonra çalışan komutları tanımlar ama tam olarak ne zaman çalışacağı garanti edilmez; özellikle container'ın entrypoint'i çalışırken hangi noktada devreye gireceği belli değildir. Hook ile entrypoint arasında sıralama garantisi olmadığı için belgeler post_start'ı, uygulama çalışmaya başlamadan önce tamamlanması gerekmeyen işler için önerir. Uygulamanın başlamadan önce ihtiyaç duyduğu her şey pre_start adımına aittir.

Alanlar şunlardır: command (zorunlu, shell veya exec biçimi), user, privileged, working_dir ve environment. Belgelerdeki örnekte container 1001 numaralı kullanıcıyla çalışırken root olarak çalışan bir hook servisi iç kayıt sistemine bildirir; uygulama istekleri karşılamak için kaydın bitmesini beklemez.

pre_stop: durdurmadan hemen önce

pre_stop, container belirli bir komutla durdurulmadan önce çalışan komutlardır: docker compose down veya terminalde Ctrl+C gibi. Container kendi kendine durursa veya aniden sonlandırılırsa bu hook'lar çalışmaz. Hook durdurma sinyali gönderilmeden önce çalıştığı için uygulama hâlâ tam olarak çalışırken bitmesi gereken işler için uygundur. Yapılandırması post_start ile aynıdır.

yaml
services:
  app:
    image: backend
    user: "1001"
    volumes:
      - data:/data
    post_start:
      - command: /opt/scripts/register-service.sh
        user: root
    pre_stop:
      - command: cp /data/app.db /data/app.db.bak
    stop_grace_period: 30s

volumes:
  data: {}

Bu dosyada iki hook ve bir durdurma ayarı bir arada. stop_grace_period, durdurma sinyalini (varsayılan olarak SIGTERM, stop_signal ile değiştirilebilir) işlemeyen bir container için Compose'un SIGKILL göndermeden önce ne kadar bekleyeceğini belirler; varsayılanı 10 saniyedir. Uygulamanız kapanırken veri yazıyorsa bu süreyi ihtiyacınıza göre ayarlayın. pre_stop hook'unun kendisinin bu süreye nasıl dahil edildiği belgelerde ayrıca açıklanmadığı için uzun süren işleri pre_stop'a koymadan önce kendi ortamınızda deneyin.

depends_on ve başlangıç sırası

depends_on ve başlangıç sırası: service_started, service_healthy, service_completed_successfully, restart

Compose container'ları her zaman bağımlılık sırasıyla başlatır ve durdurur. Bağımlılıklar depends_on, links, volumes_from ve network_mode: "service:..." ile belirlenir. Önemli ayrıntı şudur: başlangıçta Compose bir container'ın "hazır" olmasını değil, yalnızca çalışıyor olmasını bekler. Bağlantı kabul etmeden önce kendi servislerini başlatması gereken bir veritabanı için bu yetmez; bu yüzden uzun sözdizimindeki condition alanı kullanılır.

  • service_started: Kısa sözdizimine eşdeğerdir; bağımlılığın başlamış olması yeterlidir.
  • service_healthy: Bağımlılığın healthcheck ile tanımlanan sağlık kontrolünden geçmesi beklenir.
  • service_completed_successfully: Bağımlılığın başarıyla çalışıp bitmesi beklenir.
  • restart: true: Bağımlılık açık bir Compose işlemiyle, örneğin docker compose restart ile güncellenir veya yeniden başlatılırsa bu servis de yeniden başlatılır. Container öldükten sonra çalışma zamanının yaptığı otomatik yeniden başlatmalar buna dahil değildir.
  • required: false: Bağımlılık başlatılmamışsa veya yoksa Compose yalnızca uyarır. Varsayılan true değeridir.
yaml
services:
  web:
    image: example/web
    depends_on:
      db:
        condition: service_healthy
        restart: true
      cache:
        condition: service_started
        required: false
  db:
    image: postgres:18
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
  cache:
    image: redis:latest

Kısa sözdiziminde yazılmış depends_on bile docker compose config çıktısında condition: service_started ve required: true alanlarıyla normalize edilir; bu, varsayılanların ne olduğunu görmenin pratik bir yoludur. Kaldırma da bağımlılık sırasıyla yapılır: yukarıdaki örnekte web, db ve cache servislerinden önce kaldırılır.

Sık yapılan hatalar

  • Migration'ı post_start'a koymak. post_start'ın entrypoint ile sıralama garantisi yoktur; uygulama migration bitmeden başlayabilir. Hazırlık işleri pre_start'a aittir.
  • pre_stop'un her durumda çalışacağını sanmak. Container kendisi durursa veya aniden sonlandırılırsa çalışmaz.
  • depends_on listesinin hazır olmayı beklediğini sanmak. Kısa sözdizimi yalnızca başlamış olmayı bekler; hazırlık için service_healthy ve bir healthcheck gerekir.
  • pre_start adımının her up'ta çalışmasını beklemek. Başarılı ve değişmemiş adım atlanır; yeniden çalıştırmak için tanımı değiştirin veya --force-recreate kullanın.
  • Servisi yalnızca chown için root çalıştırmak. Bunun yerine root olarak çalışan bir pre_start adımı kullanın.
  • Statik ayar dosyası üretmek için init container yazmak. configs ve secrets bu iş için yeterlidir.

Sık Sorulan Sorular

pre_start adımı başarısız olursa ne olur?

Servis ve ona bağlı servisler başlatılmaz, hata docker compose up çıktısında raporlanır. Daha önce başarıyla biten adımlar geri alınmaz.

pre_start adımı ölçeklendirilmiş serviste kaç kez çalışır?

Servis için bir kez. Belgelere göre replika başına çalıştırma henüz desteklenmiyor ve servisi ölçeklendirmek adımı yeniden tetiklemiyor.

post_start ile Dockerfile'daki ENTRYPOINT arasındaki fark nedir?

ENTRYPOINT container'ın ana sürecidir. post_start ise container başladıktan sonra, ana süreçle sıralama garantisi olmadan, farklı bir kullanıcı veya ayrıcalıkla çalışabilen ek bir komuttur. ENTRYPOINT ve CMD ilişkisi için CMD ve ENTRYPOINT farkı yazısına bakın.

Tek seferlik migrate servisini tamamen bırakmalı mıyım?

Zorunlu değil. Hazırlık işi birden fazla servisin ortak ihtiyacıysa veya bağımsız olarak çağrılabilmesi gerekiyorsa ayrı servis kalıbı hâlâ uygundur.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Docker öğrenme rehberi · Docker Compose rehberi · Docker restart policy · Compose secrets ve configs · Docker volume kullanımı · CMD ve ENTRYPOINT farkı