← Kursa Dön
📄 Text · 35 min

İlk Dockerfile'ın — Adım Adım Image Oluşturma

Bu bölümde Dockerfile talimatlarını ve build context'i öğrendik. Şimdi tüm bu bilgileri birleştirerek ilk Dockerfile'ımızı sıfırdan yazacak, build edecek, çalıştıracak ve adım adım iyileştireceğiz.

Bir yemek tarifini düşün. Malzeme listesi var, adımlar var, sırası var. Tarifi takip eden herkes aynı yemeği yapabilir — İstanbul'da da, Berlin'de de, Tokyo'da da. Tarifi bir kez yazarsın, bin kez kullanırsın. İşte Dockerfile, bir Docker image'ının tarifi. Bu derste ilk tarifimizi yazacağız.


Adım 0: Projeyi Hazırlayalım

Bir Dockerfile yazmadan önce, neyi containerize edeceğimizi belirleyelim. Basit bir Node.js web uygulamasıyla başlıyoruz — dış bağımlılığı yok, sadece Node.js'in built-in http modülünü kullanıyor.

mkdir myapp && cd myapp

Şimdi uygulama dosyalarını oluşturalım:

// server.js
const http = require('http');
const fs = require('fs');
const path = require('path');

const PORT = process.env.PORT || 3000;

const server = http.createServer((req, res) => {
    if (req.url === '/health') {
        res.writeHead(200, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ status: 'healthy', uptime: process.uptime() }));
        return;
    }

    const filePath = path.join(__dirname, 'public', 'index.html');
    fs.readFile(filePath, (err, data) => {
        if (err) {
            res.writeHead(500);
            res.end('Server Error');
            return;
        }
        res.writeHead(200, { 'Content-Type': 'text/html' });
        res.end(data);
    });
});

server.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
});
// package.json
{
    "name": "myapp",
    "version": "1.0.0",
    "description": "My first Docker app",
    "main": "server.js",
    "scripts": { "start": "node server.js" }
}
<!-- public/index.html -->
<!DOCTYPE html>
<html>
<head><title>Docker App</title></head>
<body>
    <h1>Merhaba Docker! 🐳</h1>
    <p>Bu sayfa bir Docker container'ından sunuluyor.</p>
</body>
</html>

Proje yapımız şöyle:

myapp/
├── Dockerfile        # Şimdi yazacağız
├── .dockerignore     # Şimdi yazacağız
├── package.json
├── server.js
└── public/
    └── index.html

Adım 1: En Basit Dockerfile

Her yolculuk bir adımla başlar. İşte ilk Dockerfile'ımız — mümkün olan en basit hali:

FROM node:20-alpine
WORKDIR /app
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]

Beş satır. Hepsi bu. Şimdi her satırı birlikte inceleyelim:

`FROM node:20-alpine` — "Alpine Linux üzerinde Node.js 20 kurulu olan bir base image'dan başla." Bu image Docker Hub'dan gelecek, yaklaşık 130MB. Her Dockerfile bir FROM ile başlar.

`WORKDIR /app` — "Bundan sonraki tüm işlemler /app dizininde yapılsın." Dizin yoksa otomatik oluşturulur. cd /app demek gibi ama kalıcı.

`COPY . .` — "Build context'teki tüm dosyaları container'ın /app dizinine kopyala." İlk . host'taki kaynak, ikinci . container'daki hedef (WORKDIR = /app).

`EXPOSE 3000` — "Bu container 3000 portunu kullanıyor." Sadece belgeleme — portu gerçekten açmak için docker run -p gerekir.

`CMD ["node", "server.js"]` — "Container başladığında 'node server.js' komutunu çalıştır." Exec form kullanıyoruz — sinyal yönetimi doğru çalışır.


Adım 2: Build — Image'ı Oluşturalım

docker build -t myapp:v1 .

Çıktıyı inceleyelim:

Sending build context to Docker daemon  4.096kB
Step 1/5 : FROM node:20-alpine
 ---> a8c5be9b1d03
Step 2/5 : WORKDIR /app
 ---> Running in abc123def456
 ---> 1a2b3c4d5e6f
Step 3/5 : COPY . .
 ---> 2b3c4d5e6f7a
Step 4/5 : EXPOSE 3000
 ---> 3c4d5e6f7a8b
Step 5/5 : CMD ["node", "server.js"]
 ---> 4d5e6f7a8b9c
Successfully built 4d5e6f7a8b9c
Successfully tagged myapp:v1

Her step bir katman. Base image çekildi, çalışma dizini oluşturuldu, dosyalar kopyalandı, metadata eklendi, son katman oluşturuldu.

# Image oluştu mu kontrol edelim
docker images myapp
# REPOSITORY   TAG   IMAGE ID       CREATED         SIZE
# myapp        v1    4d5e6f7a8b9c   30 seconds ago  135MB

Adım 3: Run — Container'ı Çalıştıralım

docker run -d --name web -p 3000:3000 myapp:v1
# Çalışıyor mu?
docker ps
# CONTAINER ID   IMAGE      STATUS         PORTS
# abc123def456   myapp:v1   Up 5 seconds   0.0.0.0:3000->3000/tcp

# Test edelim
curl http://localhost:3000
# <!DOCTYPE html>...Merhaba Docker! 🐳...

curl http://localhost:3000/health
# {"status":"healthy","uptime":5.123}

# Logları görelim
docker logs web
# Server running on port 3000

