Ut0pia

Reader

Read the latest posts from Ut0pia.

from Ut0pia

Vous avez une instance PeerTube et vous souhaitez y importer automatiquement certaines vidéos d'une chaîne YouTube ? Avec n8n, c'est possible en quelques clics.

Le principe

n8n est un outil d'automatisation open-source (comme Zapier, mais auto-hébergeable). L'idée est simple :

  1. Récupérer le flux RSS d'une chaîne YouTube
  2. Filtrer les vidéos selon nos critères
  3. Vérifier si la vidéo existe déjà sur PeerTube
  4. Si non, l'importer via l'API PeerTube

Prérequis

  • Une instance n8n (ex: n8n.example.org)
  • Une instance PeerTube (ex: video.example.org)
  • Les credentials OAuth2 de votre PeerTube

Étape 1 : Récupérer le flux RSS YouTube

Chaque chaîne YouTube dispose d'un flux RSS. L'URL est :

https://www.youtube.com/feeds/videos.xml?channel_id=CHANNEL_ID

Pour trouver le CHANNEL_ID, allez sur la chaîne YouTube et regardez l'URL. Par exemple pour la chaîne fictive “TechTalks” :

https://www.youtube.com/channel/UC1234567890abcdef

Dans n8n, créez un nœud HTTP Request : – Method: GET – URL: https://www.youtube.com/feeds/videos.xml?channel_id=UC1234567890abcdef – Response Format: Text

Étape 2 : Parser et filtrer les vidéos

Ajoutez un nœud Code pour extraire les vidéos qui nous intéressent. Par exemple, pour ne garder que les vidéos dont le titre contient “Tutorial” :

const xml = $input.first().json.data;
const entries = xml.split('<entry>');
const videos = [];

for (let i = 1; i < entries.length; i++) {
  const entry = entries[i];
  const videoIdMatch = entry.match(/<yt:videoId>([^<]+)<\/yt:videoId>/);
  const titleMatch = entry.match(/<title>([^<]+)<\/title>/);

  if (videoIdMatch && titleMatch) {
    const title = titleMatch[1];
    // Filtrer : ne garder que les tutoriels
    if (title.toLowerCase().includes('tutorial')) {
      videos.push({
        videoId: videoIdMatch[1],
        title: title,
        url: `https://www.youtube.com/watch?v=${videoIdMatch[1]}`
      });
    }
  }
}

if (videos.length > 0) {
  return [{json: videos[0]}];
}
return [];

Vous pouvez adapter le filtre selon vos besoins : – Mots-clés dans le titre – Exclusion de certains termes – Combinaison de critères

Étape 3 : Vérifier les doublons

Avant d'importer, vérifions que la vidéo n'existe pas déjà. Ajoutez un nœud HTTP Request :

  • Method: GET
  • URL: https://video.example.org/api/v1/search/videos?search={{ encodeURIComponent($json.title) }}&searchTarget=local

Puis un nœud Code pour filtrer :

const searchResult = $input.first().json;
const videoTitle = $('Parse RSS').first().json.title;
const videoUrl = $('Parse RSS').first().json.url;

const exists = searchResult.data?.some(v => v.name === videoTitle);

if (exists) {
  return []; // Vidéo déjà présente, on arrête
}

return [{json: {title: videoTitle, url: videoUrl}}];

Étape 4 : Obtenir un token PeerTube

Pour utiliser l'API PeerTube, il faut s'authentifier. Ajoutez un nœud HTTP Request :

  • Method: POST
  • URL: https://video.example.org/api/v1/users/token
  • Content-Type: application/x-www-form-urlencoded
  • Body:
    • client_id: votre client ID
    • client_secret: votre client secret
    • grant_type: password
    • username: votre username
    • password: votre password

Astuce : Vous pouvez trouver le client_id et client_secret via l'API /api/v1/oauth-clients/local.

Étape 5 : Importer la vidéo

