Aller au contenu principal
Ben Tiga Jean Vincent
Retour au blog
  • DevOps
  • Linux
  • Spring Boot
  • Nginx

Déployer Spring Boot sur un VPS Linux : systemd, Nginx, TLS

La configuration de production que j'utilise pour exposer une API Spring Boot derrière Nginx, avec redémarrage automatique, TLS renouvelé et journaux exploitables.

Ben Tiga Jean Vincent2 min de lecture

Un java -jar lancé dans un screen tient jusqu'au premier redémarrage du serveur. Pour une API qui porte une application mobile en production, il faut un service supervisé. Voici la configuration que je déploie systématiquement.

Un utilisateur dédié, sans shell

Première règle : l'application ne tourne jamais en root.

sudo useradd --system --no-create-home --shell /usr/sbin/nologin api
sudo install -d -o api -g api /opt/api /var/log/api

Si l'application est compromise, l'attaquant hérite d'un compte sans shell, sans home et sans droits d'écriture ailleurs que dans son répertoire de logs.

L'unité systemd

[Unit]
Description=API Spring Boot
After=network-online.target postgresql.service
Wants=network-online.target
 
[Service]
User=api
Group=api
WorkingDirectory=/opt/api
EnvironmentFile=/etc/api/api.env
ExecStart=/usr/bin/java -XX:MaxRAMPercentage=70 -jar /opt/api/app.jar
SuccessExitStatus=143
Restart=always
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/var/log/api
 
[Install]
WantedBy=multi-user.target

Trois lignes méritent une explication :

  • SuccessExitStatus=143 : un arrêt propre via SIGTERM renvoie 143. Sans cette ligne, systemd considère chaque arrêt normal comme un échec.
  • MaxRAMPercentage=70 : la JVM par défaut ignore la mémoire réellement disponible sur un petit VPS et se fait tuer par l'OOM killer. On lui fixe une borne relative.
  • EnvironmentFile : les secrets vivent dans un fichier en chmod 600, pas dans l'unité systemd, qui est lisible par tous.

Nginx en terminaison TLS

L'application écoute uniquement sur la boucle locale ; Nginx est seul exposé.

upstream api_backend {
    server 127.0.0.1:8080;
    keepalive 32;
}
 
server {
    listen 443 ssl http2;
    server_name api.exemple.com;
 
    ssl_certificate     /etc/letsencrypt/live/api.exemple.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.exemple.com/privkey.pem;
 
    location / {
        limit_req zone=api burst=20 nodelay;
 
        proxy_pass http://api_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Le proxy_set_header Connection "" combiné à keepalive dans l'upstream évite de rouvrir une connexion TCP à chaque requête : gain net sur une API appelée en boucle par une application mobile.

Le cas des WebSockets

Pour un suivi GPS temps réel, la même configuration ne suffit pas : il faut autoriser la montée en version du protocole.

location /ws/ {
    proxy_pass http://api_backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 3600s;
}

Sans le proxy_read_timeout allongé, Nginx coupe la connexion au bout de soixante secondes d'inactivité, ce qui se traduit côté mobile par des reconnexions permanentes et une batterie qui fond.

Le renouvellement TLS qui n'oublie pas Nginx

certbot renew renouvelle le certificat mais ne recharge pas Nginx. Le hook de déploiement s'en charge :

echo -e '#!/bin/sh\nsystemctl reload nginx' \
  | sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

Un certificat renouvelé mais jamais rechargé expire en production exactement comme s'il n'avait pas été renouvelé. C'est l'incident le plus fréquent que j'ai eu à diagnostiquer.

Vérifier plutôt qu'espérer

systemctl status api --no-pager
journalctl -u api -n 50 --no-pager
sudo nginx -t && systemctl reload nginx
curl -sS -o /dev/null -w '%{http_code} %{time_total}s\n' https://api.exemple.com/health

Un endpoint /health exposé par Spring Boot Actuator, interrogé toutes les minutes depuis l'extérieur, détecte une panne avant que les utilisateurs ne la signalent.

Articles liés

4 min de lecture

Intégrer les API de paiement des agrégateurs sans se faire piéger

Mobile Money, cartes bancaires, agrégateurs multiples : le modèle unifié, l'idempotence et la réconciliation qui évitent les doubles débits en production.

  • Paiement
  • FinTech
  • Architecture
  • API
Lire l'article
4 min de lecture

Ma méthode pour un front-end Angular qui reste maintenable

Découpage par domaine, composants standalone, signaux contre RxJS, intercepteurs et détection de changement : les décisions que je prends au démarrage d'un projet Angular.

  • Angular
  • TypeScript
  • Architecture
  • Frontend
Lire l'article