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
- Préparer un nouveau serveur Ubuntu 24 pour Docker
- Configurer SSH
- Utiliser rsync avec préservation des hardlinks
- Adapter une stack Docker Compose à un nouvel environnement
- Gérer la migration DNS avec zero downtime
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 :
- Reverse proxy : nginx-proxy + acme-companion (Let's Encrypt automatique)
- Vidéo : PeerTube (plateforme vidéo fédérée)
- Dashboard : Homarr
- Workflow : n8n
- Blog : WriteFreely (fédéré via ActivityPub)
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 ?
- data-root : Évite de saturer la partition root avec les images Docker
- log-driver + log-opts : Limite chaque fichier de log à 10 Mo avec 3 rotations maximum, évitant l'accumulation de logs volumineux
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"
3. Synchronisation avec rsync : L'Art de Préserver les Hardlinks
3.1 Comprendre les hardlinks
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
3.4 Vérification des hardlinks après transfert
# 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.
4.2 Script de nettoyage des hardlinks orphelins
#!/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é
Toujours utiliser -H avec rsync quand des hardlinks sont impliqués. Sans ce flag, nous aurions doublé l'espace disque.
Nettoyer avant de migrer : 550 Go économisés = plusieurs heures de transfert en moins.
Tester chaque service individuellement avant de basculer le DNS. Cela permet d'identifier les problèmes de configuration.
Garder l'ancien serveur fonctionnel pendant quelques jours après la migration. Rollback facile en cas de problème.
Documenter les chemins spécifiques : certains chemins sont des héritages d'anciennes installations et peuvent surprendre.
Pièges à éviter
Ne pas oublier les volumes Docker : les certificats SSL et autres assets peuvent être dans des volumes, pas dans /data.
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.
PEP 668 sur Ubuntu 24 : pip install au niveau système ne fonctionne plus. Utiliser apt ou venv.
Noms des volumes Docker : ils sont préfixés par le nom du répertoire projet. Adapter si le répertoire change.
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
- Automatiser la détection des hardlinks orphelins avec un cron job
- Implémenter une sauvegarde 3-2-1 avec rsync vers un stockage distant
- Ajouter du monitoring (Prometheus + Grafana) pour anticiper les problèmes d'espace disque
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 :
- rsync -H pour préserver les hardlinks
- Nettoyer avant de migrer pour gagner du temps
- Tester avant de basculer le DNS
- Documenter les chemins spécifiques de votre configuration
J'espère que ce guide vous sera utile pour votre propre migration.