Skip to content

Bölüm 8 / 13

Dockerfile yazmak

.NET uygulamasını konteynerlemek, multi-stage build.

21 dakikalık okuma

Şimdiye kadar başkasının yaptığı imajları çalıştırdın. Bu ders üretme tarafı: kendi uygulamanı imaja çevirmek. Öğreneceğin şey bir yönerge listesi değil, üç karar: katmanları hangi sırayla yazarsan derleme her seferinde saniyeler sürer (önbellek), derleme araçlarını son imajdan nasıl çıkarırsın (çok aşamalı derleme), ve imajın hangi kullanıcıyla, hangi sinyal davranışıyla, hangi sertifika deposuyla çalışır (üretime uygunluk). 7. dersin PID 1 sinyal senedi burada ödenir — ENTRYPOINT'i exec biçiminde yazmak; 3. dersin köksüz çalıştırma senedi USER ile; 4. ve 5. derslerin kurum kök sertifikası senedi imajın içinde. Dersin sonunda bir ASP.NET Core uygulamasını ~110 MB'lık, köksüz, sağlık kontrollü bir imaja çevirebilir; aynı işi Node için yapabilir olursun.

01Ne öğreneceksin

  • Zihinsel model: her yönerge bir katman, katman sırası = önbellek stratejisi; derlemeyi 4 dakikadan 8 saniyeye indiren tek satır
  • Yönergeler tek tek: FROM, WORKDIR, COPY ↔ ADD, RUN, ENV ↔ ARG, EXPOSE, VOLUME, USER, HEALTHCHECK
  • ENTRYPOINT ↔ CMD ve exec ↔ shell biçimi — 7. dersin "tam 10 saniyede duruyor" tuzağının çözümü
  • .dockerignore: derleme bağlamı, derleme hızı ve sır sızıntısı
  • Çok aşamalı derleme: SDK ile derle, çalışma zamanı imajıyla koş; kaynak kod ve sır son imajda kalmaz
  • .NET örneği uçtan uca (slim ↔ Alpine ↔ chiseled) ve Node örneği (npm ci, PID 1, bellek)
  • Konteynere kurum kök sertifikası eklemek; BuildKit ve RUN --mount=type=secret; imaj boyutunu docker history ile ölçmek

02Zihinsel model: bir Dockerfile aslında ne yapar

6. derste imajın katman yığını olduğunu, her katmanın bir öncekinin üstüne yazılan dosya sistemi farkı olduğunu görmüştün. Dockerfile bu yığını üreten tariftir: her satır bir katman üretir ve katman, o satır çalıştıktan sonra dosya sisteminde ne değiştiyse odur.

FROM mcr.microsoft.com/dotnet/aspnet:8.0     # temel katmanlar (hazır gelir)
WORKDIR /app                                 # meta veri (dosya değişmez)
COPY --from=build /out .                     # yeni katman: /app altındaki dosyalar
USER app                                     # meta veri
ENTRYPOINT ["dotnet", "Basvuru.Api.dll"]     # meta veri

Buradan üç sonuç çıkar ve dersin geri kalanı bu üç sonucun sonucudur:

GerçekSonucu
Her yönerge önbelleğe alınır; girdisi değişmediyse yeniden çalışmazSıra önemlidir: az değişen şeyler yukarı, çok değişen şeyler aşağı yazılır (04. bölüm)
Bir katman değişince altındaki tüm katmanlar geçersiz olurKaynak kodu en başta kopyalarsan her küçük değişiklikte paket indirme yeniden çalışır
Katman ekler; sonraki katmanda silinen dosya alttaki katmanda dururRUN kur && RUN sil yer kazandırmaz; sır imajda kalır — çözümü 05. ve 09. bölüm

Derleme şöyle başlatılır. İkinci argüman çoğu insanın yanlış okuduğu yerdir:

$ docker build -t basvuru-api:1.4.0 .
                                      ^ bu nokta "Dockerfile'ın yeri" değil, DERLEME BAĞLAMI
$ docker build -f docker/api.Dockerfile -t basvuru-api:1.4.0 .   # Dockerfile başka yerdeyse

Derleme bağlamı, Docker motoruna gönderilen dizindir. Komutu çalıştırdığında Docker o dizini (ignore edilmeyen her şeyiyle) paketleyip motora yollar; COPY yalnızca bu bağlamın içinden kopyalayabilir. Bağlamın dışındaki bir yolu (COPY ../ortak/ /app) kopyalayamazsın — bu, monorepo'da ilk çarpılan duvardır ve çözümü bağlamı üst dizin yapıp -f ile Dockerfile'ı göstermektir.

03Yönergeler: her birinin bir kararı var

FROM — temel imaj. Her Dockerfile buradan başlar. İki kural: etiket yaz (:latest = bir sonraki derlemenin neyi çektiğini bilmemek; üretimde digest sabitlenir — 10. ders) ve aşamaya ad ver (AS build) — çok aşamalı derlemede gerekir.

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
FROM node:20-alpine AS build
FROM scratch                                # hiçbir şey yok: yalnız statik ikili dosyalar için

