Çoklu Konteyner Yönetimine Giriş: Docker Compose Nedir?

Tek bir konteyneri ayağa kaldırmak zaten yeterince basit. Ama gerçek dünyada hiçbir uygulama tek başına yaşamıyor. Bir web uygulamasının arkasında mutlaka bir veritabanı var, onun önünde bir reverse proxy var, belki bir cache katmanı var, belki bir mesaj kuyruğu var. Bunların hepsini ayrı ayrı docker run komutlarıyla yönetmeye çalışmak başlangıçta idare edilebilir gibi görünse de, zamanla gerçek bir kaosa dönüşüyor. İşte tam bu noktada Docker Compose devreye giriyor ve hayatımızı ciddi ölçüde kolaylaştırıyor.

Docker Compose Nedir?

Docker Compose, birden fazla konteyneri tek bir konfigürasyon dosyasında tanımlayıp yönetmenizi sağlayan bir araçtır. YAML formatında yazılan bu konfigürasyon dosyası sayesinde tüm servislerinizi, ağ yapılandırmanızı ve volume’larınızı tek bir yerden yönetebilirsiniz.

En temel tanımıyla şöyle düşünebilirsiniz: Docker CLI tek bir konteyneri yönetmek için kullanılıyorsa, Docker Compose bir uygulama yığınını (stack) yönetmek için kullanılır. Geliştirme ortamından test ortamına, oradan production’a kadar aynı konfigürasyonu tutarlı bir şekilde taşıyabilmek Docker Compose’un en büyük gücüdür.

Docker Compose’un Versiyonları Hakkında

Burada küçük bir not düşmem gerekiyor. Eski dokümanlarda docker-compose (tire ile) komutunu göreceksiniz. Bu, Python ile yazılmış eski Compose v1’di ve ayrı bir binary olarak kurulması gerekiyordu. Modern Docker kurulumlarında artık docker compose (boşlukla) şeklinde kullanılan ve Docker CLI’ye entegre edilmiş Compose v2 geliyor. Eğer sisteminizde her ikisi de varsa, v2’yi kullanmanızı öneririm. Bu yazıdaki örneklerin tamamında docker compose kullanacağım.

Neden Docker Compose Kullanmalıyız?

Önce sorunun kendisini somutlaştıralım. Diyelim ki bir WordPress sitesi kuracaksınız. Elle yaparsanız şöyle bir süreç izlemeniz gerekir:

# MySQL container'ını başlat
docker run -d 
  --name wordpress-db 
  -e MYSQL_ROOT_PASSWORD=gizlisifre 
  -e MYSQL_DATABASE=wordpress 
  -e MYSQL_USER=wpuser 
  -e MYSQL_PASSWORD=wppassword 
  -v mysql_data:/var/lib/mysql 
  --network wordpress-net 
  mysql:8.0

# Ağı oluşturmayı unutmadınız mı?
docker network create wordpress-net

# WordPress container'ını başlat
docker run -d 
  --name wordpress-app 
  -e WORDPRESS_DB_HOST=wordpress-db 
  -e WORDPRESS_DB_USER=wpuser 
  -e WORDPRESS_DB_PASSWORD=wppassword 
  -e WORDPRESS_DB_NAME=wordpress 
  -p 8080:80 
  --network wordpress-net 
  wordpress:latest

Bu komutları her ortamda tekrar tekrar çalıştırmanız, sıralama hatası yapmamanız, ağ ve volume’ları önceden oluşturmanız gerekiyor. Bir şeyi değiştirmeniz gerektiğinde hangisini düzenlemeniz gerektiğini hatırlamanız lazım. Takım arkadaşınıza bu ortamı kurdurmak istediğinizde ne yapacaksınız, bu komutları da paylaşacak mısınız?

Docker Compose ile aynı şeyi çok daha temiz yapabilirsiniz ve tüm bu bilgi tek bir dosyada yaşar.

İlk docker-compose.yml Dosyanız

Yukarıdaki WordPress örneğini Compose ile yazalım:

services:
  db:
    image: mysql:8.0
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: gizlisifre
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wpuser
      MYSQL_PASSWORD: wppassword
    volumes:
      - mysql_data:/var/lib/mysql

  wordpress:
    image: wordpress:latest
    restart: always
    ports:
      - "8080:80"
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: wpuser
      WORDPRESS_DB_PASSWORD: wppassword
      WORDPRESS_DB_NAME: wordpress
    depends_on:
      - db