Dernier nœud HTTP Request pour lancer l'import :

  • Method: POST
  • URL: https://video.example.org/api/v1/videos/imports
  • Headers: Authorization: Bearer {{ $json.access_token }}
  • Content-Type: multipart/form-data
  • Body:
    • targetUrl: l'URL YouTube
    • channelId: l'ID du channel PeerTube destination
    • privacy: 1 (public), 2 (unlisted), 3 (private)
    • name: le titre de la vidéo
    • category: l'ID de la catégorie (optionnel)

Planification

Ajoutez un nœud Schedule Trigger au début du workflow pour exécuter automatiquement l'import :

  • Toutes les 6 heures pour un flux actif
  • Une fois par jour pour un flux moins actif

Le workflow complet

[Schedule] → [Get RSS] → [Parse & Filter] → [Check Exists] → [Filter New] → [Get Token] → [Import]

Points d'attention

  1. Cookies YouTube : Si l'import échoue avec une erreur 403, PeerTube a peut-être besoin de cookies YouTube valides. Consultez la documentation yt-dlp pour configurer les cookies.

  2. Rate limiting : N'exécutez pas le workflow trop fréquemment pour éviter d'être bloqué par YouTube.

  3. Stockage : Les vidéos importées prennent de l'espace disque. Pensez à créer un workflow de nettoyage pour les anciennes vidéos si nécessaire.

  4. Légalité : Assurez-vous d'avoir le droit de republier les vidéos que vous importez (Creative Commons, accord de l'auteur, etc.).

Conclusion

n8n rend l'automatisation accessible à tous. Avec ce workflow, vous pouvez créer votre propre miroir PeerTube de contenus YouTube sélectionnés, tout en gardant le contrôle sur ce qui est importé grâce aux filtres.


Ressources utiles :Documentation n8nDocumentation PeerTubeAPI REST PeerTube

 
Read more...

from Ut0pia

Vous hébergez une instance PeerTube et vous souhaitez suivre vos statistiques de fréquentation sans compromettre la vie privée de vos visiteurs ? Plausible Analytics est la solution idéale.

Pourquoi Plausible ?

Plausible est une alternative open-source et respectueuse de la vie privée à Google Analytics. Contrairement à ce dernier :

  • Pas de cookies : aucune bannière RGPD nécessaire
  • Pas de données personnelles : les adresses IP sont hashées puis supprimées
  • Léger : le script fait moins de 1 Ko (vs ~45 Ko pour Google Analytics)
  • Open-source : vous pouvez l'auto-héberger

Notre infrastructure

Notre homelab utilise Docker Compose avec nginx-proxy pour le reverse proxy et Let's Encrypt pour les certificats SSL. La configuration complète est disponible sur GitHub.

Installation de Plausible

1. Configuration DNS

Ajoutez un enregistrement DNS pour votre sous-domaine analytics. Dans notre cas, nous utilisons plausible.votredomaine.org :

python3 scripts/ovh-dns.py add plausible --type A --ip VOTRE_IP
python3 scripts/ovh-dns.py add plausible --type AAAA --ip VOTRE_IPV6

2. Fichier Docker Compose

Créez le fichier services/plausible.yml :

