Votre infrastructure,
reproductible
en une commande.
Ce guide répond à une question précise : comment changer de serveur sans tout reconfigurer à la main ? La réponse tient en trois outils — DNS, Ansible, et des templates — combinés de façon à ce que rien d'essentiel ne vive uniquement sur un serveur.
Le Problème
Vous avez plusieurs serveurs OVH, chacun dédié à un projet. Des pipelines GitHub Actions déploient dessus automatiquement. Tout roule — jusqu'au jour où vous devez changer de serveur.
Ce jour-là, sans préparation, voici ce qui vous attend :
Le serveur est devenu un "pet" — une bête de compagnie irremplaçable dont vous connaissez tous les secrets par cœur. L'objectif de ce guide est d'en faire un "cattle" — une ressource anonyme et jetable, recréable à l'identique en quelques minutes.
La Solution en 3 Axes
Chaque douleur identifiée ci-dessus a une réponse précise. Les trois axes fonctionnent ensemble — retirer l'un affaiblit les deux autres.
Pourquoi le DNS est la clé
Imaginez que votre secret GitHub contient 1.2.3.4. Le jour où le serveur change, cette IP change aussi → vous devez modifier chaque repo. Maintenant imaginez que le secret contient deploy.mondomaine.com. Ce nom ne change jamais. Seul l'enregistrement DNS qui pointe vers l'IP change — une modification, un seul endroit.
C'est exactement le même principe qu'une base de données : on ne code jamais 192.168.1.5 dans l'app, on utilise db.mondomaine.com. Une infrastructure bien conçue applique ça à tout.
Un sous-domaine par serveur, pas par service
Si vous avez plusieurs serveurs pour différents projets, vous créez un sous-domaine par serveur. Chaque serveur a un nom qui représente son rôle :
projet-a.deploy.mondomaine.com→ IP du serveur projet Aprojet-b.deploy.mondomaine.com→ IP du serveur projet B
Toutes les dépendances (pipelines, scripts, clés) utilisent ces noms. Jamais les IPs directement.
Ansible, c'est quoi concrètement ?
Ansible est un outil qui exécute des instructions sur des serveurs distants via SSH. Vous décrivez ce que vous voulez dans des fichiers YAML — il s'occupe de le faire. Pas besoin d'installer quoi que ce soit sur le serveur cible.
Les 4 concepts à retenir
- Inventory La liste de vos serveurs. Ansible a besoin de savoir où se connecter. L'inventaire contient les IPs ou noms DNS de vos serveurs, leurs users SSH, et leurs clés. C'est le seul endroit où une IP brute peut apparaître.
-
Playbook
Le script d'instructions. Un playbook dit "sur ces serveurs, applique ces rôles dans cet ordre". C'est le point d'entrée que vous lancez. Exemple :
provision.ymlinstalle tout depuis zéro. -
Rôle
Une boîte réutilisable. Le rôle
nginxs'occupe uniquement de Nginx — installation, config, démarrage. Le rôledockers'occupe de Docker. Chaque rôle fait une chose bien. Les playbooks assemblent des rôles. -
Template Jinja2
Un fichier de config avec des variables. Là où vous écririez
server_name git.mondomaine.com;à la main, vous écrivezserver_name {{ item.subdomain }}.{{ domain }};. Ansible remplace les variables au moment du déploiement.
Ansible est idempotent : vous pouvez relancer le même playbook 10 fois sans risque. S'il voit que Nginx est déjà installé et configuré correctement, il ne fait rien. Il n'applique que ce qui a changé. C'est ce qui rend l'outil sûr pour du provisioning et des mises à jour.
Comment Ansible se connecte à un serveur
Ansible utilise SSH — rien de plus. Il se connecte avec votre clé SSH, exécute les instructions, et se déconnecte. Aucun agent à installer sur le serveur. La seule condition préalable est un accès SSH avec les droits sudo.
Structure du Repo Ansible
Tout votre infrastructure-as-code vit dans un seul repo Git appelé infra/. Versionné, auditable, partageable. Si ce repo existe et que votre inventaire est à jour, vous pouvez recréer n'importe quel serveur.
- inventory/ Contient les IPs et options SSH de chaque serveur. C'est le seul endroit où une IP brute peut apparaître dans tout votre système.
- group_vars/ Variables partagées par tous vos serveurs. Votre domaine, la liste des services, les clés SSH autorisées. Modifier ici = modifier partout.
-
roles/
Chaque sous-dossier est un rôle autonome. Vous pouvez réutiliser le rôle
nginxsur n'importe quel nouveau serveur sans rien changer. -
playbooks/
Ce sont les fichiers que vous lancez directement.
provision.ymlpour démarrer un serveur,migrate.ymlpour déplacer des services.
Si ça ne peut pas être recréé depuis ce repo, ça n'existe pas. Aucune configuration structurelle ne doit vivre uniquement sur un serveur.
Variables & Templates
Le principe central est de ne jamais écrire une valeur concrète (domaine, port, chemin) directement dans un fichier de config. Tout passe par des variables, définies une seule fois dans group_vars/all.yml.
group_vars/all.yml — la source de vérité
Ce fichier contient votre domaine et la liste de tous vos services. Chaque service est une entrée avec son nom, son sous-domaine, son port Docker, et le chemin où ses données sont stockées. Ansible utilise cette liste pour tout générer automatiquement.
# Votre domaine principal — utilisé dans tous les templates domain: mondomaine.com # Chaque entrée ici génère automatiquement : # - Un vhost Nginx (git.mondomaine.com → localhost:3000) # - Un certificat SSL Let's Encrypt # - Un Docker Compose services: - name: forgejo subdomain: git # → git.mondomaine.com port: 3000 # port interne Docker data_path: /opt/forgejo/data - name: homepage subdomain: home # → home.mondomaine.com port: 3001 data_path: /opt/homepage/config # Clés SSH qui auront accès au serveur. # Stockées ici → déployées automatiquement sur tout nouveau serveur. authorized_keys: - "ssh-ed25519 AAAA... lucas@workstation" - "ssh-ed25519 AAAA... github-actions-deploy"
Pour ajouter un nouveau service (par exemple Vaultwarden), il suffit d'ajouter une entrée dans cette liste et de relancer le playbook. Ansible crée le vhost Nginx, le certificat SSL, et le Docker Compose — sans que vous touchiez à autre chose.
Comment un template Jinja2 fonctionne
Un template est un fichier de config normal, mais avec des {{ variables }} à la place des valeurs fixes. Quand Ansible le déploie, il remplace chaque variable par sa valeur réelle. Un seul template génère la config pour autant de services que vous en avez.
# {{ domain }} et {{ item.port }} sont remplacés par Ansible # au moment du déploiement, depuis group_vars/all.yml services: forgejo: image: codeberg.org/forgejo/forgejo:latest restart: unless-stopped ports: - "{{ item.port }}:3000" volumes: - /opt/forgejo/data:/data environment: - FORGEJO__server__DOMAIN=git.{{ domain }} - FORGEJO__server__ROOT_URL=https://git.{{ domain }}
Nginx — Reverse Proxy Automatisé
Le rôle nginx ne demande pas de savoir quels services existent. Il lit la liste services dans les variables et génère un fichier .conf pour chacun. Si vous en ajoutez un, il apparaît automatiquement au prochain run.
Le template — un seul fichier pour tous les services
Ce fichier est utilisé en boucle. Ansible l'applique une fois par service, en remplaçant item.subdomain, domain, et item.port par les vraies valeurs à chaque passage.
# Redirige HTTP vers HTTPS server { listen 80; server_name {{ item.subdomain }}.{{ domain }}; return 301 https://$host$request_uri; } # Proxy HTTPS vers le container Docker sur localhost server { listen 443 ssl; server_name {{ item.subdomain }}.{{ domain }}; # Certificat généré par certbot (Let's Encrypt) ssl_certificate /etc/letsencrypt/live/{{ item.subdomain }}.{{ domain }}/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/{{ item.subdomain }}.{{ domain }}/privkey.pem; location / { # Transfère vers le port Docker du service proxy_pass http://localhost:{{ item.port }}; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; } }
La tâche Ansible qui l'applique
La tâche ci-dessous boucle sur votre liste de services. Pour chaque service, elle dépose le fichier de config généré dans /etc/nginx/sites-enabled/, puis recharge Nginx. Un seul `notify: reload nginx` suffit — Ansible n'exécute le rechargement qu'une fois à la fin, même si 5 vhosts ont changé.
- name: Installer Nginx apt: name: nginx state: present # loop: "{{ services }}" = une fois par service dans group_vars/all.yml - name: Générer les vhosts depuis le template template: src: vhost.conf.j2 dest: "/etc/nginx/sites-enabled/{{ item.name }}.conf" loop: "{{ services }}" notify: reload nginx # recharge une seule fois après toutes les modifications # Certbot obtient et renouvelle le SSL automatiquement - name: Obtenir les certificats SSL (Let's Encrypt) command: > certbot --nginx -d {{ item.subdomain }}.{{ domain }} --non-interactive --agree-tos -m admin@{{ domain }} loop: "{{ services }}"
Sans cette approche : pour ajouter Vaultwarden, vous éditez manuellement un nouveau fichier vaultwarden.conf dans Nginx, vous gérez le cert SSL à la main, et vous écrivez le docker-compose. Avec cette approche : vous ajoutez 4 lignes dans all.yml et relancez le playbook.
Playbook de Migration
La migration se déroule en trois phases enchaînées dans un seul playbook. Vous le lancez une fois depuis votre machine locale — il se charge du reste en SSH sur les deux serveurs.
old_server, stoppe tous les services Docker Compose, puis archive le dossier de données de chaque service en .tar.gz. Les archives sont ensuite rapatriées sur votre machine locale dans backups/.new_server et applique tous les rôles dans l'ordre : common (users, SSH), docker, nginx (vhosts générés depuis les variables), certbot (SSL), puis chaque service. À la fin de cette phase, le serveur est opérationnel — mais sans données.--- # ── PHASE 1 : Backup depuis l'ancien serveur ───────────────── - name: Backup old server hosts: old_server tasks: - name: Arrêter tous les services shell: cd /opt/{{ item.name }} && docker compose down loop: "{{ services }}" - name: Archiver les données de chaque service archive: path: "{{ item.data_path }}" dest: "/tmp/backup-{{ item.name }}.tar.gz" loop: "{{ services }}" - name: Télécharger les archives en local fetch: src: "/tmp/backup-{{ item.name }}.tar.gz" dest: ./backups/ flat: yes loop: "{{ services }}" # ── PHASE 2 : Provisioning du nouveau serveur ──────────────── # Identique à un premier setup — applique tous les rôles - name: Provision new server hosts: new_server roles: - common # users, clés SSH, firewall - docker # Docker + Compose - nginx # vhosts générés depuis group_vars/all.yml - certbot # certificats SSL - forgejo - homepage # ── PHASE 3 : Restore des données sur le nouveau serveur ───── - name: Restore data on new server hosts: new_server tasks: - name: Arrêter les services avant d'écraser les données shell: cd /opt/{{ item.name }} && docker compose down loop: "{{ services }}" - name: Envoyer les archives sur le nouveau serveur copy: src: "./backups/backup-{{ item.name }}.tar.gz" dest: /tmp/ loop: "{{ services }}" - name: Extraire les données dans le bon dossier unarchive: src: "/tmp/backup-{{ item.name }}.tar.gz" dest: "{{ item.data_path | dirname }}" remote_src: yes loop: "{{ services }}" - name: Redémarrer les services avec leurs données shell: cd /opt/{{ item.name }} && docker compose up -d loop: "{{ services }}"
Référence Rapide
Les commandes du quotidien et les vérifications à faire.
Commandes Ansible
Checklist — migration d'un serveur
inventory/new_server.yml avec l'IP du nouveau serveur OVHansible-playbook playbooks/migrate.yml -i inventory/new_server.ymldeploy.mondomaine.com → nouvelle IP. La propagation prend entre quelques minutes et 24h selon le TTL configuré.Résumé — qui fait quoi
| Problème initial | Ce qui le règle | Effort restant |
|---|---|---|
| IP dans les secrets GitHub | DNS fixe par serveur | Zéro — le secret ne change plus |
| Reconfigurer le serveur | provision.yml | Une commande |
| Clés SSH à redéployer | Rôle common + authorized_keys | Automatique au provisioning |
| Vhosts Nginx à réécrire | Template Jinja2 + boucle | Automatique |
| Migration des données | migrate.yml | Une commande |
| SSL à regénérer | Rôle certbot | Automatique au provisioning |