volumes:
  mysql_data:

Bu dosyayı docker-compose.yml adıyla kaydedin ve şu komutla çalıştırın:

docker compose up -d

Hepsi bu kadar. Docker Compose ağı otomatik oluşturdu, volume’u oluşturdu, servisleri doğru sırada başlattı. Durdurmak istediğinizde:

docker compose down

Bu komut container’ları durdurup kaldırır ama volume’larınız korunur. Volume’ları da silmek istiyorsanız docker compose down -v kullanabilirsiniz.

docker-compose.yml Dosyasının Anatomisi

Bir Compose dosyası birkaç temel bölümden oluşur. Bunları tek tek inceleyelim.

Services Bloğu

Servisler, uygulamanızın bileşenlerini tanımlar. Her servis bir container’a karşılık gelir (ya da birden fazla, ölçeklendirme yapıyorsanız). Servis içinde kullanabileceğiniz başlıca direktifler şunlardır:

  • image: Kullanılacak Docker imajı
  • build: Dockerfile’dan imaj oluşturmak için yapılandırma
  • ports: Port yönlendirme (host:container)
  • environment: Ortam değişkenleri
  • volumes: Volume bağlamaları
  • depends_on: Servis bağımlılıkları
  • restart: Yeniden başlatma politikası (always, on-failure, unless-stopped)
  • networks: Hangi ağlara bağlanacağı
  • command: Container başlarken çalıştırılacak komut
  • healthcheck: Servisin sağlık kontrolü

Networks Bloğu

Varsayılan olarak Docker Compose tüm servisler için otomatik bir ağ oluşturur ve servisleri birbirine isimlerine göre bağlar. Yani db adlı servis, diğer servislerden db hostname’iyle erişilebilir olur. Ama özel ağ yapılandırması da tanımlayabilirsiniz:

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true

Volumes Bloğu

Named volume’ları burada tanımlarsınız. Herhangi bir direktif vermezseniz Docker varsayılan ayarlarla oluşturur. Dışarıdan bir volume’u import etmek isterseniz external: true kullanabilirsiniz:

volumes:
  mysql_data:
  redis_cache:
  uploads:
    external: true

Gerçek Dünya Senaryosu: LAMP Stack

Şimdi biraz daha gerçekçi bir örneğe geçelim. Bir PHP uygulaması için tam bir LAMP stack kuralım. Bu örnekte kendi Dockerfile’ımızdan imaj oluşturacağız.

Proje yapımız şöyle olsun:

myapp/
├── docker-compose.yml
├── .env
├── php/
│   └── Dockerfile
├── nginx/
│   └── default.conf
└── src/
    └── index.php

.env dosyası hassas bilgileri saklamak için idealdir:

# .env
MYSQL_ROOT_PASSWORD=supersecret123
MYSQL_DATABASE=myappdb
MYSQL_USER=appuser
MYSQL_PASSWORD=apppassword123
APP_PORT=8080

docker-compose.yml dosyası:

services:
  nginx:
    image: nginx:alpine
    ports:
      - "${APP_PORT}:80"
    volumes:
      - ./src:/var/www/html
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - php
    networks:
      - frontend
      - backend

  php:
    build:
      context: ./php
      dockerfile: Dockerfile
    volumes:
      - ./src:/var/www/html
    networks:
      - backend
    depends_on:
      db:
        condition: service_healthy

  db:
    image: mysql:8.0
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    volumes:
      - mysql_data:/var/lib/mysql
    networks:
      - backend
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    networks:
      - backend
    volumes:
      - redis_data:/data

networks:
  frontend:
  backend:
    internal: true

volumes:
  mysql_data:
  redis_data:

Bu yapıda dikkat etmenizi istediğim birkaç nokta var. Backend ağını internal olarak işaretledik, bu sayede veritabanı ve Redis dışarıdan erişilemeyen izole bir ağda çalışıyor. PHP container’ı hem frontend hem backend ağına bağlı çünkü Nginx ile de iletişim kuruyor. depends_on direktifinde condition: service_healthy kullandık, bu sayede PHP container’ı MySQL gerçekten hazır olana kadar bekleyecek.

