Docker Nedir? Nasıl Kullanılır?
Uygulamalar zamanında fiziksel sunucular üzerinde yapılandırılarak barındırılmaktaydı. İzole uygulamalar ilave sunuculara ihtiyaç duymaktadır. Bu süreç hem maliyeti arttırıyor hem de deployment işlemlerini yavaşlatarak yönetimi zorlaştırıyordu. Sanallaştırmanın hayatımıza girmesiyle sunucularda birden fazla işletim sistemi ayağa kaldırılarak kaynaklar daha verimli kullanılmaya başlandı. Ölçeklendirme ve deployment işlemleri hızlansada her sanal makinanın işletim sistemi gerektirmesi, taşınabilirlik gibi sorunlar devam etmekteydi. Bu gibi sorunlar container-based virtualization ile çözüme kavuşmuştur. Go diliyle yazılan Docker bu noktada karşımıza çıkmaktadır.
Docker, işletim sistemi üzerine eklediği Container Engine ile uygulamaları ihtiyaç duyduğu paketlerle çalıştırmaktadır. Bu yapıyla birlikte dağıtım ve taşınabilirlik sorunları çözülürken uygulamalar arasında izolasyon da sağlanmaktadır.
Docker client-server mimarisini kullanır. Kullanıcı client aracılığıyla daemon ile iletişim kurar. Client (kinematic, command-line) aldığı komutları Daemon’a ileten kullanıcı arabirimidir. Daemon (docker server, docker engine) host üzerinde sürekli çalışarak container run, build ve distribution işlemlerini üstlenmektedir.
Image ve container olmak üzere iki ana bölümden oluşmaktadır. Aralarındaki ayrıma class ve instance ilişkisi örnek gösterilebilir. Image bir sınıf şeklinde düşünülürse sınıfın örneklenmesiyle container oluşmaktadır.
Kurulum versiyondan versiyona farklılık gösterebileceğinden resmi sitesindeki adımlara göre kurulum sağlanmalıdır.
Images
İmajlar, oluşturulacak her container için read-only şablon görevindedir. Uygulama ve gerekli ortam bilgilerini (işletim sistemi, runtime, v.b.) içermektedir. Container oluşturulmaksızın kendi kendilerine çalıştırılamazlar. Dockerfile kullanılarak oluşturulmaktadırlar.
İmajlar özel bir Registry üzerinde depolanacağı gibi Docker Registry üzerinde depolanabilmektedirler. Registry içerisinde yer alan her repository çeşitli tag’lara göre derlenmiş sürümlerden oluşmaktadır. Imaj oluşturulurken bir tag belirtilmemesi durumunda varsayılan tag latest olacaktır.
docker build . | Dockerfile adımlarına göre bir imaj oluşturur. -t NAME:TAG parametresiyle oluşturulacak imaja bir isim ve tag verilmektedir. |
docker run IMAGE | IMAGE ile belirtilen imajdan bir container oluşturarak başlatır. Mevcut imaj çalışma ortamında bulunmuyorsa registry üzerinden indirilir. --name NAME parametresiyle oluşturulacak container’a bir isim verilir. -p OUT:IN parametresiyle kullanılacak iç ve dış portlar belirtilir. -d parametresi kullanılarak container detached mod ile başlatılırsa konsol container’ın durmasını beklemez. -it parametresiyle container interactive mod ile başlatılarak container’a konsol üzerinden komut gönderilebilir. --rm parametresiyle container durduğunda otomatik silinmesi sağlanır. --env KEY=VALUE parametresiyle environment variable ayarlanabilir, toplu şekilde bir dosya ile göndermek için --env-file PATH parametresi kullanılmalıdır. -v PATH parametresiyle bir anonymous volume, -v NAME:PATH ile bir named volume, -v "FROM:TO" ile bir bind mount oluşturulur. -v $(pwd):/app şeklinde kullanılabilir. |
docker images | Çalışma ortamındaki imajları listeler. |
docker rmi IMAGE | IMAGE ile belirtilen (name, id) imajı siler. |
docker image prune | Bir taga sahip olmayan (untagged) tüm imajları siler. -a parametresi kullanılırsa tüm imajlar silinir. |
docker image inspect IMAGE | IMAGE ile belirtilen imaj metadata bilgilerini döner. |
docker tag NAME NEW_NAME | NAME ile belirtlen imaj ismini NEW_NAME olarak değiştirir. |
docker login | Docker Hub hesabına giriş yapılmasını sağlar. |
docker logout | Docker Hub hesabından çıkış yapılmasını sağlar. |
docker push REPOSITORY/IMAGE | Registry’e belirtilen imajı gönderir. |
docker pull REPOSITORY/IMAGE | Registry’den belirtilen imajı çeker. |
Containers
Imajların çalışan örnekleridir. Bir container oluşturulduğunda imajın üstüne ince bir read-write katmanı eklenmektedir. Aynı imaja bağlı birden fazla container birbirinden izole şekilde ayağa kaldırılabilir. Uygulamalar arası herhangi bir state veya data paylaşımı olmaz.
docker ps | Çalışan tüm container’ları listeler. -a parametresi kullanılırsa durmuş container’lar da listeye dahil edilir. |
docker create [OPTIONS] IMAGE | IMAGE ile belirtilen imajdan bir container oluşturur. -n parametresiyle bir container ismi verilir. |
docker start CONTAINER | CONTAINER ile belirtilen container’i detached olarak çalıştırır. -a parametresi kullanılırsa attached olarak çalıştırılır. |
docker stop CONTAINER | CONTAINER ile belirtilen container’ı durdurur. |
docker attach CONTAINER | detached olarak çalışan CONTAINER ismindeki container’ı attach eder. |
docker logs CONTAINER | CONTAINER ile belirtilen container’in geçmiş log kayıtlarını listeler. -f parametresiyle yeni kayıtları dinlemeye devam eder. |
docker rm CONTAINER | CONTAINER ile belirtilen (name, id) container’ı siler. |
docker container prune | Çalışmayan tüm container’ları siler. |
docker exec PARAMS CNTR CMD | CNTR isimli container içerisinde CMD ile belirtilen komutu çalıştırmak için kullanılır. PARAMS yerine -it değeri ve CMD yerine /bin/bash değeri verilirse container içerisinde terminal açar. |
docker cp SRC CONTAINER:DEST | SRC ile belirtilen dizinden CONTAINER ile belirtilen container içerisindeki DEST dizinine dosyaları kopyalar. Parametreler yer değiştirebilir, CONTAINER:SRC DEST ile container içerisinden dışarıya kopyalama yapılır. |
Volumes
Imajlar bir kez oluşturulduğunda read-only olduklarından değiştirilemezler. Bir değişiklik için yeniden build edilmelidirler. Container’lar imajların üstüne ince bir read-write katmanı eklediklerinden okuma ve yazma yapabilirler. Bu imajı değiştirmeden imaj içerisinde dosya işlemleri yapılabileceği anlamına gelir. Okuma yazma işlemi yapılabilmesine rağmen bir kaç sorun mevcuttur. Container’ın silinmesi durumunda yazılmış veriler de kaybolacaktır, bu sorun volume kullanılarak çözülmektedir. Container, host dosya sistemiyle etkileşime giremez. Proje dizininde yapılacak değişiklikler de çalışan container içerisine yansımayacaktır. Yeni bir imaj build işlemiyle yeni bir container oluşturulup başlatılması yerine bind mount kullanılabilir.
Volume, bir container içerisindeki klasör ve dosyalara bağlı, host tarafından yönetilen klasör ve dosyalardır. İki tür volume vardır.
- Anonymous volume container silindiğinde otomatik olarak içerisindeki veriyle birlikte volume da silinir.
-v /PATH
parametresiyle oluşturulmaktadır. Bind mount tarafından container içi verilerin override edilmemesi adına yararlı olabilir. - Named volume container’dan bağımsız olarak var olmakta ve komut aracılığıyla silinebilmektedirler.
-v NAME:/PATH
parametresiyle oluşturulurlar. Upload edilen dosyalar, veritabanı dosyaları gibi container verilerinin tutulması amacıyla kullanılırlar.
Volume kullanılarak container içerisine veri taşınabilir ve container tarafından yazılan veriler korunabilir; ancak bu değişiklikler host’a yansımaz. Volume, Docker tarafından oluşturulur ve yönetilir.
docker volume ls | Tüm volume’ları listeler. |
docker volume create NAME | NAME ile belirtilen isimde bir named volume oluşturur. Container çalıştırılırken mevcut değilse otomatik olarak oluşturulacağından genellikle buna gerek kalmaz. |
docker volume rm NAME | NAME ile belirtilen (name, id) volume’u siler. |
docker volume prune | Kullanılmayan tüm volume’ları siler. |
Bind Mounts
Volume ile benzerlik göstermektedir. Aralarındaki fark host’a container içerisindeki bir yola bağlanması gereken yolun ayarlanmasıdır. -v /ABS_HOST_PATH:CONTAINER_PATH
parametresiyle oluşturulmaktadır.
Development ortamında Container çalışırken paylaşılan verinin (source code) değişeceği durumlarda yararlıdırlar. Production ortamında kullanılmaması tavsiye edilmektedir.
Networks
Gerçek hayat senaryolarında birden fazla container’a ihtiyaç duyulacaktır. Birden fazla ana işleve sahip bir container’ın yönetimi zorlaşacağından her ana işlevin (web server, database) kendi container’ına sahip olması best-practice olarak kabul edilmektedir. Bu durum container’lerın kendi aralarında, host ile ve www ile iletişime geçme ihtiyacını doğurmaktadır.
www ile iletişim kurmak (Http, gRPC istekleri gibi) oldukça kolaydır. Hangi programlama dili kullanılırsa kullanılsın Container içerisinde ekstra bir yapılandırma gerekmeksizin normalde olduğu gibi istek gönderilebilmektedir.
Host ile iletişim kurmak (Host üzerinde çalışan bir veritabanı) için ufak bir değişiklik yapılması yeterli olacaktır. Host üzerinde localhost:3000
adresinde çalışan bir web server’a container dışından istek atıldığını düşünelim. Container içerisinde localhost
container’ın kendisi olacağından istek başarısız olacaktır. Bu sorunu çözmek adına host.docker.internal
özel adresi kullanılmalıdır. Bu adres Docker tarafından Host IP adresine dönüştürülmektedir. Böylece host.docker.internal:3000
adresine yapılan istekler Docker tarafından çözümlenerek başarıyla sonuçlanacaktır.
Container’ler arasında iletişim kurmak için iki seçecek mevcuttur. İletişim kurulacak Container IP adresi elde edilerek istek gönderilebilir, ancak bu adres zamanla değişebileceğinden kalıcı çözüm sunmaz. Bir diğer yöntemde Docker Network kullanmaktır. Bir network oluşturulup mevcut container’lar bu ağa eklenerek container’ların birbirleriyle haberleşmesi sağlanabilir.
docker run --network my-network --name container1 some-image
docker run --network my-network --name container2 other-image
Böylece iki container aynı network altında olacaktır. Birbirleriyle haberleşirken kullanacağı adreslere de container ismi yazılması (container1/api/products
gibi) yeterli olacaktır. Docker IP adreslerini bizim için çözümleyecektir.
Docker komutuyla ilgili tüm komut listesi, konsola ilgili komuttan sonra --help
flagı eklenerek docker --help
docker run --help
görülebilmektedir. Detaylı bilgi için dokümantasyon taranabilir.
docker network ls | Tüm network’leri listeler. |
docker network create NAME | NAME ile belirtilen isimde bir network oluşturur. |
docker run –network NAME IMAGE | IMAGE ile belirtilen imajdan NAME ile belirtilen network’e bir container oluşturur. |
Dockerfile
Dockerfile instruction adındaki adımlardan oluşmaktadır. Bu adımlar İmajın nasıl oluşturulacağını belirtmektedir. Imaj oluşturulduğunda bu adımlar birer katmana dönüşmektedir. Oluşturulan katmanlar sonrasında imajları verimli şekilde yeniden oluşturmak ve imajlar arasında paylaşmak için kullanılmaktadır. Docker Cache sayesinde eğer adımlar değiştirilmemişse bir sonraki imaj oluşturma işleminde önceden oluşturulmuş mevcut katman kullanılacaktır. Bu sayede build süresi kısalmaktadır. Bu adımlarda kullanılabilecek komutlarla birlikte örnek bir Dockerfile’a göz atalım.
# https://hub.docker.com/_/microsoft-dotnet
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /source
# copy csproj and restore as distinct layers
COPY *.sln .
COPY aspnetapp/*.csproj ./aspnetapp/
RUN dotnet restore
# copy everything else and build app
COPY aspnetapp/. ./aspnetapp/
WORKDIR /source/aspnetapp
RUN dotnet publish -c release -o /app --no-restore
# final stage/image
FROM mcr.microsoft.com/dotnet/aspnet:6.0
WORKDIR /app
COPY --from=build /app ./
ENTRYPOINT ["dotnet", "aspnetapp.dll"]
Yorum satırlarının #
işaretiyle başladığına ve uygulamanın son satırdaki ENTRYPOINT ["dotnet", "aspnetapp.dll"]
komutuyla ayağa kalktığına dikkat edelim. Burada ENTRYPOINT
yerine RUN
komutu kullanılsaydı bu komut container içerisinde değil image içerisinde çalıştırılmış olacaktı.
Docker Cache konusunun iyi anlaşılması adına aşağıda örnek bir Node.js uygulamasını ayağa kaldıran bir Dockerfile yer almaktadır.
FROM node
WORKDIR /app
COPY . /app
RUN npm install
CMD ["node", "app.js"]
Yukarıdaki adımlar incelendiğinde kaynak kodların /app
dizinine kopyalandığı ve sonrasında gerekli paketlerin kurulduğu görülmektedir. Adımlar dikkatli incelenirse bir optimizasyon sorunu gözlemlenmektedir. Bir adım değiştiğinde sonraki katmanlar da yeniden derlendiğinden; codebase içerisinde değişiklik yapıldığında paketlerin tekrar kurulmasına gerek yoktur.
FROM node
WORKDIR /app
COPY package.json /app
RUN npm install
COPY . /app
CMD ["node", "app.js"]
Codebase içerisinde bir değişiklik yapıldığında paket kurulumlarını içeren adım cache üzerinden okunacağından performans artmaktadır. Yalnızca package.json
içerisinde bir değişiklik yapıldığında ilgili ve sonraki adımlar tekrar derlenecektir.
FROM
FROM
komutu kendisinden sonra gelen adımlar için base image ayarlar. Dockerfile bir FROM
komutuyla başlamalıdır. Birden fazla base image belirtilebilir.
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR
WORKDIR
komutu, RUN
CMD
ENTRYPOINT
COPY
ve ADD
komutlarının hangi dizinde çalıştırılacağını belirtir. WORKDIR
belirtilmemişse sonraki adımlar tarafından kullanılmayacak olsa dahi oluşturulacaktır. Birden çok kez kullanılabilirler. Belirtilen dizinlerin relative olması durumunda bir önceki dizin baz alınacaktır.
WORKDIR /app
ADD
ADD
komutu kaynak olarak belirtilen dosya, klasör ve internet üzerindeki verileri imaj içerisinde belirtilen hedef dizine kopyalar. Birden çok kaynak belirtilebilir. Bu komuta gerçekten ihtiyaç varsa kullanılması önerilmektedir. Aksi durumda COPY
komutu tercih edilmelidir.
ADD . /app
COPY
COPY
komutu kaynak olarak belirtilen dizinden hedef dizine mevcut dosya ve klasörleri kopyalayarak taşır. ADD
komutuna göre farkı internet üzerinden değil yalnızca host içerisinden kopyalama yapabilmesidir. Birden fazla kaynak belirtilebilir.
COPY . /app
EXPOSE
EXPOSE
komutu container’ın çalışma zamanında hangi portu kullanacağını bildirir. Bir protokol belirtilmediğinde varsayılan TCP protokolü kullanılır. Bu komut dokümantasyon amacıyla hangi portların kullanılacağını bildirir, belirtilen portlar yayınlanmaz. Bir container çalıştırılacağında port yayınlamak için -p
parametresi kullanılmalıdır.
EXPOSE 8081
ENV
ENV
komutu girilen değerleri environment variable olarak ayarlar. Bu değerler build aşamasında kendisinden sonra gelen komutlar tarafından kullanılabilir ve sonrasında değiştirilebilir.
ENV PORT 80
EXPOSE $PORT
VOLUME
VOLUME
komutu container içerisinde belirtilen dizinle host üzerinde oluşturduğu dizini birbirlerine bağlayarak anonymous volume oluşturur. Alacağı değer array ya da string tipinde olabilir. Volume komutunun kullanılması yerine container başlatılırken -v
parametresiyle ayarlanması önerilmektedir.
VOLUME /app
VOLUME ["/app", "/data"]
RUN
RUN
komutu imaj oluşturulurken çalıştırılması istenen komutları yürütmektedir. Yeni bir imaj katmanı oluşturduklarından sayılarının az tutulması önerilmektedir.
RUN npm install
CMD
CMD
komutu container başlatıldığında çalıştırılması istenen komutlardır. Dockerfile içerisinde CMD
komutu belirtilmediğinde FROM
komutuyla belirlenmiş base image default komutu çalıştırılacaktır.
CMD ["node", "app.js"]
Container çalıştırılırken imaj isminden sonra bir command belirtilmişse Dockerfile içerisindeki CMD
komutu override edilecektir.
docker run some_image <command>
ENTRYPOINT
ENTRYPOINT
komutu yine container başlatıldığında çalıştırılması istenen komutlardır. CMD
komutundan farkıysa imaj isminden sonra belirtilen command override edilmek yerine belirtilen komutun sonuna eklenmesidir.
ENTRYPOINT ["dotnet", "app.dll"]
Dockerignore
Docker Client, Daemon ile iletişime geçmeden önce root dizinde .dockerignore
isminde bir dosya arar, mevcut olması durumunda içerisinde belirtilen dosya veya klasörlerin ADD
veya COPY
komutlarıyla imaja taşınması engellenmektedir. Böylece istenmeyen büyük ve hassas dosya veya dizinlerin imaja taşınmasının önüne geçilebilmektedir.
Docker Compose
Docker Compose multi-container Docker uygulamarını çalıştırmaya yarayan bir araçtır. Bu araçla birlikte container tanımları tek bir YAML dosyasından yapılarak uygulamalar bir komutla çalıştırılabilir ve durdurulabilir.
Birden fazla container’a sahip olduğumuzu düşünelim, Docker Compose kullanılmadığı taktirde network oluşturma, imaj build işlemleri, container oluşturma ve oluşturulan network’e dahil etmek gibi bir çok komut yazmamız ve bu komutları hatırlamamız gerekmektedir. Üstelik yapılacak en ufak değişikliklerde bu sürecin tekrarlanması gerekecektir. Docker Compose kullanıldığındaysa container konfigürasyonlarının docker-compose.yml
dosyasına belirtilmesi yeterli olacaktır.
version: '3.9'
services:
mongodb:
image: 'mongo'
volumes:
- data:/data/db
environment:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: password
backend:
build:
context: ./backend
dockerfile: Dockerfile
args:
some-arg: 1
ports:
- '80:80'
volumes:
- logs:/app/logs
- ./backend:/app
- /app/node_modules
env_file:
- ./env/backend.env
depends_on:
- mongodb
frontend:
build: ./frontend
ports:
- '3000:3000'
volumes:
- ./frontend/src:/app/src
stdin_open: true
tty: true
depends_on:
- backend
volumes:
data:
logs:
Bu dosya istenildiğinde tek bir yerden düzenlenerek docker-compose up
komutuyla ayağa kaldırılabilir. Tüm konfigürasyonlara buradan ulaşabilirsiniz.
Docker Compose ile çalışırken adımıza bir network oluşturulmakta ve tüm container’lar bu network’e dahil edilmektedir. Birden fazla network’e ihtiyacımız yoksa ekstradan bir network oluşturmamıza gerek kalmamaktadır.
docker compose up | YAML dosyasında belirtilen container’ları başlatır. |
docker compose down | İlgili container’ları durdurur. |
docker compose ps | YAML dosyasında belirtilen container’ları listeler. |
docker compose logs | Compose tarafından yönetilen container log bilgilerini gösterir. -f parametresi kullanılırsa yeni gelenler de eklenir. Bir container ismi eklenirse yalnızca ilgili container’a ait log bilgileri sunulur. |
docker compose rm | İlgili container’ları siler. |
docker compose build | Compose tarafından yönetilen imajları build eder. |