Heroku'dan Railway'e: Spring Boot Microservisleri Taşımak

Heroku'dan Railway'e: Spring Boot Microservisleri Taşımak
Zelkulon15 Mart 20261 dk okuma

Heroku'dan Railway'e adım adım – yolda karşılaştığımız tüm tuzaklarla birlikte. pom.xml'den Eureka Service Discovery'ye kadar.

Başlangıç noktası: Heroku'yu neden terk ettik

Heroku yıllarca bize iyi hizmet etti. Hızlı deployment, basit yapılandırma – başlamak için mükemmel. Ancak servis sayısı arttıkça maliyetler sorun olmaya başladı. En kötü şey spin-up gecikmesiydi: Basic Dynolar 30 dakika hareketsizlik sonrasında uyuyor ve bir sonraki istekte uyanmaları 30 saniye kadar sürebiliyor.

Heroku (vorher)
─────────────────────────────────────────────────────
  auth-service    ($7/Monat)   → Basic Dyno
  blog-service    ($7/Monat)   → Basic Dyno
  PostgreSQL      ($5/Monat)   → Mini Plan
  [weitere Services ...]
─────────────────────────────────────────────────────
  Gesamt: ~$46/Monat + Spin-up Delay bei jedem Kaltstart

Kısa bir değerlendirmenin ardından Railway'i seçtik. $20 kredi dahil aylık $20'lık Pro Plan başta pahalı görünebilir – ancak Railway uyumuyor. Birden fazla servis çalıştırıyorsanız hesap hızla tutmaya başlıyor.

Railway'i 5 dakikada kurma

Railway'e başlamak oldukça basit. Kayıt olduktan sonra Pro Plan'ı seçin ve yeni bir proje oluşturun. Her backend servisi için ayrı bir Railway servisi oluşturun.

  • railway.app adresinden Railway hesabı oluşturun
  • Pro Plan'ı seçin (aylık $20, $20 kredi dahil)
  • Yeni proje oluşturun ve PostgreSQL servis olarak ekleyin
  • Her microservisi ayrı bir Railway servisi olarak ekleyin ve GitHub'a bağlayın

Önemli özellik: Railway, yapılandırılan branch'e her push'ta otomatik olarak deploy eder. Manuel deployment veya CI/CD kurulumu gerekmez.

Railway Projekt
─────────────────────────────────────────────────────
  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐
  │ auth-service│  │blog-service │  │  PostgreSQL │
  │  (GitHub ↻) │  │  (GitHub ↻) │  │  (managed)  │
  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘
         └────────────────┴─────────────────┘
                   Private Network
              (.railway.internal - kostenlos)

Ortam değişkenlerini yapılandırma

Railway, servis-servis değişken referansları için çift süslü parantez sözdizimi kullanır. Bu, bir servisin değerlerini başka bir serviste değişken olarak kullanmanızı sağlar – kimlik bilgilerini manuel kopyalamanıza gerek kalmaz. Bazı servisler hem JDBC_DATABASE_URL hem de SPRING_DATASOURCE_URL gerektirir – her ikisi de aynı Railway referansına işaret edebilir.

# ── Auth-Service ────────────────────────────────────────────────
AUTH_BASE_URL="https://your-auth-service-production.up.railway.app"
AUTH_COOKIE_NAME="your_jwt_cookie"
AUTH_COOKIE_SECURE="true"
CORS_ORIGINS="https://www.yourdomain.com,https://*.vercel.app"

JDBC_DATABASE_URL="jdbc:postgresql://${{Postgres.RAILWAY_PRIVATE_DOMAIN}}:5432/${{Postgres.PGDATABASE}}?sslmode=disable"
JDBC_DATABASE_USERNAME="${{Postgres.PGUSER}}"
JDBC_DATABASE_PASSWORD="${{Postgres.POSTGRES_PASSWORD}}"
SPRING_PROFILES_ACTIVE="prod"
SWAGGER_ENABLED="false"
# ── Blog-Service (mit Liquibase) ────────────────────────────────
AUTH_JWT_SECRET="your-secret-min-256-bit"   # NIEMALS committen!
AUTH_JWT_TTL="3600"

