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

Container Güvenliği: Non-root, Read-only Filesystem, Capability'ler, Minimal Image ve Tarama

Mert Özen 14 Kas 2026 13 dk 12 görüntülenme
Container Güvenliği: Non-root, Read-only Filesystem, Capability'ler, Minimal Image ve Tarama

Container'ı güvenli çalıştırmak, varsayılan ayrıcalıkları uygulamanın gerçekten ihtiyacı olana kadar daraltmaktır: non-root kullanıcı, salt okunur dosya sistemi ve tmpfs, --cap-drop ALL, no-new-privileges, bellek ve process limitleri. Bookstore'un API ve web container'larını bu bayraklarla çalıştırıyor, şifreyi environment variable yerine dosya olarak veriyor, image'ı Trivy ile tarıyor ve docker.sock'un neden host'ta root demek olduğunu gerçek çıktılarla görüyoruz.

Sürüm notu: Bu yazıdaki örnekler Docker Engine 29.9.3, .NET 10 (aspnet:10.0-noble-chiseled-extra, ASP.NET Core Runtime 10.0.15), nginxinc/nginx-unprivileged:1.30-alpine, SQL Server 2025 (RTM-CU11) ve Trivy 0.76.2 ile denenmiştir.

Önce Tahmin Et

Bookstore.Api chiseled bir image'dan, root olmayan kullanıcıyla çalışıyor; veritabanı şifresi 12. yazıdaki gibi -e ConnectionStrings__Default=... ile veriliyor. Sunucuda container'ı çalıştırma yetkisi olmayan ama docker komutunu kullanabilen bir ekip arkadaşın bu şifreyi görebilir mi? İkinci soru: API ve web container'larını bütün Linux capability'lerini düşürerek (--cap-drop ALL) ve dosya sistemini salt okunur yaparak (--read-only) başlatırsan hangisi açılmaz?

Varsayılan Ayarlarla Container'ın Elinde Ne Var?

Container bir process ve Docker onu varsayılan olarak makul ama geniş bir ayrıcalık setiyle başlatır. Docker'ın dokümanına göre container'daki root'a 14 capability (Linux'ta root yetkisinin parçalara bölünmüş hâli) verilir: dosya sahibini değiştirmek (CHOWN), dosya izinlerini atlamak (DAC_OVERRIDE), kullanıcı değiştirmek (SETUID, SETGID), raw socket açmak (NET_RAW), 1024 altındaki port'ları dinlemek (NET_BIND_SERVICE) ve birkaçı daha. Root dosya sistemi yazılabilir, bellek, CPU ve process sayısı için varsayılan bir sınır yok. Bunların hepsi alpine ile görülebiliyor:

docker run --rm alpine:3 sh -c 'grep CapEff /proc/self/status; chown 1000 /etc/hostname && echo "chown: ok"'
docker run --rm --cap-drop ALL alpine:3 sh -c 'grep CapEff /proc/self/status; chown 1000 /etc/hostname || echo "chown: failed"'
CapEff:	00000000a80425fb
chown: ok
CapEff:	0000000000000000
chown: /etc/hostname: Operation not permitted
chown: failed

CapEff, process'in etkin capability'lerinin bit maskesi. Varsayılanda a80425fb, yani 14 bit açık; --cap-drop ALL ile sıfır. Aynı root kullanıcı artık bir dosyanın sahibini değiştiremiyor. Bu yazının konusu bu varsayılanları uygulamanın gerçekten ihtiyacı olana kadar daraltmak. Saldırı senaryosu hep aynı: uygulamadaki bir açık (bir deserialization hatası, bir command injection) saldırgana container'da kod çalıştırma imkânı verir; ondan sonraki her adım container'ın sahip olduğu ayrıcalıklarla sınırlıdır. Uygulama tarafındaki açıklar Web Güvenliği (OWASP Top 10) serisinin konusu; burada container'ın etrafındaki duvarlara bakıyoruz.

Non-root: Image'da ve Çalıştırırken

İlk katman 8. yazıda kuruldu: sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) API image'ı USER $APP_UID ile uid 1654'le, bookstore-web nginx-unprivileged ile uid 101'le, SQL Server uid 10001'le çalışıyor. Container'daki root, user namespace kullanılmadığında host'taki uid 0'dır; non-root çalışmak, container'dan kaçmayı başaran bir saldırganın host'ta da root olmamasının ilk şartı.

