Docker Best Practices: CMD, ENTRYPOINT, ADD ve COPY'yi Ne Zaman Kullanmalı (Lab'da Ölçtüm)

Docker best practices ölçtüm: shell-form CMD docker stop'u 10 sn bekletip SIGTERM'i yutuyor, exec form 0.27 sn'de duruyor. CMD, ADD, COPY ne zaman?

Yazan: Mirac Can Yılmaz 7 dk okuma
Docker Best Practices: CMD, ENTRYPOINT, ADD ve COPY'yi Ne Zaman Kullanmalı (Lab'da Ölçtüm) - DevOps yazısının kapak görseli

İki Dockerfile, tek uygulama, aralarında tek kelimelik fark var. CMD python app.py ve CMD ["python", "app.py"] aynı görünüyor, container da siz durdurmaya çalışana kadar aynı çalışıyor. Birincisini durdurmak 10.30 saniye sürdü ve uygulamanın kapanış kodu hiç çalışmadı. İkincisi 0.27 saniyede durdu. Fark buradan çıkıyor: container içinde hangi süreç PID 1 oluyor ve durdurma sinyali uygulamanıza mı, yoksa onu yok sayan bir shell'e mi ulaşıyor. Bu yazıda önce bunu ölçtüm, sonra aynısını ENTRYPOINT ile CMD argümanları için, bir de ADD ile COPY için yaptım.

CMD ve ENTRYPOINT arasındaki fark nedir?

İkisi de container başladığında ne çalışacağını belirler ve her biri iki şekilde yazılabilir:

  • Exec form (["python", "app.py"]): Docker programı doğrudan çalıştırır. Programınız PID 1 olur ve sinyalleri alır.
  • Shell form (python app.py): Docker /bin/sh -c "python app.py" çalıştırır. PID 1 artık shell, programınız onun çocuğu.

ENTRYPOINT her zaman çalışan komuttur. CMD ise varsayılan argümanları verir, docker run sırasında bunların üzerine yazabilirsiniz. İkisini birlikte kullanınca ENTRYPOINT ["python", "app.py"] + CMD ["--port", "8080"] şu demek: uygulama her zaman çalışır, --port 8080 ise değiştirebileceğiniz bir varsayılan.