Compose Komutlarına Pratik Bakış

Günlük hayatta en çok kullanacağınız komutları inceleyelim:

# Tüm servisleri arka planda başlat
docker compose up -d

# Sadece belirli bir servisi başlat
docker compose up -d nginx

# Logları takip et
docker compose logs -f

# Sadece belirli bir servisin loglarını takip et
docker compose logs -f php

# Çalışan servislerin durumunu gör
docker compose ps

# Bir servisin içinde komut çalıştır
docker compose exec php bash

# Bir servisin içinde tek seferlik komut çalıştır
docker compose run --rm php php artisan migrate

# Servisleri yeniden başlat
docker compose restart nginx

# Sadece değişen imajları yeniden oluştur ve başlat
docker compose up -d --build

# Tüm container, network ve volume'ları temizle
docker compose down -v --remove-orphans

--remove-orphans bayrağı özellikle önemli. Compose dosyanızdan bir servisi sildiğinizde eski container’lar hala çalışıyor olabilir. Bu bayrak o durumu temizler.

Environment Variables ve .env Dosyası

Compose, proje dizinindeki .env dosyasını otomatik olarak yükler. Yukarıdaki örnekte de gördünüz. Ama dikkat edilmesi gereken bir konu var: .env dosyasını kesinlikle git reponuza commit etmeyin. Bunun yerine .env.example adında bir şablon dosyası oluşturun ve onu commit edin:

# .env.example - bu dosya repoya gider
MYSQL_ROOT_PASSWORD=change_me
MYSQL_DATABASE=myappdb
MYSQL_USER=appuser
MYSQL_PASSWORD=change_me
APP_PORT=8080

.gitignore dosyanıza .env eklemeyi unutmayın.

Farklı ortamlar için farklı Compose dosyaları kullanmak da yaygın bir pratiktir. Temel docker-compose.yml dosyanızın yanına docker-compose.override.yml oluşturduğunuzda, Compose bunu otomatik olarak merge eder. Geliştirme ortamı için bu dosyaya özel ayarlar koyabilirsiniz:

# docker-compose.override.yml - sadece development ortamı için
services:
  php:
    environment:
      - APP_DEBUG=true
      - APP_ENV=development
    volumes:
      - ./src:/var/www/html:cached

  db:
    ports:
      - "3306:3306"

Production’da bu override dosyasını kullanmak istemiyorsanız:

docker compose -f docker-compose.yml up -d

Healthcheck ve depends_on ile Servis Sıralaması

depends_on direktifi sıkça yanlış anlaşılır. Sadece depends_on: - db yazdığınızda Compose container’ların başlama sırasını düzenler ama servisin gerçekten hazır olduğunu garantilemez. MySQL container’ı başlamış olabilir ama veritabanı kabul etmeye henüz hazır değildir.

Bunun çözümü healthcheck kullanmak:

services:
  db:
    image: postgres:15
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

  app:
    image: myapp:latest
    depends_on:
      db:
        condition: service_healthy

start_period özellikle yavaş başlayan servisler için faydalıdır. Bu süre boyunca başarısız healthcheck’ler retry sayısına dahil edilmez.

Servisleri Ölçeklendirmek

Docker Compose ile belirli servisleri birden fazla instance olarak çalıştırabilirsiniz. Örneğin yük altında olan bir API servisinizi ölçeklendirmek için:

docker compose up -d --scale api=3

Bu senaryoda port çakışmasına dikkat etmeniz gerekir. Eğer servisiniz sabit bir host portu kullanıyorsa ölçeklendirme hata verir. Bunun yerine port tanımını şöyle yapın:

services:
  api:
    image: myapi:latest
    expose:
      - "3000"
    networks:
      - backend

expose direktifi portu sadece ağ içinde erişilebilir kılar, host’a publish etmez. Önüne bir Nginx ya da Traefik load balancer koyarak trafiği dağıtabilirsiniz.

Gerçek Proje: Node.js + MongoDB + Redis Stack

Son olarak günümüzde çok yaygın olan bir stack’i tam olarak modelleyelim:

