Mert Özen Her Satırda Daha İleri
DevOps Docker — #12

Docker Network: Bridge, User-defined Network, Container DNS ve Port Publishing

Mert Özen 14 Kas 2026 13 dk 13 görüntülenme
Docker Network: Bridge, User-defined Network, Container DNS ve Port Publishing

Container'lar default bridge'de birbirini adıyla bulamaz; user-defined bir network'te Docker'ın gömülü DNS'i (127.0.0.11) container adlarını ve alias'ları çözer. Bookstore.Api'yi Server=bookstore-db ile SQL Server'a bağlıyor, bookstore-web'in nginx'ini aynı origin için API'ye proxy yapıyor, yalnız giriş noktasını 127.0.0.1'e yayımlıyor ve ufw'yi atlayan iptables kurallarını, deneysel nftables backend'ini ve host.docker.internal'ı gerçek çıktılarla görüyoruz.

Sürüm notu: Bu yazıdaki örnekler Docker Engine 29.9.3 (iptables ve deneysel nftables firewall backend'i), SQL Server 2025 (RTM-CU11), .NET 10 (ASP.NET Core Runtime 10.0.15), Microsoft.Data.SqlClient 7.1.2 ve nginxinc/nginx-unprivileged:1.30-alpine ile denenmiştir.

Önce Tahmin Et

Bir önceki yazıdaki SQL Server container'ı dockerseries-db adıyla çalışıyor. Bookstore.Api'yi ikinci bir container olarak başlatıp connection string'e Server=dockerseries-db yazıyorsun; iki komutta da --network yok. GET /books veritabanındaki kitapları döner mi? İkinci soru: API'yi -p 127.0.0.1:8082:8080 ile yayımlayıp container'ın içinde ASPNETCORE_URLS=http://127.0.0.1:8080 verirsen, host'tan curl localhost:8082/health ne cevap alır?

Docker'ın Network'leri

Her engine üç network'le başlar:

docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
7733fcd326d8   bridge    bridge    local
ec261aed95a9   host      host      local
a4efcd0b18f2   none      null      local

bridge, --network verilmeyen her container'ın bağlandığı default bridge. Docker host'ta sanal bir switch oluşturur, her container bu switch'e bağlı sanal bir ağ kartı ve özel bir IP alır, dışarı çıkan trafik host'un adresine NAT'lanır (Network Address Translation; özel adresleri host'un adresine çevirme). host, container'ın host'un network namespace'ini paylaşması: container kendi IP'sini almaz, host'un port'larını doğrudan dinler ve -p anlamsızlaşır. none ise yalnız loopback arayüzü olan, dışarıyla hiç konuşmayan container'lar için. Container'ın içindeki localhost'un neden host'un değil container'ın kendisi olduğunu Networking ve HTTP serisinin "IP Adresi, Subnet, Port ve NAT: localhost, 0.0.0.0 ve Container'larda Bağlantı" yazısı anlatıyor; burada Docker'ın komutlarıyla ilerliyoruz.

Bookstore.Api Veritabanına Bağlanıyor

Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) API'si 8. yazıdan beri kataloğu bellekten okuyordu. Kitap kaynağını bir interface'in arkasına aldık ve SQL Server'dan okuyan bir uygulamasını ekledik:

public interface IBookSource
{
    Task<IReadOnlyList<Book>> GetAllAsync(CancellationToken cancellationToken);
}

// BookstoreDb'den (SQL Server) kataloğu okur
public sealed class SqlBookSource(string connectionString) : IBookSource
{
    public async Task<IReadOnlyList<Book>> GetAllAsync(CancellationToken cancellationToken)
    {
        await using SqlConnection connection = new(connectionString);
        await connection.OpenAsync(cancellationToken);
        await using SqlCommand command = new("SELECT Isbn, Title, Author, PriceTry FROM dbo.Books", connection);
        await using SqlDataReader reader = await command.ExecuteReaderAsync(cancellationToken);

        List<Book> books = [];
        while (await reader.ReadAsync(cancellationToken))
        {
            books.Add(new Book(reader.GetString(0), reader.GetString(1), reader.GetString(2), reader.GetDecimal(3)));
        }
        return books;
    }
}
// Container'da ConnectionStrings__Default ortamdan gelir; yoksa katalog bellekte kalır
string? connectionString = builder.Configuration.GetConnectionString("Default");
if (string.IsNullOrWhiteSpace(connectionString))
{
    builder.Services.AddSingleton<IBookSource, InMemoryBookSource>();
}
else
{
    builder.Services.AddSingleton<IBookSource>(new SqlBookSource(connectionString));
}

