Uygulamamızın her zaman erişilebilir, güvenli ve ölçeklenebilir olması için kubernetes’in karmaşık yapısına girmeden daha basit bir şekilde docker swarm kullanarak container’larımızı orkestre edip yöneteceğiz. Ayrıca hazırladığımız bu yapı, AWS’nin Multi-AZ (Çoklu Kullanılabilirlik Bölgesi) prensibine dayanıyor. Bu sayede bir veri merkezinde genel bir arıza olsa dahi sistemimiz diğer bölgeler üzerinden hizmet vermeye devam edebilecek.
Docker Swarm Nedir ve Neden Kullanıyoruz?
Docker Swarm, Docker tarafından sunulan yerleşik bir container orkestrasyon aracıdır. Karmaşık Kubernetes yapılarına girmeden, birden fazla sunucuyu yönetmemizi sağlar.
Mimari Diyagram

Uygulamamız tek bir veri merkezinde (Availability Zone) değil. 3 farklı AZ (us-east-1a, 1b, 1c) kullanarak, bir bölgede yaşanabilecek büyük bir elektrik veya ağ kesintisinde sistemin kesintisiz çalışmasını sağlıyoruz.
Senaryo:
- Frontend: Vite/React (Nginx ile sunulan).
- Backend: Node.js REST API.
- Database: MongoDB.
- Orkestrasyon: Docker Swarm (3 Manager + 3 Worker).
- Altyapı: AWS VPC, Subnets, ALB ve Route53.
Sistemimizi iki ana bölgeye ayırıyoruz:
Public: Manager nodelarımız ve Application Load Balancer (ALB) bu bölgede bulunur. İnternetten gelen trafiği karşılayan Nginx Gateway servisimiz yine burada bulunur. Burada manager node da private olarak yer alabilirdi ancak bu yazı için manager’ı yönetmeyi kolaylaştırmak açısından public subnette tutacağım.
Private: Worker nodelarımız ve MongoDB veritabanımız bu bölgede yer alır. Dışarıdan doğrudan erişilemezler, sadece iç ağ (Overlay Network) üzerinden konuşurlar. Private subnetlerdeki makinelerimizin internetten gelen isteklere kapalı olacaklar sadece NAT Gateway aracılığıyla internetten paket indirebilir, güncelleme yapabilirler.
Peki neden 3 Manager? Docker Swarm, Raft Consensus algoritmasını kullanır. 3 Manager kullanarak, bir node çökse dahi sistemin karar verme yeteneğini (Quorum) korumasını sağlıyoruz.
Frontend (Multi-Stage Build):
React uygulamamızı node:20-alpine ile build edip, sadece statik dosyaları hafif bir nginx:alpine imajına taşıyoruz. Build sırasında VITE_API_URL değişkenini enjekte ederek, uygulamamızın hangi subdomain’e (api.testdeploy.site) istek atacağını belirliyoruz.
Nginx Gateway:
En dış katmanda duran Nginx, bir Reverse Proxy ve API Gateway görevi görür. testdeploy.site üzerinden gelenleri Frontend’e, api. üzerinden gelenleri ise Backend’e yönlendirir.
Şimdi adım adım uygulamamızın dockerfile ve docker compose dosyalarını açıklayalım. Uygulamanın nasıl bir klasör yapısına sahip olduğuna ve bu bileşenleri nasıl konteyner haline getirdiğimize bakalım.
- backend/: Node.js API kaynak kodları ve Dockerfile.
- frontend/: Vite/React kaynak kodları, projeye özel Nginx konfigürasyonu ve Dockerfile.
- docker-compose.yml: Tüm orkestrasyonun kalbi olan stack tanım dosyası.
- nginx.conf & Dockerfile.nginx: En dış katmanda trafiği karşılayacak olan Reverse Proxy ayarları.
Frontend: Multi-Stage Build & Statik Sunum



Frontend tarafında performansı artırmak ve imaj boyutunu küçültmek için Multi-Stage Build tekniğini kullanıyoruz.
Derleme Aşaması (Build Stage): node:20-alpine imajı üzerinde React projemizi derliyoruz. Burada kritik nokta, ARG VITE_API_URL kullanımıdır. Build anında backend URL’ini içeriye gömüyoruz.
Sunum Aşaması (Production Stage): Derlenen dosyaları (dist), sadece statik dosya sunmak için optimize edilmiş hafif bir nginx:alpine imajına taşıyoruz.
Backend: Node.js API

