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

Dockerfile Temelleri: FROM, RUN, COPY, WORKDIR, ENV, ARG, USER, EXPOSE, CMD ve ENTRYPOINT

Mert Özen 14 Kas 2026 13 dk 10 görüntülenme
Dockerfile Temelleri: FROM, RUN, COPY, WORKDIR, ENV, ARG, USER, EXPOSE, CMD ve ENTRYPOINT

Dockerfile'da dosya sistemine dokunan RUN, COPY, ADD ve WORKDIR layer üretir; ENV, ARG, USER, EXPOSE, CMD ve ENTRYPOINT yalnız image config'ine yazılır. Bookstore.Tools için ilk Dockerfile'ı yazıp docker build --check ile taslaktaki hataları yakalıyor, ENTRYPOINT ile CMD'nin nasıl birleştiğini, ARG ile verilen token'ın docker history'de göründüğünü, .dockerignore'un etkisini, EXPOSE'un port açmadığını ve apt-get cache tuzağını gerçek çıktılarla görüyoruz.

Sürüm notu: Bu yazıdaki örnekler Docker Engine 29.9.3 (BuildKit, Dockerfile 1 frontend'i), mcr.microsoft.com/dotnet/sdk:10.0 image'ı (SDK 10.0.404) ve Microsoft.Extensions.Hosting 10.0.15 ile denenmiştir.

Önce Tahmin Et

Bir Dockerfile'da ARG NUGET_TOKEN tanımlanmış ve private NuGet feed'inden paket çekmek için bir RUN satırında kullanılmış. Image build edildi ve registry'ye gönderildi; token'ı ne Dockerfile'da ne image'ın dosya sisteminde bıraktın. Image'ı çeken biri token'ı görebilir mi? İkinci soru: Dockerfile'da EXPOSE 8080 var. docker run'a -p vermeden container'ı başlatırsan host'tan 8080'e bağlanabilir misin?

Bookstore.Tools ve İlk Taslak

Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) katalog import aracı Bookstore.Tools'u 3. yazıda Generic Host'a taşımıştık. Bu yazıda ona gerçek bir iş verdik: data/books.csv'deki kitapları okuyor, kitabın ISBN (International Standard Book Number; kitaplara verilen uluslararası numara) değerinin 13 haneli ISBN-13 kontrol hanesini doğruluyor, geçersiz satırları atlayıp kalanları batch'ler hâlinde import ediyor. Ayarlar configuration'dan geliyor:

namespace Bookstore.Tools;

public sealed class ImportOptions
{
    public const string SectionName = "Import";

    public string File { get; set; } = "data/books.csv";
    public int BatchSize { get; set; } = 2;
    public int DelayMs { get; set; } = 1000;
}
builder.Services.Configure<ImportOptions>(builder.Configuration.GetSection(ImportOptions.SectionName));

Generic Host'un configuration'ı ortam değişkenlerini ve komut satırı argümanlarını okuduğu için aynı ayar container'da iki yoldan verilebilir: Import__BatchSize=5 ortam değişkeni ya da --Import:BatchSize=5 argümanı. Bu ikisi birazdan ENV ve CMD farkını göstermemizi sağlayacak.

Emre'nin ilk Dockerfile taslağı şöyle:

FROM mcr.microsoft.com/dotnet/sdk:10.0 as build
ARG NUGET_TOKEN
WORKDIR /src
COPY . .
RUN dotnet publish src/Bookstore.Tools/Bookstore.Tools.csproj -c Release -o /app
WORKDIR /app
ENV Import__File $IMPORT_FILE
EXPOSE 8080
CMD dotnet Bookstore.Tools.dll

Build ediliyor ve çalışıyor, ama içinde beş sorun var. Build'den önce bunları yakalamanın kolay yolu BuildKit'in build check'leri: Dockerfile frontend 1.8 ve Buildx 0.15'ten beri docker build --check image üretmeden Dockerfile'ı kurallara göre denetliyor:

