Docker İçerisinde PostgreSQL Ayağa Kaldırmak
Elly projesinde Docker Compose ile multi-tenant PostgreSQL, pgAdmin, Redis ve RabbitMQ kurulumu. Multi-stage Dockerfile, non-root user, healthcheck ve Spring Boot Docker Compose integration.
Bu makale, önceki Modern PostgreSQL Kullanımı ve İpuçları yazımızın bir devamı niteliğindedir. Uygulamalarımızı geliştirirken her ortamda (Local, Staging, Production) birebir aynı bağımlılık altyapısını kurabilmenin en iyi yolu Container (Docker vb.) mimarisinden geçer. Bu yazıda elly projesinden örneklerle hem PostgreSQL'i hem de uygulama imajını multi-stage build ile nasıl ürettiğimi paylaşıyorum.
1. Docker Compose ile PostgreSQL Kurulumu
PostgreSQL'i saf şekilde işletim sistemine kurmak (bare-metal) yerine containerize etmek izolasyon anlamında büyük fayda sağlar. Aşağıdaki docker-compose.yml dosyası, verilerin konteyner silindiğinde kaybolmaması için named volume kullanılarak yapılandırılmış modern bir PostgreSQL konfigürasyonudur.
services:
postgres-tenant1:
image: postgres:16-alpine
container_name: elly_tenant1_postgres
restart: always
environment:
POSTGRES_USER: ${DB_USER:-admin}
POSTGRES_PASSWORD: ${DB_PASSWORD:-supersecret}
POSTGRES_DB: ${DB_NAME:-elly_tenant1}
ports:
- '5433:5432'
volumes:
- pgdata_t1:/var/lib/postgresql/data
- ./docker/init-db.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}']
interval: 10s
timeout: 5s
retries: 5
postgres-tenant2:
image: postgres:16-alpine
container_name: elly_tenant2_postgres
environment:
POSTGRES_USER: ${DB_USER:-admin}
POSTGRES_PASSWORD: ${DB_PASSWORD:-supersecret}
POSTGRES_DB: ${DB_NAME:-elly_tenant2}
ports:
- '5434:5432'
volumes:
- pgdata_t2:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U $${POSTGRES_USER}']
interval: 10s
timeout: 5s
retries: 5
volumes:
pgdata_t1:
pgdata_t2:Konfigürasyon Detayları
- Alpine İmajı (
postgres:16-alpine): Debian/Ubuntu tabanlı devasa imajlar yerine, güvenlik ve performans açısından son derece hafif olan Alpine Linux tercih edildi. İmaj boyutundan tasarruf ederken build hızlarını çok artırır.ellyproduction cluster'ı da aynı imajı kullanıyor (bkz.k8s/2c-postgres.yaml). - Kalıcı Veri Depolama (
volumes: pgdata_t1, pgdata_t2): Konteyner yeniden başlatıldığında ya da silinip tekrar up edildiğinde, Docker volume içerisindeki veriler güvenle veritabanına geri bağlanır. Her tenant kendi volume'üne sahip — veriler asla karışmaz. - Başlangıç Scriptleri (
init-db.sql):elly/docker/init-db.sqldosyası/docker-entrypoint-initdb.ddizinine mount edilir. PostgreSQL imajı ilk boot'ta buradaki SQL'i çalıştırıp default tabloları, extension'ları (CREATE EXTENSION pgcrypto;), role'leri kurar. Sonraki başlatmalarda script çalıştırılmaz. - Healthcheck Mekanizması:
pg_isreadyile sadece network port'unun değil, Postgres'inaccept_connectionstate'inin de kontrol edildiğinden emin oluyoruz. Buna bağımlı servisler (Spring Boot uygulaması, RabbitMQ consumer'ları)depends_on: condition: service_healthyile DB tam manasıyla ayaktayken başlatılır. - Port Çakışması Önleme:
Tenant1 için
5433, Tenant2 için5434kullanarak aynı makinede iki Postgres'i aynı anda çalıştırıyoruz. Host'taki varsayılan5432portunu bloke etmiyoruz.
2. Bağımlı Servisler: Redis + RabbitMQ
elly sadece Postgres değil; Redis ve RabbitMQ'yu da aynı compose ile kaldırıyor. Böylece bun run dev (frontend) + ./mvnw spring-boot:run (backend) için tek komutta (docker compose up -d) altyapı hazır olur:
redis:
image: redis:7-alpine
command: >
redis-server
--appendonly yes
--maxmemory 256mb
--maxmemory-policy allkeys-lru
--requirepass ${REDIS_PASSWORD:-devpass}
ports:
- '6379:6379'
volumes:
- redis_data:/data
rabbitmq:
image: rabbitmq:3.13-management-alpine
ports:
- '5672:5672' # AMQP
- '15672:15672' # Management UI
environment:
RABBITMQ_DEFAULT_USER: ${RMQ_USER:-admin}
RABBITMQ_DEFAULT_PASS: ${RMQ_PASS:-admin123}
volumes:
- rabbit_data:/var/lib/rabbitmq3. Spring Boot Docker Compose Integration
elly pom.xml içinde spring-boot-docker-compose dependency'si mevcut. Bu bağımlılık sayesinde ./mvnw spring-boot:run komutu:
- Proje root'unda
docker-compose.ymldosyasını bulur, - Otomatik
docker compose up -dçalıştırır, - Uygulama
SIGTERMaldığında compose stack'i ayağa kalkmış hâlde bırakır (hızlı restart için) veyaspring.docker.compose.lifecycle-management=stop-on-exitayarıyla durdurur.
Geliştirici, başka bir terminal açmaya bile gerek duymadan veritabanı + cache + broker'a sahip bir backend'i iki tık ile kaldırabiliyor.
4. Uygulama İmajı: Multi-Stage Dockerfile
Sadece veritabanını değil, uygulamayı da containerize etmek gerekiyor. elly/Dockerfile multi-stage build kullanıyor:
# ---- Stage 1: Builder ----
FROM maven:3.9.9-eclipse-temurin-21 AS builder
WORKDIR /build
# Dependency cache
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn clean package -DskipTests -B
# ---- Stage 2: Runtime ----
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
# Non-root user — güvenlik
RUN addgroup -S spring && adduser -S spring -G spring
USER spring
# Asset klasörleri (JAR dışında kalan dosyalar için)
RUN mkdir -p /app/assets/images /app/assets/file
COPY --from=builder --chown=spring:spring /build/target/*.jar app.jar
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD wget -qO- http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", "-jar", "app.jar"]Neden Multi-Stage?
- Boyut tasarrufu: Final image yaklaşık %50 daha küçük — çünkü Maven + kaynak kodu sadece
builderkatmanında kalıyor, runtime'a taşınmıyor. - Güvenlik: Maven + build tool'ları production image'a sızmıyor (supply-chain attack yüzeyi düşüyor).
- Cache optimizasyonu:
pom.xmldeğişmediği sürecedependency:go-offlinekatmanı cache'ten gelir — sadece kod değişti isemvn packageçalışır, build 10x hızlanır. - Non-root
springuser: Container escape senaryolarında saldırgan root değil, spring user olarak kalır. K8ssecurityContext.runAsNonRoot: trueile uyumlu. HEALTHCHECK:/actuator/healthendpoint'i hem Docker hem de K8s liveness probe için ortak kaynak.- JRE-only final image:
jdkdeğiljre— compile etmemize gerek yok, sadece çalıştırıyoruz.
5. .dockerignore Disiplini
Tekrarlı build'lerde saniyeleri çalmak için .dockerignore kritik:
target/
.git/
.idea/
.vscode/
*.md
.env*
load-tests/
docs/
Bu sayede COPY . komutu tarafından gereksiz dosyaların context'e gitmesini engelleyip build context'i yüzde yetmişe kadar küçültebiliyoruz.
6. Geliştirme Sürecindeki Etkisi
elly projesini ayağa kaldırırken de bu yapıyı tercih ettim. Back-end takımı sadece şu komutları çalıştırarak saatlerce süren veritabanı indirme ve ayarlama işlemlerinden kurtuluyor:
# Altyapı
docker compose up -d
# Sağlık kontrolü
docker compose ps
# Uygulama (compose'u otomatik bağlar)
./mvnw spring-boot:runYeni bir developer takıma katıldığında "onboarding süresi" artık 3-4 saatten 15 dakikaya düştü. Ayrıca CI pipeline'ında GitHub Actions, aynı multi-stage Dockerfile'ı kullanarak her PR için test imajı oluşturabiliyor — yerel ile CI arasında deterministic parity sağlanıyor.
Bir sonraki durağımız, tüm bu yapıların yatayda sonsuz ölçüde nasıl scale edildiği yer olan Kubernetes olacak!