Bunu doğru yapmak hangi sorunu çözer?

  • Yavaş ve temiz olmayan kapanışlar. Shell form uygulamanızı /bin/sh arkasına saklıyor, bu yüzden SIGTERM ona hiç ulaşmıyor. Runtime bütün grace süresini bekliyor (docker stop ile 10 sn, Kubernetes'te varsayılan 30 sn), sonra SIGKILL gönderiyor. Açık istekler düşüyor, bellekteki veri diske yazılmıyor.
  • Kaybolan kapanış mantığı. finally bloğu, bağlantıların boşaltılması, "kapanıyorum" log satırı hiç çalışmıyor.
  • Sandığınız işi yapmayan argümanlar. Yalnızca CMD varken docker run img somecmd bütün komutun üzerine yazıyor ve uygulamanız hiç başlamıyor. ENTRYPOINT varken fazladan argümanlar sona ekleniyor.
  • ADD sürprizleri. ADD tar arşivlerini sessizce açıyor ve URL indirebiliyor. Bu da imajı şişiriyor, COPY'nin asla bozmadığı şekilde build cache'ini bozuyor.

Shell form ve exec form: ne kazanırsınız, ne ödersiniz

Shell form (CMD python app.py) Exec form (CMD ["python","app.py"])
İçeride PID 1 /bin/sh uygulamanız
SIGTERM'i alır hayır (shell alır) evet
docker stop süresi 10.30 sn (grace, sonra SIGKILL) 0.27 sn
Kapanış kodunu çalıştırır hayır evet
Değişken genişletme ($PORT) evet hayır (sh -c ile sarmazsanız)
Zombie reaping / PID 1 görevleri shell bir kısmını yapar --init/tini gerekir

Nasıl çalışır: sinyal nereye gidiyor

Diyagram: SIGTERM nereye gidiyor. docker stop, PID 1'e SIGTERM gönderir ve grace süresini bekler. Shell form'da PID 1 /bin/sh'tir ve sinyali çocuk python uygulamasına iletmez, bu yüzden grace süresi dolunca docker SIGKILL gönderir ve durdurma 10.30 saniye sürer, temiz kapanış olmaz. Exec form'da python PID 1'dir, handler'ı "got SIGTERM, draining" yazar ve container 0.27 saniyede temiz kapanır.

Lab

Uygulama, SIGTERM yakalayınca got SIGTERM, draining yazıp çıkan, sekiz satırlık bir Python. İki imaj da python:3.13-slim; tek fark CMD satırı. Aşağıdaki her sayı bu koşudan.

Terminal: shell-form ve exec-form container'larında docker stop. Shell-form container'da cat /proc/1/comm sh yazdırır, yani PID 1 shell'dir ve docker stop, 10 saniyelik grace'i tam bekleyip SIGKILL'den önce 10.30 saniye sürer. Exec-form container'da /proc/1/comm python yazdırır, yani PID 1 uygulamadır ve uygulama SIGTERM'i yakalayıp çıktığı için docker stop 0.27 saniye sürer.

Terminal: iki container'ın docker logs çıktısı. Shell-form container yalnızca "app started, pid 7" yazar, draining satırı yoktur, çünkü SIGTERM uygulamaya hiç ulaşmadı. Exec-form container "app started, pid 1" ve ardından "got SIGTERM, draining" yazar, çünkü kapanış handler'ı çıkıştan önce çalıştı.

  1. Shell form ve exec form. Her container'ın içinde cat /proc/1/comm çalıştırdım: shell form sh, exec form python gösteriyor. Shell-form container'da docker stop 10.30 sn sürdü ve logunda yalnızca app started, pid 7 var, SIGTERM satırı yok: shell sinyali iletmedi, Docker 10 saniyelik grace'i bekleyip SIGKILL gönderdi. Exec-form container 0.27 sn'de durdu, logunda app started, pid 1 ve ardından got SIGTERM, draining var.
  2. Beklenen şey uygulama değil, grace süresi. Shell-form container'da docker stop -t 2 2.29 sn sürdü: içeride dinleyen bir şey olmadığı için süre, verdiğiniz grace neyse o.
  3. ENTRYPOINT + CMD argümanları. ENTRYPOINT ["python", "argv.py"] ve CMD ["--port", "8080"] ile docker run img, argv.py --port 8080 çalıştırdı; docker run img --port 9090 ise argv.py --port 9090 çalıştırdı. ENTRYPOINT kaldı, CMD değişti.
  4. Tek başına CMD kırılgan. Yalnızca CMD ["python", "argv.py", "--port", "8080"] varken docker run img echo hi, hi yazdırdı ve uygulama hiç başlamadı: bir argüman bütün komutun üzerine yazdı.
  5. ADD ve COPY. COPY payload.tar.gz /dest/ tek dosya, yani tarball'ı bıraktı. ADD payload.tar.gz /dest/ üç dosya açtı, tarball'ı bırakmadı ve bu dosyaların sahibi uid 501'di: imajda benim atamadığım, arşivde saklı bir id.
  6. Kubernetes'te aynı şey. İki imajı da kind üzerinde, varsayılan terminationGracePeriodSeconds: 30 ile pod olarak çalıştırdım. Shell-form pod'u silmek 30.8 sn sürdü; exec-form pod 1.4 sn'de gitti. Kubernetes, Docker'ın 10 saniyesini değil 30 saniyeyi bekliyor, yani shell-form container her rollout'ta daha da pahalıya patlıyor.
  7. exec shell form'u kurtarıyor. CMD ["sh", "-c", "python app.py"] hâlâ PID 1'de sh bıraktı ve 10.27 sn'de durdu. CMD ["sh", "-c", "exec python app.py"] ise python'ı PID 1'e geri koydu ve 0.27 sn'de durdu: exec, shell'i kendi sürecinizle değiştiriyor, böylece sinyal ona ulaşıyor. Gerçekten değişken genişletme için bir shell'e ihtiyacınız olduğunda kullanacağınız kalıp bu.

Terminal: COPY ve ADD sonrası /dest üzerinde ls -ln. COPY tek dosya bıraktı, payload.tar.gz, sahibi uid 0. ADD arşivi üç dosyaya açtı, f1.txt, f2.txt ve f3.txt, her biri imajda atanmamış uid 501 sahipliğinde.

Nerede patladı

1. Shell-form container docker stop -t'yi yok saydı

Daha kısa grace vermek shell-form container'ı temiz kapatmadı; yalnızca SIGKILL'den önceki beklemeyi kısalttı. Tek çözüm, uygulamayı PID 1 yapmak.

2. ADD benim atamadığım bir uid ile dosya bıraktı

Tar, uid 501 taşıyordu ve ADD bunu olduğu gibi korudu, böylece /dest, kimsenin atamadığı sayısal bir id'ye ait oldu. COPY olsaydı build'in kendi sahipliğini kullanırdı.

3. Exec form değişkenleri genişletmiyor

CMD ["python", "app.py", "--port", "$PORT"], $PORT metnini birebir geçti, çünkü genişletecek bir shell yok. Çözüm CMD ["sh", "-c", "exec python app.py --port $PORT"]: shell değişkeni genişletiyor, exec ise PID 1'i python'a geri veriyor, böylece sinyaller çalışmaya devam ediyor.

ADD ve COPY: hangisi, ne zaman

COPY ADD
Yerel dosya/dizin evet evet
Yerel tar'ı otomatik açar hayır evet
Uzak URL indirir hayır evet (açmaz)
Önerilen kullanım tüm yerel dosyalar yalnızca açmak istediğiniz yerel tar

Kural: yerel dosyalar için COPY kullanın. ADD'i yalnızca yerel bir tarball'ı otomatik açmak için kullanın. URL için RUN curl -fsSL … && … && rm kullanın, böylece tek katmanda hem doğrular hem temizlersiniz.

Ne zaman hangisi

  • Uygulamanız PID 1 olsun ve sinyalleri alsın diye varsayılan olarak exec-form CMD kullanın.
  • Program sabitse ve yalnızca argümanlar değişiyorsa ENTRYPOINT (exec form) + CMD kullanın.
  • Süreciniz çocuk süreçleri reap etmiyorsa --init (ya da tini) ekleyin.
  • Shell form'u yalnızca gerçekten shell özelliğine ihtiyacınız varsa (değişken genişletme, pipe) kullanın; o zaman da gerçek bir init çalıştırın ya da uygulamanızı exec ile PID 1 yapın.
  • Varsayılan olarak COPY; ADD yalnızca yerel tar açmak için.

Özet

  • Shell form PID 1'e /bin/sh koyar; SIGTERM uygulamanıza hiç ulaşmaz; docker stop 10.30 sn bekler, sonra SIGKILL.
  • Exec form uygulamanızı PID 1 yapar; 0.27 sn'de durur ve kapanış kodunu çalıştırır.
  • ENTRYPOINT komutu sabitler, CMD üzerine yazılabilen varsayılan argümanları verir.
  • Yerel dosyalar için COPY; yalnızca yerel tar için ADD; URL için RUN curl.

Seride sırada: kind üzerinde Prometheus ve Grafana, bir cluster'ın ham metriklerini bir servisin ne zaman gerçekten sıkıntıda olduğunu gösteren panoya çeviriyorum.

Hayatta en hakiki mürşit ilimdir.
Mustafa Kemal Atatürk