~/yazilar/docker-giris
Docker'ı kurup ilk container'ı çalıştırmak
içindekiler
Bir uygulamayı sunucuya kurarken işin çoğu uygulamanın kendisiyle ilgili olmuyor. Doğru Python sürümünü bulmak, sistemdeki başka bir uygulamayla çakışan kütüphaneyi ayıklamak, "geliştiricinin makinesinde çalışıyordu" diye başlayan konuşmalar. Docker bu bağımlılıkları uygulamayla birlikte paketleyip sunucuya tek parça halinde getiriyor. Sunucuda kurulu olması gereken tek şey Docker'ın kendisi kalıyor.
Compose, Dockerfile ve güvenlik serinin sonraki yazılarında. Burada kavramlar, kurulum ve ilk container'lar var.
Dört kavram
Image, uygulamanın ve çalışması için gereken her şeyin (dosya sistemi, kütüphaneler, varsayılan komut) salt okunur bir paketi. Katmanlardan oluşuyor ve aynı base image'ı kullanan iki image o katmanları diskte paylaşıyor. Image'lar bir registry'de duruyor, varsayılanı Docker Hub. İsimleri nginx:1.27-alpine gibi depo adı ve etiketten oluşuyor.
Container, bir image'dan başlatılmış, çalışan bir süreç. Image'ın üstüne ince, yazılabilir bir katman ekleniyor ve container'ın yaptığı değişiklikler orada tutuluyor. Container silinince o katman da gidiyor. Sanal makine gibi düşünmemek lazım: kendi kernel'i yok, host'un kernel'ini paylaşıyor. Namespace'ler ve cgroup'lar sayesinde kendini ayrı bir makinedeymiş gibi görüyor.
Volume, container silinse de kalması gereken veri için. Veritabanı dosyaları, yüklenen dosyalar, sertifikalar. Docker'ın yönettiği adlandırılmış volume'lar /var/lib/docker/volumes/ altında duruyor. Bir de bind mount var, host'taki bir dizini doğrudan container'a bağlamak. Ben config dosyaları için bind mount, uygulama verisi için named volume kullanıyorum.
Network, container'ların birbirine ve dışarıya nasıl ulaştığını belirliyor. Varsayılan bridge ağında container'lar birbirini IP ile görüyor ama isimle göremiyor. Kendi oluşturduğun bir bridge ağında Docker'ın gömülü DNS'i devreye giriyor, container'lar birbirine isimle ulaşıyor. Her uygulama için ayrı ağ açmak hem bunu sağlıyor hem de uygulamaları birbirinden ayırıyor.
Kurulum
Ubuntu'nun kendi deposunda docker.io paketi var. Ben yine de Docker'ın kendi apt deposunu kullanıyorum: sürümler daha güncel, Compose ve Buildx eklentileri aynı yerden geliyor, Docker'ın dokümantasyonu da bu kuruluma göre yazılmış. İkisi aynı anda kurulu olunca sorun çıkıyor, o yüzden önce çakışabilecek paketleri kaldırıyorum:
for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do
sudo apt-get remove -y "$pkg"
doneTemiz bir sunucuda "paket kurulu değil" mesajları gelir, sorun yok. /var/lib/docker altında eski image ve volume'lar varsa bu komut onlara dokunmuyor.
Sonra deponun anahtarı ve kaynak tanımı:
sudo apt-get update
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
sudo tee /etc/apt/sources.list.d/docker.sources > /dev/null <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOFSuites satırı sürümün kod adını /etc/os-release içinden alıyor, elle yazmana gerek yok. Signed-By ile anahtarı sadece bu depoya bağlıyorum. Eski apt-key yönteminde eklenen anahtar bütün depolar için geçerli oluyordu, yani Docker'ın anahtarı teoride Ubuntu paketlerini de imzalayabiliyordu.
Paketler:
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-pluginPaket servisi hem başlatıyor hem de açılışta etkinleştiriyor. Kontrol:
systemctl status docker --no-pager | head -3
sudo docker version● docker.service - Docker Application Container Engine
Loaded: loaded (/usr/lib/systemd/system/docker.service; enabled; preset: enabled)
Active: active (running) since ...
Client: Docker Engine - Community
Version: ...
Server: Docker Engine - Community
Engine:
Version: ...Sürüm numaraları sende farklı olacak. Burada baktığım şey hem Client hem Server bölümünün görünmesi. Sadece Client görünüyorsa komut satırı aracı daemon'a ulaşamıyor.
Docker bütün image, container ve volume'ları /var/lib/docker altında tutuyor. Kök dosya sistemi küçükse bu dizin hızla büyüyüp diski dolduruyor. Ayrı bir LV'ye almak ya da kök dosya sistemini büyütmek için LVM yazısına bakabilirsin.
docker grubu ve bedeli
Her komutun başına sudo yazmadan çalıştırırsan şunu görürsün:
permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.47/containers/json": dial unix /var/run/docker.sock: connect: permission deniedYaygın çözüm kullanıcıyı docker grubuna eklemek:
sudo usermod -aG docker "$USER"
# grubun geçerli olması için oturumu kapatıp yeniden açİlk container
docker run --rm hello-worldUnable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
...
Hello from Docker!
This message shows that your installation appears to be working correctly.
...Bu kısa çıktıda birkaç şey oldu. Docker image'ı yerelde bulamadı, Docker Hub'dan çekti, bir container başlattı, container mesajını yazıp çıktı. --rm de çıkan container'ı sildi. Etiket yazmadığım için Docker latest etiketini varsaydı. Denemede sorun değil. Ama latest "en yeni sürüm" demek değil, sadece etiket verilmediğinde kullanılan isim. Bugün çektiğin latest ile üç ay sonra çektiğin farklı bir image olabilir ve hangisinin çalıştığını sonradan anlamak zorlaşır. Kalıcı her kurulumda belli bir etiket yazıyorum.
Biraz daha işe yarar bir örnek, bir web sunucusu:
docker run -d --name web01 -p 127.0.0.1:8080:80 nginx:1.27-alpine
curl -sI http://127.0.0.1:8080 | head -2HTTP/1.1 200 OK
Server: nginx/...-p 127.0.0.1:8080:80 host'un 8080 portunu container'ın 80 portuna bağlıyor, ama sadece loopback adresinde. Bunu bilerek böyle yazdım.
Port yayınlamak ve UFW
-p 8080:80 yazsaydım port bütün arayüzlerde, yani 0.0.0.0 üzerinde açılırdı. Buraya kadar beklenen davranış. Asıl sürpriz şu: UFW'de 8080 kapalı olsa bile port dışarıdan erişilebilir olurdu.
Sebep, Docker'ın iptables kurallarını kendisinin yönetmesi. Yayınlanan her port için nat tablosuna bir DNAT kuralı ekliyor. Dışarıdan gelen paket, UFW kurallarının bulunduğu INPUT zincirine hiç uğramadan container'ın IP'sine yönleniyor ve FORWARD zincirinden geçiyor. ufw status portu kapalı gösterirken port internete açık duruyor. Bunu bilmeyen biri güvenlik duvarına güvenip veritabanını dünyaya açabilir.
Benim kuralım basit. Bir servis aynı makinedeki reverse proxy'den erişilecekse 127.0.0.1 adresine bağlıyorum. Dışarıdan hiç erişilmeyecekse (veritabanı, Redis) hiç port yayınlamıyorum, container'lar ortak bir Docker ağı üzerinden konuşuyor. Gerçekten dışarı açılması gereken portları kaynak adrese göre kısıtlamak için Docker'ın DOCKER-USER zincirine kural yazmak gerekiyor. Bu kurallar Docker'ın iptables ya da nftables kullanmasına göre değiştiği için Docker dokümanındaki güvenlik duvarı sayfasından kendi sürümüne bakmanı öneririm.
Hangi adreste dinlendiğini görmek için ss yeterli:
sudo ss -tlpn | grep 8080LISTEN 0 4096 127.0.0.1:8080 0.0.0.0:* users:(("docker-proxy",pid=...,fd=...))0.0.0.0:8080 ya da [::]:8080 görüyorsan port herkese açık.
Volume ve network bir arada
nginx'in varsayılan sayfası yerine kendi içeriğimi göstermek için bir bind mount, iki container'ın birbirini isimle bulması için de bir ağ kullanıyorum:
mkdir -p ~/site && echo '<h1>web01 calisiyor</h1>' > ~/site/index.html
docker rm -f web01
docker network create deneme-net
docker run -d --name web01 --network deneme-net \
-v ~/site:/usr/share/nginx/html:ro \
-p 127.0.0.1:8080:80 \
nginx:1.27-alpine
docker run --rm --network deneme-net alpine:3 wget -qO- http://web01<h1>web01 calisiyor</h1>Mount'un sonundaki :ro container'ın bu dizine yazmasını engelliyor. nginx'in içerik dizinine yazması için bir sebep yok, yazamaması daha iyi. Son komutta geçici bir Alpine container'ı aynı ağa katıldı ve web01 ismini çözdü, çünkü deneme-net benim oluşturduğum bir ağ. Aynı komutu --network vermeden, varsayılan bridge üzerinde çalıştırsaydın bad address 'web01' hatası alırdın.
Named volume için kısa bir deneme:
docker volume create veri
docker run --rm -v veri:/data alpine:3 sh -c 'echo merhaba > /data/test.txt'
docker run --rm -v veri:/data alpine:3 cat /data/test.txtİki ayrı container çalıştı ve ikisi de silindi, ama veri volume'da duruyor. Volume'u silmek için açıkça docker volume rm veri demek gerekiyor, Docker kullanılmayan volume'ları kendiliğinden silmiyor. Bu iyi bir şey, ama zamanla diskin dolmasının sebeplerinden biri de bu.
Her gün kullandığım komutlar
Çoğunu yukarıda gördün, bir arada dursun:
docker ps # çalışan container'lar
docker ps -a # durmuş olanlar dahil
docker logs -f --tail 100 web01 # son 100 satır ve canlı akış
docker exec -it web01 sh # container içinde kabuk
docker stop web01 && docker rm web01
docker images # yereldeki image'lar
docker pull nginx:1.27-alpine
docker inspect web01 # her şeyin JSON hali
docker stats --no-stream # CPU ve bellek kullanımı
docker system df # Docker ne kadar disk kullanıyordocker exec ile girdiğin kabukta yaptığın değişiklikler container silinince kayboluyor. Bir şeyi kalıcı olarak değiştirmek istiyorsan doğru yer ya image (yani Dockerfile) ya da volume.
Çıktısı uzun olduğu için inspect'i çoğu zaman --format ile, sadece istediğim alanı alarak kullanıyorum:
docker inspect --format '{{.State.Status}} {{.RestartCount}}' web01
docker inspect --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' web01Temizlik için docker system prune var. Durmuş container'ları, kullanılmayan ağları ve isimsiz image'ları siliyor. -a eklersen hiçbir container'ın kullanmadığı bütün image'ları da siliyor. --volumes eklersen kullanılmayan anonim volume'lar da gidiyor. Docker 23'ten beri isimli volume'lar (pgdata gibi) bu temizliğe varsayılan olarak girmiyor. Onları silen komut docker volume prune -a ve üretim sunucusunda tehlikeli: o an durdurulmuş bir uygulamanın veritabanı volume'u da "kullanılmıyor" sayılıyor.
Kurulumdan hemen sonra değiştirdiğim iki varsayılan
İkisi de /etc/docker/daemon.json içinde:
{
"log-driver": "local",
"log-opts": {
"max-size": "20m",
"max-file": "5"
},
"default-address-pools": [
{ "base": "10.200.0.0/16", "size": 24 }
]
}Birincisi loglar. Varsayılan json-file sürücüsü container loglarını sınırsız büyütüyor ve çok konuşkan tek bir container diski doldurabiliyor. local sürücüsü logları sıkıştırıp döndürüyor, docker logs da aynı şekilde çalışmaya devam ediyor. Bu ayar sadece yeni oluşturulan container'lara uygulanıyor, var olanları yeniden oluşturman gerekiyor.
İkincisi adres aralıkları. Docker ağlarını varsayılan olarak 172.17.0.0/16 ve devamındaki 172.x bloklarından, onlar biterse 192.168.x bloklarından alıyor. Kampüs ağında bu aralıklardan biri gerçekten kullanılıyorsa, o subnet'teki kullanıcılar Docker'lı sunucuya ulaşamıyor. Sunucu cevabı kendi bridge'ine gönderiyor ve paket hiç dışarı çıkmıyor. Bu yüzden kurumda kullanılmadığından emin olduğun bir aralık seç. Buradaki 10.200.0.0/16 sadece örnek, ağ ekibine sormadan kopyalama.
sudo systemctl restart docker
docker info --format '{{.LoggingDriver}}'Docker'ı yeniden başlatmak çalışan container'ları durduruyor. Restart politikası olanlar (--restart unless-stopped gibi) daemon açılınca geri geliyor, yukarıdaki web01 gibi politikasız olanlar durmuş kalıyor. Bu ayarları sunucu henüz boşken yapmak bu yüzden daha rahat.
Takıldığın yerler
Birkaç hata ilk günlerde mutlaka çıkıyor.
Aynı portu iki container'a vermeye çalışırsan:
docker: Error response from daemon: driver failed programming external connectivity on endpoint web02 (...): Bind for 127.0.0.1:8080 failed: port is already allocated.Portu kimin tuttuğunu docker ps --filter publish=8080 gösteriyor. Docker dışında bir süreç tutuyorsa ss -tlpn ile bak.
Container başlıyor ama docker ps listesinde görünmüyorsa büyük ihtimalle hemen çıkmıştır. docker ps -a ile Exited (1) 3 seconds ago gibi bir durum görürsün, sebebi de neredeyse her zaman docker logs <isim> çıktısında. Container, içindeki ana süreç bittiği an duruyor. docker run -d ubuntu:24.04 gibi bir komut bu yüzden hemen çıkıyor: varsayılan komut bash, terminal bağlı olmadan başlayınca hemen bitiyor.
Aynı isimle ikinci kez docker run dersen Conflict. The container name "/web01" is already in use hatası geliyor. Durmuş container hâlâ duruyor, önce docker rm web01.
Bir de Docker Hub'ın çekme limiti var. Kampüste çok sayıda sunucu aynı NAT adresinin arkasından internete çıkıyorsa limite beklenenden çabuk takılırsın:
Error response from daemon: toomanyrequests: You have reached your pull rate limit. ...Kısa vadede docker login ile giriş yapmak limiti yükseltiyor. Uzun vadede ortamda bir pull-through cache (ör. kendi registry'n ya da Harbor) kurup sunucuları ona yönlendirmek daha temiz bir çözüm.
Tek tek docker run komutlarıyla buraya kadar gelinebiliyor. Ama üç servisli bir uygulamada bütün bayrakları hatırlayıp doğru sırayla çalıştırmak çabuk işkenceye dönüyor, o yüzden bir sonraki adım her şeyi tek dosyada tarif etmek.
Sıradaki yazı: Docker Compose ile üç servisi bir arada çalıştırmak