🎉 Tebrikler! İlk Docker image'ını build edip çalıştırdın!

Ama bu Dockerfile henüz mükemmel değil. Şimdi onu adım adım iyileştirelim.


Adım 4: .dockerignore Ekleyelim

Şu an build context küçük (birkaç KB) çünkü projemiz basit. Ama gerçek bir projede node_modules, .git, test dosyaları olacak. Hemen .dockerignore ekleyelim:

# .dockerignore
node_modules
npm-debug.log*
.git
.gitignore
.env
.env.*
Dockerfile
docker-compose*.yml
.dockerignore
README.md
docs/
tests/
coverage/
.vscode/
.idea/
*.md
*.log
.DS_Store

Adım 5: Layer Cache Optimizasyonu

Bu en önemli iyileştirme. Mevcut Dockerfile'da COPY . . tüm dosyaları bir seferde kopyalıyor. Herhangi bir dosya değiştiğinde (bir satır kod bile) npm install tekrar çalışır. Bunu düzeltelim:

FROM node:20-alpine
WORKDIR /app

# Önce sadece package dosyalarını kopyala
COPY package.json package-lock.json ./

# Bağımlılıkları yükle (sadece package.json değişince tekrar çalışır)
RUN npm install

# Sonra uygulama kodunu kopyala
COPY . .

EXPOSE 3000
CMD ["node", "server.js"]

Bu sıralama neden önemli? Docker, her COPY'de dosyaların hash'ini kontrol eder. package.json değişmediyse, npm install cache'ten gelir — saniyeler sürer. Sadece kod değiştiysen, sadece COPY . . yeniden çalışır.


Adım 6: Production-Ready Dockerfile

Şimdi tüm best practice'leri uygulayarak gerçek bir production Dockerfile yazalım:

# ============================================
# Production-Ready Node.js Dockerfile
# ============================================

# 1. Base image — belirli versiyon
FROM node:20-alpine

# 2. Metadata
LABEL maintainer="tolgahan@example.com"
LABEL version="1.0.0"

# 3. Non-root kullanıcı (güvenlik!)
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

# 4. Çalışma dizini
WORKDIR /app

# 5. Bağımlılıkları önce kopyala (cache optimizasyonu)
COPY package.json package-lock.json ./

# 6. Production bağımlılıklarını yükle
RUN npm ci --only=production && npm cache clean --force

# 7. Uygulama kodunu kopyala
COPY --chown=appuser:appgroup . .

# 8. Non-root kullanıcıya geç
USER appuser

# 9. Port bildirimi
EXPOSE 3000

# 10. Sağlık kontrolü
HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=10s \
    CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

# 11. Başlatma komutu
CMD ["node", "server.js"]

Bu Dockerfile'da neler iyileşti? Non-root user ile güvenlik artırıldı. npm ci ile deterministik bağımlılık yüklemesi sağlandı (npm install'dan farklı olarak, npm ci her zaman package-lock.json'dan birebir yükler). Cache temizlendi (npm cache clean --force). --only=production ile devDependencies hariç tutuldu. HEALTHCHECK eklendi.


Farklı Dillerde Örnekler

Python Flask

FROM python:3.12-slim
LABEL maintainer="tolgahan@example.com"

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

RUN useradd --create-home appuser
WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY --chown=appuser:appuser . .
USER appuser

ENV FLASK_APP=app.py
EXPOSE 5000

HEALTHCHECK --interval=30s --timeout=5s CMD curl -f http://localhost:5000/health || exit 1
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "app:app"]

Statik Website (Nginx)

FROM nginx:1.25-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY public/ /usr/share/nginx/html/
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=5s CMD wget -qO- http://localhost/ || exit 1

Dockerfile Debugging

Build Hatası: COPY Dosya Bulamıyor

Dosya .dockerignore'da olabilir veya build context dışında olabilir.

Cache Sürekli Kırılıyor

COPY sıralamasını kontrol et — bağımlılık dosyalarını koddan önce kopyala.

Container Sinyal Almıyor

Shell form CMD (CMD npm start) yerine exec form kullan (CMD ["node", "server.js"]).

Hadolint — Otomatik Kontrol

docker run --rm -i hadolint/hadolint < Dockerfile

Dockerfile'daki sorunları otomatik tespit eder: tag'lenmemiş base image, birleştirilmemiş RUN'lar, eksik --no-install-recommends gibi.


BuildKit Kullan — Daha Hızlı Build

export DOCKER_BUILDKIT=1
docker build -t myapp:v1 .

BuildKit bağımsız katmanları paralel build edebilir, daha akıllı cache kullanır ve daha temiz çıktı verir. Modern Docker'da genellikle varsayılan olarak aktif.


Bu Derste Ne Öğrendik?

  • Dockerfile baştan sona yazıldı: FROMWORKDIRCOPYRUNEXPOSECMD.

  • Layer caching en önemli optimizasyon: bağımlılıkları koddan önce kopyala, seyrek değişenleri üste koy.

  • .dockerignore olmadan build context şişer, image büyür, build yavaşlar.

  • Non-root user, HEALTHCHECK, exec form CMD production zorunlulukları.

  • npm ci > npm install: deterministik, tekrarlanabilir build.

  • BuildKit etkinleştir — daha hızlı build, paralel katmanlar.

Bir sonraki derste CMD ve ENTRYPOINT arasındaki farkı detaylıca öğreneceğiz.