Mert Özen Her Satırda Daha İleri
DevOps Docker — #01

Container Nedir? VM'den Farkı, Namespace, cgroup ve OCI Standardı

Mert Özen 14 Kas 2026 10 dk 11 görüntülenme
Container Nedir? VM'den Farkı, Namespace, cgroup ve OCI Standardı

Container, namespace'lerle neyi göreceği, cgroup'larla ne kadar kaynak kullanacağı sınırlanmış ve host'un kernel'ini paylaşan sıradan bir Linux process'idir. nginx'i içeriden ve dışarıdan izleyip PID'leri karşılaştırıyor, docker CLI → dockerd → containerd → runc zincirini process ağacında görüyor, aynı image'ı Podman ile çalıştırıyor ve container'ın ne zaman gereksiz olduğunu tartışıyoruz.

Sürüm notu: Bu yazıdaki örnekler Docker Desktop 4.64 (Docker Engine 29.2, containerd 2.2, runc 1.3), nginx 1.30 ve Podman 6.1 ile denenmiştir.

Önce Tahmin Et

Bir nginx container'ı başlatıyorsun ve içine girip ps diyorsun: nginx'in master process'i PID (process ID; kernel'in her process'e verdiği numara) 1 olarak görünüyor. Aynı anda Docker'ın çalıştığı makinede process listesine baksan bu nginx'i bulabilir misin? Bulursan PID'si yine 1 mi olur? İkinci soru: Alpine tabanlı bir container ile Ubuntu tabanlı bir container'da uname -r çalıştırırsan iki farklı kernel sürümü mü görürsün, yoksa aynısını mı? Cevapları bu yazının ortasında gerçek çıktılarla göreceğiz.

"Bende Çalışıyor" Problemi

Bu seri boyunca sitedeki serilerin ortak örnek uygulaması Bookstore'u (bir online kitapçı) parça parça container'a alacağız: önce Bookstore.Api, sonra Angular uygulaması bookstore-web, yanlarında da SQL Server. Ama önce container'ın neyi çözdüğünü netleştirelim.

Emre'nin Windows makinesinde Bookstore.Api sorunsuz çalışıyor. Test sunucusuna kopyalandığında ise uygulama açılmıyor: sunucuda .NET runtime'ın başka bir patch sürümü kurulu, Türkçe kültür kuralları için gereken ICU (International Components for Unicode; dil ve kültür kurallarını sağlayan kütüphane) paketi eksik, bir de ortam değişkenlerinden biri yanlış. Uygulamanın kodu aynı, ama çalıştığı ortam aynı değil. "Bende çalışıyor" cümlesi aslında "benim makinemdeki runtime, kütüphane ve ayarlarla çalışıyor" demek.

Container image'ı bu ortamı uygulamayla birlikte paketler: uygulamanın dosyaları, .NET runtime, işletim sisteminin kullanıcı tarafındaki dosyalar (libc, sertifikalar, ICU gibi paketler) ve başlangıç komutu. Microsoft'un ASP.NET Core image'ını hangi makinede çalıştırırsan çalıştır, içinde aynı runtime'ı bulursun:

docker run --rm mcr.microsoft.com/dotnet/aspnet:10.0 sh -c 'dotnet --list-runtimes; grep PRETTY_NAME /etc/os-release'
Microsoft.AspNetCore.App 10.0.15 [/usr/share/dotnet/shared/Microsoft.AspNetCore.App]
Microsoft.NETCore.App 10.0.15 [/usr/share/dotnet/shared/Microsoft.NETCore.App]
PRETTY_NAME="Ubuntu 24.04.5 LTS"

Image'da olmayan tek büyük parça kernel. Bu, container'ı anlamanın anahtarı.

Container Bir Process'tir

