CMD, container başladığında çalışacak varsayılan programı ya da varsayılan argümanları tanımlar ve kolayca değiştirilir; ENTRYPOINT ise imajın ana komutunu belirler ve imajın o komutmuş gibi çalışmasını sağlar. En yaygın düzen, programı ENTRYPOINT ile sabitleyip varsayılan bayrakları CMD ile vermektir. İkisinde de JSON dizisi biçimi (exec form) önerilir, çünkü kabuk biçimi sinyallerin uygulamaya ulaşmasını engelleyebilir.

Bu yazıda iki talimatın birlikte nasıl davrandığını, exec form ile shell form arasındaki farkı, container'ın neden 10 saniye bekleyip zorla kapandığını, Postgres resmî imajının kullandığı entrypoint betiği desenini ve Compose'taki command ile entrypoint alanlarını resmî belgelere dayanarak anlatıyoruz. Dockerfile temelleri için Dockerfile yazma rehberine, serinin diğer yazıları için Docker öğrenme rehberi sayfasına bakabilirsiniz.

CMD ne yapar?

Docker belgelerine göre CMD, imajdan bir container başlattığınızda çalışacak varsayılan programı tanımlar. Bir Dockerfile'da yalnızca bir CMD etkilidir: birden fazla yazarsanız sonuncusu geçerli olur. Bu kural çok aşamalı build'lerde ve temel imajdan devralınan ayarlarda da akılda tutulmalıdır; son aşamadaki son CMD imajın varsayılan komutudur.

En iyi uygulamalar sayfası CMD talimatının neredeyse her zaman CMD ["executable", "param1", "param2"] biçiminde kullanılmasını önerir. Apache ya da Rails gibi bir servis imajında bu CMD ["apache2","-DFOREGROUND"] gibi bir satırdır. Dil imajlarında ise CMD çoğunlukla etkileşimli bir kabuk ya da yorumlayıcı başlatır: CMD ["python"], CMD ["perl", "-de0"] veya CMD ["php", "-a"]. Böylece docker run -it python çalıştıran kullanıcı doğrudan kullanılabilir bir ortama düşer.

dockerfile
# syntax=docker/dockerfile:1
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y python3 python3-pip
RUN pip install flask==3.0.*
COPY hello.py /
ENV FLASK_APP=hello
EXPOSE 8000
CMD ["flask", "run", "--host", "0.0.0.0", "--port", "8000"]

Bu örnek belgelerdeki Flask uygulamasıdır. CMD satırı, container başladığında geliştirme sunucusunu tüm adreslerde 8000 portunda dinleyecek şekilde açar. docker run komutunda imaj adından sonra bir komut yazarsanız bu varsayılanın yerine o komut çalışır; CMD bu yüzden bir öneri, bir varsayılan gibi düşünülmelidir.

ENTRYPOINT ne yapar?

Belgeler ENTRYPOINT için en iyi kullanımın imajın ana komutunu ayarlamak olduğunu, böylece imajın o komutun kendisiymiş gibi çalıştırılabileceğini ve CMD'nin de varsayılan bayraklar olarak kullanılacağını söyler. s3cmd komut satırı aracı için verilen örnek bu düzeni çok net gösterir:

dockerfile
ENTRYPOINT ["s3cmd"]
CMD ["--help"]
bash
docker run s3cmd
docker run s3cmd ls s3://mybucket

İlk komut argüman vermediği için CMD devreye girer ve aracın yardım çıktısı gösterilir. İkinci komutta imaj adından sonra yazılan ls s3://mybucket argümanları CMD'nin yerini alır ve s3cmd bu argümanlarla çalışır. İmaj adı böylece aracın kendisine bir referans gibi kullanılır. Bu, sunucuda tek bir iş yapan yardımcı araç imajları (yedekleme, göç, rapor üretme) için kullanışlı bir desendir.

İkisi birlikte: hangisi ne zaman değişir?

İkisi birlikte: hangisi ne zaman değişir? Yalnızca CMD var, ENTRYPOINT ve CMD var, Compose command, Compose entrypoint

Aşağıdaki tablo, belgelerde anlatılan davranışları sunucu kullanımı açısından özetler. Compose satırları Compose dosyası içindeki servis alanlarına aittir.