docker build --check -f labs/05-build-check/Dockerfile.first .
echo "check exit code: $?"
Check complete, 5 warnings have been found!

WARNING: FromAsCasing - https://docs.docker.com/go/dockerfile/rule/from-as-casing/
'as' and 'FROM' keywords' casing do not match
labs/05-build-check/Dockerfile.first:1
   1 | >>> FROM mcr.microsoft.com/dotnet/sdk:10.0 as build

WARNING: SecretsUsedInArgOrEnv - https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
Do not use ARG or ENV instructions for sensitive data (ARG "NUGET_TOKEN")
labs/05-build-check/Dockerfile.first:2
   2 | >>> ARG NUGET_TOKEN

WARNING: UndefinedVar - https://docs.docker.com/go/dockerfile/rule/undefined-var/
Usage of undefined variable '$IMPORT_FILE'
labs/05-build-check/Dockerfile.first:7
   7 | >>> ENV Import__File $IMPORT_FILE

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/05-build-check/Dockerfile.first:7
   7 | >>> ENV Import__File $IMPORT_FILE

WARNING: JSONArgsRecommended - https://docs.docker.com/go/dockerfile/rule/json-args-recommended/
JSON arguments recommended for CMD to prevent unintended behavior related to OS signals
labs/05-build-check/Dockerfile.first:9
   9 | >>> CMD dotnet Bookstore.Tools.dll
check exit code: 1

(Her uyarının altındaki satır bloklarını kısalttım.) --check uyarı bulunca sıfırdan farklı kodla çıkıyor; CI'da Dockerfile'ı build etmeden önce bu adımı koşmak için yeterli. Uyarıları bir build'i durduracak hataya çevirmek istersen Dockerfile'ın başına syntax satırının altına check=error=true direktifini yazarsın. Kalan sorunları (COPY . . ve EXPOSE) check'ler yakalamıyor; onlara aşağıda geleceğiz.

Instruction'lar: Hangisi Layer, Hangisi Metadata?

Düzeltilmiş Dockerfile src/Bookstore.Tools/Dockerfile'da duruyor; build context repository'nin kökü:

# syntax=docker/dockerfile:1
FROM mcr.microsoft.com/dotnet/sdk:10.0

ARG BUILD_CONFIGURATION=Release
ARG APP_VERSION=1.0.0

WORKDIR /src
COPY Bookstore.slnx ./
COPY src/ src/
RUN dotnet publish src/Bookstore.Tools/Bookstore.Tools.csproj -c $BUILD_CONFIGURATION -o /app -p:Version=$APP_VERSION

WORKDIR /app
COPY --chown=app:app data/books.csv data/books.csv

LABEL org.opencontainers.image.title="Bookstore.Tools" \
      org.opencontainers.image.version=$APP_VERSION
ENV DOTNET_ENVIRONMENT=Production \
    Import__File=data/books.csv

USER app
ENTRYPOINT ["dotnet", "Bookstore.Tools.dll"]
CMD ["--Import:DelayMs=500"]
docker build --check -f src/Bookstore.Tools/Dockerfile .
docker build -f src/Bookstore.Tools/Dockerfile -t dockerseries-bookstore-tools:1.0.0 .
Check complete, no warnings found.
...
#7 [1/7] FROM mcr.microsoft.com/dotnet/sdk:10.0@sha256:fe0fd4e93bd2fbb47c883b7a6369e5c8d3db8bf01cdc5981f6ee35fbdf3ff4fb
#8 [2/7] WORKDIR /src
#9 [3/7] COPY Bookstore.slnx ./
#10 [4/7] COPY src/ src/
#11 [5/7] RUN dotnet publish src/Bookstore.Tools/Bookstore.Tools.csproj -c Release -o /app -p:Version=1.0.0
#11 4.835   Restored /src/src/Bookstore.Tools/Bookstore.Tools.csproj (in 4.39 sec).
#11 5.513   Bookstore.Tools -> /app/
#11 DONE 5.6s
#12 [6/7] WORKDIR /app
#13 [7/7] COPY --chown=app:app data/books.csv data/books.csv
#14 naming to docker.io/library/dockerseries-bookstore-tools:1.0.0 done