services:
  plausible:
    image: ghcr.io/plausible/community-edition:v3.1.0
    container_name: plausible
    command: /entrypoint.sh run
    healthcheck:
      test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:8000/api/health || exit 1"]
      start_period: 2m
      interval: 30s
    environment:
      - BASE_URL=https://plausible.${DOMAIN}
      - SECRET_KEY_BASE=${PLAUSIBLE_SECRET_KEY}
      - TOTP_VAULT_KEY=${PLAUSIBLE_TOTP_KEY}
      - DISABLE_REGISTRATION=invite_only
      - DATABASE_URL=postgres://plausible:${PLAUSIBLE_DB_PASSWORD}@plausible-db:5432/plausible
      - CLICKHOUSE_DATABASE_URL=http://plausible-events-db:8123/plausible_events_db
      - VIRTUAL_HOST=plausible.${DOMAIN}
      - VIRTUAL_PORT=8000
      - LETSENCRYPT_HOST=plausible.${DOMAIN}
      - LETSENCRYPT_EMAIL=${LETSENCRYPT_EMAIL}
    volumes:
      - plausible-data:/var/lib/plausible
    networks:
      - proxy-tier
      - plausible-internal
    depends_on:
      plausible-db:
        condition: service_healthy
      plausible-events-db:
        condition: service_healthy

  plausible-db:
    image: postgres:16-alpine
    container_name: plausible-db
    environment:
      - POSTGRES_USER=plausible
      - POSTGRES_PASSWORD=${PLAUSIBLE_DB_PASSWORD}
      - POSTGRES_DB=plausible
    volumes:
      - /data/plausible-db:/var/lib/postgresql/data
    networks:
      - plausible-internal
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U plausible -d plausible"]
      start_period: 1m
      interval: 10s

  plausible-events-db:
    image: clickhouse/clickhouse-server:24.12-alpine
    container_name: plausible-events-db
    environment:
      - CLICKHOUSE_SKIP_USER_SETUP=1
    volumes:
      - /data/plausible-events-db:/var/lib/clickhouse
      - /data/plausible-clickhouse-config/listen.xml:/etc/clickhouse-server/config.d/listen.xml:ro
    networks:
      - plausible-internal
    healthcheck:
      test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://127.0.0.1:8123/ping || exit 1"]
      start_period: 1m
      interval: 10s

networks:
  proxy-tier:
    external: true
  plausible-internal:
    driver: bridge

volumes:
  plausible-data:

3. Configuration ClickHouse pour IPv4

ClickHouse écoute par défaut sur IPv6. Si votre réseau Docker n'a pas IPv6, créez ce fichier :

sudo mkdir -p /data/plausible-clickhouse-config
sudo tee /data/plausible-clickhouse-config/listen.xml << 'EOF'
<clickhouse>
    <listen_host>0.0.0.0</listen_host>
</clickhouse>
EOF

4. Variables d'environnement

Ajoutez ces variables à votre fichier .env :

PLAUSIBLE_SECRET_KEY=$(openssl rand -base64 48)
PLAUSIBLE_TOTP_KEY=$(openssl rand -base64 32)
PLAUSIBLE_DB_PASSWORD=$(openssl rand -base64 24)

5. Démarrage

docker compose -f docker-compose.yml -f services/plausible.yml up -d

Le premier démarrage peut prendre 1-2 minutes. Vérifiez que tout est healthy :

docker ps | grep plausible

6. Création du compte admin

Rendez-vous sur https://plausible.votredomaine.org et créez votre compte administrateur.

Intégration avec PeerTube

Ajouter le site dans Plausible

  1. Connectez-vous à votre instance Plausible
  2. Cliquez sur “Add a website”
  3. Entrez le domaine de votre PeerTube (ex: video.ut0pia.org)

Injecter le script dans PeerTube

Dans PeerTube, allez dans Administration → Configuration → Advanced → Custom JavaScript et ajoutez :

var script = document.createElement('script');
script.defer = true;
script.dataset.domain = 'video.votredomaine.org';
script.src = 'https://plausible.votredomaine.org/js/script.js';
document.head.appendChild(script);

Attention : Le champ “Custom JavaScript” attend du JavaScript pur, pas de balises HTML <script>.

Conformité RGPD

Plausible est conçu pour être conforme au RGPD sans bannière de consentement. Selon la politique de données de Plausible :

  • Aucun cookie n'est utilisé
  • Aucune donnée personnelle n'est collectée
  • Les adresses IP sont hashées et jamais stockées
  • Pas de tracking cross-site

Vous pouvez donc l'utiliser sans afficher de bannière cookie, tout en restant transparent dans votre politique de confidentialité.

