Docker'a alma: Uygulamayı her yerde aynı çalışan bir pakete koymak
Uygulamayı Docker'a almayı işliyoruz: container nedir, "benim makinemde çalışıyordu" sorununu nasıl çözer, Dockerfile ve multi-stage build ile .NET uygulamasını nasıl paketleriz ve imajı nasıl çalıştırırız.
API'n hazır: doğru çalışıyor, hızlı, güvenli ve sağlığını bildiriyor. Şimdi onu kendi makinenden çıkarıp dünyaya açma zamanı. Ama burada klasik bir sorun devreye girer: senin makinende çalışan bir şey, başka bir makinede çalışmayabilir. Farklı işletim sistemi, eksik bir kütüphane, farklı bir .NET sürümü... İşte Docker tam bu kabusu bitirmek için var. Bugün uygulamayı, her yerde aynı çalışan taşınabilir bir pakete koymayı konuşuyoruz.
"Benim Makinemde Çalışıyordu" Sorunu
Her geliştiricinin başına gelen bir hikâye var. Kodun senin bilgisayarında kusursuz çalışır. Onu sunucuya taşırsın ve bir anda her şey patlar. Sebep genelde ortam farkıdır: senin makinende olan ama sunucuda olmayan bir ayar, bir sürüm, bir bağımlılık. Saatlerce "ama bende çalışıyordu" diye söylenirsin. Bu, yazılım dünyasının en eski ve en sinir bozucu problemlerinden biridir.
Sorunun kökü şu: uygulama tek başına yeterli değil; çalışmak için etrafındaki ortama da ihtiyaç duyar. O ortam bir makineden diğerine değiştiğinde, uygulama da farklı davranır. Peki ya uygulamayı, ihtiyaç duyduğu her şeyle birlikte tek bir pakete koyabilseydik? Nereye gidersek gidelim, o paketi açtığımızda içindeki her şey tıpatıp aynı olsaydı? İşte container fikri budur.
Container Nedir?
Container, uygulamanı ve onun çalışması için gereken her şeyi (kod, çalışma ortamı, kütüphaneler, ayarlar) tek bir kendi kendine yeten pakete koymaktır. Bu paket, hangi makinede çalışırsa çalışsın, içindekiler değişmez. Senin makinende nasıl davranıyorsa, sunucuda da aynen öyle davranır. Ortam farkı diye bir şey kalmaz, çünkü ortamı da paketin içine koymuşsundur.
Bunu bir gemi konteyneri gibi düşün; zaten adı da oradan geliyor. Eskiden bir malı gemiyle taşımak karmaşıktı; her yük farklı boyutta, farklı biçimdeydi. Standart konteynerler her şeyi değiştirdi: içine ne koyarsan koy, konteynerin dışı hep aynı. Gemi, vinç, kamyon; hepsi konteynerle nasıl çalışacağını bilir, içinde ne olduğunu umursamaz. Docker container'ı da yazılım için bu standart kutudur: içinde ne olursa olsun, dışarıdan hep aynı şekilde taşınır ve çalıştırılır.
Docker İmajı ve Container
İki terimi ayırmak önemli. Docker imajı, uygulamanın dondurulmuş bir kalıbıdır; içinde uygulaman ve tüm ihtiyaçları paketlenmiş halde durur. Container ise bu imajın çalışan bir örneğidir. İlişkiyi şöyle düşün: imaj bir sınıf gibidir, container ise o sınıftan üretilmiş bir nesne. Bir imajdan istediğin kadar container çalıştırabilirsin; hepsi aynı kalıptan gelir, hepsi birbirinin aynıdır.
Bu ayrım şunun için önemli: imajı bir kez oluşturursun, sonra onu istediğin yere taşır ve orada container olarak çalıştırırsın. İmaj değişmez ve taşınabilir; container ise onun canlı, çalışan halidir. Yani "imaj oluştur, her yere taşı, orada çalıştır" akışı, tüm dağıtım mantığının temelidir.
Dockerfile: Paketleme Tarifi
Docker'a uygulamanı nasıl paketleyeceğini bir tarif dosyasıyla anlatırsın; bu dosyanın adı Dockerfile'dır. İçinde adım adım talimatlar bulunur: hangi temel ortamdan başlanacak, kod nereye kopyalanacak, nasıl derlenecek, nasıl çalıştırılacak. .NET uygulamaları için tipik bir Dockerfile şöyle görünür:
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish -c Release -o /app
FROM mcr.microsoft.com/dotnet/aspnet:9.0
WORKDIR /app
COPY --from=build /app .
ENTRYPOINT ["dotnet", "KullaniciApi.dll"]
Bu dosya iki bölümden oluşuyor ve buna multi-stage build denir. İlk bölüm, içinde .NET SDK bulunan bir ortamdan başlar; kodu kopyalar ve uygulamayı derler. İkinci bölüm ise çok daha küçük bir ortamdan (sadece uygulamayı çalıştırmaya yeten, derleme araçları olmayan) başlar ve ilk bölümde derlenmiş çıktıyı buraya kopyalar. En sondaki ENTRYPOINT, container çalıştığında hangi komutun işletileceğini söyler.
Multi-Stage Build Neden Önemli?
Bu iki aşamalı yapının mantığını anlamak değerli. Uygulamayı derlemek için koca bir SDK'ya ihtiyacın var; ama uygulamayı sadece çalıştırmak için o kadarına gerek yok, çok daha küçük bir çalışma ortamı yeter. Multi-stage build, derlemeyi büyük ortamda yapar ama sonuç imajına sadece çalışması için gerekeni koyar.
Bir mobilya yapımını düşün. Mobilyayı üretirken bir sürü alet, tezgah, hammadde kullanırsın; koca bir atölye gerekir. Ama müşteriye teslim ederken atölyeyi değil, sadece bitmiş mobilyayı gönderirsin. Multi-stage build de tam bunu yapar: derleme "atölyesi" ilk aşamada kalır, son imaja sadece bitmiş "mobilya" yani çalışan uygulama girer. Sonuç: çok daha küçük, daha hızlı taşınan ve daha güvenli bir imaj. Küçük imaj hem depolamada hem taşımada avantaj sağlar, hem de içinde daha az şey olduğu için saldırı yüzeyi daralır.
İmajı Oluşturmak ve Çalıştırmak
Dockerfile hazır olduğunda, imajı tek bir komutla oluşturursun. Bu komut, tarifi baştan sona uygular ve sonunda çalışmaya hazır bir imaj üretir:
docker build -t kullanici-api .
Buradaki -t kullanici-api, imaja bir isim (etiket) verir; sonundaki nokta ise "tarifi bu klasörde ara" demektir. İmaj oluştuktan sonra onu bir container olarak çalıştırırsın:
docker run -p 8080:8080 kullanici-api
Buradaki -p 8080:8080, container'ın içindeki port'u senin makinenin bir port'una bağlar; böylece dışarıdan uygulamaya erişebilirsin. Bu komutu çalıştırdığın an, uygulaman container içinde ayağa kalkar. İşin güzelliği şu: bu container'ı senin makinende de, bir sunucuda da, bir bulut ortamında da çalıştırsan, içindeki her şey tıpatıp aynı davranır. "Benim makinemde çalışıyordu" cümlesi artık anlamını yitirir.
Küçük Bir Deneme
Önce makinene Docker'ı kur. Sonra projenin kök dizinine yukarıdaki gibi bir Dockerfile ekle (dosya ve proje adlarını kendi projene göre uyarla). docker build komutuyla imajı oluştur ve komutun her adımı nasıl işlediğini izle. Ardından docker run ile container'ı çalıştır ve tarayıcıdan uygulamana eriş; tıpkı yerelde çalıştırdığın gibi cevap verdiğini gör. İstersen bir adım ileri git: yirmi yedinci yazıda eklediğin /health endpoint'ine container üzerinden istek at ve sağlık kontrolünün konteyner ortamında da çalıştığını gözlemle. Bu deney, dağıtımın soyut bir kavram olmaktan çıkıp elinde tuttuğun somut bir pakete dönüşmesini sağlar.
Bir sonraki yazıda bu paketleme işini otomatikleştireceğiz. CI/CD konusuna geçecek ve GitHub Actions ile, kodu her güncellediğinde uygulamanın otomatik olarak test edilip derlenmesini ve dağıtıma hazır hale gelmesini konuşacağız. Bugün elle yaptığımız Docker adımları, orada otomatik bir sürecin parçası olacak.