İlk satırdaki # syntax=docker/dockerfile:1 bir yorum değil, parser direktifi: BuildKit'e Dockerfile'ı 1.x serisinin en güncel frontend'iyle yorumlamasını söyler; böylece yeni özellikler Docker Engine'i güncellemeden gelir. BuildKit build'i yedi adım olarak gösteriyor, ama yedi layer üretmiyor. Bunu image'ın geçmişinde görürüz:

docker history dockerseries-bookstore-tools:1.0.0
IMAGE          CREATED              CREATED BY                                      SIZE      COMMENT
f13daf0ef57b   47 seconds ago       CMD ["--Import:DelayMs=500"]                    0B        buildkit.dockerfile.v0
<missing>      47 seconds ago       ENTRYPOINT ["dotnet" "Bookstore.Tools.dll"]     0B        buildkit.dockerfile.v0
<missing>      47 seconds ago       USER app                                        0B        buildkit.dockerfile.v0
<missing>      47 seconds ago       ENV DOTNET_ENVIRONMENT=Production Import__Fi…   0B        buildkit.dockerfile.v0
<missing>      47 seconds ago       LABEL org.opencontainers.image.title=Booksto…   0B        buildkit.dockerfile.v0
<missing>      47 seconds ago       COPY --chown=app:app data/books.csv data/boo…   16.4kB    buildkit.dockerfile.v0
<missing>      47 seconds ago       WORKDIR /app                                    4.1kB     buildkit.dockerfile.v0
<missing>      47 seconds ago       RUN |2 BUILD_CONFIGURATION=Release APP_VERSI…   35.8MB    buildkit.dockerfile.v0
<missing>      50 seconds ago       COPY src/ src/ # buildkit                       2.93MB    buildkit.dockerfile.v0
<missing>      50 seconds ago       COPY Bookstore.slnx ./ # buildkit               12.3kB    buildkit.dockerfile.v0
<missing>      About a minute ago   WORKDIR /src                                    8.19kB    buildkit.dockerfile.v0
<missing>      About a minute ago   ARG APP_VERSION=1.0.0                           0B        buildkit.dockerfile.v0
<missing>      About a minute ago   ARG BUILD_CONFIGURATION=Release                 0B        buildkit.dockerfile.v0
<missing>      3 days ago           RUN /bin/sh -c powershell_version=7.6.10    …   53.1MB    buildkit.dockerfile.v0
...

Kural basit: dosya sistemine dokunan instruction'lar (RUN, COPY, ADD ve WORKDIR) layer üretir; geri kalanlar (ENV, ARG, LABEL, USER, EXPOSE, ENTRYPOINT, CMD) yalnız image'ın config'ine yazılır ve 0 B görünür. FROM ise base image'ın bütün layer'larını alır; listenin altındaki 53,1 MB'lık PowerShell kurulumu SDK image'ının kendi layer'ı. Kısaca hepsi:

  • FROM: başlangıç image'ı. Burada SDK, çünkü build'i container içinde yapıyoruz.
  • WORKDIR: sonraki instruction'ların çalışma klasörü; yoksa yaratır. RUN cd ... yerine kullanılır, çünkü her RUN yeni bir shell'dir.
  • COPY: build context'ten image'a dosya kopyalar; --chown sahibini kopyalarken ayarlar.
  • RUN: build sırasında komut çalıştırır, sonucu bir layer olur.
  • ARG: yalnız build sırasında geçerli değişken, --build-arg ile değişir.
  • ENV: image'a yazılan ve container'da da geçerli ortam değişkeni.
  • LABEL: metadata; OCI'nin (Open Container Initiative; container formatlarını standartlaştıran proje) org.opencontainers.image.* anahtarları araçların tanıdığı ortak adlar.
  • USER: sonraki RUN'lar ve container'ın process'i bu kullanıcıyla çalışır. .NET image'larında hazır gelen app kullanıcısının uid'si 1654.
  • ENTRYPOINT ve CMD: container başlarken çalışan komut ve varsayılan argümanları.