# Railway nutzt JDBC_DATABASE_URL – manche Services brauchen zusätzlich
# SPRING_DATASOURCE_URL (Spring Boot liest beide, je nach Konfiguration)
JDBC_DATABASE_URL="jdbc:postgresql://${{Postgres.RAILWAY_PRIVATE_DOMAIN}}:5432/${{Postgres.PGDATABASE}}?sslmode=disable"
SPRING_DATASOURCE_URL="jdbc:postgresql://${{Postgres.RAILWAY_PRIVATE_DOMAIN}}:5432/${{Postgres.PGDATABASE}}?sslmode=disable"
JDBC_DATABASE_USERNAME="${{Postgres.PGUSER}}"
SPRING_DATASOURCE_USERNAME="${{Postgres.PGUSER}}"
JDBC_DATABASE_PASSWORD="${{Postgres.POSTGRES_PASSWORD}}"
SPRING_DATASOURCE_PASSWORD="${{Postgres.POSTGRES_PASSWORD}}"

SPRING_PROFILES_ACTIVE="prod"
LIQUIBASE_ENABLED="true"
SWAGGER_ENABLED="false"

İlk Railway migrasyonunda en yaygın hata: Spring Boot, DATABASE_URL değişkeninden postgresql:// formatındaki URL'yi okuyamaz. Spring, JDBC_DATABASE_URL veya SPRING_DATASOURCE_URL aracılığıyla kesinlikle JDBC formatı gerektirir.

# FALSCH – Spring Boot kann dieses Format nicht verarbeiten:
DATABASE_URL="postgresql://user:pass@host:5432/db"

# RICHTIG – JDBC-Format ist Pflicht (mit oder ohne SPRING_-Präfix):
JDBC_DATABASE_URL="jdbc:postgresql://${{Postgres.RAILWAY_PRIVATE_DOMAIN}}:5432/${{Postgres.PGDATABASE}}?sslmode=disable"
SPRING_DATASOURCE_URL="jdbc:postgresql://${{Postgres.RAILWAY_PRIVATE_DOMAIN}}:5432/${{Postgres.PGDATABASE}}?sslmode=disable"

Microservisler: Parent POM sorununu çözme

Microservislerinizi monorepo'da düzenlediyseniz, ilk Railway build'inde beklenmedik bir hatayla karşılaşırsınız: Railway her servisi ayrı bir bağlamda oluşturur – üst dizindeki parent POM mevcut değildir. Çözüm basittir: her servis kendi Spring Boot parent'ını alır.

<!-- ALT: Parent-POM aus dem Monorepo (Railway kennt diesen Pfad nicht) -->
<parent>
  <groupId>com.example</groupId>
  <artifactId>parent-backend</artifactId>
  <version>1.0.0</version>
  <relativePath>../pom.xml</relativePath>
</parent>

<!-- NEU: Direkt auf Spring Boot Parent verweisen -->
<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>3.4.3</version>
  <relativePath/>
</parent>

Spring Cloud bağımlılıkları varsa, parent POM artık bunu sağlamadığından pom.xml'e bir dependencyManagement bloğu da eklenmelidir.

<properties>
  <spring-cloud.version>2024.0.1</spring-cloud.version>