Container ayrı bir işletim sistemi ya da küçük bir VM değildir. Linux kernel'inin iki özelliğiyle çevrelenmiş sıradan bir process'tir: namespace'ler process'in neyi görebileceğini (kendi PID numaraları, kendi network arayüzleri, kendi dosya sistemi görünümü, kendi hostname'i), cgroup'lar ne kadar kaynak kullanabileceğini (CPU, bellek, process sayısı) sınırlar. Bunu görmek için bir nginx container'ı başlatalım:

docker run -d --name dockerseries-web --cpuset-cpus 0-1 -p 8081:80 nginx:1.30-alpine
docker exec dockerseries-web ps -o pid,ppid,user,args
docker exec dockerseries-web hostname
PID   PPID  USER     COMMAND
    1     0 root     nginx: master process nginx -g daemon off;
   30     1 nginx    nginx: worker process
   31     1 nginx    nginx: worker process
   32     0 root     ps -o pid,ppid,user,args
7bca49930cdc

(nginx CPU sayısı kadar worker açar; çıktı kısa kalsın diye --cpuset-cpus 0-1 ile iki CPU verdim.) Container'ın içinden bakınca dünya çok küçük: nginx PID 1, iki worker ve bizim ps'imiz. Hostname de container ID'sinin kısa hâli. Şimdi aynı process'lere dışarıdan bakalım:

docker top dockerseries-web -o pid,ppid,uid,user,args
PID                 PPID                UID                 USER                COMMAND
353149              353126              0                   root                nginx: master process nginx -g daemon off;
353193              353149              101                 statd               nginx: worker process
353194              353149              101                 statd               nginx: worker process

İlk sorunun cevabı burada: nginx host tarafında da duruyor, ama PID'si 1 değil, 353149. Aynı process iki PID namespace'inde iki farklı numarayla yaşıyor. Kullanıcı sütunu da ilginç: içeride nginx görünen worker'lar dışarıda statd görünüyor. Kernel kullanıcıyı ada göre değil numaraya göre tanır; ikisi de uid 101. İçeride 101'in adı image'daki /etc/passwd'den, dışarıda host'unkinden geliyor. Bu ayrıntı ileride non-root container'ları ve volume permission'larını konuşurken önemli olacak.

cgroup tarafı da aynı derecede somut. --memory ve --cpus bayrakları, kernel'in uyguladığı dosyalara yazılan değerlerden ibaret:

docker run --rm --memory 64m --cpus 0.5 alpine:3 cat /sys/fs/cgroup/memory.max /sys/fs/cgroup/cpu.max
67108864
50000 100000

64 MB'lık bellek sınırı ve her 100 ms'de 50 ms CPU zamanı. Namespace türlerini, /proc altından nasıl okunduklarını, cgroup v2 ağacını ve .NET'in bu sınırları nasıl algıladığını İşletim Sistemi Temelleri serisinin "Container'ı İşletim Sistemi Gözüyle Anlamak" yazısı ayrıntılı anlatıyor; burada container'ı anlamaya yetecek kadarıyla kalıyoruz.

VM ile Farkı: Kernel Paylaşılır

İkinci soruya geçelim. Alpine ve Ubuntu tabanlı iki container:

docker run --rm alpine:3 grep PRETTY_NAME /etc/os-release
docker run --rm mcr.microsoft.com/dotnet/aspnet:10.0 grep PRETTY_NAME /etc/os-release
[ "$(docker run --rm alpine:3 uname -r)" = "$(docker run --rm mcr.microsoft.com/dotnet/aspnet:10.0 uname -r)" ] && echo "same kernel"
PRETTY_NAME="Alpine Linux v3.24"
PRETTY_NAME="Ubuntu 24.04.5 LTS"
same kernel

Kullanıcı tarafı farklı (bir yanda musl ve BusyBox, öbür yanda glibc ve Ubuntu paketleri), kernel aynı. "Alpine container'ı" ya da "Ubuntu container'ı" dediğimizde aslında o dağıtımın dosyalarını görürken host'un kernel'inde çalışan bir process'ten bahsediyoruz. Aşağıdaki şeklin sol yarısı bu farkı katman katman gösteriyor.

01-container-vs-vm-tr.png

VM'de hypervisor her misafire sanal bir donanım verir ve her VM kendi kernel'ini boot eder. Bu yüzden VM'ler daha güçlü bir izolasyon sağlar ama dakikalar değilse de saniyeler içinde açılır, her biri kendi kernel'i ve sistem servisleri için bellek harcar. Container'da boot edilecek bir kernel yok; Docker process'i namespace'leri ve cgroup'u hazırlayarak başlatır. Bu makinede docker run --rm alpine:3 true üç denemede 0,17 ile 0,20 saniye arasında sürdü ve bu sürenin çoğu Docker'ın kendi işi.

Bedeli de buradan çıkar. Aynı kernel'i paylaşan container'lar arasındaki sınır, VM'ler arasındaki sınır kadar kalın değildir: bir kernel açığı bütün container'ları etkiler. Çok kiracılı ortamlarda bu yüzden container'lar ek bir katmanla (her container'a ayrı hafif bir VM veren Kata Containers ya da uygulamanın sistem çağrılarını kendi user-space kernel'inde karşılayan gVisor) çalıştırılır. Container güvenliğinin günlük tarafını 13. yazıda konuşacağız.

docker run'dan Process'e: dockerd, containerd ve runc

Şeklin sağ yarısı docker run dediğinde arkada kimin ne yaptığını gösteriyor. docker komutu yalnız bir istemcidir: isteği REST (HTTP üzerinden kaynak odaklı API stili) bir API ile, Linux'ta genellikle /var/run/docker.sock unix socket'i üzerinden dockerd'ye gönderir. dockerd image'ları, network'leri, volume'leri ve Docker'a özgü kavramları yönetir; container'ı çalıştırma işini gRPC ile containerd'ye devreder. containerd her container için küçük bir shim process'i başlatır, shim de OCI (Open Container Initiative; container image ve runtime formatlarını standartlaştıran Linux Foundation projesi) runtime'ı olan runc'yi çağırır. runc namespace'leri ve cgroup'u kurar, process'i başlatır ve çıkar. Bunu Docker'ın Linux VM'indeki process ağacında görebiliriz:

docker run --rm --pid=host alpine:3 ps -o pid,ppid,args
PID   PPID  COMMAND
  277   147 /usr/bin/containerd --config /etc/containerd/containerd.toml
  297   147 /usr/local/bin/dockerd --config-file /run/config/docker/daemon.json
353126     1 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 7bca49930cdc6b27a561cc86eb93a5140a8365a9f3ef1d5b2776bddb02dc21c4 -address /run/containerd/containerd.sock
353149 353126 nginx: master process nginx -g daemon off;
353193 353149 nginx: worker process
353194 353149 nginx: worker process

(Çıktıyı bu container'ı ilgilendiren satırlara indirdim; --pid=host bu tek seferlik container'a VM'in bütün process'lerini gösteriyor.) Üç şey dikkat çekiyor. nginx'in parent'ı ne dockerd ne containerd, kendi shim'i (353126). Shim'in parent'ı 1, yani onu başlatan containerd'den bağımsız yaşıyor; containerd yeniden başlatılsa bile container çalışmaya devam ediyor ve dockerd'nin live-restore ayarı da bu tasarım sayesinde mümkün. runc listede yok, çünkü işi bitince çıktı. Shim'in -namespace moby parametresi de containerd'nin kendi namespace kavramı: Docker container'larını moby adı altında tutuyor, aynı makinede Kubernetes'in containerd'si k8s.io adını kullanır. Docker bu bilgilerin kısa bir dökümünü de veriyor:

docker info --format 'Server Version: {{.ServerVersion}}{{"\n"}}Storage Driver: {{.Driver}}{{"\n"}}Cgroup Version: {{.CgroupVersion}}{{"\n"}}Default Runtime: {{.DefaultRuntime}}{{"\n"}}OSType: {{.OSType}}'
Server Version: 29.2.1
Storage Driver: overlayfs
Cgroup Version: 2
Default Runtime: runc
OSType: linux

OCI: Docker ile Container Aynı Şey Değil

Docker 2013'te container'ı geliştiricinin günlük aracına çeviren proje oldu, ama bugün "container" Docker'a ait bir format değil. Docker, CoreOS ve diğer şirketler 2015'te Linux Foundation altında OCI'yi kurdu; Docker image formatını ve runc'yi bu çalışmaya bağışladı. OCI üç spesifikasyon yayımlıyor: image-spec (image manifest'i, layer arşivleri ve başlangıç komutunu içeren config), runtime-spec (diskte açılmış bir dosya sistemi paketinin nasıl çalıştırılacağı) ve distribution-spec (registry'lerin image dağıtırken kullandığı API). Kasım 2026 itibarıyla güncel sürümler image-spec 1.1.1, runtime-spec 1.3.0 ve distribution-spec 1.1.1.

Pratik sonucu şu: Docker ile build ettiğin bir image'ı Podman, containerd ya da Kubernetes aynen çalıştırır. Bunu denemek için Podman'ı kurmak yerine onu da bir container'da çalıştırdım (iç içe container çalıştırabilmesi için --privileged gerekiyor, yalnız bu deney için):

docker run --rm --name dockerseries-podman --privileged quay.io/podman/stable \
    sh -c 'podman --version; podman --storage-driver=vfs run --rm -q docker.io/library/nginx:1.30-alpine nginx -v; podman --storage-driver=vfs info --format "{{.Host.OCIRuntime.Name}}"'
podman version 6.1.3
nginx version: nginx/1.30.6
crun

Aynı nginx image'ı, Docker'ın hiç olmadığı bir ortamda başka bir engine (Podman) ve başka bir OCI runtime ile (C ile yazılmış crun) çalıştı. Kubernetes de aynı mantıkla 1.24'ten beri Docker'ı doğrudan kullanmıyor; kubelet, CRI (Container Runtime Interface; Kubernetes'in container runtime'larıyla konuştuğu interface) üzerinden containerd veya CRI-O ile konuşuyor. Kubernetes serisinin "Eski Kubernetes Manifest'lerini Okumak" yazısı dockershim'in kaldırılmasını anlatıyor. Bu seride Docker'ı öğreniyoruz, ama öğrendiğin image, layer ve runtime kavramları bu araçların hepsinde geçerli.

macOS ve Windows'ta Container Nerede Koşar?

Container Linux kernel'inin özelliklerine dayandığı için Windows ve macOS'ta Linux container'ı doğrudan çalışamaz. Docker Desktop bu yüzden arkada bir Linux VM'i çalıştırır ve dockerd o VM'in içindedir; senin terminalindeki docker komutu yalnız istemcidir. Yukarıdaki process ağacı da bu VM'den alındı. Windows'ta Docker Desktop varsayılan olarak WSL (Windows Subsystem for Linux; Windows'ta gerçek bir Linux kernel'i çalıştıran katman) 2'yi kullanır ve kendi docker-desktop WSL dağıtımında çalışır; Ubuntu gibi kendi WSL dağıtımından da "WSL integration" ayarıyla aynı engine'e bağlanabilirsin. Kurulum ayrıntıları bir sonraki yazıda.

Bir de Windows container'ları var. Bunlar Windows kernel'ini paylaşır, Nano Server veya Server Core tabanlı image'lardan çalışır ve yalnız Windows host'ta koşar. IIS (Internet Information Services; Windows'un yerleşik web sunucusu) üzerinde çalışan bir .NET Framework uygulamasını container'a almak gerekiyorsa tek seçenek budur. Bu seride yalnız Linux container'ları kullanıyoruz: .NET 10 uygulamaları Linux'ta çalışır, production'daki container platformlarının çok büyük kısmı Linux'tur ve Docker Desktop Windows'ta varsayılan olarak Linux container moduyla gelir.

Ne Zaman Container Gereksiz?

Container her yerde doğru cevap değil. Bookstore'un tek bir küçük API'si varsa ve onu Azure App Service gibi bir PaaS'a (Platform as a Service; altyapıyı yönetmeden uygulamanı çalıştırdığın bulut servisi) koyacaksan, platform zaten runtime'ı ve işletim sistemini senin yerine yönetiyor; Dockerfile, image registry ve base image güncellemesi ek iş olur. Ekipte kimse image'ları güncel tutmayacaksa container, yamasız bir işletim sistemini paketleyip her yere taşımanın kolay bir yolu olur.

IIS'te çalışan eski bir .NET Framework uygulaması da tipik bir istisna. Onu Windows container'ına almak mümkün, ama image'lar gigabayt mertebesinde, Linux container ekosisteminin araçlarının çoğu orada yok ve kazanç genellikle sınırlı. Böyle bir uygulamada önce .NET 10'a taşımayı düşünmek daha verimli olur. Container'ın gerçekten değer kattığı yer, aynı uygulamayı birden çok ortamda (geliştirici makinesi, CI, test, production) aynı şekilde çalıştırman gereken ve birden çok servisin farklı runtime'lara ihtiyaç duyduğu durumlardır.

Üretimde Dikkat

Container'ın kernel'i host'un kernel'idir. Image'ı ne kadar güncel tutarsan tut, host'un kernel güvenlik yamaları ayrı bir sorumluluk; managed bir platformda bu iş sağlayıcının, kendi sunucunda senin. Aynı şekilde cgroup sürümü, kernel özellikleri ve sistem saati host'tan gelir: container içinde değiştiremezsin.

Container, bir VM kadar güçlü bir güvenlik sınırı değildir. Güvenilmeyen kodu (müşterinin yüklediği bir eklenti, bir CI job'ı) çalıştıracaksan container'ı tek başına yeterli sayma; ek izolasyon katmanlarını ve non-root, read-only gibi ayarları 13. yazıda göreceğiz.

Container'ı kalıcı bir sunucu gibi düşünme. Container silindiğinde içindeki dosya sistemi değişiklikleri de gider; veritabanı dosyaları ve yüklenen dosyalar volume'lerde durmalı (11. yazı). Bir container'a exec ile girip elle düzelttiğin şey, yeni container'da yoktur.

Sık Düşülen Tuzaklar

Container'ı hafif bir VM sanmak. Bu yanlış modelle container'ın içinde SSH (uzak sunucuya şifreli bağlantı protokolü) sunucusu, cron ve uygulamayı birlikte çalıştıran image'lar ortaya çıkar. Bir container'da bir ana process olmalı; diğer işler kendi container'larına gider.

"Docker" ile "container"ı eş anlamlı kullanmak. Kubernetes cluster'ında Docker olmadan çalışan container'lar, Podman kullanan bir ekip ya da WSL'in kendi container CLI'ı karşına çıktığında bu ayrım önem kazanır. Taşınabilir olan OCI image'ıdır, Docker'a özgü olan ise dockerd'nin API'si ve araçları.

Image'ın işletim sistemini "kullandığın işletim sistemi" sanmak. Ubuntu tabanlı bir image'da çalışan uygulama, Ubuntu'nun kernel'inde değil host'unkinde çalışır. Kernel'e bağlı bir davranış (cgroup sürümü, bir sistem çağrısı) image'dan değil makineden gelir.

Container'ın içindeki kullanıcı adına güvenmek. Yukarıda gördüğümüz gibi kernel numarayla çalışır; içerideki root (uid 0), user namespace kullanılmadığı sürece host'ta da uid 0'dır.

Küçük Bir Deneme

★☆☆ dockerseries-web container'ını başlat, docker inspect -f '{{.State.Pid}}' dockerseries-web ile host tarafındaki PID'yi al ve docker top çıktısındaki master process'in PID'siyle karşılaştır. Sonra docker stop dockerseries-web ve docker rm dockerseries-web ile temizle.

★★☆ --memory 64m yerine --memory 128m --pids-limit 20 ile bir Alpine container'ı başlat ve /sys/fs/cgroup/pids.max dosyasını oku. Hangi Docker bayrağının hangi cgroup dosyasına karşılık geldiğini bir tabloya yaz.

★★★ Kendi ekibinin uygulamalarından birini düşün: hangi bağımlılıkları "bende çalışıyor" sorununa yol açıyor (runtime sürümü, native kütüphane, ortam değişkeni, saat dilimi)? Bunların hangileri image'a girer, hangileri host'tan (kernel, saat, donanım) gelmeye devam eder?

Bu yazıda container'ın ne olduğunu ve Docker'ın arkasındaki zinciri gördük. Bir sonraki yazıda Docker'ı doğru kuruyoruz: Windows'ta WSL 2 ile Docker Desktop, Linux'ta resmi repository'den Docker Engine, docker grubunun neden root'a eşdeğer olduğu ve Podman, Rancher Desktop, WSL container gibi alternatifler.