InMemoryBookSource eski sabit listeyi taşıyor; connection string verilmeyen dotnet run ve önceki yazıların komutları değişmeden çalışıyor. BookCatalog'un Türkçe sıralaması aynı kaldı, yalnız artık kitapları kaynaktan async alıyor. SqlClient'i doğrudan, bir kütüphane katmanı olmadan kullandık, çünkü konu veri erişimi değil bağlantının kendisi; Entity Framework Core (EF Core; .NET'in veritabanı erişim kütüphanesi) ve Dapper'la aynı işi yayındaki serileri anlatıyor. Uygulama başlarken hangi kaynağı kullandığını da log'luyor (Catalog source: SqlBookSource). Davranış değiştiği için API ve web image'larını 1.1.0 tag'iyle build ediyoruz; docker-bake.hcl'deki TAG varsayılanı da artık 1.1.0.

API veritabanına sa ile bağlanmamalı. db/02-app-login.sql, yalnız okuma yetkisi olan bir login oluşturuyor; şifreyi script'e yazmak yerine sqlcmd'nin değişkeninden alıyor:

USE master;
GO

IF SUSER_ID(N'bookstore_app') IS NULL
    CREATE LOGIN bookstore_app WITH PASSWORD = N'$(BOOKSTORE_DB_PASSWORD)';
ELSE
    ALTER LOGIN bookstore_app WITH PASSWORD = N'$(BOOKSTORE_DB_PASSWORD)';
GO

USE BookstoreDb;
GO

IF USER_ID(N'bookstore_app') IS NULL
    CREATE USER bookstore_app FOR LOGIN bookstore_app;
ALTER ROLE db_datareader ADD MEMBER bookstore_app;
GO

sqlcmd, -v ile tanımlanmamış bir $(...) değişkenini ortam değişkenlerinden arar; docker exec -e BOOKSTORE_DB_PASSWORD değeri shell'den container'a taşıyor. Login, user ve rol ayrımı SQL Server Derinlemesine serisinin "SQL Server'da Güvenlik Modeli: Login, User, Schema, Role, Least Privilege ve Row-Level Security" yazısında.

Default Bridge'de İsimle Bulamamak

İlk sorunun deneyi. Veritabanı 11. yazıdaki volume'le, API yanında; ikisi de default bridge'de:

docker run -d --name dockerseries-db -e ACCEPT_EULA=Y -e MSSQL_SA_PASSWORD \
    -v dockerseries-sqldata:/var/opt/mssql -v "$PWD/db:/scripts:ro" \
    mcr.microsoft.com/mssql/server:2025-latest
docker exec -e SQLCMDPASSWORD="$MSSQL_SA_PASSWORD" -e BOOKSTORE_DB_PASSWORD dockerseries-db \
    /opt/mssql-tools18/bin/sqlcmd -C -S localhost -U sa \
    -i /scripts/01-bookstore.sql -i /scripts/02-app-login.sql

docker run -d --name dockerseries-api -p 8080:8080 \
    -e ConnectionStrings__Default="Server=dockerseries-db;Database=BookstoreDb;User Id=bookstore_app;Password=$BOOKSTORE_DB_PASSWORD;TrustServerCertificate=True" \
    dockerseries-bookstore-api:1.1.0
curl -s -o /dev/null -w "GET /books: %{http_code}\n" localhost:8080/books
docker logs dockerseries-api 2>&1 | grep -E "SqlException|--->"
GET /books: 500
      Microsoft.Data.SqlClient.SqlException (0x80131904): A network-related or instance-specific error occurred while establishing a connection to SQL Server. The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server is configured to allow remote connections. (provider: TCP Provider, error: 35 - An internal exception was caught)
       ---> System.Net.Sockets.SocketException (00000005, 0xFFFDFFFF): Name or service not known

Cevap hayır. SqlClient'in uzun mesajı birçok olasılığı sayıyor ama iç exception net: Name or service not known, yani dockerseries-db adı çözülemedi. Docker'ın dokümanı bunu açıkça söylüyor: default bridge'deki container'lar birbirine yalnız IP adresiyle ulaşabilir, isimle çözüm user-defined network'lerde var. İki container aynı ağdaydı ve IP'ler docker network inspect bridge'de görünüyordu (172.18.0.2 ve 172.18.0.3), ama IP'yi connection string'e yazmak çözüm değil: container her yeniden oluşturulduğunda IP değişebilir. Eski dokümanlarda göreceğin --link bayrağı tam bu sorunu default bridge'de çözmek içindi; Docker onu legacy sayıyor ve Engine 29.6'dan beri default bridge'de kullanıldığında uyarı veriyor.

User-defined Network ve Gömülü DNS

docker network create dockerseries-net
docker network connect --alias bookstore-db dockerseries-net dockerseries-db
docker network disconnect bridge dockerseries-db

docker rm -f dockerseries-api
docker run -d --name dockerseries-api --network dockerseries-net --network-alias bookstore-api \
    -e ConnectionStrings__Default="Server=bookstore-db;Database=BookstoreDb;User Id=bookstore_app;Password=$BOOKSTORE_DB_PASSWORD;TrustServerCertificate=True" \
    dockerseries-bookstore-api:1.1.0

docker network create varsayılan olarak yeni bir bridge network oluşturur. Veritabanını durdurmadan yeni network'e bağladık ve default bridge'den çıkardık; --alias, container'ın adına ek olarak o network'te geçerli bir DNS adı veriyor. 8. yazıda söz verdiğimiz Server=bookstore-db böylece container adından bağımsız bir isim oldu: yarın veritabanını dockerseries-db-2 adlı yeni bir container'a taşısan da API'nin ayarı değişmez. Aynı network'teki bir container'dan baktık:

docker run --rm --network dockerseries-net alpine:3 sh -c \
    "grep nameserver /etc/resolv.conf; nslookup bookstore-db | tail -2; wget -qO- http://bookstore-api:8080/books | head -c 90"
nameserver 127.0.0.11
Address: 172.19.0.2

[{"isbn":"9789750738609","title":"Çalıkuşu","shelfLabel":"ÇALIKUŞU","priceTry":240.00},

User-defined network'teki her container'ın DNS sunucusu 127.0.0.11: Docker'ın gömülü DNS'i. Container adlarını ve alias'ları o network'teki IP'lere çevirir, bilmediği adları host'un DNS ayarlarına iletir. İsim çözümü network başına: bookstore-db adı yalnız dockerseries-net'e bağlı container'lar için var. API'nin log'unda Catalog source: SqlBookSource satırı, /books da veritabanından gelen kitaplar.

12-network-tr.png

Connection string'in sonundaki TrustServerCertificate=True'yu da denedik. Microsoft.Data.SqlClient bağlantıyı varsayılan olarak şifreliyor ve sunucunun sertifikasını doğruluyor; SQL Server container'ı ise kendi ürettiği self-signed bir sertifikayla geliyor. Ayarı çıkarınca bağlantı handshake'te kırıldı:

Microsoft.Data.SqlClient.SqlException (0x80131904): A connection was successfully established with the server, but then an error occurred during the pre-login handshake. (provider: TCP Provider, error: 35 - An internal exception was caught)
 ---> System.Security.Authentication.AuthenticationException: Certificate failed chain validation. Error(s): 'self-signed certificate, [Status: UntrustedRoot]

Lokal ve test ortamında bu ayar kabul edilebilir: trafik zaten host'tan çıkmayan bir bridge'de. Production'da sunucuya güvenilen bir sertifika verip TrustServerCertificate'ı kaldırmak gerekir.

Aynı Origin: nginx'ten bookstore-api'ye

10. yazıda bookstore-web ile API ayrı origin'lerdeydi ve CORS (Cross-Origin Resource Sharing; tarayıcının başka origin'e istek izni) gerekiyordu. Şimdi iki container aynı network'te olduğu için tarayıcının yalnız web container'ını görmesi yeterli: nginx /api/ ile başlayan istekleri bookstore-api'ye iletir. web/nginx/default.conf'a bir location ekledik:

    # API için aynı origin: /api/books -> container network'ünde http://bookstore-api:8080/books.
    # Docker'ın gömülü DNS'i (127.0.0.11) adı her istekte çözer, nginx API olmadan da başlar.
    location /api/ {
        resolver 127.0.0.11 valid=10s;
        set $api_upstream http://bookstore-api:8080;
        rewrite ^/api/(.*)$ /$1 break;
        proxy_pass $api_upstream;
    }

Neden düz bir proxy_pass http://bookstore-api:8080/; yazmadığımızı Tuzaklar bölümünde göreceğiz. Web container'ını network'e bağlayıp yalnız onu yayımladık; config'te API adresi artık göreli bir yol:

docker run -d --name dockerseries-web --network dockerseries-net -p 127.0.0.1:8081:8080 \
    -e API_BASE_URL=/api -e APP_ENVIRONMENT=staging dockerseries-bookstore-web:1.1.0
curl -s localhost:8081/config.json
curl -s localhost:8081/api/books | jq -r '.[0:3][] | "\(.title) | \(.priceTry)"'
curl -s -o /dev/null -w "GET :8080/books from the host: %{http_code}\n" -m 3 localhost:8080/books
docker ps --format "table {{.Names}}\t{{.Ports}}"
{
  "apiBaseUrl": "/api",
  "environment": "staging"
}
Çalıkuşu | 240.00
Dune | 420.00
Emma | 180.00
GET :8080/books from the host: 000
NAMES              PORTS
dockerseries-web   127.0.0.1:8081->8080/tcp
dockerseries-api   8080/tcp
dockerseries-db    1433/tcp

PORTS sütununda okun olmadığı satırlar yayımlanmamış port'lar: API ve veritabanı host'tan erişilemez (000, bağlantı kurulamadı), yalnız network'teki container'lar onlara ulaşır. Attack surface'i küçültmenin en kolay yolu bu: dışarıya yalnız giriş noktası açılır. Angular tarafında kod değişmedi; apiBaseUrl artık /api ve tarayıcı istekleri sayfanın kendi origin'ine gönderiyor, CORS gereksiz.

Port Publishing: 0.0.0.0, 127.0.0.1 ve Firewall

-p 8080:8080'de host tarafının adresi yazılmadığında Docker port'u bütün arayüzlerde (0.0.0.0 ve [::]) yayımlar. Farkı görmek için API'yi bir de böyle yayımladık ve sunucunun dış arayüz adresine (aşağıda 192.168.1.20) bağlandık; ağdaki başka bir makinenin göreceği bu:

docker port dockerseries-api
curl -s -o /dev/null -w "8080 -> %{http_code}\n" -m 3 http://192.168.1.20:8080/health
curl -sS -o /dev/null -m 3 http://192.168.1.20:8081/
8080/tcp -> 0.0.0.0:8080
8080/tcp -> [::]:8080
8080 -> 200
curl: (7) Failed to connect to 192.168.1.20:8081 after 0 ms: Could not connect to server

127.0.0.1:8081 dışarıdan görünmüyor, 8080 görünüyor. Linux'ta Docker bunu firewall kurallarıyla yapar:

iptables -t nat -S DOCKER | grep -E "dport (8080|8081)"
iptables -S DOCKER-USER
-A DOCKER -d 127.0.0.1/32 ! -i br-aa27fb60a622 -p tcp -m tcp --dport 8081 -j DNAT --to-destination 172.19.0.4:8080
-A DOCKER ! -i br-aa27fb60a622 -p tcp -m tcp --dport 8080 -j DNAT --to-destination 172.19.0.3:8080
-N DOCKER-USER

Yayımlanan her port bir DNAT (hedef adresi değiştirme) kuralı: host'a gelen paket container'ın IP'sine yönlendirilir. 2. yazıdaki ufw uyarısının nedeni burada. Docker'ın dokümanına göre bu nat kuralları paketleri ufw'nin INPUT ve OUTPUT zincirlerine gelmeden yönlendirdiği için ufw'ye yazdığın "8080'i kapat" kuralı yayımlanmış port'u korumaz. Docker'ın önerdiği iki yol var: port'u gerçekten dışarı açman gerekmiyorsa 127.0.0.1'e yayımla; dışarı açıp kaynağı kısıtlamak istiyorsan kuralını Docker'ın kendi kurallarından önce çalışan ve senin için boş bırakılan DOCKER-USER zincirine yaz. Firewall kurallarının kendisi Linux ve Shell serisinin konusu. Engine 28.0'dan beri 127.0.0.1'e yayımlanan port'lara aynı yerel ağdaki başka makineler de ulaşamıyor; daha eski sürümlerde bu mümkündü.

Engine 29.0 bir de deneysel nftables backend'i getirdi (dockerd --firewall-backend=nftables ya da daemon.json'da "firewall-backend": "nftables"). Ayrı bir lab engine'inde açtık; aynı -p 127.0.0.1:8081:8080 kuralı bu kez Docker'ın kendi nftables tablosunda:

