Contenu

Hugo sur KVM : installer une VM Ubuntu pour site statique

⚡ En bref

Migration progressive de Grav CMS vers Hugo — un générateur de site statique. L’objectif est d’isoler Hugo dans une VM KVM dédiée sur le NUC8i3BEH, avec nginx sur le host en proxy inverse. Le site statique généré est servi par nginx dans la VM, sans PHP, sans base de données, sans surface d’attaque applicative.

  • 🖥️ Host : NUC8i3BEH Ubuntu 24.04 — nginx proxy + KVM
  • 🗄️ Disque VM : NVMe externe Samsung X5 (exFAT → image ext4 loop)
  • 🌐 Accès : hugo-test.arleo.eu
  • 🎨 Thème : LoveIt

🧠 Pourquoi

Grav CMS est excellent mais repose sur PHP — une surface d’attaque non négligeable. Hugo génère du HTML statique pur : zéro PHP, zéro base de données, zéro vulnérabilité applicative. Les performances sont aussi radicalement meilleures — le HTML est servi directement par nginx sans traitement dynamique.

L’isolation dans une VM KVM offre un avantage supplémentaire : si la VM est compromise, le host reste intact. Le proxy nginx du host ne fait que transmettre les requêtes vers la VM sans exposer d’autres services.

Architecture finale

Internet → Cloudflare → nginx (NUC host) → VM KVM (192.168.122.69)
                                                    ↓
                                             Hugo + nginx
                                           (HTML statique)

🔧 Ce qui a été fait

1. Installation KVM sur le host Ubuntu 24.04

sudo apt install -y \
  qemu-kvm libvirt-daemon-system libvirt-clients \
  virtinst bridge-utils cloud-image-utils \
  cockpit-machines iptables

sudo usermod -aG libvirt jm
sudo usermod -aG kvm jm
sudo systemctl enable --now libvirtd

# Activer le réseau NAT par défaut
sudo virsh net-start default
sudo virsh net-autostart default

2. Problème exFAT — image ext4 loop sur le NVMe

Le disque NVMe externe Samsung X5 est formaté en exFAT — ce système de fichiers ne supporte pas les permissions Unix (chown). QEMU ne peut donc pas lire les images disque stockées dessus directement.

Solution : créer une image ext4 dans un fichier sur le volume exFAT :

# Créer une image ext4 de 25 Go sur le NVMe exFAT
sudo dd if=/dev/zero of=/mnt/X5/libvirt-pool.img bs=1G count=25 status=progress
sudo mkfs.ext4 /mnt/X5/libvirt-pool.img

# Monter l'image
sudo mkdir -p /mnt/libvirt-kvm
sudo mount -o loop /mnt/X5/libvirt-pool.img /mnt/libvirt-kvm

# Persistance au reboot
echo "/mnt/X5/libvirt-pool.img /mnt/libvirt-kvm ext4 loop,nofail 0 0" \
  | sudo tee -a /etc/fstab

# Permissions pour QEMU
sudo chown libvirt-qemu:kvm /mnt/libvirt-kvm
sudo chmod 755 /mnt/libvirt-kvm

# Pool libvirt dédié
sudo virsh pool-define-as kvm-pool dir --target /mnt/libvirt-kvm
sudo virsh pool-autostart kvm-pool
sudo virsh pool-start kvm-pool

3. Création de la VM avec cloud-init

# Télécharger l'image Ubuntu 24.04 cloud
wget -O /mnt/libvirt-kvm/ubuntu-24.04-server.img \
  https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img

# Créer le disque VM (20 Go, backing file)
qemu-img create \
  -F qcow2 -b /mnt/libvirt-kvm/ubuntu-24.04-server.img \
  -f qcow2 /mnt/libvirt-kvm/hugo-vm.qcow2 20G

sudo chown libvirt-qemu:kvm /mnt/libvirt-kvm/hugo-vm.qcow2

