Migrer un Homelab Docker Compose vers un Nouveau Serveur : Guide Complet

Après plusieurs années de bons et loyaux services, mon homelab tournant sur un Intel Atom N2800 montrait ses limites. Ce processeur de 2011 ne supportait pas les instructions AVX/AVX2 — une contrainte qui m'empêchait d'utiliser certains logiciels modernes comme Homarr v1.0+ ou la transcription automatique de PeerTube via Whisper.

La migration vers un serveur OVH SYS-1 équipé d'un Intel Xeon E-2136 était l'occasion de moderniser l'infrastructure tout en documentant le processus pour d'autres administrateurs de homelab.

Ce que vous allez apprendre

Spécifications des serveurs

Élément Ancien serveur Nouveau serveur
CPU Intel Atom N2800 (2 cœurs, pas AVX) Intel Xeon E-2136 (6 cœurs, AVX2)
RAM 4 Go 32 Go
Stockage 2 x 2 To HDD 2 x 2 To SSD
OS Ubuntu 22.04 Ubuntu 24.04 LTS
Hébergeur Kimsufi OVH SYS-1

Services à migrer

Mon infrastructure Docker Compose comprend :

Volume total des données : ~935 Go (réduit à ~350 Go après nettoyage).


1. Préparation du Nouveau Serveur

1.1 Installation de Docker avec stockage dédié

Sur Ubuntu 24 Server, la partition root est souvent limitée (typiquement 20-50 Go sur un serveur OVH). Il est crucial de configurer Docker pour stocker ses images et volumes sur une partition de données séparée.

# Installation de Docker via le script officiel
curl -fsSL https://get.docker.com | sudo sh

# Ajout de l'utilisateur au groupe docker
sudo usermod -aG docker $USER

# Arrêt de Docker pour la configuration
sudo systemctl stop docker

Configuration du stockage Docker sur /data/docker :

# Création du répertoire de données
sudo mkdir -p /data/docker

# Configuration de Docker avec data-root personnalisé
# et limitation des logs pour éviter de saturer le disque
sudo tee /etc/docker/daemon.json << 'EOF'
{
  "data-root": "/data/docker",
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}
EOF

# Redémarrage de Docker
sudo systemctl start docker

# Vérification
docker info | grep "Docker Root Dir"
# Doit afficher: Docker Root Dir: /data/docker

Pourquoi cette configuration ?

1.2 Installation des dépendances Python (PEP 668)

Ubuntu 24 implémente strictement le PEP 668 qui empêche l'installation de paquets Python via pip au niveau système. Cette décision vise à éviter les conflits entre pip et apt.

# INCORRECT sur Ubuntu 24 - génère une erreur
pip3 install requests python-dotenv
# error: externally-managed-environment

# CORRECT - utiliser les paquets système
sudo apt install -y python3-requests python3-dotenv

Alternatives si vous avez besoin de versions spécifiques :

# Option 1 : Environnement virtuel (recommandé pour le développement)
python3 -m venv ~/venv
source ~/venv/bin/activate
pip install requests python-dotenv

# Option 2 : pipx pour les outils CLI
pipx install <package>

Pour mes scripts d'organisation de médias, les versions système suffisent.

1.3 Installation de Node.js via nvm

NodeSource, longtemps la méthode recommandée, a changé ses conditions en 2023. nvm (Node Version Manager) offre plus de flexibilité :

# Installation de nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash

# Rechargement du shell
source ~/.bashrc

# Installation de la dernière LTS
nvm install --lts

# Vérification
node --version
npm --version

Avantages de nvm : – Installation dans le home utilisateur (pas de sudo) – Possibilité de switcher entre versions – Mise à jour simple via nvm install --lts


2. Configuration SSH Bidirectionnelle

Pour transférer les données de manière sécurisée, nous devons établir une connexion SSH de l'ancien vers le nouveau serveur et inversement.

2.1 Génération de clé sur l'ancien serveur

# Sur l'ANCIEN serveur
ssh-keygen -t ed25519 -N "" -f ~/.ssh/id_ed25519

# Afficher la clé publique à copier
cat ~/.ssh/id_ed25519.pub

Pourquoi ed25519 ? – Plus sécurisé que RSA à taille de clé équivalente – Plus rapide pour la génération et l'authentification – Clés plus courtes (plus faciles à copier/coller)

2.2 Autorisation sur le nouveau serveur

# Sur le NOUVEAU serveur
# Ajouter la clé publique de l'ancien serveur
echo "ssh-ed25519 AAAA... user@ancien-serveur" >> ~/.ssh/authorized_keys

# Vérifier les permissions (crucial pour SSH)
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys

2.3 Test de connexion