</properties>

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-dependencies</artifactId>
      <version>${spring-cloud.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

Sık karşılaşılan diğer hatalar ve çözümleri

Parent POM'un yanı sıra, migrasyon sırasında ortaya çıkan başka hatalar da var. İşte en önemli üçü:

⚠ Dockerfile: Yanlış JAR yolu

Problem: Dockerfile, tek servisli build'de artık mevcut olmayan bir alt dizine atıfta bulunuyor.

Fix: Alt dizin olmadan yolu kullanın – servis artık doğrudan build kökündedir.

# Falsch: Unterordner existiert nicht wenn Service eigenes Root hat
COPY --from=builder /build/service-name/target/app.jar app.jar

# Richtig:
COPY --from=builder /build/target/app.jar app.jar

⚠ Spring Cloud Gateway: NoClassDefFoundError

Problem: spring-cloud-starter-gateway-server-webmvc, Spring Boot 3.4.x ile uyumsuz ve NoClassDefFoundError: HttpRedirects hatası veriyor.

Fix: Spring Boot 3.3.8 + Spring Cloud 2023.0.5'e düşürün veya reaktif spring-cloud-starter-gateway kullanın.

<!-- Option A: Spring Boot + Spring Cloud downgraden (stabil) -->
<parent>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>3.3.8</version>
</parent>
<properties>
  <spring-cloud.version>2023.0.5</spring-cloud.version>
</properties>

<!-- Option B: Reaktives Gateway verwenden -->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

⚠ Yanlış Eureka private domain

Problem: Railway private domain'lerin "-production" eki YOKTUR – eurekaservice-production.railway.internal çalışmaz.

Fix: Ek olmadan private domain kullanın: eurekaservice.railway.internal

# Falsch – "-production" Suffix existiert nicht:
EUREKA_SERVER=http://eurekaservice-production.railway.internal:8761/eureka

# Richtig – Private Domain ohne Suffix:
EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://eurekaservice.railway.internal:8761/eureka

Railway üzerinde Eureka ile servis keşfi

Eureka'yı Heroku döneminden taşıdıysanız, muhtemelen application-prod.yml dosyanızda eureka.client.enabled=false vardır. Railway'de servis keşfinin yeniden aktif olması gerekiyor – bu açıkça etkinleştirilmelidir.

# application-prod.yml
eureka:
  client:
    enabled: true   # War auf Heroku deaktiviert – nicht vergessen!
    service-url:
      defaultZone: ${EUREKA_CLIENT_SERVICEURL_DEFAULTZONE:http://localhost:8761/eureka}
  instance:
    prefer-ip-address: true

Railway'de tüm servisler özel ağ üzerinden iletişim kurar. Eureka bir Railway servisinde çalışır ve private domain'i üzerinden erişilebilir. Önemli: domain her zaman servicename.railway.internal desenini takip eder – environment eki olmadan.

Service Discovery Flow (Railway Private Network)
─────────────────────────────────────────────────────────────
  auth-service ──register──▶ eurekaservice.railway.internal
  blog-service ──register──▶        :8761/eureka
                                         │
  gateway  ──fetch registry──────────────┘
      │
      └──route /api/auth/**──▶ auth-service (via Eureka)
      └──route /api/blog/**──▶ blog-service (via Eureka)

Domain'ler ve özel ağ

Railway'in bilmeniz gereken iki domain türü vardır:

  • Public domain: servicename-production.up.railway.app – HTTPS, dışarıdan erişilebilir, frontend'den API çağrıları için
  • Private domain: servicename.railway.internal – yalnızca dahili, ücretsiz (egress trafiği yok)
  • Değişken referansları: servis-servis yapılandırması için Railway çift süslü parantez sözdizimi
  • Port yapılandırması: Settings → Networking → Generate Domain → 8080 portunu girin
# Domains in Railway generieren:
# Settings → Networking → Generate Domain → Port 8080

# Ergebnis (Beispiel):
# Public:  your-auth-service-production.up.railway.app   (HTTPS, extern)
# Private: your-auth-service.railway.internal             (intern, kostenlos)

# PostgreSQL bietet zusätzlich eine TCP-Proxy-URL für externe Tools (z.B. DBeaver):
DATABASE_PUBLIC_URL="postgresql://${{PGUSER}}:${{POSTGRES_PASSWORD}}@${{RAILWAY_TCP_PROXY_DOMAIN}}:${{RAILWAY_TCP_PROXY_PORT}}/${{PGDATABASE}}"

# Intern nutzen Services dagegen immer die Private Domain:
DATABASE_URL="postgresql://${{PGUSER}}:${{POSTGRES_PASSWORD}}@${{RAILWAY_PRIVATE_DOMAIN}}:5432/${{PGDATABASE}}"

Maliyet karşılaştırması

Bir haftalık üretim ortamı sonrasında tablo şöyle:

Kostenvergleich (monatlich)
─────────────────────────────────────────────────────
  Heroku                     Railway
  ──────────────────         ──────────────────
  8× Basic Dyno  $56         Pro Plan     $20
  2× PostgreSQL  $10         (inkl. $20 Credits)
  ──────────────────         ──────────────────
  Gesamt:      ~$47          Gesamt:     ~$15 *

  * geschätzt nach erster Woche, abhängig vom Traffic

  Ersparnis: ~$32/Monat = ~$384/Jahr

En büyük avantaj yalnızca fiyat değil: Railway servisleri sürekli çalışıyor – uyuma yok, spin-up gecikmesi yok. Yanıt süreleri tutarlı ve GitHub push üzerinden deployment güvenilir çalışıyor.

Sonuç

Railway bizi ikna etti. Migrasyon kolay olmadı, ancak çaba değdi. Burada anlatılan tuzakları bilen biri Spring Boot servislerini birkaç saat içinde taşıyabilir. Sonuç olarak önemli notlar:

  • Secret'ları yenileyin: Migrasyondan sonra DB şifresi ve JWT secret'ı yenileyin
  • DATABASE_URL'yi kaldırın: Spring Boot jdbc:// gerektirir, postgresql:// değil
  • Parent POM'u değiştirin: Her servisi spring-boot-starter-parent'a geçirin
  • Private domain kullanın: .railway.internal üzerinden dahili iletişim ücretsizdir
  • Eureka'yı yeniden etkinleştirin: Servis keşfi gerekiyorsa eureka.client.enabled=true'yu unutmayın
Heroku'dan Railway'e: Spring Boot Microservisleri Taşımak