# Container'daki Go worker'ım SIGTERM almıyor ve graceful kapanmıyor, bu bir PID 1 sorunu mu?

> `CMD`'yi exec form'a çevirin ve `os.Interrupt` yerine `signal.NotifyContext(..., syscall.SIGTERM)` kullanın; tini yalnız fork varsa gerekir.

- Soruldu: 2026-09-12
- Yanıtlandı: 2026-09-15
- Soran: Tuğçe
- Etiketler: docker, go
- Kaynak: https://www.muhammetsafak.com.tr/sor-bakalim/containerdaki-go-workerim-sigterm-almiyor-pid-1-sorunu/
- Dil: tr-TR
- Yazar: Muhammet Şafak

---
**Soru:** Go ile yazdığım bir queue worker'ını Docker container'ında çalıştırıyorum ve Kubernetes üzerinde deploy ediyorum. Kodda `os.Interrupt` dinleyip kanal geldiğinde context'i iptal eden bir graceful shutdown mantığım var.

Ama her deploy'da worker grace period boyunca hiçbir şey yapmıyor ve sonunda SIGKILL yiyor. Loglara bakınca SIGTERM'ü hiç görmediğini fark ettim; sanki sinyal worker'a hiç ulaşmıyor. Bu bir PID 1 sorunu mu, yoksa kodumda mı bir şey eksik?


Kısa cevap: Büyük olasılıkla evet, bu bir PID 1 sorunu.

## Kısa cevap

Ama iki ayrı hata aynı anda var: sinyal büyük ihtimalle shell-form `CMD` yüzünden worker'a hiç ulaşmıyor, ayrıca kodunuz `os.Interrupt` (SIGINT) dinleyip SIGTERM'ü kaçırıyor.

## Neden

1. **Shell form sizi /bin/sh -c'nin çocuğu yapar.** `CMD ./worker` (shell form) worker'ı `sh`'nin altında çalıştırır; PID 1 olan `sh` ise SIGTERM'ü çocuğuna iletmez. Bu, sinyalin worker'a hiç ulaşmamasının bir numaralı sebebidir. Çözüm exec form: `CMD ["./worker"]` ya da `ENTRYPOINT ["/app/worker"]`.

   ```dockerfile
   # SIGTERM'ü doğrudan binary'ye iletir
   ENTRYPOINT ["/app/worker"]
   ```

2. **PID 1'in varsayılan sinyal davranışı yoktur.** Doğrudan PID 1 olsanız bile kernel, PID 1 için varsayılan sinyal dispozisyonlarını uygulamaz; handler'ı açıkça kurmak zorundasınız. Sadece `os.Interrupt` dinliyorsanız SIGTERM'ü kaçırırsınız — Kubernetes'in deploy'da gönderdiği sinyal tam olarak SIGTERM'dür.

   ```go
   ctx, stop := signal.NotifyContext(context.Background(),
       syscall.SIGTERM, syscall.SIGINT)
   defer stop()
   // ctx'i worker döngüsüne geçirin; iptal gelince yeni iş çekmeyi durdurun,
   // devam eden işi bitirin, çıkın.
   ```

3. **entrypoint script'i de bunu bozar.** Kabuk bir entrypoint kullanıyorsanız son satır mutlaka `exec "$@"` olmalı. `exec` olmadan `"$@"` çalıştırırsanız kabuk PID 1 olarak kalır ve sinyali yine yutar.

## Ne yapmalı

1. **Sinyal → context iptal → drain zincirini kurun.** SIGTERM'de worker context'ini iptal edin, yeni job çekmeyi bırakın, in-flight işi bitirin ve çıkın. Bu, `terminationGracePeriodSeconds` (varsayılan 30s) içinde tamamlanmalı; aşarsanız SIGKILL yersiniz. Uzun işler varsa grace period'u buna göre ayarlayın.

2. **Çocuk process açıyorsanız zombie reaping gerekir.** Worker başka process'ler fork ediyorsa PID 1 olarak onları reap etmeniz gerekir; bu durumda `tini`'yi image'a kendiniz eklemeniz gerekir (Docker'da bunu `docker run --init` sağlar, Kubernetes'in buna denk yerleşik bir bayrağı yoktur). Tek bir Go binary için genellikle exec form + doğru sinyal yakalama yeter, tini'ye ihtiyaç olmaz.

3. **Önce teşhisi doğrulayın, tahmin etmeyin.** Container içinde PID 1'in gerçekten binary'niz mi yoksa `sh` mi olduğunu görmek beş saniyelik iştir: `docker exec <container> ps -o pid,comm`. PID 1'de `sh` görüyorsanız teşhis kesindir. Ayrıca image'ın SIGTERM beklediğinden emin olun; `STOPSIGNAL SIGTERM` zaten varsayılandır ama farklı bir şeye ayarlanmışsa worker'ınız hiç beklemediği bir sinyal alıyor olabilir.

4. **Kubernetes'te trafiği önce kesin.** Graceful shutdown [sadece sinyal yakalamak değildir](/sor-bakalim/autoscaling-sirasinda-sigterm-ile-graceful-shutdown/). Pod sonlandırılırken readiness probe'u düşürüp bir `preStop` hook (kısa bir sleep) koyarak, SIGTERM gelmeden önce pod'un Service endpoint'lerinden çıkarılmasını sağlayın. Aksi halde drain ederken bile yeni istek/job almaya devam edebilirsiniz.

**Sonuç:** Ben olsam önce `docker exec ... ps` ile PID 1'i doğrular, `Dockerfile`'ı exec form'a çevirir, kodda `os.Interrupt` yerine `signal.NotifyContext(..., syscall.SIGTERM, syscall.SIGINT)` kullanır ve grace period içinde drain edildiğini bir deploy ile doğrulardım. tini'yi yalnızca worker gerçekten çocuk process fork ediyorsa eklerdim; aksi halde gereksiz katman olur. Sıralama: önce sinyal ulaşsın, sonra kod onu yakalasın, en son da trafiği zamanında kesin.

## İlgili Yazılar

- [Birbirine bağımlı birden fazla işi, ilk hata hepsini iptal edecek şekilde errgroup ile mi yürütmeliyim?](https://www.muhammetsafak.com.tr/sor-bakalim/birbirine-bagimli-birden-fazla-isi-ilk-hata-hepsini-iptal-edecek-sekilde/) — Sor Bakalım
- [Container'ı non-root çalıştırınca mount edilen storage dizinine yazamıyorum, izinleri nasıl çözerim?](https://www.muhammetsafak.com.tr/sor-bakalim/containeri-non-root-calistirinca-mount-edilen-storage-dizinine-yazamiyorum-izinleri-nasil/) — Sor Bakalım
- [İç içe geçmiş servis çağrılarında context iptalini doğru şekilde nasıl yayarım?](https://www.muhammetsafak.com.tr/sor-bakalim/ic-ice-gecmis-servis-cagrilarinda-context-iptalini-dogru-sekilde-nasil-yayarim/) — Sor Bakalım