Bu image'ın boyutu ayrı bir konu: SDK'yı, kaynak kodu ve build ara dosyalarını taşıdığı için diskte 1,42 GB tutuyor (arm64 bir makinede ölçüldü; amd64'te de aynı mertebede). Production'da çalışacak image'ın içinde derleyici olmamalı; ASP.NET Core Web API serisinin 31. yazısındaki multi-stage yapıyı 7. yazıda Bookstore ile derinleştireceğiz. Bu yazıda instruction'ları tek aşamada görmek için bilerek tek FROM kullandık.

Shell Form, Exec Form, ENTRYPOINT ve CMD

RUN, CMD ve ENTRYPOINT iki biçimde yazılır. Shell form (CMD dotnet Bookstore.Tools.dll) komutu /bin/sh -c'ye verir; exec form (CMD ["dotnet", "Bookstore.Tools.dll"]) programı doğrudan çalıştırır. 3. yazıda shell'in PID (process ID; kernel'in process'e verdiği numara) 1 olduğunda SIGTERM'ü yuttuğunu ve docker stop'un 10 saniye bekleyip 137 ile bittiğini görmüştük; JSONArgsRecommended uyarısının nedeni bu. Taslağı da dockerseries-bookstore-tools:draft adıyla build ettim; iki image'ın config'ine bakalım:

docker image inspect dockerseries-bookstore-tools:draft --format "draft: Entrypoint={{json .Config.Entrypoint}} Cmd={{json .Config.Cmd}} User={{.Config.User}}"
docker image inspect dockerseries-bookstore-tools:1.0.0 --format "1.0.0: Entrypoint={{json .Config.Entrypoint}} Cmd={{json .Config.Cmd}} User={{.Config.User}}"
draft: Entrypoint=null Cmd=["/bin/sh","-c","dotnet Bookstore.Tools.dll"] User=
1.0.0: Entrypoint=["dotnet","Bookstore.Tools.dll"] Cmd=["--Import:DelayMs=500"] User=app

İkisi birlikte kullanıldığında Docker çalışacak komutu ENTRYPOINT + CMD olarak birleştirir: ENTRYPOINT sabit kısım, CMD değiştirilebilir varsayılan argümanlar. docker run'da image adından sonra yazdığın her şey CMD'nin yerine geçer. Taslak image'da ENTRYPOINT olmadığı için argüman bütün komutun yerine geçiyor:

docker run --rm dockerseries-bookstore-tools:draft --Import:BatchSize=5
docker: Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: exec: "--Import:BatchSize=5": executable file not found in $PATH

Düzeltilmiş image'da aynı argüman CMD'nin yerine geçiyor ve dotnet Bookstore.Tools.dll --Import:BatchSize=5 çalışıyor. CMD'deki --Import:DelayMs=500 da gittiği için gecikme varsayılan 1000 ms'ye dönüyor:

docker run --rm dockerseries-bookstore-tools:1.0.0
docker run --rm dockerseries-bookstore-tools:1.0.0 --Import:BatchSize=5
10:51:49 info: Microsoft.Hosting.Lifetime[0] Application started. Press Ctrl+C to shut down.
10:51:49 info: Microsoft.Hosting.Lifetime[0] Hosting environment: Production
10:51:49 info: Microsoft.Hosting.Lifetime[0] Content root path: /app
10:51:49 warn: Bookstore.Tools.ImportWorker[0] Skipping Pride and Prejudice: invalid ISBN-13 9780141439510
10:51:49 info: Bookstore.Tools.ImportWorker[0] Importing 5 books from data/books.csv in 3 batches
10:51:50 info: Bookstore.Tools.ImportWorker[0] Batch 1/3 imported: Dune, Emma
10:51:50 info: Bookstore.Tools.ImportWorker[0] Batch 2/3 imported: The Hobbit, Middlemarch
10:51:51 info: Bookstore.Tools.ImportWorker[0] Batch 3/3 imported: Crime and Punishment
10:51:51 info: Bookstore.Tools.ImportWorker[0] Import finished
10:51:51 info: Microsoft.Hosting.Lifetime[0] Application is shutting down...
...
10:52:21 info: Bookstore.Tools.ImportWorker[0] Importing 5 books from data/books.csv in 1 batches
10:52:22 info: Bookstore.Tools.ImportWorker[0] Batch 1/1 imported: Dune, Emma, The Hobbit, Middlemarch, Crime and Punishment

Container'ın kendisini değiştirmeden ENTRYPOINT'i de ezebilirsin; içine bakmak için en pratik yol bu: docker run --rm --entrypoint ls dockerseries-bookstore-tools:1.0.0 -ln data. Ortam değişkeni ise -e ile verilir ve ENV'deki varsayılanı ezer; dosya yoksa araç hatayı log'layıp 1 ile çıkıyor:

docker run --rm -e Import__File=data/missing.csv dockerseries-bookstore-tools:1.0.0
echo "exit code: $?"
10:52:22 fail: Bookstore.Tools.ImportWorker[0] Cannot read import file: Could not find file '/app/data/missing.csv'.
exit code: 1

Pratik bir kural: değişmeyen program ENTRYPOINT'e, kullanıcının sık değiştireceği argümanlar CMD'ye, ortama göre değişen ayarlar ENV'e (ve çalıştırırken -e'ye).