WORKDIR — sonraki yönergelerin çalışma dizini; yoksa oluşturur. RUN cd /app && … yazmanın karşılığıdır ama cd yalnız o RUN satırında geçerli olurken WORKDIR kalıcıdır. Mutlak yol yazılır.

COPY ↔ ADD — ikisi de bağlamdan dosya alır. Fark şudur ve mülakat sorusudur:

COPYADD
Dosya kopyalarEvetEvet
Yerel .tar.gz'i kendiliğinden açarHayırEvet — sürpriz davranış
URL'den indirirHayırEvet (ama doğrulama yok, önbellek kötü)
Git deposundan çekerHayırEvet (yeni sürümler)
ÖneriVarsayılan buYalnız bilinçli arşiv açma

URL'den dosya indirmek gerekiyorsa ADD yerine RUN curl -fsSL … -o dosya && echo "<sha256> dosya" | sha256sum -c - yazılır: 5. dersin curl bayrakları burada işe yarar ve sağlama doğrulaması tedarik zinciri güvenliğinin en ucuz adımıdır (14. hat). COPY --chown=app:app ve COPY --chmod=0755 ayrıca kopyalarken sahiplik/izin ayarlar — 3. dersin izin sorunlarının çoğunu doğduğu yerde keser.

RUN — derleme sırasında komut çalıştırır ve sonucu katmana yazar. Klasik kural: birbirine bağlı adımlar tek RUN'da zincirlenir, çünkü her RUN ayrı katmandır ve ayrı katmanda silinen şey yer kazandırmaz:

# YANLIŞ — üç katman, apt önbelleği imajda kalır
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*