services:
  api:
    build:
      context: .
      dockerfile: Dockerfile
      target: production
    restart: unless-stopped
    environment:
      NODE_ENV: production
      MONGODB_URI: mongodb://appuser:${MONGO_PASSWORD}@mongo:27017/myapp
      REDIS_URL: redis://redis:6379
      JWT_SECRET: ${JWT_SECRET}
    depends_on:
      mongo:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - app-network
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.api.rule=Host(`api.myapp.com`)"

  mongo:
    image: mongo:6
    restart: unless-stopped
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: ${MONGO_ROOT_PASSWORD}
      MONGO_INITDB_DATABASE: myapp
    volumes:
      - mongo_data:/data/db
      - ./mongo-init.js:/docker-entrypoint-initdb.d/init.js:ro
    networks:
      - app-network
    healthcheck:
      test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/test --quiet
      interval: 10s
      timeout: 10s
      retries: 5
      start_period: 40s

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
    volumes:
      - redis_data:/data
    networks:
      - app-network
    healthcheck:
      test: ["CMD", "redis-cli", "--no-auth-warning", "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

networks:
  app-network:
    driver: bridge

volumes:
  mongo_data:
  redis_data:

Bu örnekte dikkat etmenizi istediğim bir detay var: MongoDB için bir init script kullanıyoruz. mongo-init.js dosyası ilk container başlangıcında çalıştırılıyor ve uygulama kullanıcısını oluşturuyor. Bu, veritabanını root kullanıcısıyla değil ayrı bir kullanıcıyla kullanmak için temiz bir yöntem.

Compose Dosyalarında Sık Yapılan Hatalar

Birkaç hatadan bahsetmeden geçemeyeceğim:

  • Şifreleri doğrudan compose dosyasına yazmak: Her zaman environment variables ve .env dosyası kullanın. Compose dosyaları genellikle versiyon kontrolüne girer.
  • depends_on’u healthcheck olmadan kullanmak: Yukarıda anlattım ama tekrar vurgulayayım. Sadece container başladı diye servisiniz hazır değildir.
  • Host path volume kullanırken izin sorunları: ./src:/var/www/html gibi bağlamalarda container içindeki kullanıcının host’taki dosyalara yazma izni olmayabilir. user direktifiyle ya da Dockerfile’da UID eşleştirerek çözebilirsiniz.
  • Restart policy’yi unutmak: Sunucu yeniden başladığında container’larınızın otomatik başlamasını istiyorsanız restart: unless-stopped veya restart: always kullanmayı unutmayın.
  • Gereksiz port expose etmek: Sadece gerçekten dışarıdan erişilmesi gereken portları host’a publish edin. Veritabanlarını ve iç servisleri dışarı açmayın.

Projenizi Dokümante Etmek

Compose dosyanız aynı zamanda bir dokümantasyon görevi görür. Yeni bir takım üyesi projeye katıldığında tek yapması gereken şey şudur:

git clone https://github.com/sirketiniz/proje.git
cd proje
cp .env.example .env
# .env dosyasını düzenle
docker compose up -d

Bu kadar. Geliştirme ortamı kurulumu için saatlerce uğraşmak yerine birkaç dakika yeterli. Bu, Docker Compose’un en somut değer önerisinden biridir ve özellikle büyüyen ekiplerde inanılmaz zaman tasarrufu sağlar.

Sonuç

Docker Compose, modern uygulama geliştirme ve dağıtımının vazgeçilmez bir parçası haline geldi. Çoklu konteynerleri elle yönetmenin getirdiği karmaşıklığı ortadan kaldırıyor, ortamlar arası tutarlılık sağlıyor ve ekip içi işbirliğini kolaylaştırıyor.

Bu yazıda temel kavramları, dosya yapısını, gerçek dünya senaryolarını ve sık yapılan hataları ele aldık. Bir sonraki adım olarak Docker Compose’u CI/CD pipeline’larınıza entegre etmeyi, birden fazla Compose dosyasını nasıl birleştireceğinizi ve production ortamı için Compose’dan Docker Swarm ya da Kubernetes’e geçiş noktalarını inceleyebilirsiniz.

Ama önce şu an elimizdekiyle başlayalım. Çalıştırdığınız ya da çalıştırmayı planladığınız bir uygulamanız varsa, bugün docker-compose.yml yazmaya başlayın. Birkaç denemenin ardından “bunu neden daha önce kullanmadım” diyeceğinize eminim.

Yorum yapın