MVC Dockerize — Docker #S1B3

Cihat Solak
lTunes Tribe
Published in
8 min readOct 9, 2021

Komut Satırı — Docker #S1B2 bölümünden sonra MVC Core projemizi nasıl dockerize edeceğimiz konusunu ele alacağız. Ayrıca Dockerfile 📄 dosyasını adım adım rötuşlayarak daha temiz ve gerçek hayata yakın hale getireceğiz.

MVC Dockerize — Net Command Line Interface
MVC Dockerize — Net Command Line Interface

NET Command Line Interface (CLI) Nedir?

Net Core Software Development Kit (SDK) kurulumunu gerçekleştirdiğimizde varsayılan olarak gelen komut satır ara yüzüdür. Bu komutlar yardımıyla uygulama oluşturabilir dotnet new, build edebilir dotnet buildveya publish dotnet publishedebiliriz. Daha fazlası için buradan inceleme yapabilirsiniz.

#S1B1 ve #S1B2 bölümlerinde projemizi manuel (el ile) publish alıp, publish klasörünün dosya yolunu Dockerfile içerisinde belirtiyorduk. Bu durumunu otomatize etmek için Net CLI komutlarının yardımıyla Dockerfile içerisinde uygulamayı publish edebiliriz. Biraz sabır o kısma da geleceğiz.

İçerik içerisinde 2 adet CLI komutu kullanacağız.
dotnet restore projenin kullanmış olduğu library, kütüphane gibi parçalarını kontrol edecek varsa bunların güncellemesini ve en son hallerinin alınmasını sağlayacaktır.

dotnet publish projenin publish edilmesini sağlacaktır.

.NET CLI komutlarını Dockerfile içerisinde kullanabilmek için .NET SDK’ya ihtiyaç vardır. Runtime ile CLI komutları kullanılamaz.

Know How — Meslek Sırrı 🏋🏻‍♂️

Web projesi oluşturduğumuzda projenin dizin içerisinde bin, obj, .exe ve .dll parçaları bulunmaktadır.

Obj: Proje build edildiğinde geçici dosyaların oluşturulduğu ve depolandığı yerdir. Amaç, sonraki build işlemlerinde değişmeyen kısımlar var ise bunu obj içerisinden alarak build performansını arttırmaktır. Kabaca build işlemiyle ilgili geçici dosyaların depolandığı yerdir.

NetCoreDockerizeWebApp.exe: Bağımsız olarak çalıştırabileceğiniz dosyadır.

NetCoreDockerizeWebApp.dll: Herhangi bir proje içerisinden çağırabileceğim dosyadır. Örneğin Dockerfile içerisinde dll dosyasını ekleriz ve container ayağa kalkarken bu .dll’i kullanır.

Publish Modları

◾️ Framework-Dependent: Projenin framework’e bağımlı olduğu durumdur. Projenin çalıştırılacağı ortamda mutlaka geliştirilen net runtime/sdk ortamının bulunması gerektiği anlamına gelir.

◾ ️Self-Contained: İhtiyaç duyulan tüm .dll dosyalarının dahil edildiği moddur. Yani çalışacak sistemde net core’un yüklü olması gerekmez. Çünkü publish dosyalarında kendisi için tüm gereklilikleri bulundurur. Fakat dosya boyutu arttığı için çok tercih edilen bir yöntem değildir.

Adım 1: Dockerfile Oluşturalım

Projemiz katmanlı mimariye sahip olmadığından dolayı Dockerfile dosyasını root içerisinde oluşturacağım fakat birden fazla library sahip olunduğu durumda Dockerfile dosyasını tüm projeleri/libraryleri kapsayacak şekilde bir üst klasöre taşımak fayda sağlayacaktır.

Dockerfile dosyasındaki her bir satır, Image tarafında bir katman (layer)’a karşılık gelmektedir.

Docker hub üzerinde .net runtime nasıl indirilir?
.Net Runtime — Docker Hub

Image oluştururken genellikle var olan Image’lerden başlanır. Bizde Net Runtime image’inden başlayabilmek için Dockerhub (Registry Servis) üzerinden uygulamamızı çalıştırabileceğimiz .Net Runtime image’ini Dockerfile dosyasına ekliyoruz. Hazır image kullanmak performans ve hız 🚀 açısından fayda sağlayacaktır. Bu içerikte registry servisi olarak docker hub kullanmama rağmen dilerseniz farklı firmalarında registry hizmetlerinden faydalanabilirsiniz. (Örn: Azure Container Registry)

Aşağıda basit Dockerfile dosyasını görüyoruz. COPY komutundan da farkedileceği üzere el ile publish edilmiş dosya yolunu belirtmişiz. Bu kısmı ileri safhalarda değiştireceğiz.