# DOĞRU — tek katman, önbellek aynı katmanda temizlenir
RUN apt-get update \
 && apt-get install -y --no-install-recommends curl ca-certificates \
 && rm -rf /var/lib/apt/lists/*

Ayrıca apt-get update ile install'ı ayrı satırlara koymak ünlü bir önbellek tuzağıdır: update katmanı önbellekten gelir, install satırına yeni bir paket eklersin ve aylar önceki paket listesiyle kurulum yapılır ("404 Not Found" ya da eski sürüm). 4. dersteki --no-install-recommends burada da geçerlidir: imaj boyutu ve saldırı yüzeyi.

ENV ↔ ARG — ikisi de değişkendir, ömürleri farklıdır:

ARGENV
Ne zaman yaşarYalnız derleme sırasındaDerlemede ve çalışma anında
Nasıl verilirdocker build --build-arg SURUM=1.4.0Dockerfile'da ya da docker run -e ile ezilir
İmajda görünür müEvet — docker history'de dururEvet — docker inspect'te durur
Sır için uygun muHayırHayır

Bu tablonun son iki satırı dersin en önemli güvenlik cümlesidir: ARG sır taşımaz. "Derleme sırasında lazım, sonra siliyorum" diye verilen NuGet token'ı docker history çıktısında okunur. Doğrusu 09. bölümdeki RUN --mount=type=secret'tır.

EXPOSE — hiçbir port açmaz. Yalnızca "bu imaj 8080 dinliyor" beyanıdır: belgeleme değeri vardır, docker run -P (büyük P) bunu kullanır ve compose/orkestratör araçları okur. Port yayımlamak hâlâ -p 127.0.0.1:8080:8080 işidir. VOLUME /veri ise o yolu anonim volume'e bağlar; kullanışlı görünür ama sürpriz anonim volume'ler üretir ve compose'daki tanımı karmaşıklaştırır — kalıcı yolu kendi tanımlaman genellikle daha iyidir.

USER — 3. ve 7. derslerin senedi. Varsayılan kullanıcı root'tur; imajın kendisi köksüz olmalıdır ki docker run yazan herkes --user hatırlamak zorunda kalmasın:

# Debian/Ubuntu tabanlı imajda kullanıcı üretmek
RUN useradd --system --uid 10001 --home /app --shell /usr/sbin/nologin app
# Alpine'da
RUN adduser -S -u 10001 -H -s /sbin/nologin app
USER app        # ya da sayıyla: USER 10001

Sayıyla yazmak (USER 10001) Kubernetes'te işe yarar: runAsNonRoot politikası kullanıcı adından UID'yi çözemez ve "container has runAsNonRoot and image has non-numeric user" hatası verir (12. ders). Resmî .NET imajlarında hazır bir app kullanıcısı (UID 1654) zaten vardır; USER app yeter. Köksüz kullanıcı 1024 altı port dinleyemez — bu yüzden uygulama 8080 dinler, 80 değil.

HEALTHCHECK — konteynerin "çalışıyor" değil "hazır" olduğunu söyler; 7. dersin senedi, 9. derste depends_on ile meyvesini verir:

HEALTHCHECK --interval=30s --timeout=3s --start-period=20s --retries=3 \
  CMD curl -fsS http://localhost:8080/health/ready || exit 1

Üç ayrıntı: (1) --start-period uygulamanın açılış süresidir, bu sürede başarısız denemeler sayılmaz — yoksa yavaş açılan uygulama daha ilk saniyede "unhealthy" olur. (2) Komut konteynerin içinde çalışır, bu yüzden curl imajda bulunmalıdır; kabuksuz/chiseled imajda curl yoktur — ya küçük bir sağlık kontrol ikilisi eklenir ya da sağlık kontrolü dışarıya (compose/orkestratör probe'una) bırakılır. (3) Sağlık ucu bağımlılıkları da yoklamalıdır (veritabanı bağlantısı), yoksa "sağlıklı ama hiçbir isteğe cevap veremeyen" konteyner elde edersin.

04ENTRYPOINT ↔ CMD: sinyal senedi

İkisi de konteyner başlayınca ne çalışacağını söyler. Aradaki fark şudur: ENTRYPOINT çalıştırılacak programdır, CMD ona verilen varsayılan argümanlardır; docker run imaj … satırının sonuna yazdığın şey CMD'yi ezer, ENTRYPOINT'i ezmez (onun için --entrypoint gerekir).

Dockerfiledocker run imajdocker run imaj --help
CMD ["dotnet","App.dll"]dotnet App.dll--help (komut tamamen değişir, patlar)
ENTRYPOINT ["dotnet","App.dll"]dotnet App.dlldotnet App.dll --help
ENTRYPOINT ["dotnet"] + CMD ["App.dll"]dotnet App.dlldotnet --help

Pratik kural: uygulama imajlarında ENTRYPOINT (imaj tek bir işi yapar), araç imajlarında ENTRYPOINT + CMD (varsayılan alt komut). Sonra asıl mesele gelir — yazım biçimi:

ENTRYPOINT ["dotnet", "Basvuru.Api.dll"]     # exec biçimi (JSON dizisi)  → DOĞRU
ENTRYPOINT dotnet Basvuru.Api.dll            # shell biçimi                → TUZAK

Shell biçiminde Docker komutu /bin/sh -c "dotnet Basvuru.Api.dll" olarak çalıştırır. Sonuç: PID 1 kabuktur, uygulaman kabuğun çocuğudur. 7. derste ölçtüğün belirti buradan gelir: docker stop SIGTERM'i PID 1'e yani sh'e gönderir, sh sinyali çocuğuna iletmez, hiçbir şey olmaz, 10 saniye sonra SIGKILL gelir. Uygulama isteği bitiremez, bağlantıyı kapatamaz, tamponu yazamaz ve çıkış kodu 137 olur. Exec biçiminde uygulaman doğrudan PID 1 olur ve SIGTERM'i kendisi alır (ASP.NET Core bunu IHostApplicationLifetime ile nazik kapanışa çevirir).

Exec biçiminin iki bedeli vardır, ikisini de bilmek gerekir:

  • Kabuk genişletmesi yoktur. ENTRYPOINT ["sh","-c","dotnet App.dll --port $PORT"] yazmadıkça $PORT genişlemez, joker çalışmaz, && çalışmaz. Değişken gerekiyorsa doğru çözüm küçük bir entrypoint.sh yazıp sonunda exec kullanmaktır: exec dotnet App.dll "$@" — exec kabuğun yerine uygulamayı geçirir, böylece PID 1 yine uygulamadır.
  • Zombi süreçler. PID 1 olan süreç, Linux'ta sahipsiz kalan süreçleri "evlat edinip" temizlemekle yükümlüdür; uygulama çalışma zamanları bunu yapmaz. Alt süreç üreten imajlarda (ör. bir işçi süreç başlatan Node) docker run --init ya da imajda tini kullanılır.

05.dockerignore: bağlam, hız ve sır

02. bölümdeki "bağlam motora gönderilir" cümlesinin faturası burada kesilir. .dockerignore yoksa bin/, obj/, node_modules/, .git/ ve yerel appsettings.Development.json dosyan da gönderilir. Üç ayrı zarar:

  1. Hız — 400 MB'lık bağlamın her derlemede paketlenmesi tek başına dakikalar eder ("Sending build context to Docker daemon 412.6MB" satırı bu yüzden okunur).
  2. Önbellek — COPY . . satırı bağlamdaki herhangi bir dosya değiştiğinde geçersiz olur; .git/ ya da obj/ her derlemede değiştiği için önbellek hiçbir zaman tutmaz.
  3. Sır — .env, *.pfx, id_rsa, .git/ (içinde geçmişteki sırlar dâhil) COPY . . ile imaja girer ve registry'ye push edilir. 13. hattaki "depoya sır girmez" kuralının konteyner karşılığıdır.
# .dockerignore — proje kökünde, Dockerfile'ın yanında
**/bin/
**/obj/
**/node_modules/
**/.vs/
**/.vscode/
.git
.gitignore
**/.env
**/*.user
**/*.pfx
**/*.p12
**/appsettings.Development.json
Dockerfile*
docker-compose*
README.md
tests/

