// Guide Infrastructure · Ansible + Nginx + Docker

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.

terminal
ansible-playbook playbooks/migrate.yml -i inventory/new.yml
✓ Connexion au nouveau serveur…
✓ Installation Docker, Nginx, Certbot…
✓ Transfert des données (forgejo, homepage)…
✓ Vhosts Nginx générés et rechargés…
✓ Services démarrés.
→ Mettre à jour le DNS. C'est tout.
01

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 :

Douleur
Mettre à jour l'adresse IP dans les secrets de chaque repo GitHub un par un
Douleur
Recréer et redéployer les clés SSH sur le nouveau serveur
Douleur
Réinstaller manuellement chaque service : Forgejo, Homepage, Nginx…
Douleur
Réécrire les vhosts Nginx et regénérer les certificats SSL pour chaque sous-domaine
Douleur
Migrer les données (dépôts Git, uploads, config) sans procédure établie
Douleur
Tout ça se répète pour chaque serveur que vous avez ou aurez
Le vrai problème de fond

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.

02

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.

01
DNS · Ne jamais stocker une IP
Les pipelines GitHub ne connaissent pas l'IP du serveur. Ils connaissent un nom DNS. Quand l'IP change, seul le DNS change — aucun repo à toucher.
02
Ansible · Le serveur comme code
Un playbook décrit l'état complet d'un serveur. Ansible l'applique sur n'importe quelle machine vierge. Nouveau serveur = une commande.
03
Templates · Config générée
Nginx et Docker Compose ne sont pas écrits à la main. Ce sont des templates remplis par des variables. Ajouter un service = une ligne dans un fichier.

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 A
  • projet-b.deploy.mondomaine.com → IP du serveur projet B

Toutes les dépendances (pipelines, scripts, clés) utilisent ces noms. Jamais les IPs directement.

03

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.yml installe tout depuis zéro.
  • Rôle Une boîte réutilisable. Le rôle nginx s'occupe uniquement de Nginx — installation, config, démarrage. Le rôle docker s'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 écrivez server_name {{ item.subdomain }}.{{ domain }};. Ansible remplace les variables au moment du déploiement.
La propriété la plus importante : idempotence

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.

04

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.

infra/ ├── inventory/ │ ├── production.yml ← adresses et accès de vos serveurs actuels │ └── new_server.yml ← nouveau serveur lors d'une migration ├── group_vars/ │ └── all.yml ← votre domaine, la liste de vos services ├── roles/ │ ├── common/ ← users, clés SSH autorisées, firewall │ ├── docker/ ← installation Docker + Docker Compose │ ├── nginx/ │ │ ├── tasks/main.yml ← install + génération des vhosts │ │ └── templates/vhost.conf.j2 ← le template Nginx (avec variables) │ ├── certbot/ ← certificats SSL Let's Encrypt automatiques │ ├── forgejo/ │ │ ├── tasks/main.yml │ │ └── templates/docker-compose.yml.j2 │ └── homepage/ │ ├── tasks/main.yml │ └── templates/config.yaml.j2 ├── playbooks/ │ ├── provision.yml ← setup complet d'un serveur from scratch │ └── migrate.yml ← backup + transfer + restore des données └── backups/ ← gitignored — archives temporaires de migration
  • 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 nginx sur n'importe quel nouveau serveur sans rien changer.
  • playbooks/ Ce sont les fichiers que vous lancez directement. provision.yml pour démarrer un serveur, migrate.yml pour déplacer des services.
Règle d'or

Si ça ne peut pas être recréé depuis ce repo, ça n'existe pas. Aucune configuration structurelle ne doit vivre uniquement sur un serveur.

05

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.

group_vars/all.yml YAML
# 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.

roles/forgejo/templates/docker-compose.yml.j2 YAML + Jinja2
# {{ 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 }}
06

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.

roles/nginx/templates/vhost.conf.j2 Nginx + Jinja2
# 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é.

roles/nginx/tasks/main.yml YAML
- 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 }}"
Ce que ça change concrètement

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.

07

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.

1
Arrêt et backup sur l'ancien serveur
Ansible se connecte sur 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/.
2
Provisioning du nouveau serveur
Ansible se connecte sur 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.
3
Restore des données sur le nouveau serveur
Les archives sont envoyées sur le nouveau serveur et extraites dans les bons dossiers. Les services sont relancés avec leurs données d'origine.
Mise à jour DNS — seule action manuelle
Vous vérifiez que tout tourne sur le nouveau serveur, puis vous mettez à jour l'enregistrement DNS de votre registrar (OVH) pour pointer vers la nouvelle IP. Les pipelines GitHub continuent de fonctionner sans modification car ils utilisent le nom DNS, pas l'IP.
playbooks/migrate.yml YAML
---
# ── 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 }}"
08

Référence Rapide

Les commandes du quotidien et les vérifications à faire.

Commandes Ansible

Premier setup d'un serveur vierge
ansible-playbook playbooks/provision.yml -i inventory/production.yml
Migration complète vers un nouveau serveur
ansible-playbook playbooks/migrate.yml -i inventory/new_server.yml
Appliquer un changement (ex : nouveau service ajouté)
ansible-playbook playbooks/provision.yml --tags nginx
Vérifier ce qui serait modifié, sans rien appliquer
ansible-playbook playbooks/provision.yml --check --diff

Checklist — migration d'un serveur

1
Créer inventory/new_server.yml avec l'IP du nouveau serveur OVH
C'est la seule fois où vous manipulez une IP directement. Elle ne sort pas de ce fichier.
2
Lancer ansible-playbook playbooks/migrate.yml -i inventory/new_server.yml
Attend la fin. Les trois phases s'exécutent automatiquement.
3
Vérifier manuellement que les services répondent sur le nouveau serveur
Accéder à chaque service via son IP directement (avant mise à jour DNS) pour confirmer que tout fonctionne.
4
Mettre à jour l'enregistrement DNS chez OVH
deploy.mondomaine.com → nouvelle IP. La propagation prend entre quelques minutes et 24h selon le TTL configuré.
5
Résilier l'ancien serveur une fois la propagation DNS confirmée
Vérifier d'abord que les pipelines GitHub déploient bien sur le nouveau serveur en regardant les logs d'une action récente.

Résumé — qui fait quoi

Problème initialCe qui le règleEffort restant
IP dans les secrets GitHubDNS fixe par serveurZéro — le secret ne change plus
Reconfigurer le serveurprovision.ymlUne commande
Clés SSH à redéployerRôle common + authorized_keysAutomatique au provisioning
Vhosts Nginx à réécrireTemplate Jinja2 + boucleAutomatique
Migration des donnéesmigrate.ymlUne commande
SSL à regénérerRôle certbotAutomatique au provisioning