Konteyner Teknolojisine Giriş: Docker Nasıl Çalışır?

Sanallaştırma dünyasında uzun yıllar boyunca sanal makineler hakimdi. Bir uygulama çalıştırmak istiyordun, tam bir işletim sistemi kuruyordun, kaynakları tahsis ediyordun ve sonunda uygulamanı başlatıyordun. Bu yaklaşım işe yarıyordu elbette, ama hem ağır hem de yavaştı. Sonra Docker geldi ve her şeyi değiştirdi. Bugün neredeyse her modern altyapıda Docker var, her iş ilanında Docker biliyor olmak bir gereklilik. Peki bu kadar konuşulan bu teknoloji aslında nasıl çalışıyor? Gelin baştan sona inceleyelim.

Konteyner Nedir, Sanal Makineden Farkı Ne?

Çoğu kişi konteyneri “hafif sanal makine” olarak tanımlar. Bu tanım tam doğru değil ama başlangıç için idare eder. Daha doğru bir tanım şu olur: Konteyner, uygulamayı ve onun bağımlılıklarını izole bir ortamda çalıştıran bir süreç grubudur.

Sanal makinede her şey donanım seviyesinde soyutlanır. Hypervisor, fiziksel donanım üzerinde sanal donanım katmanı oluşturur. Her VM kendi çekirdeğini, kendi işletim sistemini çalıştırır. Bu yüzden bir VM başlatmak genellikle dakikalar alır ve her VM onlarca GB disk alanı tüketir.

Konteynerde ise durum farklıdır. Konteynerler, host makinenin Linux çekirdeğini paylaşır. İzolasyon, çekirdek seviyesindeki iki temel özellikle sağlanır:

  • Linux Namespaces: Her konteynere ayrı PID alanı, ağ arayüzü, dosya sistemi mount noktası ve kullanıcı alanı sağlar. Konteyner kendi namespace’inde çalışır, diğer süreçleri “göremez”.
  • Control Groups (cgroups): CPU, RAM, disk I/O gibi kaynakların ne kadarının kullanılabileceğini sınırlandırır. Bir konteynerin tüm sistem kaynaklarını yutmasını engeller.

Bu mimari sayesinde konteynerler milisaniyeler içinde başlar, megabaytlar seviyesinde yer kaplar ve çok daha az kaynak tüketir.

Docker Mimarisi: Parçalar Bir Arada

Docker’ı anlamak için önce bileşenlerini tanımak gerekir.

Docker Daemon (dockerd): Arka planda çalışan asıl servis. Konteyner oluşturma, çalıştırma, durdurma gibi tüm işlemleri bu daemon yönetir. Root yetkisiyle çalışır.

Docker Client (docker): Terminalden kullandığın docker komutu aslında bir istemcidir. Komutları REST API üzerinden daemon’a iletir.

Docker Registry: İmajların depolandığı yer. Docker Hub en bilinen public registry’dir. Şirket içi kurulumlar için Harbor veya AWS ECR gibi private registry’ler kullanılır.

Docker Image: Konteynerin şablonu. Salt okunur katmanlardan oluşur. Bir imajdan istediğin kadar konteyner ayağa kaldırabilirsin.

Docker Container: İmajın çalışan örneği. İmajın üstüne yazılabilir bir katman eklenir ve bu katmanda runtime sırasında yapılan değişiklikler tutulur.

Docker Volume: Konteynerin dosya sisteminden bağımsız, kalıcı veri saklama alanı.

Kurulum: Hızlıca Ayağa Kalkalım

Ubuntu üzerinde Docker kurmak için resmi repository’yi kullanmak en sağlıklı yol. Dağıtım paket yöneticisindeki eski sürümlerle uğraşmak yerine direkt Docker’ın kendi kaynaklarına gitmeliyiz.

# Eski sürümleri temizle
sudo apt-get remove docker docker-engine docker.io containerd runc

# Gerekli paketleri kur
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg lsb-release

# Docker GPG anahtarını ekle
sudo mkdir -m 0755 -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

