Konteyner dünyasına adım attığınızda, Docker Hub’daki hazır imajlarla başlamak gayet mantıklı. Ama zamanla fark ediyorsunuz ki bu imajlar ya çok şişman, ya eksik konfigürasyonlu ya da üretim ortamınıza tam uymayan şeyler barındırıyor. İşte tam bu noktada Dockerfile yazımını öğrenmek bir lüks değil, zorunluluk haline geliyor. Kendi imajınızı yazmak size hem kontrol hem de tekrarlanabilirlik kazandırıyor.
Dockerfile Nedir ve Neden Önemlidir?
Dockerfile, bir Docker imajının nasıl oluşturulacağını tanımlayan metin tabanlı bir betik dosyasıdır. Satır satır talimatlar içerir ve her talimat imaj üzerinde bir katman (layer) oluşturur. Bu katmanlı mimari sayesinde Docker, değişmeyen katmanları önbelleğe alır ve sonraki build işlemleri çok daha hızlı tamamlanır.
Bir sysadmin olarak Dockerfile’ın size sağladığı avantajlar şunlardır:
- Tekrarlanabilirlik: Aynı Dockerfile her ortamda aynı imajı üretir
- Versiyon kontrolü: Git ile takip edilebilir, değişiklikler izlenebilir
- Şeffaflık: İmajın içinde ne olduğunu tam olarak bilirsiniz
- Güvenlik: Gereksiz paketleri dışarıda bırakarak saldırı yüzeyini küçültürsünüz
- Otomasyon: CI/CD pipeline’larına entegre edilebilir
Temel Dockerfile Talimatları
Önce yapı taşlarını tanıyalım. Her talimatın ne işe yaradığını bilmeden iyi bir Dockerfile yazamazsınız.
FROM: Temel imajı belirtir, her Dockerfile bu ile başlar RUN: İmaj oluşturma sırasında komut çalıştırır CMD: Konteyner başlatıldığında varsayılan komutu belirler ENTRYPOINT: Konteynerin ana sürecini tanımlar COPY: Dosyaları host’tan imaja kopyalar ADD: COPY gibi çalışır ama URL ve tar dosyalarını da destekler ENV: Ortam değişkeni tanımlar ARG: Build zamanı argümanı tanımlar WORKDIR: Çalışma dizinini ayarlar EXPOSE: Hangi portun dinleneceğini belgeler VOLUME: Mount noktası tanımlar USER: Sonraki komutların hangi kullanıcıyla çalışacağını belirler LABEL: İmaja metadata ekler HEALTHCHECK: Konteynerin sağlık durumunu kontrol eden komutu tanımlar
İlk Dockerfile’ınızı Yazalım
Basit bir Python Flask uygulaması için Dockerfile yazalım. Bu senaryo, gerçek dünyada en sık karşılaştığınız durumlardan biridir.
# Proje yapısı
my-flask-app/
├── app.py
├── requirements.txt
└── Dockerfile
# app.py içeriği
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return "Merhaba, Docker!"
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
Şimdi Dockerfile’ı yazalım:
# Temel imaj olarak resmi Python slim varyantını kullanıyoruz
FROM python:3.11-slim
# İmaj hakkında metadata
LABEL maintainer="[email protected]"
LABEL version="1.0"
LABEL description="Flask uygulama imaji"
# Ortam değişkenlerini ayarlayalım
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV FLASK_ENV=production
# Çalışma dizini oluştur ve ayarla
WORKDIR /app
# Bağımlılık dosyasını önce kopyala (cache optimizasyonu için)
COPY requirements.txt .
# Bağımlılıkları yükle
RUN pip install --no-cache-dir -r requirements.txt
# Uygulama kodunu kopyala
COPY . .
# Güvenlik için root olmayan kullanıcı oluştur
RUN adduser --disabled-password --gecos '' appuser
USER appuser
# Port belgele
EXPOSE 5000
# Konteynerin sağlık durumunu kontrol et
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3
CMD curl -f http://localhost:5000/ || exit 1
# Uygulamayı başlat
CMD ["python", "app.py"]
Bu Dockerfile’ı build edip çalıştıralım:
# Build et
docker build -t flask-uygulama:1.0 .
# Çalıştır
docker run -d -p 5000:5000 --name flask-app flask-uygulama:1.0
# Log kontrolü
docker logs flask-app
# Sağlık durumu kontrolü
docker inspect --format='{{.State.Health.Status}}' flask-app
Katman Optimizasyonu: Cache’i Akıllıca Kullanmak
Docker’ın en güçlü özelliklerinden biri katman önbelleklemedir. Ama bu mekanizmayı anlamadan yazılan Dockerfile’lar her build’de sıfırdan çalışır, bu da CI/CD süreçlerini yavaşlatır.
Kural basittir: Az değişen şeyler üste, çok değişen şeyler alta. Uygulama kodunuz sık değişir ama bağımlılıklarınız nispeten sabit kalır. Bu yüzden önce bağımlılık dosyasını kopyalayıp yükleyin, sonra kaynak kodu kopyalayın.
Kötü örnek:
# YANLIŞ: Her kod değişikliğinde pip install tekrar çalışır
FROM python:3.11-slim
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["python", "app.py"]
Doğru örnek:
# DOGRU: requirements.txt değişmediği sürece pip install cache'den gelir
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]
Node.js projelerinde de aynı mantık geçerlidir:
FROM node:18-alpine
WORKDIR /usr/src/app
# package.json ve lock dosyasını önce kopyala
COPY package*.json ./
# Bağımlılıkları yükle (bu katman cache'lenecek)
RUN npm ci --only=production
# Kaynak kodu kopyala
COPY src/ ./src/
# Düşük yetkili kullanıcıyla çalıştır
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
EXPOSE 3000
CMD ["node", "src/server.js"]
Multi-Stage Build: İmaj Boyutunu Küçültmek
Üretim ortamında imaj boyutu kritik önem taşır. Büyük imajlar hem güvenlik riski hem de dağıtım yavaşlığı demektir. Multi-stage build bu sorunu zarif biçimde çözer.
Go uygulaması için klasik bir örnek:
# --- Build Stage ---
FROM golang:1.21-alpine AS builder
WORKDIR /build
# Bağımlılıkları indir
COPY go.mod go.sum ./
RUN go mod download
# Kaynak kodu kopyala ve derle
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
# --- Production Stage ---
# Sadece binary'yi çalıştırmak için minimal bir imaj
FROM scratch
# SSL sertifikaları gerekiyorsa ekle
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# Sadece binary'yi kopyala
COPY --from=builder /build/main /app/main
EXPOSE 8080
ENTRYPOINT ["/app/main"]
Bu yaklaşımla golang:1.21-alpine imajının 300+ MB’ı yerine sadece binary boyutunda (genellikle 5-15 MB) bir imaj elde edersiniz. Fark inanılmaz büyük.
Java/Maven projelerinde de aynı strateji işe yarar:
# Build Stage
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn package -DskipTests
# Production Stage
FROM eclipse-temurin:17-jre-alpine
RUN addgroup -S javagroup && adduser -S javauser -G javagroup
WORKDIR /app
COPY --from=builder /build/target/*.jar app.jar
RUN chown javauser:javagroup app.jar
USER javauser
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s
CMD wget -q --spider http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", "-jar", "app.jar"]
ARG ve ENV: Build Zamanı ve Çalışma Zamanı Değişkenleri
ARG ve ENV arasındaki farkı kavramak önemlidir. ARG sadece build sırasında geçerlidir, ENV ise çalışan konteynerde de erişilebilirdir.
FROM ubuntu:22.04
# Build argümanı - sadece build sırasında kullanılır
ARG APP_VERSION=1.0.0
ARG BUILD_DATE
# Ortam değişkeni - çalışan konteynerde de erişilebilir
ENV APP_VERSION=${APP_VERSION}
ENV LOG_LEVEL=info
ENV PORT=8080
# ARG ile build metadata'sı label'a ekle
LABEL build_date=${BUILD_DATE}
LABEL app_version=${APP_VERSION}
WORKDIR /app
COPY . .
RUN echo "Versiyon ${APP_VERSION} build ediliyor"
EXPOSE ${PORT}
CMD ["./start.sh"]
Build esnasında argüman geçmek:
docker build
--build-arg APP_VERSION=2.1.0
--build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
-t uygulamam:2.1.0 .
Önemli uyarı: Şifreler ve API anahtarları için ARG veya ENV kullanmayın. Bu değerler imaj katmanlarında görünür kalır. Secret yönetimi için Docker secrets veya runtime’da environment injection kullanın.
.dockerignore Dosyası: Görmezden Gelme Listesi
Dockerfile yazarken çoğu kişinin atlattığı ama kritik olan konu .dockerignore dosyasıdır. Bu dosya olmadan build context’inize gereksiz her şey gönderilir, bu da build süresini uzatır ve imaja istemediğiniz dosyaların girmesine yol açar.
# .dockerignore dosyası
# Git ve versiyon kontrol
.git
.gitignore
.gitattributes
# Docker'ın kendi dosyaları
Dockerfile
.dockerignore
docker-compose*.yml
# Bağımlılık dizinleri (imaj içinde yeniden oluşturulacak)
node_modules/
vendor/
__pycache__/
*.pyc
*.pyo
# Test dosyaları
tests/
test/
*.test.js
coverage/
# IDE ve editör dosyaları
.vscode/
.idea/
*.swp
*.swo
# Log dosyaları
*.log
logs/
# Ortam değişkeni dosyaları (güvenlik!)
.env
.env.*
*.pem
*.key
# CI/CD
.github/
.gitlab-ci.yml
Jenkinsfile
# Dokümantasyon
README.md
docs/
Güvenlik Odaklı Dockerfile Yazımı
Güvenlik sonradan düşünülen bir şey olmamalı. Dockerfile yazarken güvenliği baştan inşa edin.
Root olmayan kullanıcı kullanımı her Dockerfile’da olmalıdır:
FROM nginx:1.25-alpine
# Nginx konfigürasyonu
COPY nginx.conf /etc/nginx/nginx.conf
COPY html/ /usr/share/nginx/html/
# nginx kullanıcısına gerekli izinleri ver
RUN chown -R nginx:nginx /usr/share/nginx/html &&
chown -R nginx:nginx /var/cache/nginx &&
chown -R nginx:nginx /var/log/nginx &&
touch /var/run/nginx.pid &&
chown nginx:nginx /var/run/nginx.pid
# Root olmayan kullanıcıya geç
USER nginx
EXPOSE 8080
CMD ["nginx", "-g", "daemon off;"]
Güvenli bir Dockerfile için genel kurallar:
- En az yetki prensibi: Uygulamayı root ile çalıştırmayın
- Minimal temel imaj: Alpine veya slim varyantlarını tercih edin, gereksiz araçlar yok
- Paket güncelleme: RUN sırasında apt-get update ve upgrade birlikte yapın
- Geçici dosyaları temizle: Cache ve geçici dosyaları aynı RUN katmanında silin
- COPY tercih edin, ADD’den kaçının: ADD’nin URL çekme özelliği güvenlik riski taşır
FROM debian:12-slim
# Paketleri güncelle ve yükle, tek RUN katmanında temizle
RUN apt-get update &&
apt-get upgrade -y &&
apt-get install -y --no-install-recommends
curl
ca-certificates &&
apt-get clean &&
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Uygulama kullanıcısı oluştur
RUN groupadd -r appgroup &&
useradd -r -g appgroup -d /app -s /sbin/nologin appuser
WORKDIR /app
COPY --chown=appuser:appgroup . .
USER appuser
EXPOSE 8080
ENTRYPOINT ["./entrypoint.sh"]
ENTRYPOINT ve CMD Arasındaki Fark
Bu ikisi sık karıştırılır ve yanlış kullanım ciddi sorunlara yol açabilir.
ENTRYPOINT: Konteynerin ana süreci, override edilmesi zordur (--entrypoint flag gerekir) CMD: Varsayılan argümanlar, docker run sonrasında kolayca override edilebilir
İkisini birlikte kullanmak en esnek yaklaşımdır:
FROM alpine:3.18
RUN apk add --no-cache curl
# ENTRYPOINT sabit komut, CMD varsayılan argüman
ENTRYPOINT ["curl"]
CMD ["--help"]
Bu şekilde:
docker run curl-imagekomutucurl --helpçalıştırırdocker run curl-image https://api.sirketim.comkomutucurl https://api.sirketim.comçalıştırır
Gerçek dünyada ise genellikle bir entrypoint script kullanılır:
FROM postgres:15-alpine
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["postgres"]
Gerçek Dünya Senaryosu: Nginx + PHP-FPM İmajı
Birden fazla servisi tek imajda çalıştırmak anti-pattern kabul edilse de bazı legacy uygulamalar için gerekli olabilir. Supervisor kullanarak bunu düzgünce yapalım:
FROM ubuntu:22.04
ENV DEBIAN_FRONTEND=noninteractive
# Gerekli paketleri yükle
RUN apt-get update &&
apt-get install -y --no-install-recommends
nginx
php8.1-fpm
php8.1-mysql
php8.1-curl
php8.1-mbstring
supervisor
curl &&
apt-get clean &&
rm -rf /var/lib/apt/lists/*
# Konfigürasyon dosyalarını kopyala
COPY config/nginx.conf /etc/nginx/nginx.conf
COPY config/php-fpm.conf /etc/php/8.1/fpm/php-fpm.conf
COPY config/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
# Uygulama kodunu kopyala
COPY --chown=www-data:www-data app/ /var/www/html/
# Gerekli dizinleri oluştur
RUN mkdir -p /run/php &&
chown www-data:www-data /run/php
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s
CMD curl -f http://localhost/health.php || exit 1
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
Build ve Debug İpuçları
Dockerfile geliştirirken işinizi kolaylaştıracak pratik komutlar:
# Build history ile katmanları incele
docker history flask-uygulama:1.0
# Belirli bir build aşamasında dur (multi-stage debugging)
docker build --target builder -t debug-imaj .
# Build context boyutunu kontrol et
du -sh .
# Çalışan konteynerin içine bak
docker run -it --rm flask-uygulama:1.0 /bin/sh
# İmaj içeriğini dive aracıyla incele
docker run --rm -it
-v /var/run/docker.sock:/var/run/docker.sock
wagoodman/dive:latest flask-uygulama:1.0
# Güvenlik taraması için trivy kullan
docker run --rm
-v /var/run/docker.sock:/var/run/docker.sock
aquasec/trivy:latest image flask-uygulama:1.0
Build loglarını daha ayrıntılı görmek için:
# BuildKit ile detaylı çıktı
DOCKER_BUILDKIT=1 docker build --progress=plain -t uygulamam:latest .
# Önbelleği temizleyerek sıfırdan build
docker build --no-cache -t uygulamam:latest .
# Belirli bir cache kaynağı kullan
docker build --cache-from uygulamam:latest -t uygulamam:new .
Sonuç
Dockerfile yazmak başlangıçta basit görünse de üretim kalitesinde imajlar oluşturmak gerçekten bir sanat. Katman optimizasyonundan güvenlik pratiklerine, multi-stage build’den sağlık kontrollerine kadar her detay önemli.
Bu yazıda öğrendiklerinizi özetleyecek olursak: temel imaj seçiminizde slim ve alpine varyantlarını tercih edin, katman sıralamasını cache mekanizmasını göz önünde bulundurarak yapın, root olmayan kullanıcı kullanımını ihmal etmeyin, .dockerignore dosyasını her projede oluşturun ve multi-stage build ile imaj boyutlarınızı dramatik biçimde küçültün.
Gerçek projelerinizde bu prensipleri uygulamaya başladığınızda farkı hemen göreceksiniz. Build süreleri kısalacak, imaj boyutları küçülecek ve güvenlik duruşunuz güçlenecek. Dockerfile’larınızı kod gibi ele alın, versiyon kontolüne ekleyin, code review sürecine dahil edin. Konteyner altyapınızın kalitesi büyük ölçüde burada şekilleniyor.