Ressources

 
Read more...

from Ut0pia

Mon fils m'a récemment présenté une formule mathématique qu'il avait trouvée par lui-même. Il s'ennuyait en cours de maths et, il a élaboré un solution à un problème : comment retrouver deux nombres quand on connaît seulement leur somme et leur produit ?

Sa solution :

x = S/2 + √(S²/4 - P)
y = S/2 - √(S²/4 - P)

où S est la somme et P le produit des deux nombres recherchés.

Intrigué par sa découverte, j'ai voulu vérifier formellement sa démonstration en utilisant un assistant de preuve mathématique. Cet article raconte notre démarche.

Le raisonnement de Jules

L'intuition géométrique

Jules a imaginé le problème à partir d'un grand carré. Son raisonnement était le suivant :

« La somme de deux nombres, c'est forcément égal à la moitié plus quelque chose, plus la moitié moins quelque chose. Du coup on peut barrer les écarts quand on fait la somme. Mais ces écarts sont utiles pour trouver le produit. »

En termes mathématiques, il a posé : – x = S/2 + d (la moyenne plus un écart) – y = S/2 – d (la moyenne moins le même écart)

Pourquoi ça marche pour la somme

x + y = (S/2 + d) + (S/2 - d) = S

Les écarts +d et -d « se barrent » — c'est exactement ce qu'il avait observé.

La visualisation avec le carré

Jules a visualisé le produit x × y comme une opération sur des carrés :

┌─────────────────┐
│                 │
│   Carré de      │    Aire = (S/2)²
│   côté S/2      │
│                 │
└─────────────────┘

Le produit (S/2 + d)(S/2 – d) correspond à l'aire du grand carré moins un petit carré de côté d :

Produit = (S/2)² - d²

C'est l'identité remarquable (a+b)(a-b) = a² – b², qu'il a retrouvée intuitivement.

Déduction de la formule

Puisque P = (S/2)² – d², on isole d :

d² = S²/4 - P
d = √(S²/4 - P)

D'où la formule finale :

x = S/2 + √(S²/4 - P)
y = S/2 - √(S²/4 - P)

Vérification formelle avec Lean 4

Pour m'assurer que le raisonnement de Jules était mathématiquement rigoureux, j'ai décidé d'utiliser Lean 4, un langage de programmation conçu pour écrire des preuves mathématiques vérifiées par ordinateur.

Qu'est-ce que Lean ?

Lean est un assistant de preuve : un langage de programmation où l'on écrit des théorèmes et leurs démonstrations. Le compilateur vérifie automatiquement que chaque étape de la preuve est logiquement correcte. Si le code compile, la preuve est mathématiquement valide.

Installation de Lean 4

J'ai installé Lean 4 en quelques commandes :

# 1. Installer elan (gestionnaire de versions Lean)
brew install elan-init

# 2. Installer Lean 4 stable
elan default stable

# 3. Créer un nouveau projet avec Mathlib
lake new theoremes_jules
cd theoremes_jules

# 4. Ajouter Mathlib comme dépendance (dans lakefile.toml)
# puis télécharger les dépendances
lake update

Le code Lean pas à pas

Voici la formalisation complète du théorème de Jules, avec des explications pour chaque partie :

import Mathlib.Data.Real.Sqrt

Import : On importe le module de Mathlib qui contient la définition de la racine carrée sur les nombres réels (Real.sqrt) et le lemme Real.sq_sqrt qui dit que (√z)² = z.


def EstSolutionSommeProduit (x y S P : ℝ) : Prop :=
  x + y = S ∧ x * y = P

Définition : On définit ce que signifie “x et y sont solutions du problème somme-produit”. C'est une proposition (Prop) qui est vraie si et seulement si : – x + y = S (la somme vaut S) – ET () – x * y = P (le produit vaut P)