# Cloud-init avec clé SSH
SSH_KEY=$(cat /home/jm/.ssh/authorized_keys | head -1)
cat > /tmp/user-data << EOF
#cloud-config
hostname: hugo-vm
users:
  - name: jm
    sudo: ALL=(ALL) NOPASSWD:ALL
    shell: /bin/bash
    lock_passwd: false
    ssh_authorized_keys:
      - $SSH_KEY
packages:
  - nginx
  - git
  - curl
runcmd:
  - systemctl enable nginx
  - systemctl start nginx
EOF

cloud-localds /mnt/libvirt-kvm/hugo-vm-cloud-init.img \
  /tmp/user-data /tmp/meta-data

4. Démarrage de la VM

sudo virt-install \
  --name hugo-vm \
  --ram 1024 \
  --vcpus 1 \
  --disk path=/mnt/libvirt-kvm/hugo-vm.qcow2,format=qcow2,bus=virtio \
  --disk path=/mnt/libvirt-kvm/hugo-vm-cloud-init.img,device=cdrom \
  --os-variant ubuntu24.04 \
  --network network=default,model=virtio \
  --graphics none \
  --import \
  --noautoconsole \
  --autostart

# Récupérer l'IP
sudo virsh net-dhcp-leases default
# → 192.168.122.69

5. Installation de Hugo Extended dans la VM

ssh jm@192.168.122.69

HUGO_VERSION="0.147.0"
wget -q -O /tmp/hugo.deb \
  "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb"
sudo dpkg -i /tmp/hugo.deb
hugo version
# hugo v0.147.0+extended linux/amd64

6. Initialisation du site Hugo avec LoveIt

mkdir -p ~/hugo-site && cd ~/hugo-site
hugo new site . --force
git init
git submodule add https://github.com/dillonzq/LoveIt.git themes/LoveIt

7. Configuration nginx dans la VM

server {
    listen 80 default_server;
    server_name _;
    root /var/www/hugo;
    index index.html;

    location = / {
        return 302 /fr/;
    }

    location / {
        try_files $uri $uri/ =404;
    }
}

8. Script de déploiement

cat > ~/deploy.sh << 'EOF'
#!/bin/bash
cd ~/hugo-site
hugo --minify
sudo rsync -a --delete public/ /var/www/hugo/
sudo chown -R www-data:www-data /var/www/hugo/
echo "Déployé — $(date)"
EOF
chmod +x ~/deploy.sh

9. Proxy nginx sur le host

server {
    listen 443 ssl http2;
    server_name hugo-test.arleo.eu;

    include snippets/ssl.conf;

    location / {
        proxy_pass         http://192.168.122.69:80;
        proxy_http_version 1.1;
        proxy_set_header   Host              $host;
        proxy_set_header   X-Real-IP         $remote_addr;
        proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto $scheme;
    }
}

Points de friction rencontrés

ProblèmeCauseSolution
Réseau libvirt inactifiptables absentapt install iptables
Permission denied sur qcow2disque exFAT → pas de chownImage ext4 loop dans le fichier exFAT
Cloud-init ne crée pas le userClé SSH absente du hostvirt-customize pour injecter directement
SSH refusé (publickey)Clé privée absente sur le hostGénérer hugo_vm + injecter via virt-customize
nginx 404Permissions home jm (750)Déplacer public/ vers /var/www/hugo

🏁 Conclusion

La VM Hugo est opérationnelle sur hugo-test.arleo.eu. Le site statique est servi par nginx dans la VM, proxifié par le host. La prochaine étape est la migration du contenu Grav vers Hugo et le développement d’un MCP Hugo pour permettre la gestion des pages directement depuis Claude.ai.

Pour aller plus loin :

  • 💡 Développer un MCP Hugo (FastAPI + OAuth) sur le modèle de grav-mcp-server
  • 💡 Automatiser le rebuild Hugo via webhook Git
  • 💡 Configurer la sauvegarde de la VM vers le NAS QNAP