Söz dizimi .gitignore'a benzer ama aynısı değildir: yollar bağlam köküne göredir, ** herhangi bir derinlik demektir ve ! ile istisna yazılır. Doğrulaması kolaydır: docker build çıktısının ilk satırındaki bağlam boyutuna bak, ya da docker run --rm imaj find / -name "*.pfx" ile imajın içinde ne kaldığını ara.

06Katman sırası: derlemeyi 4 dakikadan 8 saniyeye indirmek

Önbellek kuralı tek cümledir: bir yönergenin girdisi değişmediyse Docker o katmanı yeniden üretmez; bir katman geçersiz olursa altındaki her şey de geçersiz olur. COPY için "girdi", kopyalanan dosyaların içeriğidir. Bu yüzden bağımlılık kurulumunu kaynak kodundan ayırırsın:

# YANLIŞ — tek harf değiştirsen bile restore yeniden çalışır (dakikalar)
COPY . .
RUN dotnet restore
RUN dotnet publish -c Release -o /out

# DOĞRU — restore yalnız csproj değişince çalışır
COPY ["src/Basvuru.Api/Basvuru.Api.csproj", "src/Basvuru.Api/"]
COPY ["src/Basvuru.Core/Basvuru.Core.csproj", "src/Basvuru.Core/"]
RUN dotnet restore "src/Basvuru.Api/Basvuru.Api.csproj"
COPY . .
RUN dotnet publish "src/Basvuru.Api/Basvuru.Api.csproj" -c Release -o /out --no-restore

Aynı desen Node'da package.json + package-lock.json, Python'da requirements.txt, Java'da pom.xml ile kurulur. Kural dil bağımsızdır: bağımlılık bildirimini önce kopyala, kur, sonra kaynağı kopyala.

Çok projeli çözümlerde csproj'ları tek tek yazmak can sıkıcıdır; yaygın çözüm bunları bir arşive alıp kopyalamak ya da bağlamda küçük bir betikle üretmektir. Önbelleği doğrulamanın yolu çıktıyı okumaktır: CACHED yazan satırlar önbellekten gelmiştir.

$ docker build -t api:dev .
 => CACHED [build 3/7] RUN dotnet restore "src/Basvuru.Api/Basvuru.Api.csproj"
 => [build 4/7] COPY . .
 => [build 5/7] RUN dotnet publish … --no-restore

Önbelleğin iki bilinmesi gereken sınırı: (1) --no-cache ile tamamen kapatılır (temel imaj güncellemesi için --pull ile birlikte kullanılır); (2) yeni bir makinede/CI koşucusunda yerel önbellek yoktur — oradaki çözüm registry tabanlı önbellektir (--cache-from, 10. ders ve 18. hat).

07Çok aşamalı derleme

Bir ASP.NET Core uygulamasını derlemek için SDK gerekir (~800 MB: derleyici, NuGet, MSBuild). Çalıştırmak için yalnız çalışma zamanı gerekir (~110 MB). Tek aşamalı bir Dockerfile ikisini de imaja koyar; üstelik kaynak kodun, NuGet önbelleğin ve derleme sırasında kullandığın her şey içeride kalır — 6. dersin "silme yer kazandırmaz" kuralı yüzünden sonradan RUN rm -rf ile temizlemek işe yaramaz.

Çözüm birden çok FROM yazmaktır. Her FROM yeni bir aşama başlatır; COPY --from=<aşama> ile yalnız istediğin çıktıyı alırsın, geri kalan aşama son imaja hiç girmez:

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
… derleme …
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
COPY --from=build /out .        # yalnız yayımlanmış çıktı taşınır