FROM mcr.microsoft.com/dotnet/aspnet:5.0
WORKDIR /app
COPY /bin/Release/net5.0/publish .
ENTRYPOINT ["dotnet", "NetCoreDockerizeWebApp.dll"]

Dockerfile dosyamızı hazırladığımıza göre artık build komutuyla beraber image oluşturabiliriz.

Docker file dosyasından image nasıl oluşturulur?
docker buıild -t { imageName} {dockerfiledosyayolu}

Dockerfile dosyamızı hazırladık, build ederek image oluşturduk sıra bu image’den container oluşturmaktır. Daha önceki yazılarda bahsettiğim üzere run komutu hem container oluşturur hem de oluşturduğu container’ı ayağa kaldırır.

docker run -p 5000:80 aspnetcoremvc:v1 ile container oluşturup ayağa kaldırıyoruz. -p port belirtmek için kullanıyorum. 5000 portu işletim sistemim için vermiş olduğum port, 80 ise container içerisindeki uygulamanın portu. Sonuç olarak container da kendi içerisinde mini bir işletim sistemi ve bu işletim sistemi üzerinde projemizin çalışabilmesi için gerekli ortam ile projenin publish edilmiş dosyaları bulunuyor. 5000:80 yazımındaki amaç container’a bağlanmak için -p ile beraber iki portu birbirine bağlıyoruz.

docker run -p 5000:80 aspnetcoremvc:v1
docker run -p 5000:80 aspnetcoremvc:v1

http://localhost:5000/ adresinden uygulamaya erişebiliriz. Şimdi aynı image’den bir container daha ayağa kaldıralım. docker run -d -p 5001:80 aspnetcoremvc:v1 buradaki -d (detach) parametresiyle container yoksa oluşturacak, çalıştıracak fakat container’a bağlanmayacak.

docker run -d -p {işletim sistemi port}:{container port} {imageName}

Container ayağa kaldırırken container’a spesifik bir isim belirlemediğimizden ötürü rastgele isimlendirmeler yapıldı.

Yukarıdaki görseli incelediğimizdedocker ps komutuyla çalışır durumda containerları görüyoruz. (2 Adet). Bu containerlar birbirinden bağımsız olduğundan dolayı her iki containerda ayağa kalktığı zaman varsayılan olarak 80 portundan ayağa kalkıyor. Bunun sebebi container ayağa kaldırırken 5000:80 işletim sistemi üzerindeki portlar ile container içerisindeki portları haberleştirmemden ☎️ dolayıdır.

Adım 2: Dockerfile Dosyasını Geliştirelim

FROM mcr.microsoft.com/dotnet/sdk:5.0
WORKDIR /app
COPY . .
RUN dotnet restore
RUN dotnet publish NetCoreDockerizeWebApp.csproj -c Release -o out
WORKDIR out
ENV ASPNETCORE_URLS="http://*:2500"
ENTRYPOINT ["dotnet", "NetCoreDockerizeWebApp.dll"]

COPY . . Copy komutundaki ilk nokta, dockerfile dosyasının bulunmuş olduğu dizindeki tüm dosyaları ifade ediyor. İkinci nokta ise image içerisindeki o anki çalışma dosyasını ifade ediyor. WORKDIR komutuyla app klasörü açıp konumlandığımız için yukarıdaki COPY komutundaki ikinci nokta app klasörünü ifade etmektedir. Kopyalama ile beraber tüm dosyalar image’in içerisindeki app klasörüne kopyalanacaktır.

RUN dotnet restorepublish öncesinde güncellenecek kütüphane varsa ya da yeni bir kütüphane eklenmiş fakat Nuget Package Manager üzerinden çekilmemişse, restore işlemi ile bu tür işlemler gerçekleştirilir.

RUN dotnet publishpublish işlemini gerçekleştireceğim komuttur.

Adım 2.1: Komutları Detaylı İnceleyelim

RUN dotnet publish NetCoreDockerizeWebApp.csproj -c Release -o out

NetCoreDockerizeWebApp.csproj aslında projenin kalbidir 💖 desek pek de yanlış olmayacağını düşünüyorum. İçerisinde proje ile ilgili tüm bilgileri barındırır. Örneğin projeye yeni bir paket eklediğinizde .csproj içerisine bu paket eklenecektir.

-c Release projeyi hangi modda publish edeceğimizi belirtiyoruz.