Backend servisimiz oldukça sade bir yapıya sahip. node:20-alpine kullanarak sadece gerekli kaynak kodlarını ve bağımlılıkları (node_modules) içeriye kopyalıyoruz. Container dış dünyaya 3000 portu üzerinden hizmet verecek şekilde hazırlandı.
Nginx Gateway (Reverse Proxy)
En dış katmanda bir adet Dockerfile.nginx ve nginx.conf dosyalarımız var.
- Dockerfile.nginx: Standart Nginx imajını alıp, bizim yazdığımız özel nginx.conf dosyasını içine kopyalıyor.
- Yönlendirme: Gelen isteğin domain ismine bakıyor ve eğer api.testdeploy.site ise backend’e, testdeploy.site ise frontend’e trafiği iç ağ üzerinden paslar.
Docker Stack (Compose) Dosyası


Docker Swarm üzerinde çalıştıracağımız docker-compose.yml (Stack) dosyası, projenin nasıl ölçekleneceğini ve servislerin birbirleriyle nasıl konuşacağını belirler.
- Replicas (Ölçeklendirme): replicas: 3 ayarıyla Frontend ve Backend servislerimizin her birinden 3’er adet kopya çalıştırıyoruz. Bir konteyner çökerse, Swarm anında yenisini başlatır.
- Update Config (Kesintisiz Güncelleme): parallelism: 1 ve delay: 10s ayarları, yeni bir versiyon yüklerken konteynerlerin teker teker güncellenmesini sağlar. Bu, Zero Downtime (Sıfır Kesinti) deployment demektir.
- Network İzolasyonu:
- webnet: Nginx, Frontend ve Backend arasındaki trafiği taşır.
- backend: Sadece Backend ve MongoDB arasındadır. MongoDB’nin dış ağla hiçbir bağlantısı yoktur.
- Secrets (Güvenlik): Veritabanı bağlantı bilgilerini ve şifreleri (mongo_uri, mongo_root_username) düz metin olarak değil, Docker’ın güvenli Secret mekanizmasıyla yönetiyoruz.
Burada ek olarak Overlay Network’den de bahsetmek istiyorum.
Overlay Network Nedir?
Docker Compose dosyamızda dikkat ettiyseniz iki farklı ağ tanımladık: webnet ve backend. Ancak bu ağların sürücüsünü (driver) overlay olarak belirledik. Peki, neden standart bir bridge ağ değil de overlay?
Normalde Docker, tek bir makine üzerinde çalıştığında konteynerleri birbirine bağlamak için bridge ağını kullanır. Ancak bizim mimarimizde uygulamalarımız 5 farklı fiziksel sunucuya (node) dağılmış durumda. Overlay network, farklı AWS EC2 makineleri üzerinde çalışan konteynerlerin, sanki aynı makinedeymiş gibi birbirleriyle güvenli bir şekilde konuşmasını sağlar. Yani kısaca fiziksel ağların (farklı IP’ler, farklı subnet’ler, farklı provider’lar) üstünde çalışan, container’lara aynı sanal ağdaymış gibi iletişim kurduran ağdır.
Şimdi de Dockerfile dosyalarımızı build edip bu imajları docker hub’a pushlayalım. AWS üzerinde EC2 makinelerle çalışacağım ve bu makinelerin mimarisine uygun olacak şekilde platformu belirtiyorum build aşamasında.


Bu imajları lokalimde oluşturdum ve sonrasında Docker Desktop kullanarak Docker Hub’ıma pushladım. Çünkü dikkat ederseniz docker-compose dosyasında frontend, backend, nginx servisleri imaj olarak docker hub’ımdan çekiyordu.
Şimdi AWS panelimize girip Paris bölgesinde bir VPC oluşturarak başlayalım.




