
De Heroku à Railway étape par étape – avec tous les pièges rencontrés en chemin. Du pom.xml à la découverte de services Eureka.
Point de départ : pourquoi nous avons quitté Heroku
Heroku nous a bien servi pendant des années. Déploiements rapides, configuration simple – parfait pour démarrer. Mais avec un nombre croissant de services, les coûts sont devenus un problème. Le pire était le délai de démarrage à froid : les Dynos Basic s’endorment après 30 minutes d’inactivité et peuvent prendre jusqu’à 30 secondes pour se réveiller. C’est inacceptable pour une application professionnelle.
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
Après une brève évaluation, nous avons choisi Railway. Le plan Pro à 20 $/mois incluant 20 $ de crédits peut sembler plus cher qu’un seul Dyno au premier abord – mais Railway ne dort jamais. Et lorsqu’on fait tourner plusieurs services, le calcul devient vite favorable.
Configurer Railway en 5 minutes
Commencer avec Railway est simple. Après l’inscription, choisissez le plan Pro et créez un nouveau projet. Créez un service Railway distinct pour chaque service backend.
- Créer un compte Railway sur railway.app
- Choisir le plan Pro (20 $/mois incluant 20 $ de crédits)
- Créer un nouveau projet et ajouter PostgreSQL comme service
- Ajouter chaque microservice comme service Railway distinct et connecter à GitHub
La fonctionnalité clé : Railway déploie automatiquement à chaque push sur la branche configurée. Pas de déploiement manuel, pas de configuration CI/CD nécessaire.
Railway Projekt
─────────────────────────────────────────────────────
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ auth-service│ │blog-service │ │ PostgreSQL │
│ (GitHub ↻) │ │ (GitHub ↻) │ │ (managed) │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
└────────────────┴─────────────────┘
Private Network
(.railway.internal - kostenlos)Configurer les variables d’environnement
Railway utilise une syntaxe spéciale à doubles accolades pour les références de variables service-à-service. Cela permet d’utiliser les valeurs d’un service directement comme variables dans un autre – sans copier manuellement les identifiants. Certains services nécessitent à la fois JDBC_DATABASE_URL et SPRING_DATASOURCE_URL – les deux peuvent pointer vers la même référence Railway.
# ── 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"L’erreur la plus courante lors d’une première migration Railway : Spring Boot ne peut pas lire le format d’URL JDBC standard depuis une variable DATABASE_URL au format postgresql://. Spring exige strictement le format JDBC – que ce soit via JDBC_DATABASE_URL ou SPRING_DATASOURCE_URL.
# 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"Microservices : résoudre le problème du POM parent
Si vos microservices sont organisés dans un monorepo, vous rencontrerez une erreur inattendue lors du premier build Railway : Railway construit chaque service dans un contexte séparé – le POM parent du répertoire supérieur n’est pas disponible. La solution est simple : donnez à chaque service son propre Spring Boot parent.
<!-- 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>Si des dépendances Spring Cloud sont impliquées, il faut également ajouter un bloc dependencyManagement au pom.xml – puisque le POM parent ne le fournit plus.
<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>Autres erreurs fréquentes et leurs correctifs
Au-delà du POM parent, d’autres erreurs sont apparues lors de la migration. Voici les trois plus importantes :
⚠ Dockerfile : mauvais chemin JAR
Problem: Le Dockerfile référence un sous-répertoire dans le contexte de build qui n’existe plus dans un build service unique.
Fix: Utiliser le chemin sans sous-répertoire – le service est maintenant directement à la racine du build.
# 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 est incompatible avec Spring Boot 3.4.x et lève NoClassDefFoundError: HttpRedirects.
Fix: Rétrograder vers Spring Boot 3.3.8 + Spring Cloud 2023.0.5, ou passer au spring-cloud-starter-gateway réactif.
<!-- 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>⚠ Mauvais domaine privé Eureka
Problem: Les domaines privés Railway n’ont PAS de suffixe "-production" – eurekaservice-production.railway.internal ne fonctionne pas.
Fix: Utiliser le domaine privé sans suffixe : 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/eurekaDécouverte de services avec Eureka sur Railway
Si vous avez conservé Eureka de l’ère Heroku, vous avez probablement eureka.client.enabled=false dans votre application-prod.yml. Sur Railway, la découverte de services doit être à nouveau active – cela doit être explicitement activé.
# 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: trueSur Railway, tous les services communiquent via le réseau privé. Eureka tourne sur un service Railway et est accessible via son domaine privé. Important : le domaine suit toujours le schéma servicename.railway.internal – sans suffixe d’environnement.
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)Domaines et réseau privé
Railway distingue deux types de domaines à connaître :
- Domaine public : servicename-production.up.railway.app – HTTPS, accessible de l’extérieur, pour les appels API depuis le frontend
- Domaine privé : servicename.railway.internal – interne uniquement, gratuit (pas de trafic egress)
- Références de variables : syntaxe Railway à doubles accolades pour la configuration service-à-service
- Configuration du port : Settings → Networking → Generate Domain → entrer le port 8080
# 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}}"Comparaison des coûts
Après une semaine en production, voici le tableau :
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
Le plus grand avantage n’est pas seulement le prix : les services Railway tournent en continu – pas de mise en veille, pas de délai de démarrage à froid. Les temps de réponse sont constants et le déploiement via push GitHub fonctionne de manière fiable.
Conclusion
Railway nous a convaincus. La migration n’était pas triviale, mais l’effort en valait la peine. Quiconque connaît les pièges décrits peut migrer ses services Spring Boot en quelques heures. Points clés à retenir :
- Faire tourner les secrets : après la migration, renouveler le mot de passe DB et le JWT secret
- Supprimer DATABASE_URL : Spring Boot a besoin de jdbc://, pas de postgresql://
- Remplacer le POM parent : passer chaque service à spring-boot-starter-parent
- Utiliser les domaines privés : la communication interne via .railway.internal est gratuite
- Réactiver Eureka : ne pas oublier eureka.client.enabled=true si la découverte de services est nécessaire