Tek bir compose.yaml dosyasıyla hem geliştirme hem de üretim ortamını yönetmeye çalıştıysanız o dosyanın nasıl şiştiğini bilirsiniz. Bir tarafta uygulamanız ve veritabanınız, diğer tarafta yalnızca yerelde ihtiyaç duyduğunuz Adminer, MailHog, Redis Commander, bir de sürekli açık kalmasını istemediğiniz seed konteyneri. Üretim sunucusunda docker compose up -d dediğinizde bunların hepsi birden ayağa kalkar; RAM'i yer, port çakışması yaratır ve en kötüsü, kimlik doğrulaması olmayan bir veritabanı arayüzünü internete açar. Docker Compose profilleri tam olarak bu sorunu çözmek için var.
Aynı dosyanın ikinci baş ağrısı ortam değişkenleridir. .env dosyası mı okunuyor, kabuktaki değişken mi kazanıyor, env_file ile environment arasındaki fark ne, neden ${DB_PASS} konteynerin içinde boş görünüyor? Bu rehberde Docker Compose profillerini ve ortam değişkeni zincirini uçtan uca ele alacağız: profilleri nasıl tanımlarsınız, komut satırından ve COMPOSE_PROFILES ile nasıl etkinleştirirsiniz, dört farklı değişken kanalının öncelik sırası nedir ve tek bir komutla oluşan sonucu nasıl doğrularsınız. Compose'a yeni başlıyorsanız önce Docker Compose kullanımı yazısındaki temel yapıyı oturtmanızı öneririm; buradaki her şey onun üzerine ekleniyor.
Profil Nedir ve Hangi Sorunu Çözer#
Bir Compose profili, servislere yapıştırdığınız etikettir. Bir servise profiles alanı eklediğiniz anda o servis varsayılan olarak başlatılmaz; yalnızca ilgili profil açıkça etkinleştirildiğinde ayağa kalkar. Hiç profiles alanı olmayan servisler ise her zaman çalışır. Kural bu kadar basittir ve gücü de buradan gelir: aynı dosyayı üç farklı ortamda kullanırken servis tanımlarını çoğaltmanız gerekmez.
Profilleri özellikle şu üç senaryoda kullanırım. Birincisi geliştirici araçları: veritabanı arayüzü, sahte SMTP sunucusu, kuyruk izleyici. İkincisi tek seferlik işler: veritabanı seed konteyneri, migration çalıştırıcı, yedek geri yükleyici. Üçüncüsü isteğe bağlı bileşenler: yalnızca bazı müşterilerde kullanılan bir arama motoru ya da bir rapor işçisi. Bu üç grubu profillere ayırdığınızda üretim sunucusunda çalışan servis sayısı görünür biçimde azalır ve "bu konteyner neden ayakta?" sorusu ortadan kalkar.
Profilin alternatifi genellikle iki ayrı dosya tutmaktır: compose.yaml ve compose.dev.yaml. Bu da çalışır, ama iki dosya zamanla birbirinden ayrı düşer; birinde güncellediğiniz imaj etiketini diğerinde unutursunuz. Profil, aynı gerçeği tek dosyada tutmanın yoludur. Karmaşık kurulumlarda ikisini birlikte de kullanabilirsiniz; bunu aşağıda override dosyaları bölümünde ele alacağım.
Profilleri compose.yaml İçinde Tanımlamak#
Profil tanımı bir listedir ve bir servis birden fazla profile ait olabilir. Aşağıdaki örnekte app ile db her zaman çalışır, adminer yalnızca tools profilinde, seed ise yalnızca init profilinde ayağa kalkar:
services:
app:
image: firmaniz/web:1.4.0
depends_on:
- db
ports:
- "8080:8080"
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD tanimli degil}
volumes:
- dbdata:/var/lib/postgresql/data
adminer:
image: adminer:latest
profiles: ["tools", "dev"] # iki profile birden ait
ports:
- "127.0.0.1:8081:8080" # sadece yerel arayuze bagla
seed:
image: firmaniz/web:1.4.0
profiles: ["init"]
command: ["./bin/seed", "--truncate"]
depends_on:
- db
volumes:
dbdata:
Profili etkinleştirmenin iki yolu vardır. Komut satırında --profile bayrağını istediğiniz kadar tekrarlayabilir, ya da COMPOSE_PROFILES ortam değişkenini virgülle ayrılmış liste olarak verebilirsiniz:
# Sadece varsayilan servisler (app + db)
docker compose up -d
# Gelistirici araclariyla birlikte
docker compose --profile tools up -d
# Birden fazla profil
docker compose --profile tools --profile init up -d
# Ayni sey ortam degiskeniyle - .env dosyasina da yazilabilir
COMPOSE_PROFILES=tools,init docker compose up -d
Üçüncü ve pratikte en çok işe yarayan yol, profilli servisi adıyla hedeflemektir. docker compose run --rm seed yazdığınızda Compose o servisin profilini otomatik olarak etkinleştirir; ayrıca --profile init yazmanıza gerek kalmaz. Migration ve seed işlerini bu şekilde çalıştırmak, onların yanlışlıkla up ile başlamasını yapısal olarak imkânsız kılar.
Dikkat etmeniz gereken tek incelik bağımlılıklardır. Profilsiz bir servis, depends_on ile profilli bir servise bağlıysa ve o profil etkin değilse Compose bağımlılığı çözemez ve hata verir. Çözüm ya bağımlı servisi de aynı profile almak ya da iki profili birlikte etkinleştirmektir. Bu yüzden profilleri "yaprak" servislere (araçlar, tek seferlik işler) uygulamak, çekirdek servislere uygulamaktan çok daha az sürtünme yaratır.
Ortam Değişkenlerinin Compose'a Girdiği Dört Kanal#
Kafa karışıklığının asıl kaynağı, "ortam değişkeni" ifadesinin Compose dünyasında iki ayrı şeyi anlatmasıdır. Birincisi, compose.yaml dosyasının kendi içindeki ${...} yer tutucularını doldurmak için kullanılan değişkenlerdir (interpolasyon). İkincisi, konteynerin içinde uygulamanızın getenv() ile okuyacağı değişkenlerdir. Bu ikisi farklı kanallardan gelir ve birbirine karıştırıldığında saatler kaybedilir.
Toplamda dört kanal vardır ve şu tablo hangisinin neyi etkilediğini özetler:
| Kanal | Neyi etkiler | Örnek |
|---|---|---|
| Kabuk (shell) ortamı | Dosya interpolasyonu | export TAG=1.4.0 && docker compose up |
.env dosyası (proje kökü) | Dosya interpolasyonu | TAG=1.4.0 satırı |
environment: anahtarı | Konteyner içi ortam | environment: { LOG_LEVEL: debug } |
env_file: anahtarı | Konteyner içi ortam | env_file: [./app.env] |
En kritik cümleyi altını çizerek yazıyorum: env_file ile yüklenen değişkenler compose.yaml içindeki ${...} ifadelerini doldurmaz. Onlar yalnızca konteynerin içine enjekte edilir. Aynı şekilde .env dosyasındaki değişkenler otomatik olarak konteynerin içine girmez; yalnızca dosyayı derlerken kullanılır. İnsanların ${DB_PASS} yazıp konteyner içinde boş görmesinin sebebi neredeyse her zaman budur.
Aynı değişkeni her iki tarafta da istiyorsanız, açıkça köprü kurmanız gerekir:
services:
app:
image: firmaniz/web:${TAG:-latest} # .env'den okunur (interpolasyon)
environment:
APP_TAG: ${TAG:-latest} # konteynere de aktarilir
DB_PASSWORD: ${DB_PASSWORD} # ayni deger iki kanalda
.env Dosyası ve Değişken Önceliği#
Compose, komutu çalıştırdığınız proje dizinindeki .env dosyasını otomatik olarak okur. Başka bir dosya kullanmak isterseniz --env-file bayrağını verirsiniz. Dosya biçimi kasten sadedir: her satır ANAHTAR=deger, # ile başlayan satırlar yorumdur, kabuk komut ikamesi ($(...)) çalışmaz. Değerin etrafına tırnak koymak zorunda değilsiniz; koyarsanız tırnaklar sıyrılır.
# .env - proje kokunde durur, ASLA git'e commit edilmez
TAG=1.4.0
DB_PASSWORD=cok-guclu-bir-parola
COMPOSE_PROJECT_NAME=firmaniz
COMPOSE_PROFILES=tools
Öncelik sırası şudur ve bunu ezberlemek yerine test etmeyi öneririm: kabuktaki değişken, .env dosyasındaki değişkeni ezer. Yani .env içinde TAG=1.4.0 yazsa bile TAG=1.5.0 docker compose up dediğinizde 1.5.0 kullanılır. Bu davranış CI hatlarında altın değerindedir; aynı .env dosyası geliştiricide varsayılanı, işlem hattında ise geçersiz kılınabilen bir tabanı temsil eder.
Yer tutucularda varsayılan ve zorunluluk belirtebileceğiniz dört biçim vardır:
image: firmaniz/web:${TAG:-latest} # bos veya tanimsizsa "latest"
image: firmaniz/web:${TAG-latest} # SADECE tanimsizsa "latest"
environment:
DB_PASSWORD: ${DB_PASSWORD:?parola zorunlu} # bos/tanimsizsa hata verir ve durur
DEBUG_FLAG: ${DEBUG:+--verbose} # DEBUG doluysa bu degeri kullan
:? biçimi üretimde hayat kurtarır. Parolayı unuttuğunuzda Compose sessizce boş bir parolayla veritabanı ayağa kaldırmak yerine anlaşılır bir mesajla durur. Kritik her gizli değeri bu biçimde tanımlamayı alışkanlık edinin. Gizli bilgileri .env yerine daha sağlam bir yerde tutmak istiyorsanız Docker'da gizli bilgi yönetimi yazısında Docker secret ve dosya tabanlı yaklaşımları karşılaştırdım.
Ortam Bazlı Override Dosyaları#
Compose, compose.yaml yanında bir compose.override.yaml dosyası varsa onu otomatik olarak ikinci dosya gibi birleştirir. Bu, geliştirme makinenizde ek portlar açmak, kaynak kodu bind mount ile bağlamak ya da imaj yerine yerel derleme kullanmak için idealdir. Üretim sunucusunda bu dosya bulunmaz, dolayısıyla hiçbir riski yoktur.
# compose.override.yaml - sadece gelistirici makinesinde bulunur
services:
app:
build: . # imaj cekmek yerine yerelde derle
volumes:
- ./src:/app/src # canli kod
environment:
LOG_LEVEL: debug
Birden fazla dosyayı elle birleştirmek isterseniz -f bayrağını sırayla verirsiniz; sonraki dosya öncekini ezer. Üretim için tipik komut şöyle görünür:
# Uretim: override dosyasini devre disi birak, uretim dosyasini ekle
docker compose -f compose.yaml -f compose.prod.yaml up -d
# COMPOSE_FILE ile ayni sey, her komutta yazmak zorunda kalmazsiniz
export COMPOSE_FILE=compose.yaml:compose.prod.yaml
docker compose up -d
Birleşmenin sonucunu tahmin etmek yerine görün. docker compose config komutu profilleri, değişkenleri ve dosya birleşimini uyguladıktan sonra ortaya çıkan nihai yapılandırmayı basar. Bir sunucuya deploy etmeden önce bu çıktıya bakmak, benim gördüğüm en yüksek getirili tek alışkanlıktır:
# Nihai birlesik yapilandirmayi goster
docker compose config
# Sadece hangi servislerin calisacagini goster
docker compose --profile tools config --services
# Cozulen degiskenleri gizlemeden gormek icin
docker compose config | grep -A3 environment
Son komutu paylaşılan bir ekranda çalıştırmayın; config çıktısı parolaları çözülmüş hâlde basar. Bu, hem yararlı bir hata ayıklama aracı hem de dikkatsizce ekran görüntüsü alındığında sızıntı kaynağıdır.
Sık Yapılan Hatalar ve Tuzaklar#
.env dosyasını git'e göndermek. Bu, listenin başında olmayı hak eden hatadır çünkü sonucu geri alınamaz: parola bir kez geçmişe girdiyse dosyayı silmek yetmez, parolayı döndürmeniz gerekir. .gitignore içine .env yazın, örnek şablon olarak .env.example tutun ve içine gerçek değer koymayın. Bu kazanın nasıl gerçekleştiğini ve nasıl kapatılacağını .env dosyası ifşası yazısında ayrıntılı anlattım.
Profilli servisi güvenlik önlemi sanmak. Profil, servisin başlatılıp başlatılmayacağını belirler; başladıktan sonra hiçbir ek koruma sağlamaz. Adminer'ı tools profiline koymanız, yanlışlıkla o profili üretimde açtığınızda arayüzün internete kapalı olacağı anlamına gelmez. Port yayınlarken 127.0.0.1:8081:8080 biçimini kullanarak servisi yalnızca yerel arayüze bağlayın; profil bunun yerine geçmez.
.env içinde tırnak ve boşluk beklentisi. .env bir kabuk betiği değildir. PASS="ab cd" yazdığınızda tırnaklar sıyrılır ve değer ab cd olur; ancak PASS=ab cd yazdığınızda da değer ab cd olur, çünkü satırın tamamı alınır. Buna karşılık $(date) çalışmaz, düz metin olarak kalır. Değerde $ karakteri varsa Compose onu değişken sanmasın diye $$ yazın.
Aynı değişkeni iki kanaldan farklı değerlerle vermek. environment: ile env_file: aynı anahtarı taşıyorsa environment: kazanır. Bu mantıklıdır ama bir dosyada tanımladığınızı sandığınız değeri başka yerde ezmek, hata ayıklaması en zor durumlardan biridir. Kural olarak bir anahtarı tek bir kanaldan verin.
Değişkeni imaj etiketinde kullanıp pull unutmak. image: firmaniz/web:${TAG} yazdığınızda TAG değişince Compose yeni imajı otomatik indirmez; konteyneri yeniden oluşturur ama imaj yereldeyse eskisini kullanır. Dağıtım komutunuz docker compose pull && docker compose up -d olmalıdır.
Gerçek Bir Dağıtım Akışı#
Yukarıdaki parçaları birleştirdiğimizde, tek bir depoyla üç ortamı yöneten sade bir düzen ortaya çıkar. Depoda compose.yaml (çekirdek), compose.override.yaml (geliştirici, git'te), compose.prod.yaml (üretim farkları, git'te) ve .env.example (şablon, git'te) bulunur. Sunucuda ise yalnızca gerçek .env dosyası elle oluşturulur ve 600 izniyle korunur.
# Sunucuda ilk kurulum
install -m 600 /dev/null /srv/firmaniz/.env
nano /srv/firmaniz/.env # gercek degerleri gir
# Her dagitimda calisan komut zinciri
cd /srv/firmaniz
docker compose -f compose.yaml -f compose.prod.yaml pull
docker compose -f compose.yaml -f compose.prod.yaml up -d --remove-orphans
# Migration'i profil ile tek seferlik calistir
docker compose -f compose.yaml -f compose.prod.yaml run --rm seed
--remove-orphans bayrağını özellikle ekliyorum: bir servisi dosyadan sildiğinizde Compose onu kendiliğinden durdurmaz, sunucuda yetim bir konteyner olarak kalır. Bu bayrak, dosyada olmayan konteynerleri temizler ve "bu ne zamandır ayakta?" sorusunu ortadan kaldırır. Bu tarz çok servisli kurulumları kendi sunucunuzda çalıştırmak istiyorsanız VDS ve bulut sunucu paketlerimiz tam root erişimiyle gelir.
Sıkça Sorulan Sorular#
Docker Compose profili nasıl varsayılan olarak açık gelir#
Profilin kendisi varsayılan olarak açık gelmez; ama COMPOSE_PROFILES değişkenini .env dosyasına yazarsanız o dizinde çalışan her komut o profili etkin kabul eder. Geliştirici makinelerinde .env içine COMPOSE_PROFILES=dev satırını koymak yaygın bir yöntemdir. Üretim sunucusundaki .env dosyasında bu satır bulunmadığı için aynı depo orada yalın çalışır.
env_file ile environment arasındaki fark nedir#
environment: anahtarını doğrudan compose.yaml içine yazarsınız ve değerleri ${...} ile interpolasyondan alabilirsiniz. env_file: ise harici bir dosyayı okur ve içindeki anahtarları doğrudan konteynere aktarır, ancak Compose dosyasındaki yer tutucuları doldurmaz. Aynı anahtar iki yerde birden tanımlıysa environment: kazanır, bu yüzden bir anahtarı tek kanaldan vermek en sağlıklısıdır.
.env dosyasındaki değişkeni konteyner neden göremiyor#
Çünkü .env dosyası varsayılan olarak yalnızca compose.yaml içindeki ${...} ifadelerini doldurmak için kullanılır, konteynerin içine otomatik enjekte edilmez. Değeri uygulamanızın görmesini istiyorsanız servis tanımına environment: { DB_PASSWORD: ${DB_PASSWORD} } gibi açık bir köprü eklemeniz ya da o dosyayı env_file: ile de yüklemeniz gerekir. docker compose exec app env komutuyla konteynerin gerçekte ne gördüğünü doğrulayabilirsiniz.
Profildeki servisi tek seferlik nasıl çalıştırırım#
docker compose run --rm servis_adi komutu yeterlidir; Compose hedeflediğiniz servisin profilini otomatik olarak etkinleştirir ve --rm sayesinde iş bitince konteyner silinir. Bu, migration, seed ve yedek geri yükleme gibi işler için doğru kalıptır. Aynı işi up ile yaparsanız konteyner çıkış yaptıktan sonra bile listede kalır ve kafa karıştırır.
Compose değişkenlerinin son hâlini nasıl kontrol ederim#
docker compose config komutu bütün dosyaları birleştirir, profilleri uygular ve değişkenleri çözerek nihai yapılandırmayı basar. Yalnızca çalışacak servisleri görmek isterseniz docker compose config --services, tek bir servisin ayrıntısı içinse docker compose config app kullanabilirsiniz. Çıktının parolaları düz metin bastığını unutmayın, paylaşımlı ekranda çalıştırmayın.
Aynı .env dosyasını birden fazla proje için kullanabilir miyim#
Teknik olarak --env-file /ortak/yol/.env ile mümkündür, ancak önerilmez. Ortak bir dosya, bir projenin ihtiyaç duymadığı gizli değerleri o projenin konteynerlerine taşıma riski yaratır ve izinleri yönetmeyi zorlaştırır. Bunun yerine her projenin kendi .env dosyası olsun; gerçekten paylaşılan değerler varsa bunları dağıtım aracınızın şablonundan üretin.
COMPOSE_PROJECT_NAME neyi değiştirir#
Compose, oluşturduğu konteyner, ağ ve hacim adlarının başına proje adını ekler; varsayılan olarak proje adı bulunduğunuz dizinin adıdır. COMPOSE_PROJECT_NAME ile bunu sabitlerseniz, dizini yeniden adlandırdığınızda mevcut hacimlerinizin "kaybolmasını" engellersiniz. Aynı sunucuda aynı depoyu iki kez çalıştırmak istediğinizde de farklı proje adı vererek izole iki yığın elde edersiniz.
Kapanış#
Profiller ve ortam değişkenleri, Compose'un tek dosyayla birden çok ortamı yönetmesini sağlayan iki mekanizmadır ve ikisi de birkaç sağlam alışkanlıkla ehlileşir: profilleri yalnızca yaprak servislere verin, tek seferlik işleri run --rm ile çalıştırın, gizli değerleri ${VAR:?} biçimiyle zorunlu kılın, .env dosyasını asla depoya göndermeyin ve her dağıtımdan önce docker compose config çıktısına bakın. Bu beş madde, üretimde karşılaştığım Compose kaynaklı sorunların büyük çoğunluğunu daha oluşmadan önler.
Bu kurulumları çalıştıracağınız altyapıyı da düşünmek gerekir. Çok servisli bir Compose yığını için tam root erişimli VDS ya da ölçeklenebilir bulut sunucu paketlerimiz uygun bir başlangıçtır; sunucunun kurulumu, güncellemesi ve izlenmesiyle uğraşmak istemiyorsanız sunucu yönetimi hizmetimiz bu işi sizin yerinize üstlenir. Konteynerlerin ürettiği verinin düzenli kopyası için de yedekleme çözümümüze göz atabilirsiniz.