Eski Docker Kurulumlarını Okumak ve 2026'ya Taşımak: Legacy Builder'dan BuildKit'e, Port 80'den 8080'e, Debian'dan Ubuntu'ya, Graph Driver'dan containerd'ye
Eski bir Dockerfile'ı 2026'ya taşımak birkaç değişikliği birlikte okumaktır: legacy builder yerine BuildKit, MAINTAINER ve shell form yerine label ve exec form, .NET 8'deki port 80'den 8080'e ve root'tan app kullanıcısına geçiş, .NET 10'daki Debian'dan Ubuntu'ya geçiş, Engine 29'un containerd image store'u ve API sürümü. Bookstore'un 2021 tarihli varsayımsal Dockerfile'ını gerçek çıktılarla satır satır modernize ediyor, .NET 11 SDK'nın lokal runtime seçimini ve SOURCE_DATE_EPOCH'u deniyoruz.
Sürüm notu: Bu yazıdaki örnekler Docker Engine 29.9.3 (BuildKit 0.34.2 ve legacy builder), .NET 10 ve .NET 11 SDK 11.0.100 (mcr.microsoft.com/dotnet/sdk:11.0) ile denenmiştir; eski image'lar için mcr.microsoft.com/dotnet/core/sdk:3.1 (SDK 3.1.426) ve aspnet:6.0–aspnet:10.0 config'leri okunmuştur.
Önce Tahmin Et
Aşağıda Bookstore.Api'nin 2021'de yazılmış olabilecek Dockerfile'ı var: o yılların blog yazılarında ve şirket wiki'lerinde en sık göreceğin biçim. Bugün bu dosyayı Engine 29'da, .NET 10'a geçmiş kodla build edersen ne olur: (a) uyarılarla build olur, (b) build olur ama container 8080'den cevap vermez, (c) build edilemez? Bir de say: 12 instruction'ın kaçını değiştirmen gerekir?
Bookstore'un 2021 Dockerfile'ı
Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) varsayımsal bir geçmişi olsun: API 2021'de .NET Core 3.1 ile yazılmış ve şu Dockerfile'la container'a alınmış (labs/15-legacy/Dockerfile.2021):
FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build
MAINTAINER Bookstore Team <team@bookstore.example.com>
WORKDIR /src
COPY . .
RUN dotnet restore
RUN dotnet publish -c Release -o /app
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1
WORKDIR /app
COPY --from=build /app .
ENV ASPNETCORE_URLS http://+:80
EXPOSE 80
ENTRYPOINT dotnet Bookstore.Api.dll
Dosya o günün ölçüsüyle fena değil: multi-stage, SDK final image'da yok. Ama bu seride gördüğümüz neredeyse her konuda bir adım geride. Serinin son yazısında bu dosyayı okuyup her satırın neden böyle olduğunu ve bugün neyle değiştiğini bulacağız. Eski bir projeyi devraldığında ilk iş tam olarak bu.
Legacy Builder'dan BuildKit'e
2021'de bu dosya çoğu Linux makinesinde legacy builder ile build edilirdi; BuildKit, Engine 23.0'da (Şubat 2023) Linux'ta varsayılan builder oldu. Legacy builder Engine 29'da hâlâ var ve DOCKER_BUILDKIT=0 ile seçiliyor; eski bir CI script'inde bu değişkeni görürsen çıktının neden farklı göründüğünü anlarsın:
DOCKER_BUILDKIT=0 docker build -f labs/15-legacy/Dockerfile.2021 -t dockerseries-bookstore-api:2021 src/Bookstore.Api
DEPRECATED: The legacy builder is deprecated and will be removed in a future release.
BuildKit is currently disabled; enable it by removing the DOCKER_BUILDKIT=0
environment-variable.
Sending build context to Docker daemon 18.48kB
Step 1/12 : FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build
---> 150d074697d1
Step 2/12 : MAINTAINER Bookstore Team <team@bookstore.example.com>
---> Running in ba80c7b0f35d
---> Removed intermediate container ba80c7b0f35d
---> 50fcaf9aa513
...
Step 6/12 : RUN dotnet publish -c Release -o /app
---> Running in 5478fd112637
Microsoft (R) Build Engine version 16.7.3+2f374e28e for .NET
...
/usr/share/dotnet/sdk/3.1.426/Microsoft.Common.CurrentVersion.targets(1177,5): error MSB3971: The reference assemblies for ".NETFramework,Version=v10.0" were not found. You might be using an older .NET SDK to target .NET 5.0 or higher. Update Visual Studio and/or your .NET SDK. [/src/Bookstore.Api.csproj]
The command '/bin/sh -c dotnet publish -c Release -o /app' returned a non-zero code: 1
Legacy builder'ın izleri: "Sending build context to Docker daemon" (bütün context her build'de daemon'a gönderilir), Step N/M satırları, her instruction için bir ara container ("Running in", "Removed intermediate container") ve her adımın sonunda bir image ID. Sıralı çalışır; 7. yazıdaki paralel stage'ler, 6. yazıdaki cache mount ve build secret'ları, --check ve Bake yalnız BuildKit'te var. Docker'ın deprecated features sayfasına göre legacy builder Linux image'ları için 23.0'dan beri deprecated ama kaldırılacağı sürüm belli değil; Windows container image'ları için hâlâ kullanılıyor.
İlk sorunun cevabı da çıktıda: (c). Eski SDK image'ı bugün de çekilebiliyor, restore bile geçiyor, ama .NET Core 3.1 SDK'sı net10.0'ı tanımıyor ve onu .NET Framework 10.0 diye yorumlayıp hata veriyor. Eski Dockerfile'larda SDK sürümünü kodla birlikte güncellemeyi unutmak en sık görülen kırılma.
BuildKit'le --check çalıştırınca build'e başlamadan üç uyarı geliyor:
docker build --check -f labs/15-legacy/Dockerfile.2021 src/Bookstore.Api
Check complete, 3 warnings have been found!
WARNING: MaintainerDeprecated - https://docs.docker.com/go/dockerfile/rule/maintainer-deprecated/
Maintainer instruction is deprecated in favor of using label
labs/15-legacy/Dockerfile.2021:2
WARNING: LegacyKeyValueFormat - https://docs.docker.com/go/dockerfile/rule/legacy-key-value-format/
"ENV key=value" should be used instead of legacy "ENV key value" format
labs/15-legacy/Dockerfile.2021:11
WARNING: JSONArgsRecommended - https://docs.docker.com/go/dockerfile/rule/json-args-recommended/
JSON arguments recommended for ENTRYPOINT to prevent unintended behavior related to OS signals
labs/15-legacy/Dockerfile.2021:13
(Her uyarının altındaki kod alıntısını çıkardım.) MAINTAINER, Docker 1.13'ten (2017) beri deprecated; yerini LABEL org.opencontainers.image.authors=... alıyor. Boşlukla yazılan ENV key value eski biçim. Shell form ENTRYPOINT ise 3. yazıdaki sinyal sorununun (PID 1 problemi) ta kendisi: dotnet shell'in çocuğu olarak çalışır, docker stop'un SIGTERM'ü (kapanma isteği gönderen sinyal) ona ulaşmaz ve her deploy 10 saniye bekleyip 137 ile biter.
.NET Tarafı: Port 80, Root ve Image Adları
Dockerfile'daki ENV ASPNETCORE_URLS http://+:80 satırı o dönemin image'larının kendi varsayılanının tekrarı. Image config'lerini çekmeden, registry'den okuyarak karşılaştırdık:
for t in core/aspnet:3.1 aspnet:6.0 aspnet:7.0 aspnet:8.0 aspnet:9.0 aspnet:10.0; do
printf "%s " $t
docker buildx imagetools inspect mcr.microsoft.com/dotnet/$t \
--format '{{json (index .Image "linux/amd64").Config}}' \
| jq -c '{User, Env: [.Env[] | select(test("ASPNETCORE_|APP_UID"))]}'
done
core/aspnet:3.1 {"User":null,"Env":["ASPNETCORE_URLS=http://+:80"]}
aspnet:6.0 {"User":null,"Env":["ASPNETCORE_URLS=http://+:80"]}
aspnet:7.0 {"User":null,"Env":["ASPNETCORE_URLS=http://+:80"]}
aspnet:8.0 {"User":null,"Env":["APP_UID=1654","ASPNETCORE_HTTP_PORTS=8080"]}
aspnet:9.0 {"User":null,"Env":["APP_UID=1654","ASPNETCORE_HTTP_PORTS=8080"]}
aspnet:10.0 {"User":null,"Env":["APP_UID=1654","ASPNETCORE_HTTP_PORTS=8080"]}
Kırılma .NET 8'de: ASPNETCORE_URLS=http://+:80 gitti, yerine ASPNETCORE_HTTP_PORTS=8080 geldi ve image'lara uid'si APP_UID'de duran app kullanıcısı eklendi. Port değişikliğinin nedeni non-root: Linux'ta 1024'ün altındaki port'ları dinlemek normalde ayrıcalık ister. Docker kendi container'larında bu sınırı (net.ipv4.ip_unprivileged_port_start) 0'a çektiği için denediğimizde uid 1654 Docker'da 80'i de dinleyebildi, --cap-drop ALL ile bile; ama bu Docker'ın bir ayarı ve image'ını çalıştıran her ortamın aynısını yapacağına güvenmemek gerekir. 8080 her yerde sorunsuz. Dikkat edilecek nokta, User alanının .NET 8'den sonra da boş olması: Microsoft'un breaking change notuna göre app kullanıcısı tanımlı ama kullanmak isteğe bağlı. Yani yalnız base image'ı 8.0'a çeken eski bir Dockerfile root olarak çalışmaya devam eder; USER $APP_UID satırını sen eklemelisin (8. yazı).
Bu tabloyu bir migration'da yan yana koyunca tipik hatalar kendiliğinden çıkıyor: base image'ı 8.0'a yükseltip ASPNETCORE_URLS http://+:80 satırını bırakan Dockerfile uygulamayı yine 80'de açar; USER $APP_UID da eklenmişse uygulamanın açılıp açılmayacağı container'ı çalıştıran ortamın port ayarına kalır. Orkestratör tarafında containerPort: 80, health probe port'u, reverse proxy'nin upstream'i ve Docker'da -p 8080:80 gibi eşlemeler de aynı anda güncellenmeli. Kubernetes manifest'lerindeki karşılıklarını Kubernetes serisinin "Eski Kubernetes Manifest'lerini Okumak: dockershim, PodSecurityPolicy ve Kaldırılan API'ler (1.16–1.25)" yazısı ele alıyor.
Image adları da değişti. Mart 2019'da .NET image'ları Docker Hub'dan MCR'ye (Microsoft'un kendi container registry'si) taşındı; microsoft/dotnet ve microsoft/aspnetcore gibi Docker Hub repository'leri bir süre yönlendirildi, 2021 yazında silindi. .NET 5 ile mcr.microsoft.com/dotnet/core/sdk yolundaki core/ da kalktı (mcr.microsoft.com/dotnet/sdk). Eski yollar MCR'de eski tag'ler için hâlâ çalışıyor, yukarıdaki build bunu gösterdi, ama yeni sürümler yalnız yeni yollarda.
Debian'dan Ubuntu'ya
Tag'lerin arkasındaki işletim sistemi de değişti:
for t in 9.0 10.0; do
printf "aspnet:%s " $t
docker run --rm --entrypoint cat mcr.microsoft.com/dotnet/aspnet:$t /etc/os-release | grep PRETTY
done
aspnet:9.0 PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
aspnet:10.0 PRETTY_NAME="Ubuntu 24.04.5 LTS"
.NET 10 ile işletim sistemi belirtmeyen tag'ler Ubuntu 24.04'ü gösteriyor ve .NET 10 için Debian image'ı yayımlanmıyor (8. yazı). Uygulama kodu için çoğu zaman fark etmez; fark eden Dockerfile'daki RUN apt-get install satırları. Paket adları ve sürümleri dağıtımlar arasında aynı değil: Debian 12'de çalışan bir apt-get install Ubuntu 24.04'te başka bir sürüm kurabilir ya da paketi bulamayabilir. FROM satırında işletim sistemini açıkça yazmak (aspnet:10.0-noble, aspnet:9.0-bookworm-slim), base image'ı yükselttiğinde dağıtımın sessizce değişmesini önler ve değişikliği bir karar hâline getirir.
Engine 29: containerd Image Store, API Sürümü ve Diğerleri
Docker Engine'in kendisi de değişti. 4. yazıda gördüğümüz containerd image store, Engine 29.0'dan beri yeni kurulumlarda varsayılan:
docker info | grep -A1 "Storage Driver"
Storage Driver: overlayfs
driver-type: io.containerd.snapshotter.v1
Eski kurulumlarda bu satır Storage Driver: overlay2 der: image'ları Docker'ın kendi graph driver'ı tutar. Engine 29'a yükseltmek mevcut bir kurulumu otomatik olarak containerd store'a geçirmiyor; Docker'ın duyurusuna göre graph driver'lar deprecated ama hâlâ kullanılabilir. Geçiş bilinçli yapılır ve eski store'daki image'lar yeni store'da görünmez, yeniden çekilmeleri ya da build edilmeleri gerekir. Yeni ve eski sunucuların docker images çıktısının ve disk kullanımının (4. yazıdaki DISK USAGE / CONTENT SIZE farkı) farklı olmasının nedeni bu.
İkinci değişiklik API sürümü. docker CLI'ı ve Docker'a konuşan her araç (CI eklentileri, monitoring agent'ları, eski Testcontainers sürümleri) bir API sürümüyle istek atar. Engine 29.0 desteklediği en düşük API sürümünü 1.44'e (Docker 25) çıkardı ve eski araçlar "client version ... is too old" hatasıyla kırıldı; Docker'ın duyurusu bunu DOCKER_MIN_API_VERSION ortam değişkeni ya da daemon.json'daki "min-api-version" ile geçici olarak gevşetmeyi anlatıyor. 29.3.0 varsayılanı yeniden 1.40'a (Docker 19.03) indirdi. Engine 29.9.3'te eski bir API sürümünü DOCKER_API_VERSION ile taklit ettik:
DOCKER_API_VERSION=1.39 docker version
DOCKER_API_VERSION=1.43 docker version --format "server {{.Server.Version}} api {{.Server.APIVersion}} min {{.Server.MinAPIVersion}}"
Error response from daemon: client version 1.39 is too old. Minimum supported API version is 1.40, please upgrade your client to a newer version
server 29.9.3 api 1.56 min 1.40
Eski bir aracın Engine 29 sunucusunda bu hatayı vermesi, aracın güncellenmesi gerektiğinin işareti; sunucuda minimum sürümü düşürmek bir geçiş önlemi, kalıcı çözüm değil.
Eski kurulumları okurken karşına çıkabilecek diğer Engine 29 maddeleri, release notes'a göre: Docker Content Trust (DOCKER_CONTENT_TRUST=1 ile image imzası doğrulama) CLI'dan kaldırıldı; image imzalamanın bugünkü yolları DevSecOps serisinde. cgroup v1 desteği deprecated (en az Mayıs 2029'a kadar sürecek); çok eski bir Linux dağıtımında çalışan sunucu bu yüzden uyarı verebilir. 12. yazıda gördüğümüz nftables backend'i deneysel. Bir de Docker Compose: Python'la yazılmış docker-compose (v1) Haziran 2023'ten sonra destek almıyor; bugünkü docker compose (tire yok) bir CLI plugin'i ve compose dosyasındaki version: alanı artık yalnız bir uyarı üretiyor. Ayrıntısı Docker Compose serisinde.
.NET 11 SDK: Lokal Runtime Seçimi ve Tekrar Üretilebilir Image'lar
.NET 11 (10 Kasım 2026'dan beri genel kullanıma açık) Dockerfile'sız container üretimine (9. yazı) iki değişiklik getirdi. Birincisi bir breaking change: LocalRegistry verilmediğinde SDK eskiden önce Docker'ı, sonra Podman'ı arıyordu; .NET 11'de Windows'ta önce WSL (Windows Subsystem for Linux; Windows'ta Linux çalıştıran alt sistem) containers'ı (wslc), macOS'ta önce Apple'ın container aracını arıyor ve aracın servisi çalışıyorsa image'ı oraya veriyor. Docker ve Podman artık yedek. Windows'taki okur için pratik sonucu şu: 2. yazıda anlattığımız WSL containers kuruluysa dotnet publish -t:PublishContainer'ın ürettiği image Docker Desktop'ın değil wslc'nin store'una gidebilir ve docker images'ta görünmez. Runtime'ı sabitlemek için -p:LocalRegistry=Docker (diğer değerler Podman, Wslc, MacOSContainer) ya da aynı property'yi bir Directory.Build.props'a yazmak yeterli. Sabitlenen runtime yoksa SDK başka birine geçmiyor; Linux'ta Wslc isteyince:
dotnet publish src/Bookstore.Api/Bookstore.Api.csproj -t:PublishContainer -p:LocalRegistry=Wslc
error CONTAINER1012: The local registry is not available, but pushing to a local registry was requested.
İkincisi tekrar üretilebilir image'lar. .NET 11 SDK'nın yenilikler sayfasına göre SOURCE_DATE_EPOCH verilince image'daki zaman damgaları sabitleniyor ve aynı kaynaktan aynı digest çıkıyor. Bookstore.Api'yi .NET 11 SDK container'ıyla dört kez publish ettik: ikisi aynı epoch'la, ikisi epoch'suz:
dotnet publish src/Bookstore.Api/Bookstore.Api.csproj -t:PublishContainer \
-p ContainerImageTags=repro-1 -p:SOURCE_DATE_EPOCH="$(git log -1 --pretty=%ct)"
# ... repro-2 aynı komutla, plain-3 ve plain-4 SOURCE_DATE_EPOCH olmadan ...
docker image ls dockerseries-bookstore-api --format '{{.Tag}} {{.ID}}' | grep -E "repro|plain" | sort
plain-3 89323bb93dfc
plain-4 0230283c92af
repro-1 06fd07f0544e
repro-2 06fd07f0544e
Epoch'lu iki publish aynı image ID'yi (config digest'i) verdi, epoch'suz ikisi farklı. Epoch'u son commit'in zamanından almak, aynı commit'ten yapılan her build'in aynı digest'i üretmesi demek: bir image'ın gerçekten o commit'ten geldiğini doğrulamak ve gereksiz push'ları önlemek mümkün oluyor (SDK, registry'de aynı manifest zaten varsa tekrar göndermiyor). Bookstore.Api net10.0'ı hedeflemeye devam ediyor; bu özellikler SDK'nın, yani build ortamının sürümüne bağlı.
Kontrol Listesi: 2021 Dockerfile'ını Satır Satır 2026'ya
| 2021 | 2026 | Neden |
|---|---|---|
FROM .../dotnet/core/sdk:3.1 | FROM --platform=$BUILDPLATFORM .../dotnet/sdk:${DOTNET_VERSION} | Yeni image yolu, desteklenen SDK, QEMU (başka mimariyi taklit eden emülatör) olmadan çok mimarili build (6. ve 7. yazı) |
MAINTAINER ... | LABEL org.opencontainers.image.* | Deprecated; OCI (Open Container Initiative; container standartları) label'larını araçlar okur |
COPY . . + RUN dotnet restore | Önce csproj, cache mount'lu restore, sonra kaynak | Her kod değişikliğinde restore'u tekrarlamamak (6. yazı) |
FROM .../dotnet/core/aspnet:3.1 | aspnet:${DOTNET_VERSION}-noble-chiseled-extra | Shell'siz, non-root, Türkçe kültür ve saat dilimi verisiyle (8. yazı) |
ENV ASPNETCORE_URLS http://+:80 | Satır silinir; image'ın ASPNETCORE_HTTP_PORTS=8080'i | Image'ın varsayılanı 8080; root olmayan kullanıcı için her ortamda güvenli port |
EXPOSE 80 | EXPOSE 8080 | Dokümantasyonun gerçek port'u söylemesi |
| (yok) | USER $APP_UID | User alanı Ubuntu image'larında boş; root'a sessizce dönmemek |
| (yok) | HEALTHCHECK ... CMD ["dotnet", "Bookstore.Api.dll", "healthcheck"] | Shell'siz image'da health check (8. yazı; bedeli 14. yazıda) |
ENTRYPOINT dotnet Bookstore.Api.dll | ENTRYPOINT ["dotnet", "Bookstore.Api.dll"] | Exec form; SIGTERM uygulamaya ulaşır (3. yazı) |
Sonuç, 8. yazıdan beri src/Bookstore.Api/Dockerfile'da duran dosya. Önce Tahmin Et'teki sayının cevabı: 12 instruction'ın 9'u değişiyor ya da siliniyor; yalnız iki WORKDIR ve COPY --from=build aynı kalıyor, üstüne ARG, LABEL, USER ve HEALTHCHECK gibi yeni satırlar geliyor. Dockerfile'ın dışında da yapılacaklar var: .dockerignore (5. yazı), BuildKit cache ve Bake (6. yazı), image'ı tag yerine digest'le referans etmek ve güncel tutmak (4. ve 13. yazı), port ve kullanıcı değişikliğinin orkestratör tarafına yansıtılması.
Üretimde Dikkat
Migration'ı tek seferde değil katmanlar hâlinde yap. Önce aynı .NET sürümünde kalarak Dockerfile'ı modernize et (BuildKit, exec form, label'lar) ve davranışın değişmediğini doğrula; sonra port ve kullanıcıyı değiştir, orkestratör ayarlarıyla birlikte deploy et; en son .NET sürümünü ve base image ailesini yükselt. Üç değişikliği aynı deploy'a koyarsan bir şey kırıldığında hangisinin kırdığını bilemezsin.
Eski sunucuları kendi gözünle kontrol et. docker version (client ve server sürümü, minimum API), docker info (storage driver, cgroup sürümü, firewall backend) ve docker compose version, bir sunucunun hangi dönemde kaldığını birkaç saniyede söyler. 2. yazıdaki gibi, paylaşmadan önce bu çıktılardan makineyi tanımlayan satırları çıkar.
Eski image'ları registry'de bırakma, ama silmeden önce kimin kullandığını öğren. .NET Core 3.1 ve .NET 6 image'ları artık güvenlik yaması almıyor; onlardan türeyen image'lar 13. yazıdaki taramada her ay daha kırmızı görünecek.
Sık Düşülen Tuzaklar
Base image'ı yükseltip geri kalanına dokunmamak. 3.1'i 10.0 yapmak tek satırlık bir değişiklik gibi görünür; ama port, kullanıcı, işletim sistemi ve paket adları aynı anda değişir. Tablodaki her satırı ayrı ayrı kontrol et.
DOCKER_BUILDKIT=0'ı CI'da unutmak. Yıllar önce bir sorunu aşmak için eklenen bu değişken pipeline'ı sessizce legacy builder'da tutar: cache mount, build secret ve paralel stage'ler çalışmaz, build check yapılmaz. Eski pipeline'larda ilk aranacak satır bu.
.NET 11 SDK'ya geçip image'ın nereye gittiğini kontrol etmemek. Build makinesinde wslc kuruluysa image Docker'da görünmez ve sonraki docker push adımı "image not found" ile kırılır. CI'da LocalRegistry'yi sabitle ya da image'ı doğrudan registry'ye gönder (ContainerRegistry).
Eski blog yazısındaki komutu bugünün davranışıyla okumamak. docker volume prune (11. yazı), docker-compose, --link (12. yazı), MAINTAINER: hepsi ya davranış değiştirdi ya da deprecated. Bir komutu kopyalamadan önce yazının tarihine ve Docker'ın güncel dokümanına bak.
Küçük Bir Deneme
★☆☆ labs/15-legacy/Dockerfile.2021'ı tablodaki adımlarla kendin modernize et ve docker build --check'in uyarı vermediğini gör. Sonucu src/Bookstore.Api/Dockerfile ile karşılaştır.
★★☆ Bookstore.Api'yi -e ASPNETCORE_HTTP_PORTS=80 ile başlat ve bir yardımcı container'la (14. yazı) /proc/sys/net/ipv4/ip_unprivileged_port_start'ı oku. Değer ne? Aynı denemeyi rootless Docker'da (2. yazı) ya da Podman'da yap; sonuç değişiyor mu?
★★★ Bookstore.Api'yi .NET 11 SDK ile -p:SOURCE_DATE_EPOCH vererek iki farklı makinede (ya da biri Windows'ta, biri WSL'de) publish et ve digest'leri karşılaştır. Aynı mı? Değilse farkı hangi dosya yaratıyor?
Bu yazıyla Docker serisi bitti. Container'ın bir process olduğundan başlayıp Bookstore'un üç parçasını (Bookstore.Api, bookstore-web ve SQL Server) image'a aldık, volume ve network'le birbirine bağladık, güvenli çalıştırdık ve bozulduklarında okumayı öğrendik. Bunları her seferinde elle docker run komutlarıyla kurmak yerine tek bir dosyada tanımlamak, sırayla başlatmak ve birlikte yönetmek Docker Compose serisinin konusu; aynı image'ları bir cluster'da çalıştırmak ise yayındaki Kubernetes serisinin.