Migrer depuis Virtualmin
Ce guide vous accompagne dans la migration de sites PHP+MySQL, d'un serveur Virtualmin vers Kuploy. La démarche s'appuie sur la boîte à outils kuploy-migrate, qui automatise la copie des fichiers, les exports de bases de données et la génération des Dockerfile.
Vue d'ensemble
Le déroulé de la migration :
- Exporter — lancez le script de migration sur votre serveur Virtualmin, pour pousser les fichiers des sites et les exports de bases vers un dépôt GitHub
- Déployer — créez dans Kuploy les projets, les bases et les applications, en les faisant pointer sur ce dépôt
- Basculer le DNS — mettez à jour vos enregistrements A vers votre cluster Kuploy
Chaque site Virtualmin devient un projet Kuploy, avec sa propre base MariaDB et son déploiement applicatif.
Prérequis
- Un serveur Virtualmin, avec un accès root
git,curl,rsync,mysqletmysqldumpinstallés sur le serveur- Un jeton d'accès personnel GitHub, avec la portée
repo - Une instance Kuploy, avec un fournisseur GitHub connecté
- Un registre de conteneurs configuré dans Kuploy (Settings → Registry)
Phase 0 : préparer l'image de base
kuploy-migrate utilise une image Docker de base commune à tous les sites PHP. Construisez-la et poussez-la une fois pour toutes :
git clone https://github.com/kuploy/kuploy-migrate
cd kuploy-migrate
docker build --platform linux/amd64 -t votreregistre/php-legacy:8.1 -f base-Dockerfile .
docker push votreregistre/php-legacy:8.1
L'image de base comporte PHP 8.1, Apache avec mod_rewrite et AllowOverride All, les extensions mysqli et PDO, Composer, et des valeurs par défaut raisonnables dans php.ini : 64 Mo de téléversement, 256 Mo de mémoire, 300 s d'exécution.
Si votre cluster tourne sur des nœuds ARM, construisez pour plusieurs plateformes :
docker buildx build --platform linux/amd64,linux/arm64 --push -t votreregistre/php-legacy:8.1 -f base-Dockerfile .
Phase 1 : exporter les sites depuis Virtualmin
Lancer le script de migration
Sur votre serveur Virtualmin :
export GITHUB_PAT=ghp_votre_jeton
cd /chemin/vers/kuploy-migrate
chmod +x virtualmin/migrate.sh
# D'abord à blanc
./virtualmin/migrate.sh \
--pat $GITHUB_PAT \
--repo votreorg/virtualmin-sites \
--sites "monsite" \
--dry-run
# Puis pour de vrai
./virtualmin/migrate.sh \
--pat $GITHUB_PAT \
--repo votreorg/virtualmin-sites \
--sites "monsite"
Ce que fait le script
Pour chaque site, le script :
- analyse
/etc/webmin/virtual-server/domains/*, pour en tirer les identifiants de base de données, les noms de domaine et le mode PHP ; - copie
public_html/, avec des exclusions : journaux, archives, courriels, phpMyAdmin ; - détecte les applications PHP situées hors de
public_html/, en cherchantartisanouvendor/autoload.php; - copie la racine de l'application, en préservant le nom de répertoire d'origine ;
- exporte la base MySQL, avec les identifiants propres au site ;
- génère un Dockerfile minimal ;
- enregistre les identifiants dans un fichier local, jamais versionné dans Git ;
- pousse le tout vers le dépôt GitHub.
La structure du dépôt
Le script crée cette arborescence dans votre dépôt GitHub :
virtualmin-sites/
├── monsite/
│ ├── Dockerfile
│ ├── public_html/
│ ├── monsite/ ← la racine de l'application PHP, si détectée
│ ├── db/monsite.sql ← l'export MySQL
│ └── site.json ← les métadonnées
└── .gitignore
La détection des applications PHP
Le script cherche des répertoires de code hors de public_html/, sous ~/<site>/<site>/, ~/<site>/app/ ou ~/<site>/laravel/. Il y guette soit artisan (Laravel), soit vendor/autoload.php (n'importe quelle application Composer).
Le nom de répertoire d'origine est préservé, pour que les chemins des require() de votre index.php continuent de fonctionner. Ainsi, si votre index.php fait require('../monsite/vendor/autoload.php'), le Dockerfile copie vers /var/www/monsite/.
Un répertoire d'application personnalisé : si la racine de votre application PHP porte un nom qui sort de l'ordinaire — sita au lieu du nom du site, par exemple —, utilisez l'option --app-dir :
./virtualmin/migrate.sh \
--pat $GITHUB_PAT \
--repo votreorg/virtualmin-sites \
--sites "monsite" \
--app-dir "monsite:sita"
Le script cherchera alors l'application sous ~/<site>/sita/, en plus des emplacements par défaut. Le nom du répertoire est préservé dans le Dockerfile, pour la compatibilité des chemins.
La gestion des dépendances :
- si l'application a un
composer.json, le répertoirevendorest exclu de Git et le Dockerfile exécutecomposer install --no-devà la construction ; - s'il n'y a pas de
composer.json,vendorest versionné tel quel.
Phase 2 : déployer dans Kuploy
Vous pouvez déployer vos sites soit par l'API d'import de sites — recommandé pour les migrations en lot —, soit à la main, depuis le tableau de bord.
Option A : l'API d'import de sites (recommandée)
L'API d'import de sites crée le projet, la base, l'application, les variables d'environnement et le domaine en un seul appel par site.
Mise en place :
- Générez une clé d'API depuis Account → API Keys (voir Les clés d'API). Basculez sur l'organisation cible avant de créer la clé : elle est liée à l'organisation active au moment de sa création.
- Notez votre identifiant d'organisation et l'identifiant de votre fournisseur Git GitHub, depuis le tableau de bord
Importer tous les sites :
export API_KEY="votre-clé-d-api"
export KUPLOY_URL="https://console.example.com"
export ORG_ID="org_xxx"
export GITHUB_ID="gh_xxx"
export REPO="votreorg/virtualmin-sites"
for site_dir in ${REPO_DIR}/*/; do
site=$(basename "$site_dir")
domain=$(jq -r '.domain' "$site_dir/site.json")
db_name=$(jq -r '.db_mysql' "$site_dir/site.json")
db_user=$(jq -r '.mysql_user' "$site_dir/site.json")
db_pass=$(grep "^$site " "$CREDS_FILE" | awk '{print $4}')
curl -s -X POST "$KUPLOY_URL/api/site-import" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"organizationId\": \"$ORG_ID\",
\"name\": \"$site\",
\"domain\": \"$domain\",
\"database\": {
\"type\": \"mariadb\",
\"name\": \"$db_name\",
\"user\": \"$db_user\",
\"password\": \"$db_pass\"
},
\"source\": {
\"type\": \"github\",
\"repo\": \"$REPO\",
\"branch\": \"main\",
\"buildPath\": \"/$site\",
\"buildType\": \"dockerfile\",
\"githubId\": \"$GITHUB_ID\"
},
\"port\": 80
}" | jq .
done
L'API définit automatiquement DB_HOST, DB_DATABASE, DB_USERNAME et DB_PASSWORD sur l'application. Pour les sites Laravel, ajoutez les variables supplémentaires par le champ envVars :
"envVars": {
"APP_KEY": "base64:xxx",
"APP_ENV": "production",
"APP_DEBUG": "false"
}
Récupérez l'APP_KEY depuis le serveur d'origine :
grep APP_KEY /home/monsite/monsite/.env
Importer l'export de base de données
Une fois que l'API d'import de sites a créé le conteneur de base de données et qu'il tourne, importez l'export SQL :
# Trouver le nom du pod
kubectl get pods -n project-monsite
# Importer l'export (utilisez -i, et non -it, quand vous alimentez l'entrée standard)
kubectl exec -i <pod-mariadb> -n project-monsite -- \
mariadb -u root -p'<mot_de_passe_root>' monsite < monsite/db/monsite.sql
# Vérifier que les tables ont bien été créées
kubectl exec -i <pod-mariadb> -n project-monsite -- \
mariadb -u root -p'<mot_de_passe_root>' monsite -e "SHOW TABLES;"
Les conteneurs MariaDB utilisent la commande mariadb, et non mysql. Récupérez le mot de passe root depuis l'environnement du pod :
kubectl exec -i <pod-mariadb> -n project-monsite -- env | grep MARIADB_ROOT
Option B : la mise en place à la main (tableau de bord)
Si vous préférez configurer chaque site vous-même :
- Créez un projet — cliquez sur New Project, et nommez-le d'après le site
- Créez une base MariaDB — Add Resource → Database → MariaDB, puis renseignez le nom, l'utilisateur et le mot de passe
- Créez une application — Add Resource → Application, source GitHub, dépôt
votreorg/virtualmin-sites, branchemain, chemin de construction/<site>, type Dockerfile, port 80 - Définissez les variables d'environnement —
DB_HOST,DB_DATABASE,DB_USERNAME,DB_PASSWORD, plusAPP_KEYet consorts pour Laravel - Ajoutez le domaine — allez dans l'onglet Domains, et ajoutez votre domaine
Ajouter un domaine personnalisé
Si vous êtes passé par l'API, le domaine est déjà configuré. Si vous procédez à la main :
- Allez dans l'onglet Domains
- Ajoutez votre domaine (
monsite.com, par exemple) - Kuploy met en service un certificat TLS automatiquement, par cert-manager
Tester avant de basculer le DNS
Vérifiez que le déploiement fonctionne avant de toucher au DNS :
# À ajouter dans /etc/hosts, sur votre machine
<ip-ingress-k8s> monsite.com
Récupérez l'adresse de l'ingress :
kubectl get svc -n ingress-nginx
L'EXTERNAL-IP du service ingress-nginx-controller est l'adresse de votre répartiteur de charge.
La stratégie de migration DNS
N'indiquez pas de domaine dans l'appel à l'API d'import de sites si le DNS pointe encore vers votre ancien serveur. La création d'un domaine déclenche l'émission d'un certificat SSL (le défi HTTP-01 de Let's Encrypt), qui échouera si le domaine ne résout pas vers l'adresse de votre ingress Kuploy.
Procédez plutôt ainsi :
- Importez le site sans domaine — déployez et vérifiez d'abord qu'il fonctionne
- Mettez le DNS à jour, vers l'adresse de l'ingress Kuploy
- Ajoutez le domaine depuis le tableau de bord Kuploy, une fois le DNS propagé
Récupérer l'adresse de votre ingress Kuploy :
kubectl get svc -n ingress-nginx
# Repérez l'EXTERNAL-IP de ingress-nginx-controller
Tester avant de basculer le DNS
Vérifiez que le déploiement fonctionne avant de toucher au DNS :
# À ajouter dans /etc/hosts, sur votre machine
<ip-ingress-k8s> monsite.com
Mettre le DNS à jour
Une fois la vérification faite, mettez l'enregistrement A de votre domaine à jour, vers l'adresse de l'ingress Kuploy.
Si le DNS est géré par un bureau d'enregistrement externe (Enom, Namecheap, etc.), modifiez l'enregistrement A depuis son interface.
Si le DNS est géré par Virtualmin, son interface web vérifie que les enregistrements A correspondent à l'adresse du serveur lui-même, et refusera votre modification. Deux contournements :
Option A — décocher la validation (interface web) :
- Allez dans Server Configuration → DNS Records, pour le domaine
- Changez l'enregistrement A du domaine nu (
sitagroupgn.com., par exemple) vers l'adresse de votre ingress Kuploy - Décochez « Validate new records? », en bas
- Cliquez sur Save
Option B — la ligne de commande, qui contourne entièrement la validation :
KUPLOY_IP="<ip-ingress-kuploy>"
virtualmin modify-dns --domain monsite.com --remove-record "monsite.com. A"
virtualmin modify-dns --domain monsite.com --add-record "monsite.com. A $KUPLOY_IP"
virtualmin modify-dns --domain monsite.com --remove-record "www.monsite.com. A"
virtualmin modify-dns --domain monsite.com --add-record "www.monsite.com. A $KUPLOY_IP"
Ne modifiez que les enregistrements A du domaine nu et de www. Laissez tous les autres en place — en particulier MX, mail, webmail, SPF et DMARC, si le serveur Virtualmin continue de gérer la messagerie.
Laissez les enregistrements MX, mail, webmail, SPF et autodiscover inchangés : ils doivent continuer de pointer vers le serveur Virtualmin, s'il gère encore la messagerie.
Ajouter le domaine dans Kuploy
Une fois le DNS propagé — vérifiez avec dig +short monsite.com A :
- Allez dans Projects → monsite → Domains
- Ajoutez
monsite.com, avec HTTPS activé - Kuploy met en service un certificat TLS automatiquement, par Let's Encrypt
Autre possibilité, si vous êtes passé par l'API d'import de sites et que l'import est partial ou failed :
- Allez dans Import Sites
- Cliquez sur l'icône de crayon, sur la carte de l'import, pour ajouter le domaine
- Cliquez sur Retry — l'import reprend là où il s'était arrêté
Phase 3 : traiter le reste des sites en lot
Lancez le script de migration sur plusieurs sites à la fois :
./virtualmin/migrate.sh \
--pat $GITHUB_PAT \
--repo votreorg/virtualmin-sites \
--sites "site1 site2 site3 site4 site5"
Puis répétez la mise en place côté Kuploy — projet, base, application, variables d'environnement, domaine — pour chaque site.
Dépannage
L'import est partial, avec « domain_creation: HTTP request failed »
L'étape du domaine a échoué, probablement parce que le DNS ne pointe pas encore vers le cluster Kuploy. Le projet, la base et l'application du site ont bien été créés ; le domaine et le déploiement, non.
Le correctif :
- Allez dans Import Sites
- Cliquez sur l'icône de crayon, sur la carte de l'import
- Videz le champ Domain
- Cliquez sur Save, puis sur Retry
L'import est alors relancé sans création de domaine. Vous ajouterez celui-ci plus tard, depuis le tableau de bord, après la migration DNS.
L'import est success, mais le site est injoignable
success signifie que toutes les ressources ont été créées et la construction déclenchée ; mais la construction et le déploiement sont asynchrones. Vérifiez :
- Allez dans Projects → monsite → Application → Deployments, pour voir l'état de la construction
- Si elle a échoué, cherchez les erreurs dans les journaux de déploiement
- Si elle a réussi mais qu'aucun pod ne tourne, cliquez sur Deploy pour relancer un déploiement
Le bouton Retry tourne sans avancer
L'import attend une étape longue : une construction, la création de ressources Kubernetes. Cela peut arriver si l'API de votre cluster est lente.
- Patientez — une construction peut prendre jusqu'à une demi-heure sur un gros site
- Si elle reste coincée, rechargez la page pour réinitialiser l'interface, puis vérifiez l'état de l'import
La construction échoue avec « failed to read dockerfile: is a directory »
Le buildPath de l'import pointe sur un sous-répertoire (/monsite, par exemple), mais le constructeur n'y a pas trouvé le Dockerfile. Assurez-vous que :
- le Dockerfile existe bien sous
<buildPath>/Dockerfile, dans votre dépôt ; - le chemin de construction commence par
/—/monsite, et nonmonsite.
L'import de l'export de base échoue
Si kubectl exec reste bloqué ou renvoie des erreurs :
# Vérifier que le pod tourne
kubectl get pods -n project-monsite
# Vérifier le mot de passe root de MariaDB
kubectl exec -i <pod-mariadb> -n project-monsite -- env | grep MARIADB_ROOT
# Pensez à utiliser -i (et non -it) quand vous alimentez l'entrée standard
kubectl exec -i <pod-mariadb> -n project-monsite -- \
mariadb -u root -p'<mot_de_passe_root>' monsite < monsite/db/monsite.sql
Quelques réserves
La messagerie
Virtualmin gère la messagerie (Postfix, Dovecot) domaine par domaine. Kuploy ne s'en occupe pas. Vos options :
- garder Virtualmin en service pour la seule messagerie — retirez l'hébergement web, conservez les enregistrements MX ;
- migrer vers un prestataire de messagerie externe : Google Workspace, Zoho, Mailgun.
phpMyAdmin
Le script de migration retire le phpMyAdmin embarqué dans chaque site. Déployez au besoin une seule instance de phpMyAdmin partagée, comme application Kuploy distincte.
Le mode PHP
Virtualmin utilise php_mode=fcgid (FastCGI). L'image de base utilise mod_php, le module Apache. Cela convient à la très grande majorité des sites. Si l'un des vôtres repose sur des pools PHP par utilisateur, il demandera peut-être un ajustement.
Les tâches planifiées
Les tâches cron de Virtualmin doivent être migrées à la main. Inspectez les crontabs existantes :
crontab -u monsite -l
Recréez-les comme planifications Kuploy, ou comme CronJob Kubernetes.
Les gros sites
GitHub applique une limite indicative d'environ 1 Go par dépôt, et de 100 Mo par fichier. Si un site comporte de très gros fichiers multimédias, envisagez de les servir depuis S3 ou un stockage objet, plutôt que de les inclure dans le dépôt Git.
Les options du script
| Option | Obligatoire | Description |
|---|---|---|
--pat | Oui | Le jeton d'accès personnel GitHub |
--repo | Oui | Le dépôt cible, au format propriétaire/nom — créé s'il n'existe pas |
--sites | Oui | La liste, séparée par des espaces, des répertoires de sites sous /home/ |
--site-path | Non | Associe un site à un chemin de départ personnalisé (format site:/chemin, répétable) |
--app-dir | Non | Associe un site à un nom de répertoire d'application PHP personnalisé (format site:répertoire, répétable) |
--base-img | Non | L'image Docker de base (par défaut : ceduth/php-legacy:8.1) |
--dry-run | Non | Prévisualise sans exécuter |
Le script enregistre les identifiants de bases de données dans /tmp/virtualmin-migrate/credentials.txt — en local seulement, jamais versionné. Servez-vous-en lors de la création de vos bases dans Kuploy.