Image'daki USER bir varsayılan, garanti değil. docker run --user 0 ya da Kubernetes'te runAsUser: 0 onu ezer; bazen bir "permission denied" hatasını hızlıca geçmek için yapılır ve öyle kalır. Kubernetes'te runAsNonRoot: true, image root olarak başlamak isterse pod'u reddeder; bütün bu alanlar Kubernetes serisinin "Kubernetes Güvenliği: ServiceAccount, RBAC, Pod Security Standards, securityContext ve NetworkPolicy" yazısında. Aşağıdaki Docker bayraklarının neredeyse hepsinin securityContext'te bir karşılığı var.

Read-only Filesystem ve tmpfs

--read-only, container'ın root dosya sistemini salt okunur bağlar. Saldırgan bir binary indirip çalıştıramaz, uygulamanın dosyalarını değiştiremez, bir config dosyasına satır ekleyemez. Bedeli, uygulamanın yazmaya ihtiyaç duyduğu her yerin açıkça belirtilmesi. İkinci sorunun cevabının ilk yarısı bookstore-web'de çıktı (10. yazının ★★★ alıştırması):

docker run --name dockerseries-web --read-only -e API_BASE_URL=/api dockerseries-bookstore-web:1.1.0
/docker-entrypoint.sh: Launching /docker-entrypoint.d/40-runtime-config.sh
/docker-entrypoint.d/40-runtime-config.sh: line 8: can't create /usr/share/nginx/html/config.json: Read-only file system

Başlangıç script'imiz config.json'u web root'una yazıyordu. nginx'in kendisi de /tmp'ye pid dosyasını ve geçici dosyalarını yazar. Çözüm, yazılabilir tek yeri /tmp yapmak ve onu tmpfs olarak vermek. Script artık /tmp/config.json'a yazıyor, nginx bu tek dosyayı oradan sunuyor ve Dockerfile'daki COPY --chown=101:101 ... config.json satırına gerek kalmadı:

envsubst '${API_BASE_URL} ${APP_ENVIRONMENT}' \
    < /etc/bookstore/config.template.json \
    > /tmp/config.json
    # Başlangıçta /tmp'ye yazılır (40-runtime-config.sh), salt okunur web root'una değil
    location = /config.json {
        root /tmp;
        add_header Cache-Control "no-store";
    }

Image 1.2.0 tag'iyle build edildi (docker-bake.hcl'deki TAG varsayılanı da artık 1.2.0); --read-only --tmpfs /tmp:rw,size=8m ile açılınca /tmp'de config.json, nginx.pid ve nginx'in *_temp klasörleri var, başka hiçbir yer yazılabilir değil. Bookstore.Api ise --read-only ile ek ayar olmadan açıldı; yine de ona da bir /tmp veriyoruz. .NET runtime, dotnet-counters ve dotnet-dump gibi araçların bağlandığı diagnostik socket'ini /tmp'de açıyor; tmpfs'li API container'ında /tmp'ye baktığımızda dotnet-diagnostic-1-...-socket dosyası oradaydı. /tmp yazılamazsa uygulama çalışır ama bir sonraki yazıda kullanacağımız teşhis araçları ona bağlanamaz.

Capability'leri Düşürmek ve no-new-privileges

Bookstore'un hiçbir container'ı varsayılan capability'lerin hiçbirine ihtiyaç duymuyor: API ve web 1024'ün üstündeki port'ları dinliyor, dosya sahibi değiştirmiyor, kullanıcı değiştirmiyor. İkisini de bütün capability'leri düşürerek, no-new-privileges ile ve sınırlarla başlattık:

docker run -d --name dockerseries-api --network dockerseries-net --network-alias bookstore-api \
    --read-only --tmpfs /tmp:rw,size=16m \
    --cap-drop ALL --security-opt no-new-privileges \
    --memory 256m --cpus 1 --pids-limit 100 \
    -v "$PWD/secrets:/run/secrets:ro" \
    dockerseries-bookstore-api:1.2.0
docker run -d --name dockerseries-web --network dockerseries-net -p 127.0.0.1:8081:8080 \
    --read-only --tmpfs /tmp:rw,size=8m \
    --cap-drop ALL --security-opt no-new-privileges \
    --memory 64m --pids-limit 50 \
    -e API_BASE_URL=/api dockerseries-bookstore-web:1.2.0