DurumSonuç
Yalnızca CMD varCMD varsayılan programdır; docker run sonrasına yazılan komut onun yerine geçer
ENTRYPOINT ve CMD varENTRYPOINT ana komuttur; CMD varsayılan argümanlardır ve docker run argümanlarıyla değişir
Compose commandİmajın varsayılan komutunu, örneğin CMD talimatını geçersiz kılar
Compose entrypointENTRYPOINT talimatını geçersiz kılar; null değilse imajın varsayılan komutu da yok sayılır
command veya entrypoint nullİmajdaki varsayılan kullanılır
command veya entrypoint [] ya da boş metinİmajdaki varsayılan yok sayılır, yani boş değere çekilir

Tablodaki dördüncü satır sık gözden kaçar: Compose'ta entrypoint verdiğiniz anda imajın CMD değeri de devre dışı kalır. Eski davranışı korumak istiyorsanız command alanını da birlikte yazmanız gerekir. Docker CLI tarafında da docker run komutunun --entrypoint adlı bir bayrağı vardır; ayrıntılı davranışı için CLI başvurusuna bakın.

Exec form ve shell form farkı

Her iki talimat da iki biçimde yazılabilir. JSON dizisi biçimine exec form denir: CMD ["flask", "run", "--host", "0.0.0.0", "--port", "8000"]. Düz metin biçimine shell form denir: CMD flask run --host 0.0.0.0 --port 8000. Dockerfile genel bakış sayfası bu iki sürüm arasında ince farklar olduğunu ve bunlardan birinin SIGTERM ve SIGKILL gibi sinyallerin nasıl yakalandığı olduğunu belirtir.

Compose SSS sayfası farkın pratik sonucunu anlatır: metin biçimi Docker'ın süreci bir kabuk üzerinden çalıştırmasına yol açar ve bu kabuk sinyalleri düzgün iletmez. Uygulamanız durdurma sinyalini hiç almadığı için kapanmaz, Docker da bekleme süresi dolunca onu zorla öldürür. Aynı sayfa, Compose'un kendi command ve entrypoint değerlerini her zaman JSON biçiminde kullandığını, dolayısıyla Compose dosyasında yaptığınız geçersiz kılmalar için bu konuda endişelenmenize gerek olmadığını da ekler.

Docker'ın build denetimleri de bu konuyu yakalar. Shell form ile yazılmış bir CMD ya da ENTRYPOINT, JSONArgsRecommended kuralını tetikler ve build çıktısında işletim sistemi sinyalleriyle ilgili beklenmedik davranışı önlemek için JSON argümanlarının önerildiğini söyleyen bir uyarı görürsünüz. Bu kuralı bir hata olarak ele almak ve denetimleri yönetmek build check yazısının konusudur.

Shell form'un hâlâ yeri vardır. RUN talimatında boru (|) ve && gibi kabuk özelliklerini kullanmak doğaldır. Belgeler shell form RUN komutlarının /bin/sh -c yorumlayıcısıyla çalıştığını ve bir borudaki yalnızca son komutun çıkış kodunun değerlendirildiğini hatırlatır. Hata durumunda build'in durması için set -o pipefail && eklenir; Debian tabanlı imajlardaki dash gibi bu seçeneği desteklemeyen kabuklarda ise exec form ile açıkça bash seçilir:

dockerfile
RUN ["/bin/bash", "-c", "set -o pipefail && wget -O - https://some.site | wc -l > /number"]

Kabuk özelliği gerektiren bir başlangıç komutunu exec form ile yazmanın yolu da aynıdır: kabuğu programın kendisi yaparsınız. Compose belgesi command alanının imajdaki SHELL talimatı bağlamında otomatik çalışmadığını, ortam değişkeni genişletme gibi kabuk özelliklerine ihtiyaç varsa komutun açıkça bir kabuk içinde çalıştırılması gerektiğini söyler.

PID 1, sinyaller ve 10 saniyelik bekleme

PID 1, sinyaller ve 10 saniyelik bekleme: Dockerfile'da CMD ve ENTRYPOINT için exec form kullanmak., Mümkünse uygulamaya SIGTERM için açık bir sinyal işleyici eklemek...