-o out projeyi out klasörüne publish edeceğimizi belirtiyoruz. Eğer burada /out deseydim, / (slash) karakteriyle başladığı için app klasörü içerisinde oluşturmaz yeni bir klasör oluştururdu. Eğer WORKDIR ile konumlandığımız klasör içerisinde out klasörü oluşturup oluşturulan klasöre publish etmek istiyorsak /(slash) karakteri kullanmamalıyız. Kısaca bu komut app içerisinde out isimli klasör oluşturacak ve içerisine projenin publish edilmiş dosyalarını atıcaktır.

WORKDIR out komutuyla beraber out klasörüne konumlanıyorum.

ENV ASPNETCORE_URLS=http://*:2500 burada environment belirtiyorum. Containerlar kendi içerisinde bir bilgisayar ve bu bilgisayarın içerisinde de küçük bir işletim sistemi barındırır şeklinde düşünelim. Bu işletim sistemi üzerinde SDK ve SDK üzerinde de uygulamamızın publish edilmiş dosyaları yer alıyor. Bu durumda uygulama container içerisinde localhostta ayağa kalkacağı için uygulamaya erişemeyiz. Çünkü container içerisinde local diye bir kavram yok. (Canlı ortam gibi düşünebiliriz.) Container içerisindeki uygulamaya dışarıdan erişebilmek için bir ip adresi üzerinden çalışması gereklidir. Bu durumda da container’ın ip adresini bilmediğimiz için * (yıldız) karakterini kullanarak senin ip adresin herhangi bir adres olabilir fakat portun bu olacak diyoruz.

Tekrar düzenlediğimiz Dockerfile dosyasından image oluşturalım.

Bir image ile iki farklı container oluşturmak
Bir image ile iki farklı container oluşturmak

docker run -p 5000:2000 --name newtestcontainer2 aspnetcoremvc:v2

5000 portu işletim sistemini, 2000 portu ise container içerisindeki portu temsil eder. Container içerisindeki uygulamaya erişebilmek için portları birbirine eşitliyorum. Bu şu demek oluyor: ben 5000 portuna gittiğimde aslında container içerisindeki 2500 portuna 🛫 karşılık gelecektir.

Adım 3: Dockerfile Dosyasını Bir Üst Seviyeye Çıkaralım

MultiStage Build

Dockerfile içerisinde uygulama inşaa 🏗 ederken birden fazla base image’dan faydalanmaya verilen addır.

Neden birden fazla image’e ihtiyaç duyayım? 🤔

Adım 2'de Net CLI komutlarını kullabilmek için SDK image’inden faydalanarak containerlar ayağa kaldırdık. Tag olarak v2 olan image’ı SDK image’ından, v1 olanı Runtime image’ından oluşturdum. Her iki image arasındaki boyut farkı yaklaşık 3.8 kat. 😯 Tag V2 olan image’ın dosya boyutu büyük olduğundan bu image’dan container ayağa kaldırmak veya bir yerden diğer yere taşımak zor olacaktır. Bu nedenle bu image özelinde optimizasyon yapılmalı ve image boyutunu küçültmeliyiz. İşte burada MultiStage build devreye giriyor.

FROM mcr.microsoft.com/dotnet/sdk:5.0 as sdkbuild
WORKDIR /app
COPY . .
RUN dotnet restore
RUN dotnet publish NetCoreDockerizeWebApp.csproj -c Release -o out
FROM mcr.microsoft.com/dotnet/aspnet:5.0
WORKDIR /app
COPY -–from=sdkbuild /app/out .
ENV ASPNETCORE_URLS="http://*:2500"
ENTRYPOINT ["dotnet", "NetCoreDockerizeWebApp.dll"]

Dockerfile dosyamızı yeniden revize ettik. Farkedildiği üzere 2 farklı image kullandım. İlk image ile uygulamanın publish işlemini gerçekleştireceğim, ikinci image ile uygulamayı çalıştıracağım ve böylece sdk’ya sahip büyük boyutlu image’dan kurtulacağım.

as sdkbuild bu komut ile image’a takma isim (alias) veriyorum. İkinci evrede docker hub üzerinden net core runtime image’ini FROM komutuyla aldık.
Bu işlemden sonra haliyle sdkbuild içerisinde, app içerisindeki out klasörüne publish edilmiş dosyaları runtime image içerisine kopyalamamız gereklidir. Bunu copy komutuyla yapacağız. WORKDIR komutuyla app klasörüne konumlanıyorum ve ardından COPY --from=sdkbuild /app/out . komutuyla sdkbuild image’inin /app/out klasörünün içeriğini runtime içerisindeki app klasörüne kopyalıyorum.

Buraya kadar her şey okey gibi görünse de son bir rötuş yapmamızda fayda var. Devam!