docker inspect dockerseries-api --format \
    'User={{.Config.User}} ReadonlyRootfs={{.HostConfig.ReadonlyRootfs}} CapDrop={{.HostConfig.CapDrop}} SecurityOpt={{.HostConfig.SecurityOpt}} PidsLimit={{.HostConfig.PidsLimit}}'
docker ps --format "table {{.Names}}\t{{.Status}}"
curl -s localhost:8081/api/books | jq -r '.[0:2][] | .title'
User=1654 ReadonlyRootfs=true CapDrop=[ALL] SecurityOpt=[no-new-privileges] PidsLimit=100
NAMES              STATUS
dockerseries-web   Up 20 seconds
dockerseries-api   Up 20 seconds (healthy)
dockerseries-db    Up 27 seconds
Çalıkuşu
Dune

İkinci sorunun cevabı: tmpfs verildiğinde ikisi de açılıyor, health check geçiyor ve kitaplar veritabanından geliyor. --cap-drop ALL ile başlayıp yalnız gerçekten gereken capability'yi --cap-add ile geri vermek doğru sıra; tersini (varsayılanlardan tek tek çıkarmak) yapınca neyin açık kaldığını kimse bilmez. no-new-privileges, process'in ve çocuklarının yeni ayrıcalık kazanmasını engelliyor: image'da setuid bit'li bir binary (su, sudo, ping'in eski sürümleri) olsa bile onu çalıştırmak root yetkisi getirmez. Chiseled image'da böyle bir binary zaten yok; bayrak, image değiştiğinde bile geçerli kalan bir sigorta.

SQL Server'a bu bayrakları uygulamadık: veritabanı motoru dosya sahipliği ve bellek yönetimi için ek ayrıcalıklar kullanabiliyor ve resmi dokümanı bu yapılandırmayı anlatmıyor. Bir bayrağı container'a eklemeden önce o yazılımın üreticisinin desteklediği ayarları kontrol et; veritabanını network'te yayımlamamak (12. yazı) ve non-root çalıştırmak onun için ana iki katman.

Resource Limit'leri: Bellek, CPU ve Process Sayısı

--memory ve --cpus'un .NET GC'sine (Garbage Collector; kullanılmayan belleği geri toplayan bileşen) etkisini 8. yazıda gördük; güvenlik açısından anlamları başka: limit vermezsen bir container bütün sunucunun belleğini tüketip yanındaki her şeyi kernel'in OOM (Out Of Memory; bellek bitince process öldüren mekanizma) killer'ına yem edebilir. --pids-limit ise sık unutulan üçüncüsü; container'daki process ve thread sayısına sınır koyar. Bir fork bomb (sürekli kendini çoğaltan process) denedik:

docker run --rm --pids-limit 50 alpine:3 sh -c 'for i in $(seq 1 100); do sleep 30 & done; wait' 2>&1 | sort | uniq -c
      1 sh: can't fork: Resource temporarily unavailable

50'nci process'te kernel yeni fork'u reddetti; container kendi içinde sıkıştı, host etkilenmedi. .NET thread'leri de bu sayıya dahil olduğu için API'ye 100 verdik; boşta bir ASP.NET Core uygulaması birkaç düzine thread kullanır, yük altında thread pool büyür. Değeri kendi uygulamanın docker stats'taki PIDS sütununa bakarak, payıyla seç.

Secret'lar: Environment Variable Değil, Dosya

İlk sorunun cevabı evet. Environment variable'lar container'ın config'inin parçası ve docker inspect onları olduğu gibi gösterir:

docker inspect dockerseries-api --format '{{range .Config.Env}}{{println .}}{{end}}' | grep ConnectionStrings
ConnectionStrings__Default=Server=bookstore-db;Database=BookstoreDb;User Id=bookstore_app;Password=<password>;TrustServerCertificate=True

(Şifreyi ben gizledim; komut onu açık metin olarak yazıyor.) docker komutunu çalıştırabilen herkes, yani docker grubundaki herkes ve Docker API'sine erişen her araç bu değeri okur. Environment variable'lar ayrıca child process'lere aktarılır ve hata raporlarına, crash dump'lara ve "bütün ortamı log'la" diyen debug satırlarına sızar. 5. ve 6. yazılarda bunun build tarafını (ARG/ENV ile verilen secret'ın docker history'de görünmesi) görmüştük; runtime tarafı aynı sorunun ikinci yarısı.

Daha iyi yol secret'ı salt okunur bir dosya olarak vermek. ASP.NET Core'un key-per-file configuration provider'ı bir klasördeki her dosyayı bir ayar olarak okur; dosyanın adı key, içeriği değer, adındaki __ yine bölüm ayracı. Bookstore.Api'ye tek satır ekledik:

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
// Dosya olarak secret'lar: /run/secrets/ConnectionStrings__Default, ConnectionStrings:Default olur
builder.Configuration.AddKeyPerFile(directoryPath: "/run/secrets", optional: true);
mkdir -p secrets
printf '%s' "$DB_CONN" > secrets/ConnectionStrings__Default
sudo chown 1654 secrets/ConnectionStrings__Default
chmod 0400 secrets/ConnectionStrings__Default
ls -ln secrets
-r--------    1 1654     0              125 Nov 14 12:38 ConnectionStrings__Default

Dosyayı yalnız container'daki uid 1654 okuyabiliyor; host'ta başka bir kullanıcı okuyamıyor. Klasörü yukarıdaki komutta -v "$PWD/secrets:/run/secrets:ro" ile bağladık; container'ın environment variable'larında artık ConnectionStrings geçmiyor ve docker inspect yalnız bir bind mount gösteriyor. /run/secrets yolunu bilerek seçtik: Docker Compose ve Swarm secret'ları da bu klasöre dosya olarak bağlar, aynı kod orada da çalışır. secrets/ klasörünü .gitignore'a ve .dockerignore'a ekledik; secret'ın repository'ye ya da build context'e girmemesi dosyayı oluşturmak kadar önemli. Production'da bu dosyayı bir secret store'dan (Azure Key Vault, HashiCorp Vault, Kubernetes Secret) gelen bir mekanizma doldurur; o tarafı Configuration ve Secret Management serisi anlatıyor.

Minimal Image ve Tarama

Image'da olmayan bir paketin açığı da olmaz. Bookstore.Api image'ını ve tam Ubuntu tabanlı aspnet:10.0 image'ını Trivy ile taradık. Trivy'yi de bir container olarak çalıştırdık; image'ı Docker socket'ini bağlamak yerine docker save ile tar dosyasına alıp verdik (nedeni Tuzaklar bölümünde):

mkdir -p scan
docker save -o scan/api.tar dockerseries-bookstore-api:1.2.0
docker save --platform linux/amd64 -o scan/aspnet.tar mcr.microsoft.com/dotnet/aspnet:10.0
docker run --rm -v "$PWD/scan:/scan:ro" -v dockerseries-trivy-cache:/root/.cache aquasec/trivy:0.76.2 \
    image --input /scan/api.tar --scanners vuln
INFO	[ubuntu] Detecting vulnerabilities...	os_version="24.04" pkg_num=10
...
/scan/api.tar (ubuntu 24.04)
============================
Total: 7 (UNKNOWN: 0, LOW: 1, MEDIUM: 6, HIGH: 0, CRITICAL: 0)

┌──────────┬────────────────┬──────────┬──────────┬───────────────────┬──────────────────┐
│ Library  │ Vulnerability  │ Severity │  Status  │ Installed Version │  Fixed Version   │
├──────────┼────────────────┼──────────┼──────────┼───────────────────┼──────────────────┤
│ libc6    │ CVE-2026-18374 │ MEDIUM   │ fixed    │ 2.39-0ubuntu8.9   │ 2.39-0ubuntu8.12 │
│          │ CVE-2026-84243 │          │          │                   │                  │
...
│ libicu74 │ CVE-2025-5222  │ LOW      │ affected │ 74.2-1ubuntu3.1   │                  │
└──────────┴────────────────┴──────────┴──────────┴───────────────────┴──────────────────┘

(Tabloyu kısalttım ve başlık sütununu çıkardım.) Chiseled-extra image'da Trivy'nin gördüğü 10 işletim sistemi paketi var ve 7 bulgu; aynı taramada tam Ubuntu image'ı 97 paket ve 36 bulgu (Total: 36 (UNKNOWN: 0, LOW: 5, MEDIUM: 31, HIGH: 0, CRITICAL: 0)) verdi. Uygulamanın Bookstore.Api.deps.json'u ve .NET runtime'ının kendisi için bulgu yok.

Listeyi okurken ilk soru "fix var mı?". Status sütunu cevabı veriyor: fixed, Ubuntu'nun düzeltilmiş paketi yayımladığı ama bizim image'ın henüz eski sürümü (8.9) taşıdığı anlamına geliyor. Chiseled image'da apt-get upgrade yapamazsın, yapmamalısın da: düzeltme Microsoft'un base image'ı yeniden build etmesiyle gelir ve senin işin yeni base image'la yeniden build etmek. affected ise henüz düzeltmesi olmayan bir açık; yapabileceğin, açığın uygulamanı gerçekten etkileyip etkilemediğini değerlendirmek. Bir CVE'nin (Common Vulnerabilities and Exposures; açıklara verilen ortak kimlik) image'da olması, saldırganın ona ulaşabildiği anlamına gelmez: ICU'daki (International Components for Unicode; dile özel metin kurallarını sağlayan kütüphane) bir açık, uygulama dışarıdan gelen veriyle o fonksiyonu çağırmıyorsa pratikte bir risk olmayabilir. Bunun kararı ve kaydı (Trivy'de .trivyignore) ekibin işi; false positive'leri sessizce susturmak değil.

