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
.envdosyası 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/htmlgibi bağlamalarda container içindeki kullanıcının host’taki dosyalara yazma izni olmayabilir.userdirektifiyle 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-stoppedveyarestart: alwayskullanmayı 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.