ARG ve ENV: Build Zamanı, Çalışma Zamanı ve Secret'lar

ARG build sırasında, ENV hem build sırasında hem container'da geçerli. Bu yüzden ENV'e yazılan her şey docker inspect ile okunur ve container'ın içinden görülür; secret'ın oraya yazılmayacağı açık. ARG'ın daha güvenli olduğu sanılır, çünkü container'da görünmez. İlk sorunun cevabı için küçük bir Dockerfile:

FROM alpine:3
ARG NUGET_TOKEN
RUN echo "restoring with a token of ${#NUGET_TOKEN} characters"
RUN echo "this step does not use the token"
docker build --build-arg NUGET_TOKEN=fake-token-for-demo -t dockerseries-argsecret labs/05-arg-secret
docker history --no-trunc --format "{{.CreatedBy}}" dockerseries-argsecret | head -4
RUN |1 NUGET_TOKEN=fake-token-for-demo /bin/sh -c echo "this step does not use the token" # buildkit
RUN |1 NUGET_TOKEN=fake-token-for-demo /bin/sh -c echo "restoring with a token of ${#NUGET_TOKEN} characters" # buildkit
ARG NUGET_TOKEN=fake-token-for-demo
CMD ["/bin/sh"]

Cevap: evet. Token hiçbir dosyaya yazılmadı, ama ARG'dan sonraki her RUN satırı image'ın geçmişine build argümanlarıyla birlikte kaydedildi; tokenı kullanmayan ikinci RUN bile. Image'ı çekebilen herkes docker history ile okur. Yukarıdaki kendi image'ımızın geçmişindeki RUN |2 BUILD_CONFIGURATION=Release APP_VERSI… satırı da aynı mekanizma; orada sakıncası yok, çünkü değerler gizli değil. Secret'ı build'e vermenin doğru yolu BuildKit'in RUN --mount=type=secret'i: secret yalnız o RUN süresince bir dosya olarak bağlanır, ne layer'a ne geçmişe yazılır. Private NuGet feed'i örneğiyle bir sonraki yazıda kuruyoruz.