docker info | grep "Firewall Backend"
nft list tables
nft list ruleset | grep "dport 8081"
iptables -S DOCKER-USER
 Firewall Backend: nftables
table ip docker-bridges
table ip6 docker-bridges
		iifname != "docker0" ip daddr 127.0.0.1 tcp dport 8081 counter packets 0 bytes 0 dnat to 172.18.0.2:8080 comment "DNAT"
		iifname != "lo" ip daddr 127.0.0.1 tcp dport 8081 counter packets 0 bytes 0 drop comment "DROP REMOTE LOOPBACK"
iptables: No chain/target/match by that name.

Son satır önemli: nftables backend'inde DOCKER-USER zinciri yok. Docker'ın dokümanı kendi kurallarını ayrı bir nftables tablosunda yazmanı söylüyor; Swarm mode ile de kullanılamıyor ve hâlâ deneysel, seçenekleri değişebilir. Engine'in nft aracını da bulması gerekiyor; araç kurulu olmayan bir host'ta daemon failed to find nft tool hatasıyla hiç başlamadı. Production sunucusunda varsayılan iptables backend'inde kal. Docker Desktop'ta ise yayımlanan port'ların varsayılan olarak localhost'a bağlanması ayarlardan seçilebiliyor.

Container'dan Host'a: host.docker.internal