Kazanç üç kalemdir ve sözlüde bu üçü sayılır: boyut (registry trafiği, dağıtım süresi), saldırı yüzeyi (derleyicisi olmayan bir imajda saldırgan kod derleyemez — 14. hat), sır ve fikrî mülkiyet (kaynak kod, .git, NuGet kimlik bilgileri son imajda yoktur). Ek yetenekler: aşamalar birbirinden bağımsızsa paralel derlenir (BuildKit); --target test ile belirli bir aşamada durdurulabilir (CI'da test aşaması, 18. hat); COPY --from=nginx:alpine /etc/nginx/nginx.conf ile başka bir imajdan da dosya alınabilir.

08Uçtan uca .NET örneği

Bu dosya, 5. hattaki ASP.NET Core uygulaması için üretime uygun bir başlangıçtır. Her satırı yukarıdaki bölümlerden birine dayanıyor:

# syntax=docker/dockerfile:1

########## 1) derleme aşaması ##########
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src

# kurum vekili/kök sertifikası gerekiyorsa (17-04, 17-05 senedi)
COPY certs/kurum-kok.crt /usr/local/share/ca-certificates/kurum-kok.crt
RUN update-ca-certificates

# önbellek için önce proje dosyaları (06. bölüm)
COPY ["src/Basvuru.Api/Basvuru.Api.csproj", "src/Basvuru.Api/"]
COPY ["src/Basvuru.Core/Basvuru.Core.csproj", "src/Basvuru.Core/"]
RUN dotnet restore "src/Basvuru.Api/Basvuru.Api.csproj"

COPY . .
RUN dotnet publish "src/Basvuru.Api/Basvuru.Api.csproj" \
      -c $BUILD_CONFIGURATION -o /out \
      --no-restore \
      -p:UseAppHost=false

########## 2) çalışma aşaması ##########
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app

# giden HTTPS çağrıları için kurum kökü burada da lazım
COPY certs/kurum-kok.crt /usr/local/share/ca-certificates/kurum-kok.crt
RUN update-ca-certificates

ENV ASPNETCORE_URLS=http://0.0.0.0:8080 \
    DOTNET_EnableDiagnostics=0

COPY --from=build /out .

USER app
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --start-period=20s --retries=3 \
  CMD ["dotnet", "Basvuru.Api.dll", "--healthcheck"]
ENTRYPOINT ["dotnet", "Basvuru.Api.dll"]

Dikkat edilen noktalar:

  • -p:UseAppHost=false — yerel platform için çalıştırılabilir bir apphost üretmeyi kapatır; konteynerde zaten dotnet App.dll ile çalıştırıyoruz, birkaç MB ve gereksiz bir mimari bağı gider.
  • ASPNETCORE_URLS=http://0.0.0.0:8080 — 7. dersin senedi: konteyner içinde localhost dinleyen uygulamaya yayımlanan porttan erişilemez. 8080 seçilmesinin nedeni köksüz kullanıcının 1024 altı port açamamasıdır.
  • Kök sertifika iki aşamada da kopyalanır: derleme aşamasında NuGet kurum vekilinden geçmek için, çalışma aşamasında uygulamanın giden HTTPS çağrıları için. .NET Linux'ta sistem sertifika deposunu kullanır, bu yüzden update-ca-certificates yeterlidir — 5. dersteki "curl -k / sslVerify false" yanlış refleksinin doğru karşılığı budur.
  • TLS'i uygulamada sonlandırmıyoruz. Sertifikayı konteynere koyup https:// dinlemek yerine 5. dersteki düzen korunur: TLS nginx'te biter, konteyner düz HTTP dinler, uygulama ForwardedHeaders ile gerçek şema ve IP'yi öğrenir.

Temel imaj seçimi (6. dersin tablosunun pratiği). Aynı uygulama için ölçülen tipik büyüklükler:

Temel imajKabacaNe zaman
aspnet:8.0 (Debian)~220 MBVarsayılan; en uyumlu, hata ayıklaması kolay
aspnet:8.0-alpine~110 MBKüçük; musl — yerel kütüphane bağımlılığı varsa önce dene (6. ders)
aspnet:8.0-noble-chiseled~105 MBKabuk ve paket yöneticisi yok; saldırı yüzeyi en düşük, üretim için tercih
+ -extra eki~+15 MBChiseled'da globalization/ICU gerekiyorsa

Chiseled'ın bedeli 7. derste konuşulmuştu: docker exec … sh çalışmaz, curl tabanlı HEALTHCHECK yazılamaz, teşhis yardımcı konteynerle yapılır. Bu bedeli kabul etmek üretimde doğru karardır; geliştirme imajının farklı (kabuklu) olması da meşrudur — ama o zaman "üretimde çalışan imaj test edilenle aynı değildir" riskini bilerek alırsın.

Alpine seçtiysen kullanıcı üretmeyi ve musl notunu unutma; ayrıca dotnet publish -r linux-musl-x64 ile -r linux-x64 farkı tam da 6. derste anlatılan çalışma zamanı kimliği meselesidir.

09Node örneği ve BuildKit ile sır

12. hattın senedi: aynı ilkelerin Node tarafı. Üç fark öne çıkar — npm ci, NODE_ENV ve PID 1.

# syntax=docker/dockerfile:1
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
# npm install DEĞİL: ci kilit dosyasına birebir uyar, tekrarlanabilirdir
RUN --mount=type=cache,target=/root/.npm npm ci

FROM node:20-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM node:20-alpine AS final
ENV NODE_ENV=production
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev
COPY --from=build /app/dist ./dist
USER node                      # node imajlarında hazır gelir (UID 1000)
EXPOSE 3000
ENTRYPOINT ["node", "dist/server.js"]
  • npm ci ↔ npm install: ci kilit dosyasını okur, node_modules'ı siler ve birebir kilitteki sürümleri kurar; install kilidi güncelleyebilir. Konteyner tekrarlanabilirlik içindir — ci kullanılır.
  • NODE_ENV=production: yalnız bir alışkanlık değil; birçok kütüphane (Express dâhil) buna bakıp geliştirme kolaylıklarını kapatır, npm ci --omit=dev geliştirme bağımlılıklarını dışarıda bırakır.
  • PID 1: Node varsayılan olarak zombi toplamaz ve bazı kurulumlarda SIGTERM'i işlemez. Üretimde docker run --init (ya da orkestratörün karşılığı) kullanılır ve uygulamada process.on('SIGTERM', …) ile nazik kapanış yazılır.
  • Bellek: Node'un eski sürümlerinde heap sınırı cgroup limitini görmez; --memory 512m koyup node --max-old-space-size=384 vermezsen konteyner 137 ile ölür. .NET bu konuda daha iyidir (cgroup farkındalığı, 7. ders) ama DOTNET_GCHeapHardLimit gibi ayarlarla ince ayar yine mümkündür.

BuildKit ve sır. Yukarıdaki --mount=type=cache satırı BuildKit özelliğidir: paket önbelleğini katmana yazmadan derlemeler arasında paylaşır. Aynı mekanizma sır için de vardır ve 03. bölümdeki ARG sorununun doğru çözümüdür:

# Dockerfile
RUN --mount=type=secret,id=nuget_pat \
    dotnet nuget add source https://nuget.kurum.local/v3/index.json \
      --name kurum --username ci \
      --password "$(cat /run/secrets/nuget_pat)" --store-password-in-clear-text \
 && dotnet restore

# derleme
$ docker build --secret id=nuget_pat,env=NUGET_PAT -t api:dev .

Sır yalnızca o RUN süresince /run/secrets/ altında görünür; katmana yazılmaz, docker history'de çıkmaz, imajı çeken kimse göremez. Aynı biçimde --mount=type=ssh ile özel Git deposundan bağımlılık çekilir. BuildKit bugün varsayılandır; # syntax=docker/dockerfile:1 satırı en güncel söz dizimini çözümlemesini sağlar.

Boyutu ölçmek — "küçülttüm" demek yetmez, rakam gösterilir:

$ docker image ls basvuru-api
$ docker history basvuru-api:1.4.0            # hangi katman kaç MB
$ docker build --no-cache --progress=plain .  # adım adım süre ve çıktı
$ docker image inspect basvuru-api:1.4.0 -f '{{.Os}}/{{.Architecture}}'

docker history sık sık şaşırtır: en büyük katman genellikle apt ile kurulan bir araç ya da COPY . . ile giren node_modules/.git'tir. Farklı mimari için imaj üretmek (--platform linux/amd64,linux/arm64, buildx) 6. dersteki "exec format error" tuzağının çözümüdür ve 10. derste manifest listesiyle birlikte işlenir.

10Sık yapılan on hata

HataBelirtisi / bedeliDoğrusu
COPY . . en baştaHer küçük değişiklikte restore/npm ci yeniden koşarBağımlılık bildirimi önce (06. bölüm)
.dockerignore yokDev bağlam, tutmayan önbellek, imaja giren sır05. bölümdeki dosya
Kullanıcı tanımlanmamışKonteyner root çalışır; bind mount'ta root sahipli dosyalarUSER (sayısal UID)
FROM …:latestDerleme tekrarlanabilir değil; sürüm sessizce atlarEtiket yaz, üretimde digest sabitle (10. ders)
ENTRYPOINT shell biçimiKapanış tam 10 sn sürer, çıkış kodu 137, veri kaybıExec biçimi ya da exec "$@" (04. bölüm)
Sır ARG/ENV iledocker history/inspect ile okunur--mount=type=secret, çalışma anında kasa
apt-get önbelleği temizlenmemişOnlarca MB gereksiz katmanAynı RUN'da rm -rf /var/lib/apt/lists/*
Tek aşamalı derlemeSDK + kaynak kod + NuGet önbelleği imajdaÇok aşamalı (07. bölüm)
Uygulama localhost dinliyorPort yayımlandığı hâlde bağlantı reddediliyor0.0.0.0 (08. bölüm)
Sağlık kontrolü yok"Çalışıyor ama hazır değil"; compose/orkestratör körleşirHEALTHCHECK + hazır olma ucu (9. ders)

11Mini alıştırma — imajını küçült ve sertleştir

Yaklaşık 25 dakika · Docker kurulu ortam + herhangi bir ASP.NET Core ya da Node projesi

  1. Kötü hâli yaz. Tek aşamalı bir Dockerfile üret: FROM … sdk:8.0, COPY . ., RUN dotnet publish, CMD dotnet … (shell biçimi, .dockerignore yok). Derle, docker image ls ile boyutu not al.
  2. Bağlamı ölç. Derleme çıktısındaki bağlam boyutunu yaz. Sonra .dockerignore ekle, yeniden derle, iki rakamı karşılaştır.
  3. Önbelleği kur. Proje dosyalarını önce kopyalayıp restore'u ayır. Kaynak kodda tek bir harf değiştir, time docker build … ile yeniden derle ve süreyi ilk derlemeyle karşılaştır; çıktıda kaç satırın CACHED olduğunu say.
  4. Çok aşamalı yap. Çalışma aşamasını aspnet:8.0 (ya da node:20-alpine) yap, yalnız çıktıyı COPY --from=build ile al. Boyutu 1. adımdaki rakamla karşılaştır; docker history ile en büyük üç katmanı yaz.
  5. Kaynak kod gerçekten gitti mi? docker run --rm -it imaj sh -c 'find / -name "*.cs" -o -name ".git" 2>/dev/null | head' çalıştır (kabuğu olan bir imajla). Aynı denemeyi tek aşamalı imajda yap ve farkı yaz.
  6. Sinyal senedi. Shell biçimli CMD ile time docker stop süresini ölç; exec biçimine çevirip yinele. İki ölçümü ve docker inspect -f '{{.State.ExitCode}}' çıktılarını yan yana yaz.
  7. Köksüzleştir. USER ekle; konteyner içinde id çalıştırıp UID'yi doğrula. Uygulamanın yazmaya çalıştığı bir yol varsa --read-only --tmpfs /tmp ile çalıştırıp hatayı gör ve düzelt.
  8. Sır sızdır, sonra kapat. ARG GIZLI=abc123 + ENV ile bir değer geçir, docker history --no-trunc ve docker inspect çıktısında ara. Sonra aynı değeri --mount=type=secret ile kullanıp iki çıktıda da bulunmadığını doğrula.

12Kendini sına

Katman önbelleği nasıl çalışır; Dockerfile'da yönergeleri hangi sıraya göre yazarsın? .dockerignore olmazsa hangi üç zarar doğar?

Her yönerge bir katman üretir ve girdisi değişmediyse Docker katmanı önbellekten alır; bir katman geçersiz olursa altındaki bütün katmanlar yeniden çalışır. Bu yüzden sıra değişme sıklığına göre kurulur: en az değişen (temel imaj, sistem paketleri), sonra bağımlılık bildirimi (csproj, package.json + kilit dosyası) ve kurulum, en sonda kaynak kod (COPY . .) ve derleme. COPY . . en başa yazılırsa tek harflik değişiklik restore/npm ci adımını yeniden tetikler. .dockerignore yoksa üç zarar: (1) hız — bağlam olduğu gibi motora gönderilir, yüzlerce MB her derlemede paketlenir; (2) önbellek — her derlemede değişen .git/, obj/, node_modules/ yüzünden COPY . . katmanı hiçbir zaman tutmaz; (3) sır ve boyut — .env, *.pfx, .git geçmişi imaja girer ve registry'ye push edilir.

ENTRYPOINT ile CMD arasındaki fark nedir; exec biçimi ile shell biçimi arasındaki fark neye yol açar?

ENTRYPOINT çalıştırılacak programdır, CMD ona verilecek varsayılan argümanlardır. docker run imaj <komut> satırının sonuna yazılan şey CMD'yi ezer; ENTRYPOINT'i ezmek için --entrypoint gerekir. Uygulama imajlarında ENTRYPOINT, araç imajlarında ENTRYPOINT + CMD (varsayılan alt komut) kullanılır. Exec biçimi (["dotnet","App.dll"]) komutu doğrudan çalıştırır: uygulama PID 1 olur ve SIGTERM'i kendisi alır, nazik kapanış çalışır. Shell biçimi (dotnet App.dll) komutu /bin/sh -c ile sarar: PID 1 kabuk olur, sinyali çocuğuna iletmez, docker stop 10 saniye bekler ve SIGKILL gönderir — belirti "her konteyner tam 10 saniyede duruyor" ve çıkış kodu 137'dir. Kabuk genişletmesi gerekiyorsa küçük bir entrypoint.sh yazılır ve son satırda exec kullanılır (exec dotnet App.dll "$@"), böylece PID 1 yine uygulamadır. Alt süreç üreten imajlarda zombi toplama için --init/ tini eklenir.

Çok aşamalı derleme nedir, hangi üç kazancı vardır? Bir .NET uygulaması için tipik iki aşamayı ve seçilecek temel imajları anlat.

Çok aşamalı derlemede birden çok FROM yazılır; her biri ayrı bir aşamadır ve son imaja yalnız COPY --from=<aşama> ile taşınan dosyalar girer. .NET'te birinci aşama sdk:8.0 ile restore + publish yapar, ikinci aşama aspnet:8.0 ile yalnız /out çıktısını alıp çalıştırır. Üç kazanç: boyut (~800 MB SDK yerine ~110-220 MB çalışma zamanı; registry trafiği ve dağıtım süresi), saldırı yüzeyi (son imajda derleyici, paket yöneticisi, kabuk yok), sır/fikrî mülkiyet (kaynak kod, .git, NuGet kimlik bilgileri son imajda kalmaz — tek aşamada rm ile silmek işe yaramaz çünkü dosya alt katmanda durur). Temel imaj seçimi: Debian tabanlı aspnet:8.0 en uyumlu varsayılan; -alpine küçüktür ama musl kullanır (yerel kütüphane uyumu sınanmalı); chiseled kabuksuz ve paket yöneticisiz olduğu için üretimde en güvenli seçimdir, bedeli exec … sh ile hata ayıklayamamak ve curl tabanlı HEALTHCHECK yazamamaktır.

Derleme sırasında gereken bir özel depo token'ını nasıl verirsin; imajı üretime uygun hâle getiren dört satır hangileridir?

Token kesinlikle ARG ya da ENV ile verilmez: ikisi de imajda kalır ve docker history --no-trunc / docker inspect ile okunur. Doğrusu BuildKit'in sır bağlamasıdır: RUN --mount=type=secret,id=nuget_pat … yazılır, değer docker build --secret id=nuget_pat,env=NUGET_PAT ile geçirilir; sır yalnız o RUN süresince /run/secrets/ altında görünür ve katmana yazılmaz. Özel Git deposu için --mount=type=ssh aynı işi yapar. Üretime uygunluğu veren satırlar: USER (sayısal UID ile köksüz çalışma — Kubernetes'in runAsNonRoot denetimi ad çözemez), ENTRYPOINT exec biçiminde (nazik kapanış), ASPNETCORE_URLS=http://0.0.0.0:8080 (konteyner dışından erişim ve 1024 altı port kısıtı) ve HEALTHCHECK (bağımlılıkları da yoklayan hazır olma ucu; --start-period ile açılış süresi tanınır). Kurum vekili varsa buna kök sertifikanın COPY + update-ca-certificates ile hem derleme hem çalışma aşamasına eklenmesi de eklenir — sslVerify false ya da curl -k çözüm değildir.

