Tüm yazılar
Altyapı 6 dk okuma

Infrastructure Naming & Isolation: Docker Compose Best Practices

COMPOSE_PROJECT_NAME ile Docker Compose proje izolasyonu: varsayılan ad tuzağı, aynı sunucuda çoklu ortam, CI/CD ile branch başına izole stack.

#docker #docker-compose #devops #isolation #ci-cd

Aynı sunucuda birden fazla konteyner projesi çalıştırmaya başladığınız an, Docker Compose proje izolasyonu bir lüks olmaktan çıkıp zorunluluğa dönüşür. Tipik senaryo şöyle: aynı VDS üzerinde iki ayrı iş için iki compose stack'i ayağa kaldırılıyor ve ikisi de deposunu app adlı bir dizinde tutuyor. Sonuç, birinin docker compose down komutunun diğerinin konteynerlerini durdurması oluyor. Sorun Docker'da değil, adlandırmanın şansa bırakılmasında.

Bu yazıda Compose'un proje adını nereden türettiğini, COMPOSE_PROJECT_NAME değişkeni ve -p bayrağının bu adı nasıl kontrol altına aldığını, aynı makinede dev/stage ortamlarını yan yana çalıştırmayı ve CI/CD hattında branch başına izole stack kurmayı anlatacağım. Otomasyon danışmanlığında kurduğum hemen her sistemde bu kalıbı kullanıyorum; örneğin n8n tabanlı Instagram otomasyonu kurulumlarında n8n, veritabanı ve yardımcı servisler tek compose dosyasında yaşar ve proje adı sözleşmesi olmadan bu stack'leri yönetmek kısa sürede kaosa döner.

Baştan bir kavramsal netlik: burada anlattığım şey ad alanı izolasyonudur, güvenlik izolasyonu değil. Doğru adlandırma; konteyner, ağ ve volume çakışmalarını tamamen ortadan kaldırır, ama aynı Docker daemon'ını paylaşan stack'ler kernel seviyesinde birbirinden yalıtılmış olmaz. Güvenlik sınırı gerekiyorsa ayrı makine, ayrı kullanıcı ad alanı veya en azından ayrı daemon düşünmelisiniz. "Yüzde yüz izolasyon" ifadesini bu yazıda yalnızca adlandırma ve kaynak sahipliği bağlamında kullanıyorum.

Varsayılan proje adı nereden gelir, neden tuzaktır

Compose, siz aksini söylemedikçe proje adını compose dosyasının bulunduğu dizinin adından türetir. /opt/app dizinindeki bir stack'in proje adı app olur; konteynerler app-web-1, varsayılan ağ app_default, adlandırılmış volume'lar app_pgdata biçiminde oluşur. Bu davranış tek projeli bir makinede zararsızdır, ama iki koşulda patlar:

  • Aynı dizin adı, farklı projeler: İki ayrı işi app, api veya backend gibi jenerik dizinlerde tutuyorsanız Compose ikisini aynı proje sayar. up komutu diğerinin konteynerlerini "artık tanımda yok" diye silmeye kalkar, down komutu yanlış stack'i indirir.
  • Aynı proje, farklı dizinler: Deploy betiğiniz kodu bir gün /opt/app, ertesi gün /srv/app-v2 dizininden çalıştırırsa Compose bunları iki ayrı proje görür; eski konteynerler sahipsiz kalır, portlar dolu olduğu için yeni stack ayağa kalkamaz.

İkinci senaryo özellikle sinsidir çünkü hata mesajı size adlandırmadan bahsetmez; sadece "port is already allocated" dersiniz ve yanlış yerde saatler harcarsınız. Volume tarafı daha da tehlikelidir: proje adı değiştiğinde Compose yeni ve boş bir volume oluşturur, veritabanınız "kayboldu" sanırsınız. Veri aslında eski adla duran volume'dadır ama bunu fark etmek paniğin ortasında kolay değildir.

COMPOSE_PROJECT_NAME ve -p bayrağı