Ters yön de sık lazım oluyor: container'daki API, host'ta doğrudan kurulu bir SQL Server'a ya da host'ta dotnet run ile çalışan bir servise bağlanmak istiyor. Container içinde localhost container'ın kendisi olduğu için host'a başka bir adla ulaşmak gerekir. Docker Desktop bunun için host.docker.internal adını hazır sunuyor. Linux'taki Docker Engine'de bu ad kendiliğinden yok; --add-host'un özel host-gateway değeriyle eklenir:

docker run --rm --add-host host.docker.internal:host-gateway alpine:3 grep host.docker.internal /etc/hosts
172.18.0.1	host.docker.internal

Ad, default bridge'in host tarafındaki adresine (gateway) gidiyor. Bunu SQL Server'ı host'taki bir servis gibi yayımlayıp API'yi Server=host.docker.internal,1433 ile bağlayarak denedik. Veritabanı port'u -p 127.0.0.1:1433:1433 ile yayımlıyken /books 500 döndü; -p 1433:1433 ile 200. Host'taki bir servis yalnız 127.0.0.1'i dinliyorsa container ona gateway adresinden ulaşamaz. Host'ta kendi kurduğun servisler için de aynısı geçerli: container'dan erişilecekse bridge'in gateway adresini ya da bütün arayüzleri dinlemeli, ve o zaman firewall'u yukarıdaki gibi düşünmen gerekir.