Ağımızı toplam 6 farklı alt ağa (Subnet) böldük:
- 3 Public Subnet: İnternetten gelen trafiği karşılayan Application Load Balancer (ALB) ve Docker Swarm Manager nodelarımız burada yer alacak. Bu makinelerin internete doğrudan kapısı vardır. Bu kapıyı da onlara IGW açacak.
- 3 Private Subnet: Backend servislerimiz ve MongoDB veritabanımız buradadır. Bu alan internete tamamen kapalıdır; bu da saldırganların veritabanınıza doğrudan ulaşmasını fiziksel olarak engeller. Ancak bu makineler internete çıkabilir bunu da NAT Gateway sağlayacak. Neden internete çıkabilirler? Docker imajı indirmek, güncelleme almak vs.
Bu ağ yapısında manager nodelarımız public subnetlerde (eu-west-3a, 3b, 3c) üzerinde yer alacak. Public olması sayesinde SSH ile bağlanıp yönetim işini kolayca halledebileceğiz.
Woker nodelarımız ise private subnetlerde yer alacak. Bu makinelerin internete doğrudan bir kapısı yoktur; sadece NAT Gateway üzerinden dışarıya (imaj çekmek için) çıkabilirler, ancak dışarıdan kimse onlara ulaşamaz.
Şimdi bu VPC’i oluşturalım ve devam edelim. Manager, worker nodelarımız ve alb için gerekli security grupları tanımlayalım.

Bu ayarlarla kaydediyorum. Daha sonrasında tekrar düzenle deyip source bölümüne şimdi oluşturduğumuz security group’u atayacağım. Yani kendi kendine referans vereceğim.

Bu sayede sadece bu gruba üye olan sunucular birbirleriyle konuşabilir, dış dünya kapıda kalır.

Bu security group ise load balancer için yani dışardan gelen istek load balancer’a gelecek. Load Balancer zaten public subnette yer alacak.

Manager düğümlerin hem cluster’ı yönetir hem de Nginx Gateway aracılığıyla ALB’den gelen trafiği kabul eder. Burada HTTP | Port: 80 | Source: sg-alb olarak seçiyoruz. Böylece Manager’lara sadece ALB üzerinden trafik gelebilir
Şimdi sırada EC2 makinelerini ayağa kaldırmak var. İlk olarak “swarm-manager-1” adıyla manager nodelardan birini oluşturacağım.


Mimarı diyagramına göre VPC içerisinde public subnetlerden birine yerleştirdim. Aynı zamanda olışturmuş olduğumuz security groupları da tanımladım.

Ve sistem kurulurken gerekli paketleri de beraberinde kurabilmesi için User Data scripti ekledim. Bu script paket güncellemelerini ve docker kurulumunu yapıyor.
#!/bin/bash
# Paket listesini güncelle
apt-get update -y
apt-get upgrade -y
# Gerekli bağımlılıkları kur
apt-get install -y ca-certificates curl gnupg
# Docker'ın resmi GPG anahtarını ekle
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
# Docker deposunu kaynak listesine ekle
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null
# Docker Engine kurulumu
apt-get update -y
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# Docker servisini başlat
systemctl start docker
systemctl enable docker
# 'ubuntu' kullanıcısını docker grubuna ekle (sudo kullanmadan komut çalıştırmak için)
usermod -aG docker ubuntu
Şimdi sırada bu makineye bağlanıp Swarm Manager rolünü atamak var.

swarm-manager grubunda ufak bir düzenleme yapıyorum ssh için kendi bilgisayarımın IP adresini atıyorum. Bu sayede kendi bilgisayarımdan SSH yapabileceğim. HTTP kısmına dokunmuyorum çünkü sadece Load Balancer’dan gelen istek kabul edilecek.
Docker Swarm Kurulumu

Neden bu IP? AWS içindeki makineler birbirleriyle bu özel (Private) IP üzerinden konuşur. Bu komutla Manager-1’i “Ben bu kümenin lideriyim ve diğer makineler bana bu IP’den ulaşabilir” şeklinde tanımlıyoruz.
Buradaki join token’i bir yere kopyalıyorum bu token aracılığıyla worker nodelarımı oluşturacağım. Ancak benim bir diper ihtyacım olan şey diğer manager nodeları da oluşturabilmek. Mimaride 3 manager node’umuz vardı.