Çözüm basit ama disiplin ister: proje adını hiçbir zaman dizin adına bırakmayın, her zaman açıkça verin. Bunun üç yolu var ve öncelik sırası şöyledir: komut satırındaki -p bayrağı en güçlüsüdür, onu COMPOSE_PROJECT_NAME ortam değişkeni izler, en sonda compose dosyasındaki üst seviye name: alanı gelir. Ben otomasyonlarda ortam değişkenini tercih ediyorum çünkü betiğin çağırdığı her compose komutuna tek noktadan enjekte edilir ve bir komutta -p yazmayı unutma riski ortadan kalkar.

Proje adının biçimsel kuralları da önemli: yalnızca küçük harf, rakam, tire ve alt çizgi içerebilir, harf veya rakamla başlamalıdır. Büyük harfli bir branch adını doğrudan proje adı yaparsanız Compose hata verir; bu yüzden adı üreten kod her zaman normalize etmelidir. Proje adı verildiğinde tüm türetilmiş kaynaklar öngörülebilir hale gelir: musteri-stage-web-1 konteyneri, musteri-stage_default ağı, musteri-stage_pgdata volume'u. docker ps çıktısına bakan herkes hangi kaynağın hangi işe ait olduğunu tek bakışta görür; görsel temizliğin operasyonel değeri, gece 02:00'de alarm çözerken ortaya çıkar.

Aynı sunucuda dev ve stage'i yan yana çalıştırmak

Her ortama ayrı sunucu almak her zaman gerçekçi değil. Küçük ve orta ölçekli projelerde dev ile stage'i aynı makinede, aynı compose dosyasından çalıştırmak gayet mümkün; tek şart her ortamın kendi proje adıyla ayağa kalkması. Veri zenginleştirme motoru gibi çok bileşenli sistemlerde bu yaklaşım bana ciddi altyapı maliyeti kazandırdı: aynı sunucuda zengin-dev ve zengin-stage stack'leri, her biri kendi ağı, kendi Postgres volume'u ve kendi worker kümesiyle yıllardır sorunsuz koşuyor.

Pratikte dikkat ettiğim üç nokta var:

  • Host portlarını ortam değişkeninden alın: Proje adı ağları ve volume'ları ayırır ama host'a yayınlanan portları ayıramaz; iki stack aynı 8080'i isterse ikincisi başlayamaz. Port numarasını .env dosyasına taşıyıp ortam başına farklı değer veriyorum.
  • Ortam başına ayrı .env dosyası kullanın: --env-file bayrağıyla stage.env veya dev.env seçmek, hem proje adını hem portları hem de sırların referanslarını tek dosyada toplar.
  • Volume adlarını asla elle sabitlemeyin: Compose dosyasında name: ile sabitlenmiş bir volume, proje ön ekini almaz ve iki ortam aynı veriye yazmaya başlar. Bu, fark edilmesi en zor çakışma türüdür.

CI/CD hattında branch başına izole stack

Bu kalıbın asıl gücü CI/CD'de ortaya çıkıyor. Her feature branch'i için sunucuda geçici bir önizleme ortamı ayağa kaldırmak istiyorsanız, proje adını branch'ten türetmeniz yeterli. Aşağıdaki betik tam olarak bunu yapıyor: branch adını Compose'un kabul edeceği biçime normalize ediyor, COMPOSE_PROJECT_NAME değişkenini ortama enjekte ediyor ve stack'i güncelliyor. Betiği pipeline'ın deploy adımında çalıştırıyorum:

import os
import re
import subprocess
import sys


def proje_adi(uygulama, ortam, dal):
    """Branch adindan gecerli bir Compose proje adi uretir."""
    ham = f"{uygulama}-{ortam}-{dal}"
    ad = re.sub(r"[^a-z0-9_-]+", "-", ham.lower())
    ad = ad.strip("-_")[:63]
    if not ad or not ad[0].isalnum():
        sys.exit(f"gecersiz proje adi: {ham!r}")
    return ad


def compose(proje, *argumanlar):
    ortam = dict(os.environ, COMPOSE_PROJECT_NAME=proje)
    komut = ["docker", "compose", *argumanlar]
    sonuc = subprocess.run(komut, env=ortam)
    if sonuc.returncode != 0:
        sys.exit(f"komut basarisiz ({sonuc.returncode}): {' '.join(komut)}")