Üretimde Dikkat

Her uygulama grubu için kendi user-defined network'ünü oluştur. Aynı network'teki container'lar birbirinin bütün port'larına ulaşabilir; Bookstore'un veritabanının, aynı sunucudaki başka bir uygulamanın container'ıyla aynı ağda durması için bir neden yok. Bir container birden çok network'e bağlanabildiği için (API hem web'in hem veritabanının network'ünde gibi) ayrımı ince yapabilirsin; docker network create --internal ise dışarıya hiç çıkışı olmayan bir network oluşturur.

Yalnız giriş noktasını yayımla ve mümkünse 127.0.0.1'e: önünde bir reverse proxy varsa o proxy dışarıyı dinler, container'lar yalnız localhost'u. Veritabanı port'unu yayımlamak gerekiyorsa (bir yönetim aracı için) yine 127.0.0.1'e yayımla ve aracı SSH (Secure Shell; şifreli uzak bağlantı) tüneliyle kullan.

IP adresine bağımlı hiçbir ayar yazma. Container IP'leri yeniden oluşturmada değişir; connection string'lerde, nginx config'inde ve health check'lerde container adı ya da alias kullan.

Birden çok container'ı, network'ü ve volume'ü elle docker run komutlarıyla kurmak bu yazıda öğretici ama günlük işte hata üretir; aynı yapıyı tek bir dosyada tanımlamak Docker Compose serisinin konusu. Kubernetes'te ise Service ve cluster DNS'i aynı işi görür: Kubernetes serisinin "Kubernetes Service: ClusterIP, NodePort, LoadBalancer, DNS ve EndpointSlice" yazısı.