# Depuis l'ANCIEN serveur, tester la connexion
ssh new_user@nouveau-serveur "hostname && uptime"

Dans un système de fichiers, un hardlink est une référence directe au même inode sur le disque. Le fichier n'existe qu'une fois physiquement, mais apparaît à plusieurs endroits dans l'arborescence.

/data/source/fichier.mkv  (fichier original)
         |
         +-- hardlink --> /data/organise/fichier.mkv

Cela permet : – D'organiser les fichiers de différentes manières – De ne pas doubler l'espace disque – D'avoir plusieurs “vues” sur les mêmes données

3.2 L'importance du flag -H

Sans le flag -H, rsync copie chaque chemin comme un fichier séparé :

# MAUVAIS : duplique les données
rsync -avz /data/ user@serveur:/data/
# Résultat : 350 Go de fichiers + 350 Go de hardlinks = 700 Go

# CORRECT : préserve les hardlinks
rsync -avzH /data/ user@serveur:/data/
# Résultat : 350 Go total (hardlinks préservés)

3.3 Commande de synchronisation complète

# Synchronisation avec préservation des hardlinks
rsync -avzH --progress \
  --exclude='*.tmp' \
  --exclude='*.part' \
  /data/ new_user@nouveau-serveur:/data/

# Pour les syncs subséquentes, ajouter --delete
# pour supprimer les fichiers effacés sur la source
rsync -avzH --progress --delete \
  /data/ new_user@nouveau-serveur:/data/

Explication des flags :-a : Archive (préserve permissions, propriétaire, timestamps, liens symboliques) – -v : Verbose (affiche les fichiers transférés) – -z : Compression durant le transfert – -H : Préserve les hardlinks (crucial pour notre cas) – --progress : Affiche la progression – --delete : Supprime les fichiers absents de la source