# Repository'yi ekle
echo 
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu 
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# Docker'ı kur
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

# Mevcut kullanıcıyı docker grubuna ekle (sudo olmadan çalıştırmak için)
sudo usermod -aG docker $USER
newgrp docker

Kurulumu doğrulamak için klasik test:

docker run hello-world
docker --version
docker info

İmajlar ve Katmanlı Dosya Sistemi

Docker’ın en zekice tasarım kararlarından biri katmanlı dosya sistemidir. Her imaj, üst üste bindirilmiş salt okunur katmanlardan oluşur. Bir Dockerfile’daki her satır yeni bir katman oluşturur.

Bunu somutlaştıralım. Şu komutu çalıştır:

docker pull nginx
docker image history nginx

Çıktıda her katmanın boyutunu ve hangi komuttan oluştuğunu görürsün. Şimdi neden bu önemli?

Diyelim ki 5 farklı uygulamayı aynı Ubuntu base imajıyla çalıştırıyorsun. Docker bu base katmanı her imaj için ayrı ayrı depolamaz. Disk üzerinde tek bir kopyası vardır ve tüm imajlar bu katmanı paylaşır. Bu sayede hem disk alanından tasarruf edilir hem de imaj çekme işlemleri hızlanır. Zaten sahip olduğun katmanlar yeniden indirilmez.

Yaygın kullanılan imaj komutları:

# İmaj listele
docker images
docker image ls

# İmaj çek
docker pull ubuntu:22.04

# İmaj sil
docker image rm nginx
docker rmi nginx:latest

# Kullanılmayan tüm imajları temizle
docker image prune -a

# İmaj detaylarını gör
docker image inspect nginx

Dockerfile Yazmak: İlk Gerçek Dünya Örneği

Teoride kalmamanın yolu kendi imajını oluşturmaktan geçer. Basit bir Node.js uygulaması için Dockerfile yazalım.

Önce proje yapısı:

myapp/
├── Dockerfile
├── package.json
└── app.js

app.js içeriği:

# app.js
cat << 'EOF' > app.js
const http = require('http');
const port = process.env.PORT || 3000;

const server = http.createServer((req, res) => {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Merhaba Docker Dunyasi!n');
});

server.listen(port, () => {
  console.log(`Sunucu ${port} portunda calisiyor`);
});
EOF

Şimdi Dockerfile:

# Base imaj olarak resmi Node.js kullan
FROM node:18-alpine

# Calısma dizinini ayarla
WORKDIR /app

# Package dosyalarini kopyala (once bunlari, sonra kodu)
# Bu siralama build cache'i verimli kullanir
COPY package*.json ./

# Bagimliliklari yukle
RUN npm install --production

# Uygulama kodunu kopyala
COPY . .

# Port bilgisini belirt (belgelesel amac)
EXPOSE 3000

# Konteynerin calistiracagi komut
CMD ["node", "app.js"]

İmajı build et ve çalıştır:

# İmajı oluştur
docker build -t myapp:v1 .

# Build sürecini ve katmanları gör
docker build -t myapp:v1 --no-cache .

# Konteyneri çalıştır
docker run -d -p 8080:3000 --name mywebapp myapp:v1

# Test et
curl http://localhost:8080

# Logları kontrol et
docker logs mywebapp
docker logs -f mywebapp  # Canlı takip

Burada dikkat etmen gereken bir nokta var. package.json dosyasını uygulama kodundan önce kopyalıyoruz. Docker build sırasında her katmanı cache’ler. Eğer package.json değişmediyse npm install adımı cache’den gelir, build işlemi çok daha hızlı tamamlanır. Sadece uygulama kodun değiştiyse son COPY . . katmanından itibaren yeniden build alınır.

Konteyner Yönetimi: Günlük Operasyonlar

Prodüksiyon ortamında konteynerlerle çalışırken en çok kullandığın komutlar bunlar olacak:

# Çalışan konteynerleri listele
docker ps
docker ps -a  # Durmuş olanlar dahil tüm konteynerler

# Konteyner başlat / durdur / yeniden başlat
docker start mywebapp
docker stop mywebapp
docker restart mywebapp