theorem formule_jules (S P : ℝ) (h : S^2 / 4 - P ≥ 0) :
    EstSolutionSommeProduit
      (S / 2 + Real.sqrt (S^2 / 4 - P))
      (S / 2 - Real.sqrt (S^2 / 4 - P))
      S P := by

Énoncé du théorème : – S P : ℝ — S et P sont des nombres réels – h : S^2 / 4 - P ≥ 0 — on suppose que S²/4 – P ≥ 0 (sinon la racine carrée n'existe pas dans ℝ) – Les deux arguments suivants sont x et y selon la formule de Jules – EstSolutionSommeProduit ... S P — on veut prouver que x et y sont bien solutions – by — on commence la preuve en mode tactique


  constructor

Tactique constructor : Comme on doit prouver une conjonction (A ∧ B), cette tactique sépare le but en deux sous-buts : 1. Prouver que x + y = S 2. Prouver que x * y = P


  · ring

Première partie (prouver x + y = S) : – ring — résout automatiquement l'égalité algébrique

En effet : (S/2 + √...) + (S/2 - √...) = S/2 + S/2 = S


  · have h1 : (S / 2 + Real.sqrt (S^2 / 4 - P)) * (S / 2 - Real.sqrt (S^2 / 4 - P))
            = (S / 2)^2 - (Real.sqrt (S^2 / 4 - P))^2 := by ring
    rw [h1, Real.sq_sqrt h]
    ring

Deuxième partie (prouver x × y = P) : – have h1 : ... := by ring — on établit un fait intermédiaire : le produit (a+b)(a-b) = a² – b² (identité remarquable), prouvé par ringrw [h1, Real.sq_sqrt h] — on réécrit en utilisant h1, puis on applique le lemme Real.sq_sqrt qui dit que (√z)² = z quand z ≥ 0 (ce qu'on sait grâce à l'hypothèse h) – ring — on conclut que (S/2)² – (S²/4 – P) = S²/4 – S²/4 + P = P

Le verdict

✓ Build successful

Le code compile — donc la preuve est mathématiquement correcte. Le théorème de Jules est formellement vérifié : sa formule donne bien deux nombres dont la somme est S et le produit est P.

Contexte historique

François Viète (1540-1603)

Jules a redécouvert ce qu'on appelle les relations de Viète, du nom du mathématicien français François Viète.

En 1591, Viète publie In artem analyticem isagoge, un ouvrage qui fonde l'algèbre moderne. Il est le premier à utiliser des lettres pour désigner les inconnues (voyelles A, E, I...) et les constantes (consonnes B, C, D...). On lui doit l'invention du calcul littéral.

Les formules de Viète établissent que pour un polynôme du second degré x² – Sx + P = 0 : – La somme des racines = S – Le produit des racines = P

C'est exactement le problème solutionné par mon fils.

Le lien précis avec la formule de Jules

Jules a résolu le problème inverse de Viète : connaissant S et P, retrouver x et y.

Si x et y vérifient x + y = S et x × y = P, alors x et y sont les racines du polynôme :

t² - St + P = 0

La formule quadratique classique donne :

t = (S ± √(S² - 4P)) / 2

En réécrivant cette formule :

t = S/2 ± √(S² - 4P)/2
t = S/2 ± √((S² - 4P)/4)
t = S/2 ± √(S²/4 - P)      ← C'est la formule de Jules !
Viète (1591) Jules (2024)
Si x, y sont racines de t² – St + P = 0 Connaissant S et P
Alors x + y = S et xy = P Trouver x = S/2 + √(S²/4 – P)
(sens direct) (sens réciproque)

Mon fils a donc retrouvé la formule quadratique par un raisonnement purement géométrique, ce qui est exactement dans l'esprit des travaux de Viète — établir le lien entre les racines d'un polynôme et ses coefficients.

Sources :Formules de Viète – Math93François Viète – CultureMath (ENS)Biographie de Viète – Bibmath

Les mathématiciens grecs et l'algèbre géométrique

L'approche de Jules — raisonner avec des carrés et des aires — rappelle celle des mathématiciens grecs anciens.

Le Livre II des Éléments d'Euclide (vers 300 av. J.-C.) contient ce qu'on appelle l'algèbre géométrique. Euclide y démontre les identités remarquables par des constructions de carrés et de rectangles, exactement comme Jules l'a fait intuitivement.

Par exemple, la proposition 4 du Livre II énonce que « le carré de la droite entière est égal aux carrés des segments, et à deux fois le rectangle contenu sous les deux segments » — c'est notre identité (a+b)² = a² + 2ab + b².

Cette tradition géométrique a influencé les mathématiciens arabes comme Al-Khwarizmi (VIIIe siècle), dont le traité Abrégé du calcul par la restauration et la comparaison utilise les identités remarquables d'Euclide pour résoudre les équations du second degré.

Sources :Livre II des Éléments d'Euclide – WikipédiaIdentité remarquable – WikipédiaLes identités remarquables – Automaths

Conclusion

À 15 ans, Jules a redécouvert de manière autonome un résultat fondamental de l'algèbre, en utilisant une intuition géométrique remarquable. Son approche — décomposer deux nombres symétriquement autour de leur moyenne — est élégante et montre une compréhension profonde de la structure mathématique.

Ce qui me frappe, c'est que son raisonnement suit exactement la tradition des mathématiciens grecs : penser l'algèbre géométriquement, avec des carrés et des aires. Sans le savoir, il a refait le chemin d'Euclide.

Grâce à Lean 4, nous avons pu vérifier formellement que sa démonstration est mathématiquement correcte. Le théorème est prouvé.


Preuve formellement vérifiée avec Lean 4 et Mathlib

 
Read more...

from Ut0pia

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"

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

  • 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 :

  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.

 
Lire la suite...

from Ut0pia

Ou comment une simple importation de vidéo s'est transformée en partie d'échecs avec Google


Le problème : “Sign in to confirm you're not a bot”

Tout a commencé par une erreur banale. Mon instance PeerTube, hébergée sur video.ut0pia.org, refusait obstinément d'importer des vidéos YouTube. Dans les logs, un message laconique :

Sign in to confirm you're not a bot

YouTube, dans sa croisade contre le scraping, bloque désormais systématiquement les requêtes provenant de serveurs. Peu importe que PeerTube utilise yt-dlp, l'excellent outil open-source de téléchargement — sans authentification, c'est un mur.

Première idée : les cookies

La solution classique avec yt-dlp est d'utiliser des cookies d'une session YouTube authentifiée. Simple en théorie :

  1. Exporter les cookies depuis le navigateur
  2. Les donner à yt-dlp via --cookies fichier.txt
  3. Profit

J'exporte donc mes cookies YouTube au format Netscape (le format standard pour yt-dlp) et je les place dans /data/peertube/config/youtube-cookies.txt.

Le piège du wrapper Python

PeerTube n'appelle pas yt-dlp directement. Il l'exécute via Python :

/usr/bin/python3 /data/bin/yt-dlp [arguments...]

Ma première idée brillante : créer un wrapper bash qui injecte automatiquement l'option --cookies. Je remplace le binaire yt-dlp par un script :

#!/bin/bash
exec /data/bin/yt-dlp.real --cookies /config/youtube-cookies.txt "$@"

Résultat ? Une magnifique erreur Python :

SyntaxError: invalid syntax

Évidemment. Python essaie d'interpréter mon script bash comme du code Python. Retour à la case départ.

La solution native : le fichier de config yt-dlp

yt-dlp supporte nativement un fichier de configuration dans ~/.config/yt-dlp/config. Chaque ligne est un argument. Je crée donc :

--cookies /config/youtube-cookies.txt

Je place ce fichier dans ~/.config/yt-dlp/config, je relance l'import... et ça échoue toujours.

YouTube contre-attaque : l'invalidation des cookies

Les logs montrent maintenant une erreur différente. YouTube a détecté que les cookies, exportés depuis mon PC en France, sont soudainement utilisés depuis un serveur avec une IP différente.

Résultat : cookies invalidés. Session terminée. Retour au message “Sign in to confirm you're not a bot”.

La danse des cookies

S'ensuit une série de tentatives :

  1. Export classique → Cookies invalidés après quelques minutes
  2. Nouvel export → Même résultat
  3. Export sans naviguer sur YouTube après → Toujours invalidé

YouTube est paranoïaque. Dès que les cookies sont utilisés depuis une nouvelle IP, il les révoque. Et le simple fait de continuer à naviguer sur YouTube après l'export semble accélérer le processus.

La méthode qui fonctionne (enfin !)

Après plusieurs échecs, je découvre la procédure recommandée par les développeurs de yt-dlp :

  1. Ouvrir une fenêtre de navigation privée (incognito)
  2. Se connecter à YouTube
  3. Aller directement sur https://www.youtube.com/robots.txt — une page statique qui ne déclenche pas de tracking
  4. Exporter immédiatement les cookies avec une extension dédiée
  5. Fermer la fenêtre sans naviguer ailleurs

L'idée est de créer une session “propre” qui n'a jamais vraiment été utilisée. YouTube ne peut pas détecter de comportement suspect puisqu'il n'y a eu aucune activité.

Je suis la procédure à la lettre. J'injecte les nouveaux cookies. Je lance l'import.

Notifying user root its video import https://www.youtube.com/watch?v=xxx is finished.
Creating transcription job for video...

Victoire.

Rendre la configuration persistante

Le problème avec Docker, c'est que tout ce qui est dans le conteneur disparaît au redémarrage. Il faut monter les fichiers de configuration depuis l'hôte.

Dans services/peertube.yml, j'ajoute les volumes :

volumes:
  - /data/peertube/yt-dlp-config-peertube:/home/peertube/.config/yt-dlp:ro
  - /data/peertube/config:/config

Le fichier de config yt-dlp pointe vers les cookies :

--cookies /config/youtube-cookies.txt

Et les cookies sont dans /data/peertube/config/youtube-cookies.txt sur l'hôte.

Ainsi, même après un docker compose down && docker compose up -d, la configuration survit.

Bonus : Redis qui refuse de persister

En cours de route, j'ai aussi découvert que Redis refusait de fonctionner :

MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk.

La solution : passer Redis en mode AOF (Append Only File) au lieu de RDB :

redis:
  image: redis:7-alpine
  command: redis-server --dir /data --appendonly yes

Un problème de moins.

Leçons apprises

  1. Toujours vérifier quel utilisateur exécute le processus — root et les autres utilisateurs n'ont pas le même HOME
  2. Les cookies sont fragiles — YouTube les invalide dès qu'il détecte une anomalie
  3. La méthode incognito + robots.txt fonctionne — elle crée des cookies “vierges” moins susceptibles d'être invalidés
  4. Docker nécessite des montages explicites — ce qui n'est pas monté disparaît au redémarrage
  5. Lire les logs, encore et encore — chaque message d'erreur est un indice

Pour le futur

Les cookies finiront par expirer ou être invalidés. Quand ça arrivera, il suffira de :

  1. Refaire la procédure incognito + robots.txt
  2. Mettre à jour /data/peertube/config/youtube-cookies.txt
  3. Aucun redémarrage nécessaire — yt-dlp relit le fichier à chaque exécution

C'est le prix à payer pour héberger sa propre plateforme vidéo. Mais au moins, mes vidéos sont chez moi.


Configuration : PeerTube sur Docker, derrière nginx-proxy avec Let's Encrypt automatique. Serveur : Intel Atom N2800 (oui, ça tourne). OS : Linux.

 
Lire la suite...