Sık Düşülen Tuzaklar

Uygulamanın container içinde 127.0.0.1'e bind olması. İkinci sorunun deneyi:

docker run -d --name dockerseries-api2 -p 127.0.0.1:8082:8080 \
    -e ASPNETCORE_URLS=http://127.0.0.1:8080 dockerseries-bookstore-api:1.1.0
docker logs dockerseries-api2 2>&1 | grep "Now listening"
curl -sS -m 3 localhost:8082/health
      Now listening on: http://127.0.0.1:8080
curl: (56) Recv failure: Connection reset by peer

Uygulama çalışıyor, port yayımlanmış, ama bağlantı reddediliyor. Docker'ın yönlendirdiği paket container'ın ağ kartına (172.x adresine) geliyor, uygulama ise yalnız container'ın kendi loopback'ini dinliyor. Container'da uygulama her zaman bütün arayüzleri dinlemeli: .NET image'larının ASPNETCORE_HTTP_PORTS=8080 varsayılanı bunu yapıyor (http://[::]:8080), sorun genelde bir launchSettings.json alışkanlığının ASPNETCORE_URLS=http://localhost:... olarak container'a taşınmasıyla çıkıyor. Node ve Python sunucularında da aynı tuzak var (0.0.0.0 yerine localhost).

nginx'te upstream adını başlangıçta çözdürmek. İlk denemede location'a düz proxy_pass http://bookstore-api:8080/; yazdık ve web container'ını API olmadan başlattık:

nginx: [emerg] host not found in upstream "bookstore-api" in /etc/nginx/conf.d/default.conf:25

nginx, proxy_pass'teki sabit adları config'i okurken bir kez çözer ve çözemezse hiç başlamaz. Container'ların başlama sırasına bağımlı bir web katmanı istemeyiz; üstelik API container'ı yeniden oluşturulup IP'si değişirse nginx eski IP'ye gitmeye devam ederdi. resolver 127.0.0.11 ve değişkenli proxy_pass, adın her istekte (burada 10 saniyelik cache'le) Docker'ın DNS'inden çözülmesini sağlıyor. API yoksa yalnız /api istekleri hata alıyor, sayfa açılıyor. Değişken kullanınca nginx path'i kendisi düzeltmediği için rewrite satırı /api önekini atıyor.

Container'ları default bridge'de bırakmak. İsim çözümü yok, bütün container'lar aynı ağda ve bunun bir kontrolü yok. --network vermeyi unuttuğun her docker run bu ağa düşer; birbirini bulması gereken container'lara her zaman bir user-defined network ver.

Docker Desktop'ta çalışanı sunucuda da öyle sanmak. host.docker.internal Desktop'ta hazır, Linux sunucuda --add-host ister; Desktop'ta port'ların nereye bağlandığı da bir ayara bağlı. Lokalde çalışan bir kurulumu sunucuya taşırken bu ikisini kontrol et.

Küçük Bir Deneme

★☆☆ docker network create --internal dockerseries-internal ile bir network oluştur, bir alpine container'ını ona bağla ve wget -qO- https://mcr.microsoft.com dene. Aynı network'teki ikinci bir container'a adıyla ulaşabiliyor musun?

★★☆ Veritabanını dockerseries-net'ten çıkarıp ayrı bir dockerseries-data network'üne al; API'yi iki network'e de bağla (docker network connect). Web container'ından bookstore-db adı hâlâ çözülüyor mu?

★★★ API container'ını docker rm -f ile silip aynı adla yeniden oluştur ve web üzerinden /api/books'u iste. Sonra nginx config'ini düz proxy_pass http://bookstore-api:8080/; ile build edip aynı denemeyi yap. API'nin IP'si değiştiğinde ikinci config ne yapıyor?

Bookstore'un üç container'ı artık birbirini adıyla buluyor ve dışarıya yalnız web açık. Ama container'ların kendisi hâlâ varsayılan ayrıcalıklarla çalışıyor: yazılabilir bir dosya sistemi, bir dizi Linux capability'si ve sınırsız process sayısı. Bir sonraki yazıda container güvenliğine geçiyoruz: non-root, read-only filesystem, capability'leri düşürmek, minimal image'lar, docker inspect'te görünen secret'lar ve image taraması.