Şimdi sırayla diğer 2 manager makineyi ve 3 worker makineyi kurmaya başlayalım.
Burada her bir adımı tekrar etmeyeceğim diğer 2 manager’ı kalan 2 subnet’e ekleyerek oluşturuyoruz.
Manager-2’yi Cluster’a Dahil Etme

Manager token’i kullanarak bu makineyi de 2. Manager node olarak atadım.
Manager 1 üzerinde kontrol sağlayalım.

2 adet node görünüyor.
Aynı mantıkla diğer manager node’u ve worker nodeları da oluşturup join edeceğiz. Manager ve Worker nodelar için join tokenlar faklıdır. Zaten ilk görselde bunu anlattım.
Şimdi worker nodelardan birini oluşturalım.
Worker Makine Oluşturulması

Peki worker nodelara nasıl bağlanacağız? Public ipleri yok. Doğrudan SSH yapamayız. Bunun için manager-1 makinesini workerlara atlamak için kullanacağım.


chmod 400 awskey.pem diyerek de yetkilerini ayarladım.

Worker node’umun iç ağdaki ip adresini kullanarak bağlanıyorum. Eğer bağlanamıyorsak swarm-common security group’umuzda SSH portuna izin vermemişiz demektir.

Burada seçtiğim SG, swarm-manager’a ait security group. Yani swarm-manager 22 portundan gelebilir diyorum.

Ve worker node’umuzu ekledik. Aynı işlemi diğer nodelar içinde tekrar edeceğiz.

Cluster Durum Analizi
- Leader (10.0.3.133): Orkestra liderimiz.
- Reachable (10.0.20.13 ve 10.0.40.115): Diğer iki Manager düğümü hazır bekliyor. Eğer Leader çökerse, bu ikisinden biri saniyeler içinde yeni liderliği devralacak (High Availability).
- Workers (Son 3 satır): Üç işçi düğümü de cluster’a başarıyla bağlanmış. Hepsinin durumu “Ready” ve kullanılabilirliği “Active”.
Docker Secrets Oluşturma

mongo_root_username, mongo_root_password ve mongo_uri nereden geliyor? Docker-compose dosyasına tekrar bakalım.

Secrets tanımlamalarımız burada yer alıyor.

- Placement Constraints: Manager’ları sadece orkestrasyon ve dış trafik (Nginx) için kullanırken, uygulama ve veritabanı gibi ağır yükleri Worker düğümlerine ekledik. Bu, güvenlik ve performans için en iyi pratiktir.
- External Secrets: Şifreleri dosya içine yazmadık ve sistemin kendi güvenli belleğinden (external) okumasını sağladık.
Dosyanın tam hali:
version: "3.8"
services:
nginx:
image: eryklkn19/swarm-nginx:1.0.0
ports:
- "80:80"
networks:
- webnet
deploy:
replicas: 2
placement:
# Nginx'leri yönetilebilir olması için Manager düğümlerine koyuyoruz
constraints: [node.role == manager]
update_config:
parallelism: 1
delay: 10s
frontend:
image: eryklkn19/swarm-frontend:1.0.0
networks:
- webnet
deploy:
replicas: 3
placement:
# Asıl iş yükünü Worker düğümlerine dağıtıyoruz
constraints: [node.role == worker]
update_config:
parallelism: 1
delay: 10s
backend:
image: eryklkn19/swarm-backend:1.0.0
networks:
- webnet
- db-net
secrets:
- mongo_uri
deploy:
replicas: 3
placement:
constraints: [node.role == worker]
update_config:
parallelism: 1
delay: 10s
mongodb:
image: mongo:latest
networks:
- db-net
volumes:
- mongo-data:/data/db
secrets:
- mongo_root_username
- mongo_root_password
environment:
# MongoDB'nin root yetkilerini az önce oluşturduğumuz secret'lardan okumasını sağlar
MONGO_INITDB_ROOT_USERNAME_FILE: /run/secrets/mongo_root_username
MONGO_INITDB_ROOT_PASSWORD_FILE: /run/secrets/mongo_root_password
deploy:
replicas: 1
placement:
# Veri güvenliği ve kalıcılığı için Worker'da çalıştırıyoruz
constraints: [node.role == worker]
networks:
webnet:
driver: overlay
db-net:
driver: overlay
volumes:
mongo-data:
secrets:
# 'external: true' diyerek bu secret'ların manuel olarak (docker secret create)
# daha önce oluşturulduğunu Swarm'a bildiriyoruz.
mongo_uri:
external: true
mongo_root_username:
external: true
mongo_root_password:
external: true