# Konteyneri zorla durdur
docker kill mywebapp

# Konteynere bağlan (bash ile)
docker exec -it mywebapp sh
docker exec -it mywebapp bash

# Konteyner içinde tek komut çalıştır
docker exec mywebapp cat /etc/os-release

# Konteyner istatistiklerini izle
docker stats
docker stats mywebapp --no-stream

# Konteyner detayları
docker inspect mywebapp

# Konteyner sil (önce durdurulmalı)
docker rm mywebapp

# Çalışan konteyneri direkt sil
docker rm -f mywebapp

# Tüm durmuş konteynerleri temizle
docker container prune

Sık yaptığım bir hata: Konteyner silindiğinde içindeki veriler de gider. Bunu öğrenmek için bir MySQL konteyneri çalıştırıp veri yazıp sonra silmek yeterli. Kalıcı veri için Volume kullanmak şart.

Volume Kullanımı: Veriyi Kalıcı Hale Getir

Gerçek bir senaryo üzerinden gidelim. MySQL veritabanını konteyner içinde çalıştırıyorsun. Konteyneri silip yeniden oluşturduğunda tüm verilerini kaybetmek istemiyorsun.

# Volume oluştur
docker volume create mysql_data

# Volume listesi
docker volume ls

# Volume detayı (diskde nerede olduğunu görmek için)
docker volume inspect mysql_data

# MySQL konteynerini volume ile başlat
docker run -d 
  --name mysql_db 
  -e MYSQL_ROOT_PASSWORD=gizli_sifre 
  -e MYSQL_DATABASE=uygulamam 
  -v mysql_data:/var/lib/mysql 
  -p 3306:3306 
  mysql:8.0

# Konteyneri silip yeniden oluştur, veri hala orada
docker rm -f mysql_db
docker run -d 
  --name mysql_db 
  -e MYSQL_ROOT_PASSWORD=gizli_sifre 
  -v mysql_data:/var/lib/mysql 
  -p 3306:3306 
  mysql:8.0

Bind mount da sıkça kullanılan bir yöntem. Özellikle geliştirme ortamında çok işe yarar. Host makindeki bir dizini direkt konteynere bağlarsın, kod değişikliklerini anında konteynere yansıtırsın.

# Mevcut dizini konteynere bağla (geliştirme için ideal)
docker run -d 
  --name devapp 
  -v $(pwd):/app 
  -p 3000:3000 
  myapp:v1

# Kullanılmayan volume'ları temizle
docker volume prune

Docker Network: Konteynerler Arası İletişim

Tek konteyner işler halledilir. Ama gerçek dünyada bir web uygulaması en az birkaç bileşenden oluşur: web sunucu, uygulama, veritabanı, cache. Bunların birbiriyle konuşması için network yapılandırması gerekir.

Docker varsayılan olarak üç network ile gelir:

  • bridge: Varsayılan. Aynı host üzerindeki konteynerler için.
  • host: Konteynerin host network’ünü doğrudan kullanması için.
  • none: Ağ bağlantısı olmayan konteynerler için.

Kendi network’ünü oluşturmak daha iyi bir pratiktir:

# Custom network oluştur
docker network create app_network

# Network listesi
docker network ls

# Konteynerleri bu network'te başlat
docker run -d 
  --name database 
  --network app_network 
  -e MYSQL_ROOT_PASSWORD=gizli 
  -e MYSQL_DATABASE=appdb 
  mysql:8.0

docker run -d 
  --name webapp 
  --network app_network 
  -p 8080:3000 
  myapp:v1

# Aynı network içindeki konteynerler isimle birbirine ulaşabilir
docker exec webapp ping database

# Network detayı
docker network inspect app_network

Custom bridge network kullandığında konteynerler birbirlerine isimle (container name) ulaşabilir. Bu çok önemli bir özellik. Uygulamanın veritabanı bağlantı stringinde localhost yerine database yazarsın ve Docker DNS’i gerisini halleder.

Docker Compose ile Çok Konteynerli Uygulamalar