docker compose stop bir container'ı durdurmak için önce SIGTERM gönderir ve varsayılan olarak 10 saniye bekler; süre dolunca SIGKILL ile süreci zorla öldürür. Servisleriniz her yeniden oluşturmada ya da durdurmada bu kadar bekliyorsa, Compose SSS sayfasına göre container'larınız SIGTERM aldıklarında kapanmıyor demektir. Önerilen çözümler sırasıyla şunlardır:

  • Dockerfile'da CMD ve ENTRYPOINT için exec form kullanmak.
  • Mümkünse uygulamaya SIGTERM için açık bir sinyal işleyici eklemek.
  • Uygulamanın anladığı bir sinyali stop_signal ile ayarlamak, örneğin SIGINT.
  • Uygulamayı değiştiremiyorsanız onu s6 gibi hafif bir init sistemiyle ya da dumb-init veya tini gibi bir sinyal vekiliyle sarmak.

Docker tarafında bunun hazır bir karşılığı var. Çoklu süreç belgesine göre docker run --init bayrağı container'a ana süreç olarak küçük bir init süreci ekler ve container kapanırken tüm alt süreçlerin toplanmasını üstlenir; belgeler bunun container içinde systemd ya da sysvinit gibi tam bir init sistemi kullanmaktan daha iyi olduğunu belirtir. Compose'taki karşılığı init: true alanıdır: sinyalleri ileten ve süreçleri toplayan bir init süreci (PID 1) çalıştırır, kullanılan ikili dosya platforma bağlıdır. Bekleme süresini stop_grace_period ile değiştirirsiniz; belirtilmezse 10 saniyedir.

Aşağıdaki Compose dosyası bu alanları ve entrypoint/command geçersiz kılmalarını bir arada gösterir. Docker CLI 29.8.1, Docker Compose v5.5.1 ile docker compose config --quiet çıkış kodu 0 verdi. $$HOSTNAME yazımındaki çift dolar, değişkenin Compose tarafından değil container içindeki kabuk tarafından çözülmesi içindir; bu kaçış kuralı Compose değişken interpolasyonu yazısında anlatılıyor.

yaml
services:
  worker:
    image: alpine:3.21
    entrypoint: ["/bin/sh", "-c"]
    command: ["echo \"hello $$HOSTNAME\""]
    init: true
    stop_signal: SIGINT
    stop_grace_period: 30s
  tool:
    image: alpine:3.21
    entrypoint: []
    command: ["ls", "-l", "/"]

tool servisinde entrypoint: [] imajın varsayılan entrypoint'ini boşaltır ve command listesi doğrudan çalıştırılacak program olur. Bu, entrypoint betiği olan bir imajda betiği atlayıp içerideki bir aracı tek seferlik çalıştırmak istediğinizde işe yarar.

Entrypoint betiği deseni

Bir aracı başlatmak birden fazla adım gerektiriyorsa ENTRYPOINT bir yardımcı betikle birlikte kullanılabilir. Belgeler Postgres resmî imajının betiğini örnek verir: betik ilk argüman postgres ise veri dizininin sahipliğini ayarlar, dizin boşsa veritabanını ilklendirir ve sunucuyu yetkisiz kullanıcıyla başlatır; değilse kendisine verilen komutu çalıştırır.

bash
#!/bin/sh
set -e

if [ "$1" = 'postgres' ]; then
    chown -R postgres "$PGDATA"

    if [ -z "$(ls -A "$PGDATA")" ]; then
        gosu postgres initdb
    fi

    exec gosu postgres "$@"
fi

exec "$@"

Buradaki kritik ayrıntı exec komutudur. Belgelere göre betik bu yerleşik komutu kullandığı için en sonda çalışan uygulama container'ın PID 1 süreci olur ve container'a gönderilen Unix sinyallerini doğrudan alır. exec olmadan betik kabuğu PID 1 olarak kalır ve yukarıda anlatılan sinyal sorunu geri gelir. Betik Dockerfile'a şöyle bağlanır:

dockerfile
COPY ./docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["postgres"]
bash
docker run postgres
docker run postgres postgres --help
docker run --rm -it postgres bash

Üç komut aynı imajı üç farklı şekilde kullanır: ilki varsayılan CMD ile Postgres'i başlatır, ikincisi sunucuya parametre geçirir, üçüncüsü ise betiğin son satırı sayesinde tamamen farklı bir araç olan bash'i açar. Kendi betiğinizi kopyalarken belgelerdeki çoklu süreç örneklerinde olduğu gibi COPY --chmod=755 ile çalıştırma iznini aynı adımda verebilirsiniz.

