Geçenlerde kendi sunucumda yeni bir yan ürünün prototipini geliştirirken, her seferinde servisleri manuel olarak ayağa kaldırmak veya karmaşık deployment script’leriyle uğraşmak yerine daha hızlı bir yol aradım. Docker Compose, özellikle single-host ortamlar için, uygulamaları hızla devreye almak ve yönetmek konusunda benim için hep pratik bir çözüm olmuştur. Bu yazıda, self-hosted uygulamalarınızı Docker Compose ile daha verimli ve güvenilir bir şekilde çalıştırmak için benim de sıklıkla kullandığım beş temel yaklaşımı paylaşacağım.
Docker Compose, birden fazla container’ı tanımlayan ve çalıştıran bir araçtır. Bir YAML dosyası kullanarak uygulamanızın tüm servislerini, ağlarını ve depolama birimlerini tek bir yerden yapılandırmanıza olanak tanır. Bu sayede, karmaşık bağımlılıkları olan uygulamaları tek bir komutla ayağa kaldırabilir, durdurabilir ve yönetebilirsiniz. Bu yaklaşım, özellikle hızlı prototipleme, geliştirme ortamları ve küçük ölçekli üretim dağıtımları için büyük kolaylık sağlar.
1. Minimal Compose Dosyalarıyla Başlangıç Maliyetini Azaltmak
Self-hosted bir uygulama geliştirirken veya dağıtırken, başlangıçtaki kurulum karmaşasını minimize etmek çok önemlidir. Çoğu zaman, bir servisi ayağa kaldırmak için ihtiyacımız olan tek şey bir image tanımı ve belki birkaç port ve volume ayarıdır. İlk başta her detayı düşünerek devasa bir docker-compose.yaml dosyası oluşturmaya çalışmak, hem zaman kaybı hem de gereksiz bir yorgunluktur. Ben genellikle en temel gereksinimlerle başlar, uygulama çalıştıkça ve ihtiyaçlar netleştikçe dosyayı iteratif olarak geliştiririm.
Bir veritabanı, bir API servisi ve bir frontend’den oluşan tipik bir web uygulaması için ilk docker-compose.yaml dosyam genelde şöyle görünür:
version: '3.8'
services:
database:
image: postgres:15-alpine
restart: unless-stopped
environment:
POSTGRES_DB: myapp_db
POSTGRES_USER: myuser
POSTGRES_PASSWORD: mypassword
volumes:
- db_data:/var/lib/postgresql/data
api:
build: ./api
restart: unless-stopped
ports:
- "8000:8000"
environment:
DATABASE_URL: postgres://myuser:mypassword@database:5432/myapp_db
depends_on:
- database
frontend:
build: ./frontend
restart: unless-stopped
ports:
- "3000:3000"
depends_on:
- api
volumes:
db_data:
Bu örnekte, database, api ve frontend servislerini tanımladım. Her servisin temel bir image veya build komutu, restart politikası, gerekli environment değişkenleri ve ports var. Özellikle depends_on ile servisler arası bağımlılıkları belirtmek, servislerin doğru sırada başlamasını sağlıyor. volumes tanımı ise veritabanı verilerinin kalıcı olmasını güvence altına alıyor. Bu sade yaklaşım, uygulamanın çekirdek fonksiyonlarını hızla test etmeme ve ilk iterasyonlarda gereksiz detaylarla boğulmamamı sağlıyor.
Bu minimal yapı, bir üretim ERP’sinin yeni bir modülünü test ederken veya kendi Android uygulamamın backend’ini geliştirirken sıkça başvurduğum bir yöntemdir. Özellikle bir konsepti hızla doğrulamam gerektiğinde, karmaşık YAML dosyalarının getirdiği ek yükten kaçınmak, projenin momentumunu korumama yardımcı oluyor. İlk başta her şeyi mükemmel yapmaya çalışmak yerine, çalışır bir iskeletle yola çıkmak, sonraki adımları daha net görmemi sağlıyor.
2. Ortam Değişkenlerini Akıllıca Yönetmek: .env ve Compose Değişkenleri
Uygulamalarımı self-host ederken, hassas bilgilerin (API anahtarları, veritabanı şifreleri) docker-compose.yaml dosyası içinde açıkça yazılı kalmasını asla istemem. Bu hem güvenlik riski taşır hem de farklı ortamlar (geliştirme, test, üretim) arasında yapılandırmayı yönetmeyi zorlaştırır. İşte bu noktada .env dosyaları ve Docker Compose’un değişken yönetimi yetenekleri devreye giriyor. Benim için bu, esnek ve güvenli bir dağıtımın temel taşlarından biridir.
Bir .env dosyası, docker-compose.yaml dosyanızın bulunduğu dizinde yer alan basit bir metin dosyasıdır ve içinde anahtar-değer çiftleri bulunur:
# .env dosyası
POSTGRES_PASSWORD=mysupersecretpassword
API_KEY=anothersecretkey
FRONTEND_PORT=3000
docker-compose.yaml dosyasında bu değişkenleri $VARIABLE_NAME veya ${VARIABLE_NAME} şeklinde referans alabilirim:
version: '3.8'
services:
database:
image: postgres:15-alpine
environment:
POSTGRES_DB: myapp_db
POSTGRES_USER: myuser
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} # .env dosyasından çekilir
volumes:
- db_data:/var/lib/postgresql/data
api:
build: ./api
ports:
- "8000:8000"
environment:
DATABASE_URL: postgres://myuser:${POSTGRES_PASSWORD}@database:5432/myapp_db
API_SECRET: ${API_KEY}
depends_on:
- database
frontend:
build: ./frontend
ports:
- "${FRONTEND_PORT}:${FRONTEND_PORT}" # .env dosyasından çekilir
depends_on:
- api
volumes:
db_data:
Bu sayede, hassas bilgileri YAML dosyasından ayırabilir ve .env dosyasını git gibi versiyon kontrol sistemlerinden hariç tutabilirim (genellikle .gitignore içine eklerim). Ayrıca, farklı ortamlarda farklı .env dosyaları kullanarak aynı docker-compose.yaml dosyasını yeniden kullanabilirim. Örneğin, geliştirme ortamı için dev.env ve üretim için prod.env oluşturup docker compose --env-file prod.env up -d gibi komutlarla spesifik ortamları ayağa kaldırabilirim.
Bir üretim ERP’sinde çalışırken, test ve üretim ortamları arasında veritabanı bağlantı bilgilerini veya harici servis anahtarlarını yönetmek her zaman bir meydan okumaydı. .env dosyaları, bu geçişleri sorunsuz hale getirerek, aynı Compose dosyasını farklı ortamlar için güvenle kullanmamıza olanak tanıdı. Bu sayede, “çalışan kod” kavramını sadece uygulama koduyla sınırlı tutmayıp, dağıtım yapılandırmasını da bu kapsamda değerlendirmiş oluyoruz.
3. Persistent Data İçin Volume’ları Doğru Kullanmak
Container’lar doğası gereği efemerdir; yani durdurulduklarında veya silindiklerinde içindeki tüm veriler kaybolur. Self-hosted uygulamalarımda veritabanları, kullanıcı yüklemeleri veya yapılandırma dosyaları gibi kalıcı verilere ihtiyacım olduğunda, Docker Volume’ları kullanmak kaçınılmazdır. Yanlış volume yapılandırması, bir sunucunun yeniden başlatılmasında tüm verilerin kaybolması gibi ciddi sorunlara yol açabilir. Bu yüzden volume’ları doğru şekilde yapılandırmak, uygulamanın sürekliliği ve veri güvenliği açısından kritik öneme sahiptir.
Docker Compose’da iki ana volume türü kullanıyorum:
-
Named Volumes (İsimli Volume’lar): Docker tarafından yönetilen ve genellikle
docker volume lskomutuyla görebileceğiniz volume’lardır. Bunlar, verilerin container’dan bağımsız olarak saklanması için en iyi yoldur ve genellikle veritabanları gibi kritik veriler için tercih edilir.version: '3.8' services: database: image: postgres:15-alpine volumes: - db_data:/var/lib/postgresql/data # Named volume volumes: db_data: # Volume tanımı -
Bind Mounts (Bağlantı Mount’ları): Host makinenin belirli bir dizinini doğrudan container içine bağlar. Bu, geliştirme sırasında kod değişikliklerini anında yansıtmak veya host üzerindeki yapılandırma dosyalarını container’a sağlamak için kullanışlıdır. Ancak, üretimde veritabanları için Named Volumes daha güvenli ve yönetilebilir bir seçenektir.
version: '3.8' services: api: build: ./api volumes: - ./api:/app # Host dizinini container'a bağlar
db_data gibi named volume’ları kullandığımda, docker compose down komutuyla servisleri durdursam bile veritabanı verilerim güvende kalır. Bu, özellikle bir güncelleme veya hata durumunda servisi yeniden başlatmam gerektiğinde büyük bir rahatlık sağlar.
Bir müşteri projesinde, yanlışlıkla bir bind mount kullanarak veritabanı verilerini host üzerindeki geçici bir dizine bağladığımı hatırlıyorum. Sunucu yeniden başlatıldığında tüm veriler uçup gitmişti. Bu olay bana Named Volumes ve Bind Mounts arasındaki farkı ve her birinin ne zaman kullanılması gerektiğini acı bir şekilde öğretti. O zamandan beri, kritik veriler için her zaman Named Volumes kullanmaya özen gösteriyorum ve hatta yedekleme stratejimi de bu volume’ları göz önünde bulundurarak tasarlıyorum.
4. Ağ Yapılandırmasını Optimize Etmek: Custom Networks ve Port Mapping
Docker Compose ile birden fazla servisi çalıştırdığımda, servisler arası iletişimin hem güvenli hem de verimli olması gerekir. Varsayılan olarak Docker Compose, uygulamanız için tek bir varsayılan ağ oluşturur ve tüm servisleri bu ağa bağlar. Ancak, daha fazla izolasyon, güvenlik veya karmaşık bir ağ topolojisi gerektiğinde, custom networks tanımlamak ve port mapping stratejilerini optimize etmek çok işime yarar.
Örneğin, bir frontend servisi sadece Nginx reverse proxy tarafından erişilebilirken, backend servisi sadece frontend’den veya başka bir internal servisten erişilebilir olmalıdır. Bu senaryoda, farklı ağlar tanımlayarak servisler arası erişimi kısıtlayabilirim:
version: '3.8'
services:
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
networks:
- external_network
- internal_network
frontend:
build: ./frontend
networks:
- internal_network
api:
build: ./api
networks:
- internal_network
database:
image: postgres:15-alpine
networks:
- internal_network
environment:
POSTGRES_DB: myapp_db
POSTGRES_USER: myuser
POSTGRES_PASSWORD: mypassword
volumes:
- db_data:/var/lib/postgresql/data
networks:
external_network:
driver: bridge
internal_network:
driver: bridge
volumes:
db_data:
Bu yapılandırmada, nginx servisi hem external_network (internete açık) hem de internal_network (uygulama servisleri arası iletişim) üzerinde yer alıyor. frontend, api ve database servisleri ise sadece internal_network üzerinde çalışıyor. Bu sayede, database servisine dışarıdan doğrudan erişim engellenmiş oluyor. Ayrıca, servisler kendi aralarında isimleriyle (örneğin, api servisi database servisine database:5432 üzerinden erişebilir) iletişim kurabilir, bu da yapılandırmayı basitleştirir.
Bir bankanın iç platformunu geliştirirken, farklı servislerin farklı güvenlik bölgelerinde çalışması gerekiyordu. Custom networks kullanarak bu bölgeler arasında sıkı bir izolasyon sağladık. Sadece belirli servislerin birbirleriyle konuşmasına izin vererek, olası bir güvenlik ihlalinin yayılmasını kısıtlamış olduk. Bu, sadece Docker Compose ortamlarında değil, genel olarak ağ segmentasyonu ve Zero-Trust mimarisi prensiplerinin bir yansımasıdır. Nginx’in sadece dışarıya açık olması, diğer servislerin ise kendi iç ağlarında güvenli bir şekilde barınması, bu tür mimarilerde temel bir adımdır.
5. Sağlık Kontrolleri (Health Checks) ve Restart Politikaları ile Güvenilirliği Artırmak
Self-hosted bir uygulamanın sadece “çalışır” olması yetmez; aynı zamanda “sağlıklı” olması ve beklenmedik durumlarda otomatik olarak toparlanabilmesi gerekir. Docker Compose, healthcheck mekanizması ve restart politikaları ile bu güvenilirliği artırmamıza olanak tanır. Bir servisin gerçekten çalışır durumda olup olmadığını anlamak ve çöktüğünde otomatik olarak yeniden başlatılmasını sağlamak, operasyonel yükümü ciddi oranda azaltır.
Bir healthcheck tanımı, Docker’ın bir container’ın içindeki uygulamanın durumunu periyodik olarak kontrol etmesini sağlar. Örneğin, bir web servisi için belirli bir HTTP endpoint’ine istek atarak yanıt alıp almadığını kontrol edebiliriz:
version: '3.8'
services:
api:
build: ./api
ports:
- "8000:8000"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 20s
restart: unless-stopped
depends_on:
- database
Bu örnekte, api servisi için healthcheck tanımladım. Docker, her 30 saniyede bir http://localhost:8000/health adresine curl ile istek atacak. Eğer 10 saniye içinde yanıt alamazsa veya HTTP hata kodu dönerse (curl’ün -f bayrağı ile), bu bir başarısızlık olarak sayılacak. Üç başarısız denemeden sonra container sağlıksız (unhealthy) olarak işaretlenecek. start_period, uygulamanın başlaması için yeterli zaman tanır; bu süre içinde sağlık kontrolü başarısız olsa bile yeniden başlatma tetiklenmez.
restart politikaları ise bir container’ın ne zaman otomatik olarak yeniden başlatılacağını belirler:
- no: Otomatik yeniden başlatma yok.
- on-failure: Container bir hata koduyla (sıfır olmayan) çıkarsa yeniden başlatır.
- always: Container her zaman yeniden başlatılır (manuel olarak durdurulana kadar).
- unless-stopped: Container manuel olarak durdurulmadığı sürece her zaman yeniden başlatılır (benim en çok kullandığım).
Kendi yan ürünümün backend’inde bir Redis servisi kullanıyordum ve bazen Redis container’ı bellek yetersizliği (OOM) nedeniyle çöküyordu. restart: unless-stopped politikası sayesinde, Redis otomatik olarak yeniden başlıyor ve uygulamamın kesintisiz çalışmasını sağlıyordu. Ancak, healthcheck ekleyerek Redis’in gerçekten çalışır durumda olduğunu doğrulamak, sadece container’ın yaşıyor olmasından öte, uygulamanın gerçekten hizmet verebildiğinden emin olmamı sağladı. Bu ikili yaklaşım, self-hosted sistemlerimin dayanıklılığını artırmak için vazgeçilmez bir kombinasyondur.
graph TD;
A["Kullanıcı Erişimi"] --> B["Nginx Reverse Proxy"];
B --> C{"External_Network"};
C --> N["Nginx"];
N --> D{"Internal_Network"};
D --> E["Frontend Servisi"];
E --> F["API Servisi"];
F --> G["Veritabanı Servisi (PostgreSQL)"];
style A fill:#D4E6F1,stroke:#3498DB,stroke-width:2px;
style B fill:#D4E6F1,stroke:#3498DB,stroke-width:2px;
style C fill:#D4E6F1,stroke:#3498DB,stroke-width:2px;
style N fill:#D4E6F1,stroke:#3498DB,stroke-width:2px;
style D fill:#D4E6F1,stroke:#3498DB,stroke-width:2px;
style E fill:#D4E6F1,stroke:#3498DB,stroke-width:2px;
style F fill:#D4E6F1,stroke:#3498DB,stroke-width:2px;
style G fill:#D4E6F1,stroke:#3498DB,stroke-width:2px;
Sonuç
Docker Compose, self-hosted uygulamaları hızlı ve güvenilir bir şekilde ayağa kaldırmak için benim gibi saha tecrübesi olan birisi için bile paha biçilmez bir araçtır. Minimalist docker-compose.yaml dosyalarıyla hızlıca başlamak, ortam değişkenlerini .env ile akıllıca yönetmek, kritik veriler için Named Volumes kullanmak, ağ yapılandırmasını Custom Networks ile optimize etmek ve son olarak Health Checks ile Restart Policies kullanarak uygulamanın dayanıklılığını artırmak, benim sıkça başvurduğum temel stratejilerdir.
Bu yaklaşımlar, sadece bir uygulamayı çalıştırmakla kalmaz, aynı zamanda onu daha yönetilebilir, güvenli ve esnek hale getirir. Her bir adım, yaşadığım gerçek dünya sorunlarından ve çözümlerden süzülerek gelmiştir. Umarım bu rehber, kendi self-hosted projelerinizde Docker Compose’dan en iyi şekilde faydalanmanıza yardımcı olur ve daha az operasyonel baş ağrısı yaşamanızı sağlar. Bir sonraki yazımda belki Docker Compose ile CI/CD entegrasyonu üzerine deneyimlerimi paylaşırım.