Des milliers de tutoriels apprennent React, Django ou Symfony. Une petite minorité va jusqu'à l'URL publique. Le dernier chapitre s'arrête sur un serveur de développement qui écoute le port 3000, avec une phrase du type « le déploiement fera l'objet d'un prochain article ». Ce prochain article n'arrive jamais.
Résultat : beaucoup de gens savent écrire une application web complète et n'ont jamais vu la leur répondre à une adresse que quelqu'un d'autre peut taper. La mise en ligne est rangée du côté « ops », comme si elle demandait un métier séparé. Elle repose sur quatre briques, dont trois se configurent en une soirée, et la quatrième une fois pour toutes.
Voici ces quatre briques, dans l'ordre où elles s'assemblent, avec le stack précis utilisé pour les exemples : Ubuntu Server 26.04 LTS, Caddy 2.11 et Node.js 24 LTS, état d'août 2026.
Le trou entre « ça marche » et « c'est en ligne »
La situation type est facile à décrire. Une application tourne en local. Elle répond sur http://localhost:3000, la base de données est peuplée, les tests passent. Le code est sur GitHub. Et au moment de la mettre sur un CV, dans un portfolio ou dans un mail à un client, il n'y a rien à donner d'autre qu'un lien vers un dépôt.
Un lien GitHub demande à l'interlocuteur de cloner, d'installer des dépendances, de créer une base et de lancer un serveur avant de voir quoi que ce soit. Personne ne le fait. Une URL cliquable qui affiche l'écran d'accueil en deux secondes change la nature de la conversation, et c'est précisément ce que décrit l'article sur la construction d'un portfolio quand on n'a pas encore de client.
Le blocage tient à ce que les quatre morceaux sont documentés séparément. Le registrar explique comment acheter un domaine sans parler de serveur. L'hébergeur explique comment créer une machine sans parler de DNS. La documentation TLS parle de certificats sans dire quel processus doit écouter derrière. Chaque page est correcte, aucune ne montre l'assemblage.
Le principe : une requête, cinq étapes
Tout devient plus simple une fois qu'on suit une requête du navigateur jusqu'au code. Il y a cinq étapes, et chaque brique à installer correspond à l'une d'elles.
Le nom de domaine ne fait qu'une chose : il traduit un nom lisible en adresse IP. C'est une ligne dans un annuaire mondial. Il n'héberge rien, ne stocke rien et ne sait pas ce que fait ton application.
Le VPS est une machine allumée en permanence, avec une adresse IP publique fixe. Serveur privé virtuel : une part d'une machine physique, avec son propre système, sa propre RAM et ses propres processus.
Le serveur web reçoit sur les ports 80 et 443, présente le certificat, puis transmet la requête à ton application sur une adresse locale. Ce rôle d'intermédiaire s'appelle un reverse proxy. Si les numéros de port et les codes de statut restent flous, l'article sur ce qui se passe entre le navigateur et le serveur couvre cette partie en détail.
Le certificat prouve le lien entre le nom et le serveur. Une autorité vérifie que la machine qui répond à monprojet.fr contrôle bien ce nom, puis signe un certificat que le navigateur accepte. Let's Encrypt fait ça gratuitement et automatiquement.
L'ordre compte. Le certificat s'obtient en prouvant le contrôle du domaine, ce qui suppose que le DNS pointe déjà vers le bon serveur. Acheter le domaine et poser les enregistrements en premier évite une série d'échecs qui ont un coût réel, détaillé plus bas.
Étape 1 : le domaine et les enregistrements DNS
Un domaine se loue à l'année auprès d'un bureau d'enregistrement. Pour un projet personnel, l'extension importe peu tant qu'elle est lisible et courte. Un point à connaître pour le .fr : selon les règles rappelées par service-public.fr, le titulaire doit être établi dans un État membre de l'Union européenne, en Norvège, en Islande, au Liechtenstein ou en Suisse, et cette condition vaut pendant toute la durée de détention.
Une fois le domaine acheté, tout se joue dans sa zone DNS. Deux types d'enregistrements suffisent pour démarrer, avec l'adresse IP de ton serveur en valeur :
Type Nom Valeur TTL
A @ 203.0.113.42 3600
A www 203.0.113.42 3600
AAAA @ 2001:db8::42 3600
Le @ désigne le domaine nu, A pointe vers une adresse IPv4 et AAAA vers une IPv6. Le TTL indique combien de secondes les résolveurs ont le droit de garder la réponse en cache : une valeur basse pendant la mise en place, quitte à la remonter ensuite, évite d'attendre une heure après chaque correction.
Pour vérifier que la zone est bien publiée, dig +short monprojet.fr depuis n'importe quelle machine doit renvoyer l'IP du serveur. Tant que cette commande ne répond rien, inutile de passer à la suite.
Étape 2 : le VPS, et les dix minutes qui le rendent habitable
Hetzner, OVHcloud, Scaleway, DigitalOcean, Infomaniak : les offres se ressemblent beaucoup sur l'entrée de gamme, autour de 2 vCPU et 4 Go de RAM, ce qui tient largement une application avec sa base de données et quelques centaines de visiteurs par jour. Les tarifs bougent plus souvent qu'on ne le croit, Hetzner ayant par exemple publié un ajustement entré en vigueur le 15 juin 2026 : regarde la grille du jour plutôt qu'un chiffre lu dans un tutoriel de l'an dernier.
À la création, choisis une image Ubuntu Server 26.04 LTS, maintenue jusqu'en avril 2031, et colle ta clé publique SSH dans le formulaire. Si tu n'en as pas encore :
ssh-keygen -t ed25519 -C "deploy@monprojet"
cat ~/.ssh/id_ed25519.pub
Première connexion, création d'un utilisateur non privilégié, transfert de la clé :
ssh root@203.0.113.42
adduser deploy
usermod -aG sudo deploy
rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy
Ensuite, un fichier déposé dans /etc/ssh/sshd_config.d/ coupe l'accès par mot de passe et la connexion directe en root :
# /etc/ssh/sshd_config.d/99-durcissement.conf
PasswordAuthentication no
PermitRootLogin no
Garde ta session root ouverte, ouvre un second terminal, vérifie que ssh deploy@203.0.113.42 fonctionne après systemctl restart ssh, et seulement là ferme la première. Cette précaution évite le classique enfermement dehors, qui oblige à passer par la console de secours de l'hébergeur.
Reste le pare-feu :
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443
sudo ufw enable
sudo ufw status
La ligne 443 sans suffixe ouvre le TCP et l'UDP. Ce détail a son importance : Caddy sert HTTP/3 par défaut depuis la version 2.6, et HTTP/3 passe par UDP. Une règle limitée au TCP fait silencieusement retomber tous les clients sur HTTP/2, sans message d'erreur nulle part. Le port de ton application, lui, n'apparaît pas dans cette liste, et c'est voulu.
Étape 3 : faire tourner l'application comme un service
Lancer npm start dans une session SSH marche jusqu'à la fermeture du terminal. Un tmux repousse le problème jusqu'au premier redémarrage de la machine. La réponse propre sur une distribution moderne s'appelle systemd, le gestionnaire de services déjà présent sur Ubuntu : il démarre le processus au boot, le relance s'il plante, et centralise ses logs.
Le code va dans /srv/monprojet, les secrets dans un fichier séparé lisible du seul utilisateur de service, comme le recommande le principe de configuration par variables d'environnement des 12 facteurs :
# /etc/systemd/system/monprojet.service
[Unit]
Description=Application monprojet
After=network-online.target
Wants=network-online.target
[Service]
Type=exec
User=deploy
WorkingDirectory=/srv/monprojet
EnvironmentFile=/etc/monprojet.env
ExecStart=/usr/bin/node /srv/monprojet/server.js
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
Puis :
sudo systemctl daemon-reload
sudo systemctl enable --now monprojet
sudo systemctl status monprojet
journalctl -u monprojet -f
Un détail qui change tout : ton application doit écouter sur 127.0.0.1:3000 et pas sur 0.0.0.0:3000. En écoutant sur 127.0.0.1, le processus n'est joignable que depuis la machine elle-même, donc uniquement par le reverse proxy. En écoutant sur 0.0.0.0, il est exposé à l'Internet entier, en clair et sans certificat, dès qu'une règle de pare-feu est mal posée.
Étape 4 : Caddy devant, HTTPS compris
Pour un premier déploiement, je conseille Caddy plutôt que le couple Nginx et Certbot. Nginx reste la référence en production et il faudra savoir le lire tôt ou tard. Mais Caddy demande une configuration de trois lignes là où l'autre en demande trente, et il obtient le certificat sans commande supplémentaire. Sur un premier projet, ça retire la moitié des occasions de se tromper.
Installation, avec les commandes du dépôt officiel :
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
| sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
| sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo chmod o+r /usr/share/keyrings/caddy-stable-archive-keyring.gpg
sudo chmod o+r /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy
Le fichier /etc/caddy/Caddyfile tient en quelques lignes :
{
email contact@monprojet.fr
}
monprojet.fr {
encode zstd gzip
reverse_proxy 127.0.0.1:3000
}
www.monprojet.fr {
redir https://monprojet.fr{uri} permanent
}
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
journalctl -u caddy -f
Le fait d'écrire un nom de domaine en tête de bloc suffit à déclencher la demande de certificat auprès de Let's Encrypt, la redirection HTTP vers HTTPS et le renouvellement automatique. Les logs montrent l'échange en direct, ce qui rend le diagnostic simple quand ça coince.
Une fois cette étape franchie, une URL en https répond, avec un cadenas et aucun avertissement. C'est le moment où le projet devient montrable.
Étape 5 : déployer une nouvelle version
Reste la question qui revient dès le lendemain : comment mettre en ligne le correctif écrit ce matin. Le premier niveau est un script de quatre lignes, posé sur le serveur, que tu lances après avoir poussé sur ta branche principale.
#!/usr/bin/env bash
set -euo pipefail
cd /srv/monprojet
git fetch --prune
git reset --hard origin/main
npm ci --omit=dev
npm run build
sudo systemctl restart monprojet
Le set -euo pipefail en tête arrête le script à la première commande qui échoue, au lieu de redémarrer un service avec des dépendances à moitié installées. Le git reset --hard garantit que l'état du serveur correspond exactement à celui du dépôt, sans fichier modifié à la main resté en travers.
Le niveau suivant consiste à faire exécuter ce script par une action déclenchée au push, plutôt que par toi en SSH. C'est le principe d'un pipeline, détaillé dans l'article sur l'intégration et le déploiement continus, et travaillé en pratique dans la formation CI/CD avec GitHub Actions. Passer au pipeline avant d'avoir réussi un déploiement manuel complique inutilement le diagnostic : quand rien ne marche, il faut pouvoir distinguer un problème d'application d'un problème de pipeline.
Les pièges qui coûtent une soirée
Relancer la demande de certificat en boucle. Let's Encrypt tolère 5 échecs de validation par identifiant et par compte et par heure, avec un rechargement d'un échec toutes les 12 minutes. Cinq tentatives ratées parce que le DNS n'est pas encore propagé, et l'attente commence. L'environnement de test de l'autorité, disponible à une adresse dédiée, existe précisément pour ces essais.
Oublier l'enregistrement www. Un certificat couvre les noms demandés, pas leurs variantes. Sans enregistrement DNS ni bloc de configuration pour www, la moitié des visiteurs qui tapent l'adresse de mémoire tombent sur une erreur.
Commiter le fichier .env. Le dépôt part sur GitHub avec les identifiants de base de données et les clés d'API dedans. Les robots qui scannent les dépôts publics trouvent ces clés en quelques minutes, et l'article sur ce que coûte une application non sécurisée détaille la suite.
Tout faire tourner en root. Un processus applicatif n'a aucune raison d'avoir les droits d'administration de la machine. Le service tourne sous un utilisateur dédié, avec accès en écriture aux seuls dossiers dont il a besoin, dans l'esprit du principe de moindre privilège.
Ne jamais sauvegarder la base. Un VPS peut disparaître, un DROP TABLE peut partir sur la mauvaise connexion. Un dump quotidien envoyé ailleurs que sur la machine elle-même prend dix minutes à mettre en place et vaut plus que toute autre optimisation.
Laisser le renouvellement du domaine au hasard. Un domaine expiré rend le site injoignable et le certificat impossible à renouveler. Active le renouvellement automatique, et méfie-toi des courriels qui annoncent une expiration imminente avec une date que tu ne reconnais pas : c'est un grand classique du hameçonnage sur les titulaires de noms de domaine.
Déployer sans savoir lire les logs. Quand une page renvoie 502, l'information est dans journalctl -u monprojet et journalctl -u caddy, presque jamais ailleurs. La formation Linux et ligne de commande pour développeurs couvre cette lecture, et l'article sur la méthode de debug s'applique telle quelle à un serveur.
Après le premier déploiement
Une fois cette chaîne en place, elle se reproduit en une demi-heure pour le projet suivant, et l'essentiel du travail se déplace vers ce qui vient après : surveiller que le site répond, gérer les montées de version du système, séparer un environnement de test de la production. La suite logique passe par les conteneurs et l'automatisation, sujets traités dans la roadmap DevOps et dans les formations DevOps et déploiement du catalogue.
Il reste que la première fois demande une soirée, pas un semestre. Le reste, c'est de la répétition.
Questions fréquentes
Un VPS est-il obligatoire, ou une plateforme gérée suffit-elle ?
Faut-il utiliser Docker pour un premier déploiement ?
Combien de temps met un changement DNS à être visible ?
Que faire quand le site renvoie 502 Bad Gateway ?
systemctl status), écoute-t-il sur le port attendu (ss -tlnp), et l'adresse du reverse_proxy correspond-elle à ce port. Le 502 vient presque toujours d'une application arrêtée au démarrage, avec la cause exacte dans ses propres logs.