CI'da tarama bir kapı olarak kullanılır: --exit-code 1 ile Trivy, filtreden geçen bir bulgu varsa 1 döner ve pipeline durur. Bizim image için --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 0 döndü; aynı komut MEDIUM'u da dahil edince 1 döndü. Bu kapıyı pipeline'a koymak, SBOM (Software Bill of Materials; image'daki bileşenlerin listesi) üretmek ve image'ı imzalamak DevSecOps serisinin konusu. Docker Desktop'la gelen Docker Scout CLI'ı (docker scout cves) da aynı işi yapıyor; bu yazıda açık kaynak olduğu ve her ortamda aynı çalıştığı için Trivy'yi kullandık.

Base image tarafında seçenekler son bir yılda arttı. Chiseled ve distroless image'ları 8. yazıda konuştuk. Docker, Aralık 2025'te Docker Hardened Images'ı (minimal ve sürekli yamalanan base image kataloğu) Apache 2.0 lisansıyla herkese ücretsiz açtı; image'lar dhi.io registry'sinden ücretsiz bir Docker hesabıyla çekiliyor, ek SLA (Service Level Agreement; hizmet seviyesi taahhüdü) ve uzun destek ücretli katmanlarda. Bu yazıda Hardened Images'ı hesap gerektirdiği için denemedik.

