Dockerfile Oluşturucu
Node, Python, Go veya Java için çok aşamalı Dockerfile üretin. Root olmayan kullanıcı, katman önbelleği ve uyumlu bir .dockerignore ile birlikte.
Örneğin 20-alpine ya da 3.12-slim. Boş bırakılırsa derleme adımı üretilmez.
Manifest dosyaları kaynak kodundan önce kopyalanır, böylece tek satırlık bir değişiklik tüm bağımlılıkları yeniden kurmaz. Çok aşamalı derlemede son imajta derleyici ve derleme önbelleği yer almaz.
Özet (TL;DR)
- Kaynak dosyalar bağımlılık kurulumundan önce kopyalanırsa tek satırlık bir değişiklik bütün bağımlılık ağacını yeniden kurdurur; araç manifest dosyalarını her zaman önce kopyalar.
- Çok aşamalı derlemede son imaja derleyici, build önbelleği ve kaynak ağacı girmez; Go için runtime tabanı
alpine:3.20, Java içineclipse-temurin:21-jreolur. - Container varsayılan olarak uid 0 ile çalışır. Araç node imajındaki hazır
nodekullanıcısını kullanır, diğerlerinde uid 1001 ile gerçek bir hesap açar. - .dockerignore yoksa
node_modulesve.gitbuild context'ine girer, bu da hem yavaşlatır hem de katman önbelleğini bozar.
1.2 GB imaj, 90 MB olması gerekirken
Docker'da ilk çalışan Dockerfile'ı yazmak kolay: FROM, COPY . ., RUN npm install, CMD. Sorun docker images çıktısına baktığınızda başlıyor. Küçük bir Node servisi 1.2 GB yer kaplıyor ve içinde derleyici, npm önbelleği, test dosyaları, .git klasörü ve iki kopya node_modules var. Aynı servis doğru yazılmış bir Dockerfile ile 90 MB civarında duruyor. Fark, imajda ne olduğu değil, imajda gereksiz olarak ne kaldığı.
Katman önbelleği ve kopyalama sırası
Dockerfile'daki her komut bir katman üretiyor ve Docker bir katmanı, girdileri değişmediği sürece yeniden çalıştırmıyor. Bir katman geçersiz olduğunda ise ondan sonraki her katman da geçersiz oluyor. Bu iki cümle, aşağıdaki iki dosya arasındaki farkı açıklıyor.
# YANLIŞ: kaynak önce kopyalanıyorFROM node:20-alpineWORKDIR /appCOPY . .RUN npm ci # her kod değişikliğinde baştan çalışırCMD ["node", "dist/index.js"] # DOĞRU: manifest önceFROM node:20-alpineWORKDIR /appCOPY package.json package-lock.json* ./RUN npm ci # yalnızca lock dosyası değişince çalışırCOPY . .CMD ["node", "dist/index.js"]İlk dosyada tek bir satır düzelttiğinizde COPY . . katmanı değişiyor, dolayısıyla npm ci de yeniden koşuyor ve build süresi bağımlılık sayısına bağlı olarak dakikalara çıkıyor. İkincisinde aynı düzeltme yalnızca son katmanı etkiliyor, build birkaç saniyede bitiyor. Araç lock dosyalarını sondaki yıldızla kopyalıyor, çünkü COPY var olmayan bir yol için hata veriyor ve hiçbir projede üç lock dosyasının üçü birden bulunmuyor.
Çok aşamalı derleme
Çok aşamalı yapıda üç FROM var: bağımlılıkları çözen deps, kaynakları derleyen build ve yalnızca çalıştırmaya yarayan runtime. Son imaja sadece runtime aşamasında kalan şeyler giriyor, öncekiler build makinesinde kalıyor. Kazanç en çok derleyici gerektiren dillerde görünüyor.
| Çalışma ortamı | Build tabanı | Runtime tabanı | Son imajda olmayan |
|---|---|---|---|
| Node | node:20-alpine | node:20-alpine | Kaynak ağacı, build önbelleği |
| Go | golang:... | alpine:3.20 | Go toolchain'in tamamı |
| Java | maven:... | eclipse-temurin:21-jre | JDK ve Maven |
| Python | python:... | python:... | Derleme sırasında gereken build araçları |
| PHP | php:... | php:... | Composer dev bağımlılıkları |
Go'da fark en dramatik olanı: statik derlenmiş bir binary'nin çalışması için toolchain gerekmiyor, dolayısıyla 800 MB'lık golang imajı yerine 8 MB'lık alpine yetiyor. Java tarafında JDK ile JRE ayrımı aynı işi görüyor.
Root ile çalışmak ve .dockerignore
Container aksi söylenmedikçe uid 0 ile, yani root olarak çalışıyor. Bu, uygulama içindeki bir açığın doğrudan container içinde root yetkisi anlamına gelmesi demek ve bir container kaçışıyla birleştiğinde host tarafında gerçek bir sorun. Araç nonRoot seçildiğinde işi yarım bırakmıyor: node imajında zaten var olan node hesabını kullanıyor, diğer imajlarda uid 1001 ile gerçek bir kullanıcı açıyor ve çalışma dizininin sahipliğini ona veriyor. Alpine ve Debian'da adduser bayrakları farklı olduğu için tag içinde alpine geçip geçmediğine bakıyor.
.dockerignore ise en sık atlanan dosya. Yoksa docker build çağrısı bulunduğunuz dizinin tamamını daemon'a gönderiyor: node_modules, .git geçmişi, .env dosyaları, coverage raporları. Bu hem ilk adımı yavaşlatıyor hem de COPY . . üzerinden imaja gereksiz dosya sokuyor. Araç Dockerfile ile birlikte bir .dockerignore da üretiyor ve **/.env deseni sayesinde ortam dosyaları build context'ine hiç girmiyor.
CMD ile ENTRYPOINT farkı
CMD varsayılan komutu belirtiyor ve docker run imaj baska-komut yazdığınızda tamamen değişiyor. ENTRYPOINT ise sabit kalıyor, komut satırında verdiğiniz şeyler ona argüman olarak ekleniyor. Uygulama sunucusu çalıştıran imajlarda CMD yeterli; imajı bir CLI aracı gibi kullandıracaksanız ENTRYPOINT daha uygun.
Uyarı
CMD node dist/index.js biçiminde (shell form) yazmayın. Bu biçim süreci /bin/sh -c altına alıyor, sh de SIGTERM sinyalini iletmediği için container her durdurulduğunda 10 saniyelik kill zaman aşımını doldurmayı bekliyor. Araç bu yüzden komutu her zaman CMD ["node", "dist/index.js"] gibi exec form olarak üretiyor.Sıkça Sorulan Sorular
- İmajım neden bu kadar büyük?
- Üç yaygın sebep var. Birincisi build context: .dockerignore yoksa node_modules ve .git imaja giriyor. İkincisi tek aşamalı yapı: derleyici, paket yöneticisi önbelleği ve kaynak ağacı son imajda kalıyor. Üçüncüsü taban imajı:
node:20ilenode:20-alpinearasında birkaç yüz megabaytlık fark var.docker history imajçıktısı hangi katmanın kaç megabayt eklediğini gösterir, oradan başlayın. - Build önbelleği neden hiç tutmuyor?
- Neredeyse her zaman kopyalama sırası yüzünden.
COPY . .satırı kurulum adımından önce geliyorsa her kod değişikliği o katmanı geçersiz kılıyor ve sonrasındaki her şey yeniden koşuyor. Manifest dosyalarını ayrı kopyalayıp kurulumu hemen ardından yapın. İkinci sebep, Dockerfile içinde tarih veya rastgele değer üreten bir ARG kullanmak; bu her build'de girdiyi değiştirir. - alpine mi slim mi?
- Alpine musl libc kullanıyor, glibc değil. Saf JavaScript veya Go için sorun çıkmaz ve imaj belirgin biçimde küçüktür. Native uzantı derleyen Python paketlerinde (numpy, pandas, psycopg2) alpine'de tekerlek bulunamadığı için kaynaktan derleme başlar, build hem uzar hem de bazen imaj slim'den büyük çıkar. Python tarafında varsayılan olarak slim, Node ve Go tarafında alpine mantıklı bir başlangıç.
- Küçük bir proje için çok aşamalı derleme şart mı?
- Derleme adımı olmayan bir projede kazanç sınırlı: Python'da doğrudan çalışan bir uygulamada tek aşama yeterli olabilir. Ama build komutu olan her projede fark hemen görünür hale geliyor, çünkü TypeScript derleyicisi, bundler ve dev bağımlılıkları son imajda kalmıyor. Go'da proje ne kadar küçük olursa olsun çok aşamalı yapmaya değer, orada oran 800 MB ile 15 MB arasında.
- Container neden hemen kapanıyor?
- Docker, PID 1 olarak çalışan süreç bittiğinde container'ı sonlandırır. Uygulamanız arka plana geçen bir süreç başlatıp çıkıyorsa (bazı servis yöneticileri veya
&ile başlatılan komutlar) container da çıkar. Uygulamayı ön planda çalıştırın. Diğer sık sebep, CMD'deki yolun imaj içinde bulunmaması:docker run --rm -it imaj shile içeri girip dosyanın gerçekten orada olup olmadığına bakın. Çok aşamalı yapıda derleme çıktısı runtime aşamasına kopyalanmamışsa da aynı hata görülür.