Tek tek docker run komutları yazmak küçük projeler için idare eder. Ama birden fazla servisi koordineli şekilde yönetmek için Docker Compose vazgeçilmez.

Bir blog uygulaması için örnek docker-compose.yml:

version: '3.8'

services:
  db:
    image: postgres:15
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: blogdb
      POSTGRES_USER: bloguser
      POSTGRES_PASSWORD: guvenli_sifre
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    restart: unless-stopped

  web:
    build: .
    ports:
      - "8080:3000"
    depends_on:
      - db
      - redis
    environment:
      DATABASE_URL: postgresql://bloguser:guvenli_sifre@db:5432/blogdb
      REDIS_URL: redis://redis:6379
    restart: unless-stopped

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - web
    restart: unless-stopped

volumes:
  postgres_data:
# Tüm servisleri başlat
docker compose up -d

# Logları takip et
docker compose logs -f

# Belirli servisin logları
docker compose logs -f web

# Servislerin durumu
docker compose ps

# Tek bir servisi yeniden başlat
docker compose restart web

# Scale up (birden fazla instance)
docker compose up -d --scale web=3

# Her şeyi durdur ve temizle
docker compose down

# Volume'ları da sil
docker compose down -v

Pratik Güvenlik Notları

Docker ile çalışırken gözden kaçan birkaç güvenlik konusu var.

Root olmayan kullanıcıyla çalıştırmak iyi bir alışkanlık. Dockerfile’ında şunu ekle:

FROM node:18-alpine

WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .

# Root olmayan kullanıcı oluştur
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

# Bu kullanıcıya geç
USER appuser

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

Bunun dışında birkaç önemli nokta:

  • Minimal base imaj kullan: node:18-alpine, node:18 yerine çok daha küçük ve daha az saldırı yüzeyi sunar.
  • Gizli bilgileri imaja koyma: Şifreler, API anahtarları Dockerfile’da veya imajda olmamalı. Environment variable veya Docker secrets kullan.
  • İmajları düzenli güncelle: docker pull ile base imajları güncel tut, bilinen açıkları kapatırsın.
  • Rootless Docker: Docker daemon’ı root yetkisi olmadan çalıştırma imkanı da var, özellikle production ortamlar için araştırılmaya değer.

Temizlik ve Bakım

Zamanla disk dolmaya başlar. Docker imajları, durdurulmuş konteynerler, kullanılmayan volume’lar ve network’ler yer kaplar.

# Tüm kullanılmayan kaynakları tek komutla temizle
docker system prune -a

# Sadece volume olmadan temizle
docker system prune

# Disk kullanımını gör
docker system df

# Ayrıntılı disk kullanımı
docker system df -v

Bu komutu production ortamda dikkatli kullan. -a flag’i kullanılmayan tüm imajları siler, sonradan lazım olacak imajları da götürebilir.

Sonuç

Docker’ı öğrenmek ilk başta karmaşık görünebilir ama temel kavramları oturtunca her şey yerine oturuyor. İmajlar değişmez şablonlar, konteynerler bu şablonların çalışan örnekleri, volume’lar kalıcı veri için, network’ler iletişim için. Bu dört kavramı içselleştirdiğinde Docker’ın geri kalanı kendiliğinden netleşiyor.

Pratik önerim şu: Üretim ortamına atlamak yerine önce kendi geliştirme ortamında Docker kullanmaya başla. Çalıştırdığın uygulamaları birer birer konteynere al, Compose dosyaları yaz, volume ve network yönetimini öğren. Hatalar yapacaksın, konteynerler ayağa kalkmayacak, portlar çakışacak. Bu sorunlarla boğuşmak seni en hızlı öğreten şey olacak.

Bir sonraki adım olarak Docker Compose’u derinlemesine öğrenmek, ardından Kubernetes’e geçiş mantıklı bir yol haritası. Ama acele etme. Docker’a hâkim olmadan Kubernetes ile boğuşmak sadece hayal kırıklığı yaratır. Temeller sağlam olsun, üstüne inşa etmek kolaylaşır.

Yorum yapın