if __name__ == "__main__":
    dal = os.environ.get("CI_BRANCH", "main")
    proje = proje_adi("veriapp", "onizleme", dal)
    compose(proje, "pull", "--quiet")
    compose(proje, "up", "-d", "--remove-orphans", "--wait")
    print(f"aktif stack: {proje}")

Buradaki kritik ayrıntı dict(os.environ, ...) kullanımı: mevcut ortamı kopyalayıp yalnızca proje adını ekliyoruz, böylece pipeline'ın kendi değişkenleri korunuyor. --remove-orphans bayrağı da önemli; compose dosyasından servis sildiğinizde eski konteynerin ortalıkta kalmasını engeller. Aynı betik merge sonrası temizlik adımında down --volumes argümanlarıyla çağrıldığında, yalnızca o branch'in stack'ini söker.

Temizlik disiplini ve adlandırma sözleşmesi

Branch başına stack kurmanın bedeli, silinmeyi bekleyen stack'lerin birikmesidir. docker compose down -v komutu yalnızca doğru proje adıyla çağrıldığında doğru kaynakları siler; yanlış dizinden veya yanlış -p değeriyle çalıştırılırsa ya hiçbir şey silmez ya da yanlış stack'i indirir. Bu yüzden temizliği insana değil, envanteri denetleyen bir betiğe bırakıyorum. Aşağıdaki betik docker compose ls çıktısını okuyup beklenen ön eklere uymayan "sahipsiz" stack'leri raporluyor:

import json
import subprocess

BEKLENEN_ONEKLER = ("veriapp-prod", "veriapp-stage", "veriapp-onizleme-")


def calisan_stackler():
    sonuc = subprocess.run(
        ["docker", "compose", "ls", "--all", "--format", "json"],
        capture_output=True,
        text=True,
        check=True,
    )
    return json.loads(sonuc.stdout)


def sahipsizleri_bul(stackler):
    sahipsiz = []
    for stack in stackler:
        ad = stack.get("Name", "")
        if not ad.startswith(BEKLENEN_ONEKLER):
            sahipsiz.append((ad, stack.get("Status", "bilinmiyor")))
    return sahipsiz


if __name__ == "__main__":
    bulgular = sahipsizleri_bul(calisan_stackler())
    if not bulgular:
        print("sahipsiz stack yok, envanter temiz")
    for ad, durum in bulgular:
        print(f"SAHIPSIZ: {ad} ({durum})")
        print(f"  temizlik: docker compose -p {ad} down --volumes")

Betik kasıtlı olarak silmiyor, yalnızca raporluyor ve doğru temizlik komutunu öneriyor; volume silmek geri döndürülemez bir işlem olduğu için son kararı insana bırakmak bence doğru denge. Cron ile günde bir çalıştırıp çıktıyı bildirime bağlamak yeterli.

Adlandırma sözleşmesi olarak yıllardır şu kalıbı kullanıyorum ve tavsiye ederim: uygulama-ortam kalıcı ortamlar için (veriapp-prod, veriapp-stage), uygulama-onizleme-branch geçici olanlar için. Kurallar basit: yalnızca küçük harf ve tire, jenerik kelime yok (app, test, deneme yasak), ad her zaman betikle üretilir ve elle yazılmaz. Sözleşme ne kadar sıkıcıysa o kadar iyi çalışır.

Özetlemek gerekirse: Compose'un dizin adından türettiği varsayılan proje adı, çoklu proje barındıran her sunucuda er ya da geç çakışma üretir. COMPOSE_PROJECT_NAME ile adı açıkça vermek; konteyner, ağ ve volume ad alanlarını tamamen ayırır, CI/CD'de branch başına önizleme ortamını mümkün kılar ve temizliği betikleştirilebilir hale getirir. Kendi altyapınızda benzer bir düzen kurmak veya mevcut kaosu toparlamak istiyorsanız, danışmanlık hizmetlerimi anlattığım ana sayfa üzerinden /#contact bölümüne uğrayıp bana yazabilirsiniz; bu tür altyapı düzenlemeleri çoğu zaman bir günlük çalışmayla yıllarca sürecek baş ağrısını önlüyor.