Adım 4: Dockerfile Dosyasına Rötuş: Optimizasyon

Şimdi Dockerfile içerisinde İlk evrede COPY . . komutuyla tüm klasörleri app klasörü içerisine kopyalıyoruz. Dockerfile içerisindeki her satır image içerisinde bir katman oluşturacaktır. Şöyle ki projenizde ufak bir değişiklik (örneğin JS dosyasında function ismini değiştirdiniz) yaptığınızda COPY . . katmanı cache’den çekmeyecek ve tekrar yeniden bir katman oluşturacaktır. Bu durumda beraberinde performans kaybı 🐢 yaşatacaktır.

FROM mcr.microsoft.com/dotnet/sdk:5.0 as sdkbuild
WORKDIR /app
COPY *.csproj .
RUN dotnet restore
COPY . .
RUN dotnet publish NetCoreDockerizeWebApp.csproj -c Release -o out
FROM mcr.microsoft.com/dotnet/aspnet:5.0
WORKDIR /app
COPY --from=sdkbuild /app/out .
ENV ASPNETCORE_URLS="http://*:2500"
ENTRYPOINT ["dotnet", "NetCoreDockerizeWebApp.dll"]

Yeniden Dockerfile dosyamızı revize ettik.

COPY *.csproj . csproj dosyasını app klasörü içerisine kopyalayacak komuttur. Ardından restore komutu ve onun ardından da COPY . . komutuyla tüm dosyaların kopyalanması işlemi birbirini takip ediyor.

Buradaki mantık şudur; örneğin projeye yeni bir controller eklediğinizde ya da nuget üzerinden yeni bir paket eklediğinizde .csproj bu durumdan etkilenecektir. Projemizin .dll ile ilgisi olduğundan dolayı COPY *.csproj . katmanı sıfırdan oluşacak fakat diğer dosyalarda herhangi bir değişiklik olmadığından dolayı COPY . . katmanı, sıfırdan dosyaları kopyalamak yerine cache de kopyalanmış olan dosyaları kullanacaktır.

Örneğin projede wwwroot içerisine yeni bir CSS/JS dosyası ekledik. Bu durumda COPY *.csproj . komutu ve RUN dotnet restore komutu cache üzerinden getirilecektir. Neden? Çünkü .csproj da bir değişiklik yapmadık projeye statik dosya ekledik. Bu durumda da COPY . . komutu artık cache’den değil yeniden sıfırdan kopyalama işlemi gerçekleştirecektir.

Docker her bir katmanı (layer) cachelemesindeki temel sebeplerden biri performansdır. Dockerfile içerisinde katman katman kodlamanız performans ve beraberinde hızlı şekilde image oluşturmanıza olanak sağlar.

docker build -t aspnetcoremvc:v3 . komutuyla beraber image boyutunu aspnetcoremvc:v3 857 MB’dan 224MB’a düşürdük.

Tag -> V2: Net SDK

Tag -> V3: Net Runtime

Docker içerisinde image’ları oluştururken Net CLI komutlarını kullanabilmek için Net Core SDK kullanmalıyız. Projenizin publish edilmiş dosyalarını elde ettikten sonra Net Core Runtime image’i üzerinden kodlamamıza devam ederek image boyutunda ciddi düşüş sağlayabiliriz.

.dockerignore

.dockerignore aslında aşina olduğumuz .gitignore ile benzer işleve sahiptir. Dockerfile içerisinde kopyalama komutları yazdığımız zaman (image içerisindeki herhangi bir klasöre kopyalama işlemi gerçekleştirildiğinde) kopyalanmasını ya da taşınmasını istemediğimiz dosyaları .dockerignore ile belirliyoruz.

Peki bunlar hangi dosyalar olabilir?

Projenin kök dizininde yer alan bin ve obj dosyalarının image içerisine taşınmasına gerek yoktur. Çünkü obj dosyası build işlemindeki performans açısından önemliydi, bin klasörüne ise publish işlemini image içerisinde zaten gerçekleştirdiğimden dolayı ihtiyaç duymuyorum.

**/bin çift yıldız, projemde ki tüm klasörleri ara (iç içe klasör yapıları da olabilir) herhangi bir klasör içerisinde bin klasörü var ise bunu dahil etme anlamındadır.

**/Dockerfile* İlk iki yıldız Dockerfile dosyasının herhangi bir konumda olabileceğini söylüyor Sondaki yıldız ise Dockerfile dosya adından sonra ne gelirse gelsin bunların hiçbirini image içerisine taşımayacağı anlamına gelmektedir.

Aşağıda örnek .dockerignore dosyası bırakıyorum.

**/.classpath
**/.dockerignore*
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md

--

--