Bu şekilde manager üzerinde deploy komutuyla servislerin yayına girmesini sağladık.

- Frontend (3/3): Tüm kopyalar aktif.
- MongoDB (1/1): Veritabanı aktif.
- Nginx (2/2): Her iki giriş kapısı da (*:80->80/tcp) açık ve trafik bekliyor.
AWS Application Load Balancer (ALB) Kurulumu


3 manager makineyi seçip ‘include as pending below’ seçeneğine tıklıyoruz. ‘swarm tg-80’ adıyla isimlendirip oluşturdum.

Şimdi de “swarm-alb” adıyla load balancer’ı oluşturuyoruz.


Swarm-common security group’un ayarlarına all traffic olarak swarm-common grubunun kendisini tanımladım. Bu gruba üye olan tüm makineler (Manager ve Worker), birbirlerine veri gönderebilir.
Şimdi ALB DNS adresinden kontrol sağlayalım.


Uygulamamız açıldı. Şimdi route53 üzerinde hosted zonelarımızı tanımlayalım testdeploy.site frontend için api.testdeploy.site adresi ise backend için olacak. A kayıtlarını giriyoruz ve ALB’i seçiyoruz “route traffic to” seçeneğinde.



Bu tanımları yaptıktan sonra nginx conf’umuz içerde backend ve frontend servislerine doğru bir şekilde yönlendirme yapacaktır.

Hatırlarsanız nginx.conf’da frontend ve backend için upstream komutuyla gerekli yönlendirmeleri yapmıştık.

docker service logs -f swarm_app_nginx diyerek nginx servisinin loglarını izleyebiliriz.


Nginx servisimiz için belirlediğimiz 2 adet kopya (replica) şu an aktif olarak çalışıyor. Gördüğünüz gibi kopyalardan biri manager node’da diğeri de worker node’da çalışıyor yani 2 sunucuda çalışan kopyalarımız var. Her iki konteyner de yaklaşık 3 saattir (Running 3 hours ago) kesintisiz ve hatasız bir şekilde hizmet veriyor.
Peki ya container’ı silersem ne olacak?
docker rm -f 76bf55135dd9

Swarm, “Bu servisin 2 kopyası olması gerekiyordu, şu an 1 tane kaldı” dediği anda (Desired State vs Current State) devreye girdi. Sadece 1 saniye içinde (Preparing 1 second ago), sildiğin konteynerin yerine tamamen yeni bir ID ile yeni bir konteyner oluşturuldu. yeni konteyneri eski yerinde (ip-10-0-3-133) değil, daha uygun gördüğü başka bir node üzerinde (ip-10-0-40-115) Ready durumuna getirdi.

Bu da docker swarm’ın self healing yapısına bir örnektir. Herhangi bir container çökerse dengelemek için anında yenisini oluşturur.
SSL Aktif Etme
ACM (AWS Certificate Manager) kullanarak domainlerimize SSL ekleyeceğim.


Create records in Route 53 diyorum. Bu buton, gerekli olan CNAME kayıtlarını Route 53 Hosted Zone’una otomatik olarak ekleyecektir.

“swarm-alb” 443 listener’ı ekledim. Sertifika olarak testdeploy.site’ı ekledim.


backend tarafında bir sorun vardı. stack dosyasında mongodb tanımını yanlış yazmışım servis ismini ‘db’ olarak güncelleyip tekrar deploy ettim ve backend servisi başarıyla çalıştı.


Sonuç başarılı.
NOT: AWS üzerinde kullandığımız tüm kaynakları işimiz bittikten sonra silmeyi unutmayalım. Aksi durumda kullanım için AWS ücretlendirecektir. Tabii eğer deneme amaçlı kullanıyorsanız bunu unutmayalım.