COPY, ADD ve .dockerignore

COPY build context'teki dosyaları image'a kopyalar ve tek yaptığı budur. ADD aynısını yapar, artı iki iş daha: kaynak bir URL ya da Git repository'si olabilir ve lokal bir tar arşivi verilirse onu açar. 4. yazıda gördüğümüz ADD alpine-minirootfs-3.24.4-x86_64.tar.gz / satırı bu ikinci özelliği kullanıyordu. Docker'ın önerisi, bu ek davranışlara gerçekten ihtiyacın yoksa COPY kullanmak; bir dosyanın sessizce açılması ya da dışarıdan indirilmesi sürpriz olmasın.

COPY --chown=app:app dosyanın sahibini kopyalarken ayarlar. Kopyalayıp ardından RUN chown -R yazmak aynı sonucu verir gibi görünür ama dosyaların ikinci bir kopyasını yeni bir layer'da üretir ve image büyür. Sonuç:

docker run --rm --entrypoint ls dockerseries-bookstore-tools:1.0.0 -ln data
total 4
-rw-r--r-- 1 1654 1654 314 Nov 14 10:50 books.csv

Dosya app kullanıcısına (1654) ait; --chown olmadan uid 0'a, yani root'a ait olurdu ve app olarak çalışan araç okuyabilir ama yanına bir checkpoint dosyası yazamazdı.

Build context, docker build'in son argümanı olan klasör (.); COPY yalnız oradan kopyalayabilir. .dockerignore bu klasörden nelerin hiç gönderilmeyeceğini söyler. Yukarıdaki geçmişte COPY src/ src/ layer'ı 2,93 MB'tı: o sırada .dockerignore yoktu ve host'ta build ettiğim bin/ ve obj/ klasörleri de image'a girdi. Repository köküne şunu ekledim:

**/bin/
**/obj/
out/
.git/
.vs/
.vscode/
*.md
docker history dockerseries-bookstore-tools:1.0.0 | grep "COPY src"
<missing>      6 seconds ago        COPY src/ src/ # buildkit                       45.1kB    buildkit.dockerfile.v0

2,93 MB'tan 45,1 kB'a. Boyuttan önemlisi doğruluk: host'un obj/ klasörü container'daki restore'un ürettiği dosyaların üzerine yazılınca build'in nasıl kırıldığını ASP.NET Core Web API serisinin "ASP.NET Core Web API'yi Docker'a Almak ve GitHub Actions ile CI/CD Kurmak" yazısında NETSDK1064 hatasıyla görmüştük. Taslaktaki COPY . . ise bunun en genişi: .git klasöründen geliştiricinin lokal .env dosyasına kadar her şeyi image'a taşıyabilir. Ne kopyaladığını açıkça yaz ve .dockerignore'u her projede ilk dosyalardan biri yap.

EXPOSE Port Açmaz

İkinci sorunun cevabı da kısa: hayır. EXPOSE image'ın config'ine "bu image şu port'u dinler" diye bir not düşer, başka bir şey yapmaz. nginx image'ında EXPOSE 80 var:

docker run -d --name dockerseries-web nginx:1.30-alpine
docker port dockerseries-web
curl -sS -m 3 localhost:80
curl: (7) Failed to connect to localhost:80 after 0 ms: Could not connect to server

docker port boş döndü, host'tan bağlantı yok. Port'u yayımlayan -p'dir. EXPOSE'un tek pratik etkisi -P (büyük P) bayrağında: Docker expose edilmiş bütün port'ları host'taki rastgele port'lara bağlar:

docker run -d -P --name dockerseries-web nginx:1.30-alpine
docker port dockerseries-web
80/tcp -> 0.0.0.0:32768
80/tcp -> [::]:32768

Taslaktaki EXPOSE 8080 bu yüzden iki kez yanlıştı: araç hiçbir port dinlemiyor ve olsa bile o satır port açmazdı. EXPOSE'u, image'ı kullanacak kişiye dokümantasyon olarak düşün; HTTP servisleri için 8. yazıda kullanacağız.