13Sınav köşesi

Yazılıda nasıl sorulur

(1) Bir ASP.NET Core (ya da Node) uygulamasını çalıştıran Dockerfile yazınız. (2) COPY ile ADD arasındaki fark nedir? (3) ENTRYPOINT ile CMD arasındaki fark nedir? (4) ARG ile ENV arasındaki fark nedir? (5) Çok aşamalı derleme (multi-stage build) nedir, ne kazandırır? (6) .dockerignore ne işe yarar? (7) Katman önbelleği nasıl çalışır; Dockerfile'da bağımlılık kurulumu neden kaynak kopyalamadan önce yazılır? (8) EXPOSE yönergesi portu açar mı? (9) Konteyneri root olmayan kullanıcıyla çalıştırmak için Dockerfile'da ne yapılır? (10) İmaj boyutunu küçültmek için hangi yöntemleri kullanırsınız (en az dört madde)?

Sözlüde nasıl anlatılır — 2 dakikalık taslak

Düzen: (1) Model — "Dockerfile bir katman yığını tarifidir; her yönerge bir katman, bir katman değişince altındakiler geçersiz olur. Bu yüzden Dockerfile'ı değişme sıklığına göre sıralarım: temel imaj, bağımlılık bildirimi ve restore, en sonda kaynak kod." (2) Somut kazanç — "Çok aşamalı derlemeyle SDK imajında derleyip çalışma zamanı imajında koşuyorum; 800 MB'lık SDK yerine 110 MB'lık chiseled imaj çıkıyor, kaynak kod ve NuGet kimlik bilgileri son imajda kalmıyor." (3) Üretim ayrıntıları — "USER ile köksüz çalıştırırım, sayısal UID veririm çünkü Kubernetes runAsNonRoot denetiminde ad çözemez; ENTRYPOINT'i exec biçiminde yazarım, yoksa PID 1 kabuk olur ve SIGTERM iletilmez — belirtisi her konteynerin tam 10 saniyede durup 137 ile çıkmasıdır. Uygulamayı 0.0.0.0:8080'e bağlarım." (4) Güvenlik — "Sırrı ARG ile vermem, docker history'de okunur; BuildKit'in --mount=type=secret bağlamasını kullanırım. .dockerignore olmadan derleme yapmam: hem bağlamı küçültür hem .env/.git'in imaja girmesini engeller. Kurum vekili varsa kök sertifikayı imaja tanıtırım, curl -k ile geçiştirmem." (5) Kıdem vuruşu — "İmaj boyutunu docker history ile ölçer, en büyük katmanı gerekçelendiririm; temel imajı etiketiyle sabitler, üretimde digest kullanırım ve imajı düzenli yenilerim — çünkü yama ancak yeniden derlemeyle kapanır."