Hangi base image'ı seçersen seç, onu güncel tutmak ayrı bir iş. 4. yazıdaki digest pin, build'i tekrar üretilebilir yapar ama image'ı o günün açıklarıyla dondurur; pin'i güncel tutan bir bot gerekir. Renovate'in docker:pinDigests preset'i FROM satırlarındaki tag'e digest ekler ve yeni digest çıktığında pull request açar:

FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled-extra@sha256:cae5b962bfb786606def03d250c6a645cc98839095c89e806e4bd8978a68115b AS final
{
  "extends": ["config:recommended", "docker:pinDigests"]
}

GitHub'ın Dependabot'u da package-ecosystem: "docker" ile Dockerfile'lardaki base image tag'lerini güncelleyen pull request'ler açar. İki config'i de GitHub'da çalıştırmadık; yalnız dokümanlarına göre yazdık.

Üretimde Dikkat

Docker daemon'u root olarak çalışır ve onu yönetebilen herkes host'ta root'tur. Docker'ın güvenlik dokümanı daemon'u yalnız güvenilen kullanıcılara açmayı söylüyor. Bu riski azaltmanın iki yolu var. Rootless mode'u 2. yazıda kurduk: daemon'un kendisi root olmayan bir kullanıcıyla çalışıyor. Daemon'u root bırakıp container'ları ayrı bir uid aralığına eşlemek de mümkün: daemon.json'a "userns-remap": "default" yazınca Docker dockremap adında bir kullanıcı oluşturur ve container'daki uid 0, /etc/subuid'deki aralıktan root olmayan bir uid'ye eşlenir. Dokümana göre bedelleri var: --pid=host ve --network=host kullanılamaz, bind mount'larda sahiplik önceden ayarlanmalı, açıldığında mevcut image'lar ve container'lar görünmez olur ve containerd image store bu modda kullanılmaz. Bu yazıda userns-remap'i çalıştırmadık.

