Déployer Spring Boot sur Hetzner
Suivez ça sur votre propre serveur. Commencer gratuitement, sans carte
Spring Boot n’a jamais convergé vers une seule façon devinable de construire et de démarrer une application JVM, comme Rails a Puma ou Laravel a PHP-FPM : Maven ou Gradle, un simple jar ou un WAR, une demi-douzaine de commandes de démarrage raisonnables selon la façon dont un projet est organisé. Un Dockerfile contourne tout ça en précisant exactement quel JDK, quel outil de construction et quelle commande finale l’application a besoin. À la fin de ce guide, vous aurez une application Spring Boot 4 construite à partir de son propre Dockerfile, qui tourne sur un serveur Hetzner derrière Caddy avec un vrai certificat, appuyée sur Postgres, avec des sauvegardes nocturnes et un processus de mise en production court et répétable.
Ce qu’il vous faut
- Un compte Hetzner Cloud et un serveur. Le CX23 à coût optimisé (2 vCPU, 4 Go de mémoire, 40 Go NVMe) est affiché à 5,99 €/mois hors TVA ; une construction Maven ou Gradle peut être gourmande en mémoire, donc si la construction elle-même se fait tuer en plein milieu, redimensionnez vers un CX33 (4 vCPU, 8 Go, 8,99 €/mois) pour la construction et redescendez ensuite.
- Un nom de domaine auquel vous pouvez ajouter un enregistrement DNS.
- Une paire de clés SSH sur votre machine (
ssh-keygen -t ed25519si vous n’en avez pas déjà une). - Une application Spring Boot construite avec Maven ou Gradle, ciblant Java 17 ou plus récent, le minimum exigé par Spring Boot 4. Ce guide en construit une nouvelle depuis Spring Initializr pour que vous puissiez suivre même sans application existante.
- Environ quinze minutes pour que le serveur et le DNS se stabilisent, surtout passées à attendre plutôt qu’à taper.
Étape 1 : créer le serveur et noter son IP
Dans la Hetzner Cloud Console, ouvrez (ou créez) un projet, puis choisissez Add Server. Choisissez un emplacement, choisissez Ubuntu 24.04 comme image, puis sous le type de serveur choisissez le CX23 à vCPU partagé. Sous SSH keys, ajoutez la clé publique de votre machine, puis créez le serveur.
ssh root@YOUR_SERVER_IP
Vous devriez voir la bannière de bienvenue d’Ubuntu et atterrir sur un shell root.
Étape 2 : pointer votre domaine vers le serveur
Ajoutez un enregistrement A chez votre fournisseur DNS pour le nom d’hôte que vous voulez utiliser, pointant vers l’IP du serveur :
Type: A
Name: myapp (ou @ pour le domaine nu)
Value: YOUR_SERVER_IP
TTL: 300
dig +short myapp.example.com
Vous devriez voir l’IP de votre serveur s’afficher avant de continuer, puisque Caddy a besoin que ça résolve déjà à l’étape 7.
Étape 3 : installer Docker sur le serveur
apt-get update
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh
systemctl enable --now docker
ufw allow OpenSSH
ufw allow 80
ufw allow 443
ufw --force enable
docker --version
docker compose version
Vous devriez voir une version de Docker et une version de Compose affichées, pas une erreur « command not found ».
Étape 4 : générer l’application et lier $PORT
Sur votre propre machine, générez un projet depuis start.spring.io (ou curl) : Maven, Java 21, Spring Boot 4.1.x, avec au minimum les dépendances Web et PostgreSQL Driver.
curl https://start.spring.io/starter.tgz \
-d dependencies=web,postgresql,actuator \
-d javaVersion=21 -d bootVersion=4.1.2 -d type=maven-project \
-d name=myapp -d baseDir=myapp | tar -xzvf -
cd myapp
Chaque méthode de construction sur Railyard, et la configuration manuelle de ce guide, s’attend à ce que l’application se lie au port qu’on lui indique via $PORT. Spring Boot lit ça à travers server.port, qui correspond à la variable d’environnement SERVER_PORT grâce au liage souple de Spring, donc aucun changement de code n’est nécessaire, définissez-la simplement comme variable d’environnement à l’étape 6. Ajoutez le point de terminaison de santé Actuator, que la vérification de santé de l’étape 6 utilise :
management:
endpoint:
health:
show-details: never
endpoints:
web:
exposure:
include: health
Vérifiez que la structure du projet est bien là :
ls pom.xml src/main/java
Étape 5 : écrire le Dockerfile
FROM maven:3.9.9-eclipse-temurin-21 AS build
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn package -DskipTests -B
FROM eclipse-temurin:21-jre-alpine AS runtime
WORKDIR /app
RUN addgroup -S app && adduser -S app -G app
COPY --from=build /build/target/*.jar app.jar
USER app
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
L’étape de construction copie pom.xml en premier et exécute dependency:go-offline avant de copier le reste du code source, pour que le cache de couches de Docker évite de retélécharger chaque dépendance quand seul votre code Java a changé, pas pom.xml lui-même. L’étape d’exécution part d’un JRE plutôt que d’un JDK complet, puisque rien à l’exécution n’a besoin d’un compilateur, et tourne comme utilisateur non root app plutôt que root. Remplacez maven:3.9.9-eclipse-temurin-21 et pom.xml/mvn par gradle:9-jdk21 et build.gradle/gradle build si votre projet utilise Gradle à la place ; la forme de la construction en deux étapes est identique dans les deux cas.
Vérifiez que ça construit :
docker build -t myapp .
Vous devriez voir BUILD SUCCESS de Maven à mi-chemin, et la couche finale de l’image se terminer sans erreur.
Étape 6 : écrire le fichier Compose
Copiez votre application sur le serveur (scp, rsync, ou un git clone), puis ajoutez compose.yml et un fichier .env à côté :
mkdir -p /opt/myapp
cat > /opt/myapp/.env <<'EOF'
POSTGRES_PASSWORD=a-long-random-password
EOF
chmod 600 /opt/myapp/.env
services:
app:
build: .
image: myapp:latest
restart: unless-stopped
env_file: .env
environment:
SERVER_PORT: "8080"
SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/myapp
SPRING_DATASOURCE_USERNAME: myapp
SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD}
depends_on:
db:
condition: service_healthy
expose:
- "8080"
db:
image: postgres:16.4-alpine
restart: unless-stopped
environment:
POSTGRES_USER: myapp
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: myapp
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U myapp"]
interval: 5s
timeout: 5s
retries: 10
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
depends_on:
- app
volumes:
db_data:
caddy_data:
caddy_config:
SPRING_DATASOURCE_PASSWORD et les autres variables SPRING_DATASOURCE_* sont la façon dont le liage souple de Spring fait correspondre une variable d’environnement à la propriété équivalente spring.datasource.*, sans avoir à modifier application.yml pour chaque environnement. depends_on: condition: service_healthy empêche l’application de démarrer avant que Postgres puisse réellement accepter des connexions. Vérifiez que ça s’analyse :
docker compose config
Étape 7 : ajouter Caddy et démarrer la pile
myapp.example.com {
reverse_proxy app:8080
}
docker compose up -d --build
docker compose ps
curl -I https://myapp.example.com/actuator/health
Vous devriez voir les trois services à l’état Up (db à l’état healthy), et le curl retourner HTTP/2 200 avec {"status":"UP"} dans le corps.
Étape 8 : sauvegarder Postgres
cat > /opt/myapp/backup.sh <<'EOF'
#!/bin/sh
cd /opt/myapp
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
docker compose exec -T db pg_dump -U myapp myapp | gzip > /opt/myapp/backups/$TIMESTAMP.sql.gz
find /opt/myapp/backups -name '*.sql.gz' -mtime +14 -delete
EOF
mkdir -p /opt/myapp/backups
chmod +x /opt/myapp/backup.sh
(crontab -l 2>/dev/null; echo "0 3 * * * /opt/myapp/backup.sh") | crontab -
Lancez-le une fois à la main et vérifiez que ça a fonctionné : ./backup.sh && ls -lh /opt/myapp/backups. Vous devriez voir un fichier .sql.gz avec l’horodatage du jour. Copiez-les aussi en dehors du serveur, pour que perdre la machine ne veuille pas dire perdre aussi la sauvegarde.
Étape 9 : livrer une mise à jour
git pull
docker compose build app
docker compose up -d app
Ajoutez une migration Flyway ou Liquibase si votre schéma change entre les versions ; un simple spring.jpa.hibernate.ddl-auto en production est une chose que ce guide omet volontairement, puisque laisser Hibernate modifier un schéma de production au démarrage est une vraie façon de perdre des données sur une mauvaise migration. Vérifiez que c’est passé :
docker compose logs --tail=50 app
curl -I https://myapp.example.com/actuator/health
Le raccourci : avec Railyard
Un dépôt avec son propre Dockerfile est la première méthode de construction que Railyard vérifie, avant Railpack, avant Nixpacks, avant tout le reste, et pour un projet JVM, ce n’est pas un repli, c’est le bon outil pour une forme de construction avec trop de réponses valides pour deviner en sécurité. Poussez un dépôt avec le Dockerfile de l’étape 5, et Railyard le construit et le déploie à chaque poussée de la même façon que les étapes 6 à 9 le font à la main, avec Postgres ajouté comme module géré dont la chaîne de connexion est injectée comme variable au lieu d’être copiée vous-même dans un fichier compose, et un serveur Hetzner provisionné directement depuis Railyard à la taille que vous choisissez.
Pour en savoir plus, voyez méthodes de construction, déployer depuis un dépôt, bases de données et sauvegardes, et les comparaisons de coûts d’hébergement.
Dépannage
La construction se fait tuer sans erreur claire. Maven ou Gradle qui résout et compile un véritable arbre de dépendances peut avoir besoin de plus de mémoire qu’un CX23 n’en a de libre pendant docker compose up -d --build ; redimensionnez temporairement, ou ajoutez du swap comme solution de secours moins coûteuse (fallocate -l 2G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile).
L’application n’arrive pas à joindre Postgres, « Connection refused ». Confirmez que SPRING_DATASOURCE_URL utilise db comme hôte, pas localhost ; à l’intérieur du réseau de Compose, db est la seule adresse qui résout vers le conteneur Postgres.
Port déjà utilisé. Apache ou Nginx par défaut d’Ubuntu est peut-être déjà lié au port 80 ou 443. ss -tlnp | grep -E ':80|:443' montre ce qui l’occupe.
Le HTTPS ne s’émet pas. docker compose logs caddy montrant des erreurs de certificat répétées signifie presque toujours que le DNS de l’étape 2 ne s’est pas encore propagé, ou pointe vers la mauvaise IP. Relancez dig +short myapp.example.com et comparez.
Le jar se construit mais le conteneur se ferme immédiatement. Vérifiez docker compose logs app pour une trace de pile au démarrage, le plus souvent une variable d’environnement manquante que Spring attendait ou une connexion à Postgres qu’il n’a pas pu établir avant que la vérification de santé de depends_on aurait dû bloquer ; confirmez que la vérification de santé de l’étape 6 passe réellement avec docker compose ps.
FAQ
Dois-je utiliser Maven, ou est-ce que Gradle fonctionne pareil ? Gradle fonctionne pareil ; remplacez l’image de construction par gradle:9-jdk21 et les deux lignes RUN par gradle dependencyInsight puis gradle build -x test, la forme en deux étapes de l’étape 5 ne change pas autrement.
Un CX23 suffit-il en production ? Pour une API de petite à moyenne taille avec un trafic modeste, oui, une fois que la construction elle-même est gérée séparément comme dans le dépannage ci-dessus ; l’empreinte mémoire de la JVM au repos est la principale chose à surveiller sur une machine de 4 Go, ajustez -Xmx explicitement si vous ajoutez d’autres services au même serveur plus tard.
Pourquoi Java 21 plutôt qu’une version plus récente ? Spring Boot 4 n’exige que Java 17 ou plus récent ; 21 est la version LTS actuelle avec le support d’image de base et d’outillage le plus large, c’est pourquoi ce guide la fixe, pas parce qu’elle est obligatoire.
Comment ajouter une tâche en arrière-plan ou planifiée ? L’annotation @Scheduled de Spring tourne dans le même processus et n’a besoin d’aucun conteneur séparé ; un worker véritablement séparé (consommant une file, par exemple) est un second service dans compose.yml construit à partir du même Dockerfile avec une commande d’entrée différente.
Quelle est la vraie différence avec un déploiement Rails ou Laravel ici ? La forme (serveur, Dockerfile, Postgres, Caddy, sauvegarde, mise en production) est identique ; la seule vraie différence, c’est que Rails et Laravel obtiennent tous les deux la détection de construction de Railyard sans Dockerfile du tout, alors que la forme de construction de Spring Boot est vraiment trop variée pour être devinée en sécurité, donc le Dockerfile ici n’est pas optionnel comme il le serait pour eux.
Voilà une configuration de production complète : un serveur, Docker Compose qui fait tourner votre application et Postgres, Caddy qui gère le HTTPS, une sauvegarde nocturne, et un processus de mise en production court. Gardez le Dockerfile, compose.yml et le Caddyfile dans votre dépôt aux côtés de l’application, et chaque futur déploiement, c’est le même git pull, build, redémarrage de l’étape 9.
Pourquoi les gens choisissent Railyard pour ça
- Votre propre compte infonuagique. Déployez sur DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, ou n'importe quel serveur Linux par SSH, facturé directement par votre fournisseur, sans majoration.
- Un prix fixe par équipe. L'offre gratuite est gratuite pour toujours : un serveur plus un serveur de base de données, trois applications, sans carte. Pro coûte 19 $ par mois pour jusqu'à cinq serveurs et un nombre illimité d'applications. Team coûte 49 $ par mois avec des serveurs illimités. Aucuns frais par application, par déploiement, par siège ou par gigaoctet.
- Des gabarits en un clic. Déployez des applications open source, comme des bases de données, des outils d'analyse et des outils de développement, sur votre propre serveur en un clic.
- Des bases de données gérées. Postgres, MySQL et Redis, avec des sauvegardes planifiées, vérifiées par restauration, et la récupération à un point dans le temps.
- Déploiement par push. Déployez depuis GitHub, GitLab ou la CLI, avec HTTPS automatique et des mises en production sans interruption.