Üretimde Dikkat

Bu yazıdaki image bilerek tek aşamalı: SDK, kaynak kod ve build ara dosyalarıyla 1 GB'ın üzerinde. Production image'ında yalnız runtime ve publish çıktısı olmalı; bunu multi-stage build ile 7. yazıda, .NET'e özel image seçimini 8. yazıda yapacağız.

Base image'ı bilinçli seç ve tag'ini sabitle. FROM mcr.microsoft.com/dotnet/sdk:10.0 her patch'te değişir; bu genellikle istediğin şey (güvenlik yamaları), ama build'lerin tekrar üretilebilir olmasını istiyorsan 4. yazıdaki gibi digest pin ve otomatik güncelleme kullan.

docker build --check'i CI'a koy. Saniyeler süren bu adım shell form CMD'yi, secret adlı ARG'ları ve tanımsız değişkenleri image üretilmeden yakalar. Container'ı root olmayan bir kullanıcıyla (USER app) çalıştırmak da varsayılan olsun; nedenlerini 13. yazıda ayrıntılı göreceğiz.

Sık Düşülen Tuzaklar

apt-get update ile apt-get install'ı ayrı RUN satırlarına yazmak. Build cache her satırı ayrı saklar. Install satırına yeni bir paket eklediğinde update satırı değişmediği için cache'ten gelir:

FROM ubuntu:24.04
RUN apt-get update
RUN apt-get install -y --no-install-recommends curl jq
#5 [2/3] RUN apt-get update
#5 CACHED
#6 [3/3] RUN apt-get install -y --no-install-recommends curl jq

Paket listesi ilk build günündeki hâliyle kalır; haftalar sonra install, artık depoda olmayan bir paket sürümünü indirmeye çalışıp 404 ile kırılabilir ya da eski bir sürümü kurar. Doğrusu ikisini tek RUN'da birleştirmek ve listeyi aynı layer'da silmek:

FROM ubuntu:24.04
RUN apt-get update \
    && apt-get install -y --no-install-recommends curl jq \
    && rm -rf /var/lib/apt/lists/*

Dosyayı bir RUN'da yaratıp sonraki RUN'da silmek. Silme yeni bir layer'a yalnız "silindi" işareti yazar; dosya alttaki layer'da durur ve image küçülmez. Temizliği dosyayı üreten RUN'ın içinde yap.

Secret'ı ARG ya da ENV ile vermek. Yukarıda gördüğümüz gibi ikisi de image'ın içinde kalır.

WORKDIR yerine RUN cd /app yazmak. Her RUN yeni bir shell'de çalışır; cd sonraki satıra taşınmaz.

Küçük Bir Deneme

★☆☆ Taslak Dockerfile'daki beş uyarıyı tek tek düzelt ve her düzeltmeden sonra docker build --check çalıştır. Hangi uyarı hangi satırdan geliyordu?

★★☆ Dockerfile'a COPY data/books.csv data/books.csv ve ardından RUN chown app:app data/books.csv yazdığın bir varyant hazırla. docker history ile iki yaklaşımın layer sayısını ve boyutunu karşılaştır.

★★★ data/books.csv'yi image'a gömmek yerine çalışma anında vermenin iki yolunu dene: -v ile bir dosya bağlamak ve -e Import__File=... ile yolu değiştirmek. Hangisinin hangi senaryoda (geliştirme, CI, production) daha doğru olduğunu yaz.

İlk Dockerfile'ımız hazır ve her instruction'ın image'a ne eklediğini biliyoruz. Bir sonraki yazıda build'i hızlandırıyoruz: layer sırası ve cache invalidation, NuGet ve npm için cache mount'lar, secret'ı image'a sızdırmadan private feed'den restore ve birden çok image'ı tek komutla derleyen docker buildx bake.