Sunucu Yönetimi & Linux

    Dockerfile Yazımında En İyi Pratikler

    Küçük, hızlı ve güvenli imajlar üretmek için Dockerfile yazımının en iyi pratiklerini anlatan rehber.

    12 dk okuma Güncellendi: 10 Temmuz 2026

    Çalışan bir Dockerfile yazmak kolaydır; birkaç satır RUN komutu, bir COPY ve CMD ile uygulamanız ayağa kalkar. Ama "çalışan" bir Dockerfile ile "iyi" bir Dockerfile arasında ciddi bir uçurum vardır. İyi yazılmamış bir imaj yüzlerce megabayt gereksiz dosya taşır, her küçük kod değişikliğinde dakikalarca yeniden derlenir, içinde derleme araçlarını ve gizli bilgileri barındırır ve çoğu zaman root olarak çalışarak sunucunuzu gereksiz risk altına sokar. Üretime çıkan bir imaj günde onlarca kez çekilip başlatılacaksa, bu farklar doğrudan maliyet ve güvenlik olarak size geri döner.

    Bu rehberde bir imajın nasıl katmanlardan oluştuğunu, katman sırasını önbellek lehine nasıl düzenleyeceğinizi, çok aşamalı derleme ile son imajı nasıl küçülteceğinizi, hangi temel imajı seçmeniz gerektiğini, .dockerignore ile derleme bağlamını nasıl temizleyeceğinizi ve USER ile root'suz çalışmayı kıdemli bir sistem yöneticisinin gözünden ele alacağız. Docker'ın temel kavramlarını (imaj, konteyner, katman) henüz oturtmadıysanız önce Docker nedir başlangıç rehberi yazımıza göz atmanızı öneririm; buradaki her pratik o temeller üzerine kuruludur.

    Katman Mantığı ve Önbellek Nasıl Çalışır#

    Bir Docker imajı, üst üste bindirilmiş salt-okunur katmanlardan oluşur. Dockerfile'daki her FROM, RUN, COPY ve ADD komutu yeni bir katman üretir. Docker bir imajı derlerken bu komutları sırayla işler ve her katman için bir önbellek anahtarı hesaplar. Eğer bir komut ve girdileri bir önceki derlemeye göre değişmemişse, Docker o katmanı sıfırdan üretmez; hazır olanı önbellekten kullanır. Bu yüzden Using cache satırlarını görürsünüz.

    Kritik nokta şudur: önbellek zincirlemedir. Bir katman önbellekten kaçırılırsa (cache miss), ondan sonraki bütün katmanlar da yeniden çalışır. Yani Dockerfile'ın 3. satırındaki bir şey değişirse, 4, 5, 6... hepsi baştan derlenir. Bu davranış, komutların sırasının neden bu kadar önemli olduğunu açıklar: en az değişen şeyleri üste, en sık değişen şeyleri alta koymak istersiniz.

    COPY ve ADD için önbellek anahtarı, kopyalanan dosyaların içeriğinin sağlamasından (checksum) hesaplanır. RUN için ise anahtar, komut metninin kendisidir; komut satırı aynı kaldığı sürece Docker onun çıktısının da aynı olacağını varsayar. Bu yüzden RUN apt-get update gibi komutlar bir tuzaktır: metin değişmediği için Docker haftalarca eski önbelleği kullanır ve siz güncel paket listelerini alamazsınız. Aşağıdaki bölümlerde bu tuzaklardan nasıl kaçınacağımızı göreceğiz.

    Katman Sırasını Önbellek İçin Optimize Etmek#

    En sık yapılan hata, bağımlılık kurulumundan önce tüm kaynak kodu kopyalamaktır. Şu iki örneği karşılaştıralım. Önce kötü sürüm:

    FROM node:20-slim
    WORKDIR /app
    COPY . .                 # Her kod değişikliğinde bu katman kaçar
    RUN npm install          # ...bu yüzden npm install de her seferinde çalışır
    CMD ["node", "server.js"]
    

    Burada COPY . . bütün projeyi kopyaladığı için, server.js içinde tek bir karakteri değiştirseniz bile o katmanın önbelleği kaçar ve hemen ardından gelen npm install da sıfırdan çalışır. Oysa bağımlılıklarınız haftalarca değişmemiş olabilir. Doğru sürümde bağımlılık tanımını ayrı kopyalarız:

    FROM node:20-slim
    WORKDIR /app
    COPY package.json package-lock.json ./   # Sadece bağımlılık tanımı
    RUN npm ci                               # package-lock değişmedikçe önbellekten gelir
    COPY . .                                 # Kod değişse bile npm ci tekrar çalışmaz
    CMD ["node", "server.js"]
    

    Bu düzende yalnızca package.json veya package-lock.json değiştiğinde npm ci yeniden çalışır. Günlük kod değişikliklerinizde bağımlılık katmanı önbellekten gelir ve derleme saniyeler sürer. Aynı ilke her dilde geçerlidir: Python'da requirements.txt, PHP'de composer.json, Go'da go.mod ve go.sum önce kopyalanır.

    Bir diğer önemli pratik, RUN komutlarını mantıklı biçimde birleştirmek ve aynı katmanda temizlik yapmaktır. Paket yöneticisinin bıraktığı önbellek dosyaları ayrı bir RUN ile silinirse imaj küçülmez; çünkü dosyalar bir önceki katmanda zaten yazılmıştır. Doğrusu şudur:

    RUN apt-get update && apt-get install -y --no-install-recommends \
            curl \
            ca-certificates \
        && rm -rf /var/lib/apt/lists/*
    

    --no-install-recommends önerilen ama zorunlu olmayan paketleri atlar; rm -rf /var/lib/apt/lists/* aynı katmanda paket indeksini temizler. İkisi birden imaj boyutunu gözle görülür şekilde düşürür.

    Çok Aşamalı Derleme ile Son İmajı Küçültmek#

    Bir uygulamayı derlemek için gereken araçlar (derleyiciler, node_modules dev bağımlılıkları, build zinciri) ile onu çalıştırmak için gerekenler tamamen farklıdır. Tek aşamalı bir Dockerfile, derleme araçlarını da son imaja taşır; sonuç, içinde asla kullanılmayacak yüzlerce megabaytlık araç barındıran şişkin bir imajdır. Çok aşamalı derleme (multi-stage build) tam bu sorunu çözer: bir aşamada derlersiniz, ürünü ikinci temiz aşamaya kopyalarsınız, derleme aşamasını çöpe atarsınız.

    Bir Go uygulaması için klasik örnek:

    # 1. Aşama: derleme
    FROM golang:1.22 AS builder
    WORKDIR /src
    COPY go.mod go.sum ./
    RUN go mod download
    COPY . .
    RUN CGO_ENABLED=0 go build -o /bin/app ./cmd/server
    
    # 2. Aşama: çalıştırma (sadece ikili dosya)
    FROM gcc:distroless AS runtime
    COPY --from=builder /bin/app /app
    USER nonroot:nonroot
    ENTRYPOINT ["/app"]
    

    Burada golang:1.22 imajı 800 MB'ın üzerindeyken, son imaj yalnızca statik ikili dosyayı ve çalışma zamanı temelini içerir; çoğu zaman 20-30 MB seviyesine iner. COPY --from=builder ifadesi, birinci aşamada üretilen çıktıyı ikinci aşamaya taşıyan sihirli satırdır. Node.js için de mantık aynıdır: builder aşamasında npm ci && npm run build çalıştırır, üretim aşamasına yalnızca dist/ klasörünü ve npm ci --omit=dev ile kurulmuş üretim bağımlılıklarını taşırsınız.

    Aşağıdaki tablo tek aşamalı ve çok aşamalı yaklaşımın pratik farkını özetler:

    ÖlçütTek aşamalıÇok aşamalı
    Son imaj boyutuBüyük (derleme araçları dahil)Küçük (sadece çalışma zamanı)
    Saldırı yüzeyiGeniş (derleyici, shell, paketler)Dar (asgari dosya)
    Çekme (pull) süresiUzunKısa
    Gizli bilgi sızmasıAra dosyalar imajda kalırAra aşama atılır

    Çok aşamalı derlemenin gizli bir güvenlik faydası da vardır: derleme sırasında kullandığınız özel npm token'ı, SSH anahtarı veya .git geçmişi yalnızca builder aşamasında kalır, son imaja hiç geçmez.

    Doğru Temel İmajı Seçmek#

    Temel imaj (base image) seçimi, hem boyut hem güvenlik açısından belki de en etkili tek karardır. node:20 gibi tam sürümler tüm bir Debian dağıtımını içerir ve kolayca 1 GB'a yaklaşır; oysa node:20-slim gereksiz paketleri atarak bunu birkaç yüz megabayta indirir, node:20-alpine ise musl libc tabanlı minik Alpine Linux üzerinde çoğu zaman 50-60 MB'a kadar iner.

    Etiket türüYaklaşık tabanKullanım durumu
    - (tam)BüyükHızlı prototip, exotik sistem bağımlılıkları
    slimOrtaÇoğu üretim uygulaması için dengeli seçim
    alpineKüçükBoyutun kritik olduğu, saf uygulamalar
    distrolessEn küçükDerlenmiş ikili + asgari saldırı yüzeyi

    Alpine cazip görünse de bir uyarı gerekir: musl libc, glibc'ye alışkın bazı kütüphanelerle (özellikle Python'ın derlenmiş wheel'leri veya belirli native modüller) uyumsuzluk çıkarabilir; bu durumda gizli derleme hataları veya çalışma zamanı sorunlarıyla uğraşırsınız. Genel bir tavsiye olarak: çoğu üretim yükü için slim sağlam ve dengeli bir başlangıçtır. Boyut gerçekten kritikse ve uyumluluğu test ettiyseniz alpine'a geçin. Google'ın distroless imajları ise shell bile içermez; bu, bir saldırganın konteyner içinde komut çalıştırmasını çok zorlaştırır ama hata ayıklamayı da zorlaştıracağını unutmayın.

    Son ve en önemlisi: latest etiketi kullanmayın. FROM node:latest bugün 20 sürümünü çekerken üç ay sonra 22'yi çeker ve derlemeniz sessizce bozulur. Her zaman sabit bir sürüm sabitleyin (node:20.11-slim gibi); mümkünse imajı sha256 özetiyle pinleyin (node:20-slim@sha256:...). Bu, tekrarlanabilir derlemenin (reproducible build) temel şartıdır ve tıpkı Git ile sürüm kontrolünde commit'leri sabitlemek gibi, "benim makinemde çalışıyordu" sorununu ortadan kaldırır.

    .dockerignore ile Derleme Bağlamını Küçültmek#

    docker build komutunu çalıştırdığınızda Docker, öncelikle o dizindeki tüm bağlamı (build context) daemon'a gönderir. Projenizin kökünde .git geçmişi, node_modules, log dosyaları, yerel .env dosyaları varsa bunların hepsi ağ üzerinden gönderilir; bu hem yavaştır hem de tehlikelidir. .env içindeki bir veritabanı parolası yanlışlıkla imaja kopyalanabilir. .dockerignore dosyası tam olarak .gitignore gibi çalışır ve bu dosyaları bağlamdan çıkarır:

    # .dockerignore
    .git
    node_modules
    npm-debug.log
    Dockerfile
    .dockerignore
    .env
    .env.*
    *.log
    dist
    coverage
    .vscode
    .idea
    README.md
    

    node_modules'ı yok saymak özellikle önemlidir: konteyner içinde bağımlılıkları zaten npm ci ile temiz kuracaksınız, yerel makinenizdeki (belki farklı işletim sistemine derlenmiş) node_modules'ı taşımanın hiçbir anlamı yoktur; üstelik COPY . . ile bunu imaja sokarsanız bozuk native modüllerle karşılaşabilirsiniz. .env ve .env.* kalıplarını mutlaka ekleyin; sırların (secret) imaja sızmasının en yaygın yolu, unutulan bir .env dosyasının COPY . . ile taşınmasıdır.

    İyi bir .dockerignore, hem derleme bağlamını küçülterek derlemeyi hızlandırır hem de önbellek isabetini artırır: bağlama gereksiz dosyalar dahil edilmediğinde, alakasız bir log dosyasının değişmesi COPY . . katmanının önbelleğini kaçırmaz.

    COPY, ADD ve Root Olmayan Kullanıcı ile Sertleştirme#

    COPY ve ADD sık karıştırılır. Kural basittir: neredeyse her zaman COPY kullanın. İkisi de yerel dosyaları imaja kopyalar, ancak ADD bunun üzerine iki "sihirli" davranış ekler: bir URL'den dosya indirebilir ve yerel tar arşivlerini otomatik açar. Bu gizli davranışlar, Dockerfile'ı okuyanı yanıltır ve bir URL'den indirme durumunda güvenlik riski yaratır. Bir arşivi açmak istiyorsanız bunu açıkça RUN tar ile yapmak daha okunaklıdır. Uzaktan dosya indirmek için RUN curl kullanın; böylece aynı katmanda sağlama doğrulaması ve temizlik de yapabilirsiniz.

    DurumÖneri
    Yerel dosya/dizin kopyalamaCOPY
    URL'den indirmeRUN curl (doğrulama + temizlik ile)
    Yerel tar açmaRUN tar (niyet açık olsun)
    Çok aşamalı transferCOPY --from=aşama

    Gelelim en çok atlanan güvenlik pratiğine: root olmayan kullanıcı. Varsayılan olarak konteyner içindeki süreçler root (UID 0) olarak çalışır. Bir güvenlik açığı yüzünden saldırgan konteynere sızarsa, root yetkileriyle çok daha fazla zarar verebilir; çekirdek zafiyetleriyle konteynerden kaçış (container escape) riski de artar. Çözüm, uygulamayı ayrılmış, yetkisiz bir kullanıcıyla çalıştırmaktır:

    FROM node:20-slim
    WORKDIR /app
    
    COPY package.json package-lock.json ./
    RUN npm ci --omit=dev
    
    COPY . .
    
    # Yetkisiz bir kullanıcı oluştur ve dosya sahipliğini ona ver
    RUN groupadd --system --gid 1001 nodejs \
        && useradd --system --uid 1001 --gid nodejs appuser \
        && chown -R appuser:nodejs /app
    
    USER appuser
    
    EXPOSE 3000
    CMD ["node", "server.js"]
    

    USER appuser satırından sonraki tüm komutlar ve konteyner başladığında çalışan süreç, root değil bu yetkisiz kullanıcı olarak koşar. Dikkat edilmesi gereken nokta: kullanıcı, yazması gereken dizinlerin sahibi olmalıdır; bu yüzden chown ile /app sahipliğini devrederiz. 1024'ün altındaki portları (80, 443 gibi) yetkisiz kullanıcı dinleyemez; bu yüzden uygulamayı 3000 veya 8080 gibi yüksek bir portta çalıştırıp, dışarıya açmayı ters vekil sunucuya (reverse proxy) bırakmak en temiz yaklaşımdır.

    Birkaç ek sertleştirme önerisi: gerçekten gerekmiyorsa RUN içinde sudo kurmayın; sırları ARG/ENV yerine BuildKit'in --mount=type=secret özelliğiyle geçirin (çünkü ENV ile verilen sır imaj katmanlarında ve geçmişinde kalır); HEALTHCHECK ekleyerek orkestrasyon katmanının konteynerin sağlığını izleyebilmesini sağlayın. Ürettiğiniz imajları docker scout veya trivy gibi araçlarla düzenli olarak tarayıp bilinen zafiyetleri kontrol edin.

    Bir Araya Getirmek: Tam Örnek Dockerfile#

    Yukarıdaki tüm pratikleri tek bir Node.js üretim imajında birleştirelim. Bu Dockerfile çok aşamalı derleme, önbellek dostu katman sırası, slim temel imaj, sabit sürüm etiketi ve root'suz kullanıcının hepsini içerir:

    # ---- Derleme aşaması ----
    FROM node:20.11-slim AS builder
    WORKDIR /app
    COPY package.json package-lock.json ./
    RUN npm ci
    COPY . .
    RUN npm run build
    
    # ---- Üretim aşaması ----
    FROM node:20.11-slim AS runtime
    ENV NODE_ENV=production
    WORKDIR /app
    
    COPY package.json package-lock.json ./
    RUN npm ci --omit=dev && npm cache clean --force
    
    # Sadece derlenmiş çıktıyı önceki aşamadan al
    COPY --from=builder /app/dist ./dist
    
    RUN groupadd --system --gid 1001 nodejs \
        && useradd --system --uid 1001 --gid nodejs appuser \
        && chown -R appuser:nodejs /app
    USER appuser
    
    EXPOSE 3000
    HEALTHCHECK --interval=30s --timeout=3s \
        CMD node -e "require('http').get('http://localhost:3000/health', r => process.exit(r.statusCode === 200 ? 0 : 1))"
    CMD ["node", "dist/server.js"]
    

    Bu şablon, çoğu web uygulaması için sağlam bir başlangıç noktasıdır; dilinize göre paket yöneticisi komutlarını değiştirmeniz yeterlidir. Bu imajı bir sunucuda çalıştırmak için root yetkisi ve Docker kurulu bir ortama ihtiyacınız olur; paylaşımlı hostingler bunu vermez, bu yüzden konteyner yükleri için sanal ya da fiziksel sunucu tercih etmelisiniz.

    Sıkça Sorulan Sorular#

    Alpine mı slim mi kullanmalıyım?#

    Çoğu üretim uygulaması için slim daha güvenli bir varsayılandır çünkü glibc tabanlıdır ve uyumluluk sorunları çıkarma olasılığı düşüktür. Alpine imajları çok daha küçüktür ancak musl libc kullandığı için özellikle Python native modülleri veya belirli C kütüphaneleriyle beklenmedik hatalar verebilir. Boyut sizin için kritikse ve uygulamanızı Alpine üzerinde test ettiyseniz alpine'a geçebilirsiniz; aksi halde slim ile başlayın.

    Çok aşamalı derleme her projede gerekli mi?#

    Derleme adımı gerektiren her projede kesinlikle faydalıdır. Go, Rust, Java, TypeScript veya webpack/vite ile paketlenen ön yüz projeleri gibi bir "build" aşaması olan her uygulamada son imajı dramatik biçimde küçültür ve derleme araçlarını dışarıda bırakır. Yalnızca yorumlanan ve derleme adımı olmayan basit bir betik çalıştırıyorsanız (örneğin tek dosyalık bir Python scripti) tek aşama yeterli olabilir; ama çoğu gerçek uygulamada çok aşamalı derleme standart pratiktir.

    RUN komutlarını neden tek satırda birleştiriyoruz?#

    Her RUN komutu ayrı bir katman oluşturduğu için, bir katmanda oluşturulan geçici dosyayı sonraki bir katmanda silmek imajı küçültmez; dosya alttaki katmanda hâlâ durur. Bu yüzden paket kurulumu ve ardından gelen temizliği (rm -rf /var/lib/apt/lists/* gibi) && ile aynı RUN içinde yaparız. Böylece geçici dosyalar hiçbir zaman kalıcı bir katmana yazılmamış olur ve son imaj gerçekten küçülür.

    Neden latest etiketini kullanmamalıyım?#

    latest etiketi zamanla farklı sürümlere işaret eder; bugün derlediğiniz imaj ile üç ay sonra derlediğiniz imaj sessizce farklı olabilir ve derlemeniz beklenmedik biçimde bozulabilir. Tekrarlanabilir ve öngörülebilir derlemeler için her zaman node:20.11-slim gibi sabit bir sürüm belirtin, hatta kritik sistemlerde imajı sha256 özetiyle pinleyin. Bu, ekibinizdeki herkesin ve üretim sunucunuzun tam olarak aynı temeli kullanmasını garanti eder.

    Konteynerimi neden root olmayan kullanıcıyla çalıştırmalıyım?#

    Varsayılan olarak konteyner süreçleri root olarak çalışır; bir güvenlik açığı sömürüldüğünde saldırgan bu yetkiyle çok daha fazla zarar verebilir ve konteynerden ana makineye kaçış riski artar. Dockerfile'da yetkisiz bir kullanıcı oluşturup USER ile geçiş yaparak, olası bir ihlalin etki alanını daraltırsınız. Bu, düşük maliyetli ama etkisi yüksek bir güvenlik önlemidir ve üretime çıkan her imajda uygulanması önerilir.

    .dockerignore dosyası olmazsa ne olur?#

    .dockerignore yoksa Docker, derleme dizinindeki her şeyi (örneğin .git geçmişi, node_modules, .env dosyaları, loglar) derleme bağlamı olarak daemon'a gönderir. Bu, derlemeyi yavaşlatmasının yanında ciddi bir güvenlik riski taşır: bir .env dosyası içindeki veritabanı parolası COPY . . ile imaja sızabilir. İyi bir .dockerignore, hem derlemeyi hızlandırır hem de önbellek isabetini artırır hem de sırlarınızı imajın dışında tutar.

    Kapanış#

    Dockerfile yazmak yüzeyde birkaç komut ekleme işi gibi görünür, ama gördüğümüz gibi katman sırası, çok aşamalı derleme, doğru temel imaj, .dockerignore ve root'suz kullanıcı gibi birkaç bilinçli karar; imajınızı hem çok daha küçük, hem çok daha hızlı, hem de çok daha güvenli hale getirir. Bu pratikler tek seferlik bir çaba değil, üretime çıkan her imajda tekrarlayacağınız bir disiplindir; bir kez alışkanlık haline geldiğinde ise hem CI/CD süreçleriniz hızlanır hem de güvenlik ekibiniz rahat eder.

    Konteyner yüklerinizi çalıştıracak sağlam bir altyapı arıyorsanız, tam yetkili (root) erişim ve izole kaynak sunan VDS sunucularımız ile bulut sunucularımız Docker tabanlı üretim ortamları için idealdir. Sunucu yönetimiyle uğraşmak istemiyorsanız sunucu yönetimi hizmetimiz ile kurulum, güncelleme ve güvenlik sertleştirmesini sizin adınıza üstlenebiliriz; verilerinizi güvende tutmak için de yedekleme çözümlerimize göz atabilirsiniz. Docker ekosistemini daha iyi kavramak için Docker nedir başlangıç rehberimizle devam edebilir, diğer teknik yazılarımız için Bilgi Merkezi araçlarımızı inceleyebilirsiniz.

    DockerDockerfileKonteyner

    Uygulamaya geçmeye hazır mısınız?

    NVMe SSD, ücretsiz SSL ve %99.9 uptime garantisiyle Clou.TR hosting ve sunucu çözümleriyle projenizi hayata geçirin.