Bir container'da birden fazla süreç

Belgeler bir container'ın ana sürecinin Dockerfile sonundaki ENTRYPOINT ve/veya CMD olduğunu ve her container'da tek bir sorumluluk alanı tutmanın en iyi uygulama olduğunu söyler. Bir servisin kendi alt süreçlerini açması sorun değildir; Apache birden çok işçi süreci başlatır. Gerçekten birden fazla servis gerekiyorsa üç yol gösterilir: süreçleri arka planda başlatıp wait -n ile bekleyen bir sarmalayıcı betik, ana süreci bash iş denetimiyle (set -m ve fg %1) öne alan bir betik ya da supervisord gibi bir süreç yöneticisi. Belgelere göre süreç yöneticisi en çok uğraş gerektirendir, çünkü hem kendisini hem yapılandırmasını imaja eklemeniz gerekir. Çoğu durumda ayrı servisleri ayrı container'lara bölüp Compose ile birbirine bağlamak daha sade bir çözümdür.

Kabuksuz imajlarda dikkat

FROM scratch ile oluşturulan en küçük imajlarda, belgelerdeki örnekte de görüldüğü gibi CMD ["/hello"] exec form ile yazılır; scratch boş bir başlangıç noktasıdır ve içinde siz eklemedikçe kabuk bulunmaz. Docker Hardened Images için geçiş listesi de benzer bir uyarı yapar: çalışma zamanı imajları kabuk içermez, kabuk komutlarının dev etiketli build aşamalarına taşınması gerekir. Aynı liste, seçtiğiniz imajın entry point'ini docker inspect ya da imaj belgeleriyle kontrol etmenizi ve uygulamanız farklı bir başlangıca dayanıyorsa ENTRYPOINT veya CMD talimatlarınızı güncellemenizi söyler. Temel imaj seçimi base image yazısında ayrıca ele alınıyor.

Sık yapılan hatalar

Sık yapılan hatalar: Servis imajında CMD node server.js gibi shell form kullanmak: durdurma sinyali..., Entrypoint betiğinin son satırında exec unutmak, Compose'ta yalnızca...
  • Servis imajında CMD node server.js gibi shell form kullanmak: durdurma sinyali uygulamaya ulaşmaz ve kapanış 10 saniye sürer.
  • Entrypoint betiğinin son satırında exec unutmak: kabuk PID 1 olarak kalır.
  • Compose'ta yalnızca entrypoint değiştirip imajın CMD değerinin korunacağını sanmak.
  • Aynı Dockerfile'da iki CMD yazıp ilkinin çalışacağını beklemek: yalnızca sonuncusu geçerlidir.
  • Compose command içinde $DEGISKEN genişlemesi beklemek: kabuk özelliği için komutu /bin/sh -c içinde çalıştırın ve doları $$ ile kaçırın.
  • Kabuksuz bir çalışma zamanı imajında shell form başlangıç komutu kullanmak.

Sık Sorulan Sorular

CMD mi ENTRYPOINT mi kullanmalıyım?

Uygulama sunucusu gibi bir servis için exec form bir CMD genellikle yeterlidir. İmajın belirli bir aracın kendisi gibi davranmasını istiyorsanız aracı ENTRYPOINT, varsayılan bayrakları CMD yapın.

Container neden durdurulurken 10 saniye bekliyor?

Süreç SIGTERM sinyaline tepki vermiyor. Exec form kullanın, gerekirse init: true ekleyin, stop_signal ile uygulamanın anladığı sinyali seçin veya uygulamaya sinyal işleyici ekleyin.

Compose command alanında shell form yazarsam ne olur?

Compose bu alanı kendi tarafında JSON biçiminde kullanır ve imajın SHELL talimatı bağlamında çalıştırmaz. Kabuk özelliği gerekiyorsa komutu açıkça /bin/sh -c ile verin.

init: true ne işe yarar?

Container'da PID 1 olarak sinyalleri ileten ve alt süreçleri toplayan küçük bir init süreci çalıştırır. Alt süreçlerini düzgün toplamayan uygulamalar için belgelerin önerdiği çözümdür.

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 · COPY ve ADD farkı · ARG ve ENV farkı · Dockerfile build check · Compose lifecycle hook · Dockerfile yazma rehberi