Bayrakları ekip standardına çevir. Burada her docker run'a elle yazdığımız --read-only, --cap-drop ALL, no-new-privileges ve limitler Docker Compose dosyasında ve Kubernetes manifest'inde birer satır; varsayılan şablona koyarsan her yeni servis bunlarla başlar.

Taramayı tek seferlik değil, sürekli yap. Bugün temiz olan image yarın yeni bir CVE ile kırmızıya döner. Registry'deki image'ları periyodik olarak tara ve base image güncellemelerini pull request olarak al.

Sık Düşülen Tuzaklar

/var/run/docker.sock'u container'a bağlamak. CI runner'ları, monitoring agent'ları ve bazı tarama araçları bunu ister. Socket'e erişen bir container, Docker'a host'un kök dizinini bağlayan yeni bir container başlatmasını söyleyebilir:

docker run --rm -v /var/run/docker.sock:/var/run/docker.sock docker:29.9.3-cli \
    docker run --rm -v /:/host alpine:3 sh -c 'id; head -1 /host/etc/shadow'
uid=0(root) gid=0(root) groups=0(root),0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)
root:*::0:::::

İki satırlık komut host'un şifre hash'lerinin tutulduğu /etc/shadow'u okudu. Docker socket'ini bağlamak, o container'a host'ta root yetkisi vermekle aynı şey. Trivy'yi socket yerine docker save çıktısıyla çalıştırmamızın nedeni bu.

Sorunu --privileged ile "çözmek". Bir container bir şeyi yapamadığında --privileged eklemek hatayı giderir, çünkü Docker'ın dokümanına göre bu bayrak bütün capability'leri ve host'un bütün cihazlarını verir, seccomp ve AppArmor korumalarını kapatır. Doğru yol hangi capability'nin gerektiğini bulup yalnız onu --cap-add ile vermek.

Secret'ı image'a koymak. COPY secrets/ ... ya da build sırasında oluşturulan bir dosya, image'ı çeken herkese secret'ı dağıtır ve image'ın bütün layer geçmişinde kalır. Build sırasında gereken secret için 6. yazıdaki RUN --mount=type=secret, çalışma anında gereken için dosya mount'u.

Taramadaki her bulguyu image'ı değiştirerek kapatmaya çalışmak. Chiseled bir image'a paket yöneticisi ekleyip yamamak, minimal image'ın bütün kazancını geri alır. Base image'ın yeni sürümünü bekle ya da daha güncel bir base image'a geç; beklenemeyecek bir açık varsa bunu bilinçli bir karar olarak kaydet.

Küçük Bir Deneme

★☆☆ API container'ını --cap-drop ALL ile ama --tmpfs /tmp olmadan, --read-only ile başlat. Uygulama açılıyor mu? docker stats'ta PIDS sütunu boşta ve /books'a art arda istek atarken kaç gösteriyor?

★★☆ SQL Server container'ını --cap-drop ALL --security-opt no-new-privileges ile başlat. Açılıyor mu? Açılmıyorsa log'daki hatadan hangi capability'yi geri vermen gerektiğini bulmaya çalış ve sonucu üreticinin dokümanıyla karşılaştır.

★★★ aquasec/trivy ile dockerseries-bookstore-web:1.2.0'ı tara. Bulgular hangi katmanda (Alpine paketleri mi, nginx mi)? --format cyclonedx ile bir SBOM üret ve içinde Angular paketlerinin olup olmadığına bak; neden yok?

Container'lar artık en az ayrıcalıkla çalışıyor. Bunun bir bedeli var: chiseled image'da shell yok, dosya sistemi salt okunur, capability'ler düşürülmüş; bir şey ters gittiğinde içeri girip bakmak eskisi kadar kolay değil. Bir sonraki yazıda container debugging'e geçiyoruz: logs, inspect, stats ve events ile sistemli bir teşhis akışı, shell'siz bir container'a yardımcı container ve docker debug ile bakmak ve 137 ile ölen Bookstore.Api.