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↔CMDve 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 boyutunudocker historyile ö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çek | Sonucu |
|---|---|
| Her yönerge önbelleğe alınır; girdisi değişmediyse yeniden çalışmaz | Sı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 olur | Kaynak kodu en başta kopyalarsan her küçük değişiklikte paket indirme yeniden çalışır |
| Katman ekler; sonraki katmanda silinen dosya alttaki katmanda durur | RUN 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:
COPY | ADD | |
|---|---|---|
| Dosya kopyalar | Evet | Evet |
Yerel .tar.gz'i kendiliğinden açar | Hayır | Evet — sürpriz davranış |
| URL'den indirir | Hayır | Evet (ama doğrulama yok, önbellek kötü) |
| Git deposundan çeker | Hayır | Evet (yeni sürümler) |
| Öneri | Varsayılan bu | Yalnı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:
ARG | ENV | |
|---|---|---|
| Ne zaman yaşar | Yalnız derleme sırasında | Derlemede ve çalışma anında |
| Nasıl verilir | docker build --build-arg SURUM=1.4.0 | Dockerfile'da ya da docker run -e ile ezilir |
| İmajda görünür mü | Evet — docker history'de durur | Evet — docker inspect'te durur |
| Sır için uygun mu | Hayır | Hayı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).
| Dockerfile | docker run imaj | docker run imaj --help |
|---|---|---|
CMD ["dotnet","App.dll"] | dotnet App.dll | --help (komut tamamen değişir, patlar) |
ENTRYPOINT ["dotnet","App.dll"] | dotnet App.dll | dotnet App.dll --help |
ENTRYPOINT ["dotnet"] + CMD ["App.dll"] | dotnet App.dll | dotnet --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$PORTgenişlemez, joker çalışmaz,&&çalışmaz. Değişken gerekiyorsa doğru çözüm küçük birentrypoint.shyazıp sonundaexeckullanmaktır:exec dotnet App.dll "$@"—execkabuğ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 --initya da imajdatinikullanı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:
- 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).
- Önbellek —
COPY . .satırı bağlamdaki herhangi bir dosya değiştiğinde geçersiz olur;.git/ya daobj/her derlemede değiştiği için önbellek hiçbir zaman tutmaz. - 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 birapphostüretmeyi kapatır; konteynerde zatendotnet App.dllile ç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çindelocalhostdinleyen 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-certificatesyeterlidir — 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, uygulamaForwardedHeadersile 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 imaj | Kabaca | Ne zaman |
|---|---|---|
aspnet:8.0 (Debian) | ~220 MB | Varsayılan; en uyumlu, hata ayıklaması kolay |
aspnet:8.0-alpine | ~110 MB | Küçük; musl — yerel kütüphane bağımlılığı varsa önce dene (6. ders) |
aspnet:8.0-noble-chiseled | ~105 MB | Kabuk ve paket yöneticisi yok; saldırı yüzeyi en düşük, üretim için tercih |
+ -extra eki | ~+15 MB | Chiseled'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:cikilit dosyasını okur,node_modules'ı siler ve birebir kilitteki sürümleri kurar;installkilidi güncelleyebilir. Konteyner tekrarlanabilirlik içindir —cikullanı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=devgeliş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 uygulamadaprocess.on('SIGTERM', …)ile nazik kapanış yazılır. - Bellek: Node'un eski sürümlerinde heap sınırı cgroup limitini görmez;
--memory 512mkoyupnode --max-old-space-size=384vermezsen konteyner 137 ile ölür. .NET bu konuda daha iyidir (cgroup farkındalığı, 7. ders) amaDOTNET_GCHeapHardLimitgibi 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
| Hata | Belirtisi / bedeli | Doğrusu |
|---|---|---|
COPY . . en başta | Her küçük değişiklikte restore/npm ci yeniden koşar | Bağımlılık bildirimi önce (06. bölüm) |
.dockerignore yok | Dev bağlam, tutmayan önbellek, imaja giren sır | 05. bölümdeki dosya |
| Kullanıcı tanımlanmamış | Konteyner root çalışır; bind mount'ta root sahipli dosyalar | USER (sayısal UID) |
FROM …:latest | Derleme tekrarlanabilir değil; sürüm sessizce atlar | Etiket yaz, üretimde digest sabitle (10. ders) |
ENTRYPOINT shell biçimi | Kapanış tam 10 sn sürer, çıkış kodu 137, veri kaybı | Exec biçimi ya da exec "$@" (04. bölüm) |
Sır ARG/ENV ile | docker history/inspect ile okunur | --mount=type=secret, çalışma anında kasa |
apt-get önbelleği temizlenmemiş | Onlarca MB gereksiz katman | Aynı RUN'da rm -rf /var/lib/apt/lists/* |
| Tek aşamalı derleme | SDK + kaynak kod + NuGet önbelleği imajda | Çok aşamalı (07. bölüm) |
Uygulama localhost dinliyor | Port yayımlandığı hâlde bağlantı reddediliyor | 0.0.0.0 (08. bölüm) |
| Sağlık kontrolü yok | "Çalışıyor ama hazır değil"; compose/orkestratör körleşir | HEALTHCHECK + 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
- Kötü hâli yaz. Tek aşamalı bir Dockerfile üret:
FROM … sdk:8.0,COPY . .,RUN dotnet publish,CMD dotnet …(shell biçimi,.dockerignoreyok). Derle,docker image lsile boyutu not al. - Bağlamı ölç. Derleme çıktısındaki bağlam boyutunu yaz. Sonra
.dockerignoreekle, yeniden derle, iki rakamı karşılaştır. - Ö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ınCACHEDolduğunu say. - Çok aşamalı yap. Çalışma aşamasını
aspnet:8.0(ya danode:20-alpine) yap, yalnız çıktıyıCOPY --from=buildile al. Boyutu 1. adımdaki rakamla karşılaştır;docker historyile en büyük üç katmanı yaz. - 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. - Sinyal senedi. Shell biçimli
CMDiletime docker stopsüresini ölç; exec biçimine çevirip yinele. İki ölçümü vedocker inspect -f '{{.State.ExitCode}}'çıktılarını yan yana yaz. - Köksüzleştir.
USERekle; konteyner içindeidçalıştırıp UID'yi doğrula. Uygulamanın yazmaya çalıştığı bir yol varsa--read-only --tmpfs /tmpile çalıştırıp hatayı gör ve düzelt. - Sır sızdır, sonra kapat.
ARG GIZLI=abc123+ENVile bir değer geçir,docker history --no-truncvedocker inspectçıktısında ara. Sonra aynı değeri--mount=type=secretile 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."