# Sur le nouveau serveur, vérifier qu'un fichier a plusieurs liens
ls -li /data/media/*/fichier.mkv
# La première colonne (inode) et la troisième (link count) doivent montrer > 1

# Exemple de sortie :
# 1234567 -rw-r--r-- 2 new_user new_user 5.0G Dec 31 fichier.mkv
#                    ^-- 2 = le fichier existe à 2 endroits (hardlink)

4. Nettoyage Pré-Migration : 550 Go Économisés

4.1 Identification du problème

En analysant l'espace disque, j'ai découvert que de nombreux hardlinks étaient devenus “orphelins” : les fichiers sources avaient été supprimés, mais les hardlinks restaient dans les répertoires organisés.

Un hardlink orphelin (link count = 1) n'est plus un hardlink — c'est juste un fichier normal qui occupe de l'espace.

#!/usr/bin/env python3
"""
clean-orphan-hardlinks.py
Supprime les fichiers qui étaient des hardlinks mais dont la source
a été supprimée (link count = 1).
"""
import os
import sys

# Répertoires à nettoyer
DIRS = ['/data/perso', '/data/pro']
DRY_RUN = '--dry-run' in sys.argv

deleted = 0
freed = 0

for directory in DIRS:
    if not os.path.isdir(directory):
        continue
    
    for root, dirs, files in os.walk(directory, topdown=False):
        for filename in files:
            filepath = os.path.join(root, filename)
            try:
                stat = os.stat(filepath)
                # Un hardlink a un link count > 1
                # Si link count = 1, la source a été supprimée
                if stat.st_nlink == 1:
                    freed += stat.st_size
                    if DRY_RUN:
                        print(f"[DRY-RUN] Would delete: {filepath}")
                    else:
                        os.remove(filepath)
                    deleted += 1
            except OSError:
                pass
        
        # Nettoyer les répertoires vides
        if not DRY_RUN:
            try:
                if not os.listdir(root):
                    os.rmdir(root)
            except OSError:
                pass

print(f"{'[DRY-RUN] ' if DRY_RUN else ''}"
      f"Deleted: {deleted} files, Freed: {freed / (1024**3):.2f} GB")

Utilisation :

# Prévisualisation (recommandé d'abord)
python3 clean-orphan-hardlinks.py --dry-run

# Exécution réelle
sudo python3 clean-orphan-hardlinks.py

4.3 Résultats du nettoyage

Le nettoyage a permis d'économiser environ 550 Go d'espace disque, réduisant le temps de transfert de ~15 heures à ~5 heures.

4.4 Nettoyage des anciens services

Profitez de la migration pour supprimer les données obsolètes :

# Exemples d'anciens services remplacés
rm -rf /data/mysql        # Si remplacé par PostgreSQL

5. Création des Réseaux Docker

Notre architecture utilise des réseaux Docker pour isoler les services :

# Réseau pour le reverse proxy (services publics)
docker network create proxy-tier

# Réseau interne pour les services avec bases de données
docker network create internal

Architecture réseau :

Internet
    |
    v
+-------+     proxy-tier     +-----------+
| nginx |<------------------>| Services  |
| proxy |                    | publics   |
+-------+                    +-----------+
                                   |
                             internal (isolé)
                                   |
                     +-------------+-------------+
                     |             |             |
                 +-------+   +---------+   +--------+
                 |Postgre|   |  Redis  |   | Autres |
                 +-------+   +---------+   +--------+

6. Adaptation des Chemins et Configurations

6.1 Modification des volumes Docker Compose

Les chemins diffèrent souvent entre les deux serveurs :

Élément Ancien Nouveau
Utilisateur ancien_user new_user
Projet /home/ancien_user/cloud /home/new_user/services-web
# Mise à jour automatique dans tous les fichiers de service
cd ~/services-web

# Remplacer les anciens chemins
find services/ -name "*.yml" -exec \
  sed -i 's|/home/ancien_user|/home/new_user|g' {} \;

# Vérification
grep -r "/home/ancien_user" services/
# Ne doit rien retourner

6.2 Migration des fichiers hors /data

Certains fichiers de configuration sont ailleurs :

# Cron jobs
scp ancien_user@ancien:/etc/cron.d/mon-cron /tmp/
sed -i 's|/home/ancien_user/cloud|/home/new_user/services-web|g' /tmp/mon-cron
sudo cp /tmp/mon-cron /etc/cron.d/
sudo chmod 644 /etc/cron.d/mon-cron

# Dotfiles importants
scp ancien_user@ancien:~/.ssh/authorized_keys ~/.ssh/

7. Mise à Niveau des Services (Grâce au Nouveau CPU)

7.1 Homarr : De la version legacy à v1.0+

L'Intel Atom N2800 ne supportant pas AVX, nous étions bloqués sur l'ancienne version de Homarr. Le Xeon E-2136 supporte AVX2, permettant de passer à la nouvelle version.

Ancien fichier (legacy) :

services:
  homarr:
    image: ghcr.io/ajnart/homarr:latest  # Version legacy
    environment:
      - VIRTUAL_PORT=7575  # Port différent
    volumes:
      - /data/homarr/configs:/app/data/configs
      - /data/homarr/icons:/app/public/icons
      - /data/homarr/data:/data

Nouveau fichier (v1.0+) :

services:
  homarr:
    image: ghcr.io/homarr-labs/homarr:latest  # Nouvelle image
    environment:
      - VIRTUAL_PORT=3000  # Nouveau port
      - AUTH_SECRET=${HOMARR_AUTH_SECRET}  # Nouveau : secret d'authentification
    volumes:
      - /data/homarr:/appdata  # Structure de volumes simplifiée

Migration des données :

# La structure de données a changé, il faut repartir de zéro
sudo rm -rf /data/homarr/*
sudo mkdir -p /data/homarr
sudo chown new_user:new_user /data/homarr

# Générer le secret d'authentification
openssl rand -hex 32
# Ajouter dans .env : HOMARR_AUTH_SECRET=<valeur>

7.2 PeerTube : Activation de la transcription Whisper

Avec le support AVX2, nous pouvons maintenant activer la transcription automatique des vidéos :

# Éditer la configuration PeerTube
nano /data/peertube/config/local-production.json
{
  "video_transcription": {
    "enabled": true,
    "engine": "whisper",
    "engine_options": {
      "model": "small"
    }
  }
}

Note : La transcription utilise CTranslate2 qui requiert AVX. Sur l'ancien serveur, cette fonctionnalité causait un “Illegal instruction (core dumped)”.


8. Migration des Volumes Docker

Certains volumes Docker ne sont pas dans /data mais dans le stockage Docker :

# Sur l'ANCIEN serveur : export des volumes
cd /tmp
docker run --rm -v certs:/data -v /tmp:/backup alpine \
  tar cvf /backup/vol-certs.tar -C /data .
docker run --rm -v vhost.d:/data -v /tmp:/backup alpine \
  tar cvf /backup/vol-vhost.tar -C /data .
docker run --rm -v html:/data -v /tmp:/backup alpine \
  tar cvf /backup/vol-html.tar -C /data .

# Transfert vers le nouveau serveur
scp /tmp/vol-*.tar new_user@nouveau:/tmp/

# Sur le NOUVEAU serveur : import des volumes
docker volume create certs
docker volume create vhost.d
docker volume create html

docker run --rm -v certs:/data -v /tmp:/backup alpine \
  tar xvf /backup/vol-certs.tar -C /data
docker run --rm -v vhost.d:/data -v /tmp:/backup alpine \
  tar xvf /backup/vol-vhost.tar -C /data
docker run --rm -v html:/data -v /tmp:/backup alpine \
  tar xvf /backup/vol-html.tar -C /data

Note sur le nommage : Les volumes sont préfixés par le nom du projet Docker Compose (le répertoire). Il faut adapter le nom si le répertoire change.


9. Basculement DNS

9.1 Stratégie zero-downtime

Notre approche : 1. Préparer tout sur le nouveau serveur (services prêts mais non exposés) 2. Faire une dernière synchronisation rsync 3. Arrêter les services sur l'ancien serveur 4. Basculer le DNS 5. Démarrer les services sur le nouveau serveur

9.2 Mise à jour DNS

# Obtenir la nouvelle IP publique
NEW_IP=$(curl -s ifconfig.me)
echo "Nouvelle IP: $NEW_IP"

# Mettre à jour l'enregistrement A chez votre registrar
# Les sous-domaines (CNAME) pointent vers le domaine principal

# Vérification de la propagation
watch -n 30 'dig +short mondomaine.org'

9.3 Vérification des certificats SSL

Les certificats Let's Encrypt ont été importés avec les volumes Docker. Ils seront automatiquement renouvelés par acme-companion :

# Vérifier les certificats existants
docker exec letsencrypt ls -la /etc/nginx/certs/

# Forcer le renouvellement si nécessaire
docker exec letsencrypt /app/signal_le_service

10. Démarrage et Vérification

10.1 Ordre de démarrage

cd ~/services-web

# 1. Infrastructure de base (reverse proxy + SSL)
docker compose up -d
sleep 30  # Attendre que nginx-proxy soit prêt

# 2. Services avec dépendances (ex: PeerTube + PostgreSQL + Redis)
docker compose -f docker-compose.yml -f services/peertube.yml up -d
sleep 30

# 3. Services média
docker compose -f docker-compose.yml -f services/media.yml up -d

# 4. Autres services
docker compose -f docker-compose.yml -f services/homarr.yml up -d
docker compose -f docker-compose.yml -f services/n8n.yml up -d
docker compose -f docker-compose.yml -f services/writefreely.yml up -d

10.2 Vérification des services

# État des conteneurs
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"

# Tests HTTPS
for host in video www n8n blog; do
  echo -n "$host.mondomaine.org: "
  curl -sI --connect-timeout 5 "https://$host.mondomaine.org" 2>/dev/null | head -1
done

10.3 Vérification des logs

# Logs du reverse proxy
docker logs nginx-proxy --tail=50

# Logs d'un service spécifique
docker logs nom-conteneur --tail=50

# Logs en temps réel
docker compose -f docker-compose.yml -f services/service.yml logs -f

Bonnes Pratiques et Leçons Apprises

Ce qui a bien fonctionné

  1. Toujours utiliser -H avec rsync quand des hardlinks sont impliqués. Sans ce flag, nous aurions doublé l'espace disque.

  2. Nettoyer avant de migrer : 550 Go économisés = plusieurs heures de transfert en moins.

  3. Tester chaque service individuellement avant de basculer le DNS. Cela permet d'identifier les problèmes de configuration.

  4. Garder l'ancien serveur fonctionnel pendant quelques jours après la migration. Rollback facile en cas de problème.

  5. Documenter les chemins spécifiques : certains chemins sont des héritages d'anciennes installations et peuvent surprendre.

Pièges à éviter

  1. Ne pas oublier les volumes Docker : les certificats SSL et autres assets peuvent être dans des volumes, pas dans /data.

  2. Attention aux permissions : certains conteneurs tournent avec des UID spécifiques (ex: WriteFreely en UID 5000). Un chown -R new_user:new_user /data peut casser ces services.

  3. PEP 668 sur Ubuntu 24 : pip install au niveau système ne fonctionne plus. Utiliser apt ou venv.

  4. Noms des volumes Docker : ils sont préfixés par le nom du répertoire projet. Adapter si le répertoire change.

  5. Ports des nouvelles versions : vérifier la documentation quand on met à jour une image (ex: Homarr v1.0+ utilise le port 3000, pas 7575).

Améliorations futures


Conclusion

Cette migration m'a pris environ une journée complète, dont la majorité pour le transfert des données. Le nettoyage préalable des hardlinks orphelins a été la clé pour réduire ce temps de manière significative.

Le passage à un CPU moderne ouvre de nouvelles possibilités : transcription automatique des vidéos PeerTube, utilisation de la dernière version de Homarr, et de la marge pour de futurs services.

Les points les plus importants à retenir :

  1. rsync -H pour préserver les hardlinks
  2. Nettoyer avant de migrer pour gagner du temps
  3. Tester avant de basculer le DNS
  4. Documenter les chemins spécifiques de votre configuration

J'espère que ce guide vous sera utile pour votre propre migration.