Migrating Websites from DirectAdmin to ISPConfig

Some time ago I faced a serious challenge: migrating a large number of similar websites from DirectAdmin to ISPConfig with maximum automation. The hosting platform was changed, and doing everything manually simply wasn’t an option.

To solve this, I wrote a better CLI wrapper for ISPConfig that focuses on usability while reusing the default API functions underneath: ispconfig-cli

1. Prepare migration configuration

First, define all domains and databases you want to migrate. The idea is to create everything on the ISPConfig side first, then sync files and databases afterward.

migration/config.json

[
  {
    "website_from": "test1.mydomain.com",
    "website_name": "my2domain.com",
    "mysql_base": "my2domaincom",
    "mysql_user": "my2domaincomUSER",
    "mysql_pass": "my2domaincomPASS"
  }
]

2. Run the bootstrap script

This script creates everything required on the ISPConfig side (domains, users, databases, etc.). It’s safe to run multiple times — it will just add missing resources if something wasn’t created before.

migration/bootstrap.php

$result = json_decode(addWebDomain(array(
    'domain'                => $website_name,
    'client_id'             => $client_id,
    'directive_snippets_id' => 1,
)), true);
...
$result = json_decode(addDatabaseUser(array(
    'database_user'     => $db_user,
    'database_password' => $item['mysql_pass'],
    'client_id'         => $client_id,
    'server_id'         => $server_id,
)), true);
...
$result = json_decode(addDatabase(array(
    'database_name'    => $db_name,
    'database_user_id' => $item['database_user_id'],
    'parent_domain_id' => $item['website_id'],
    'client_id'        => $client_id,
    'server_id'        => $server_id,
)), true);

3. Sync files and databases

Finally, run the Bash script to rsync site files, fix ownership and permissions, dump/restore databases, and rewrite application configs with the new environment values.

migration/sync-db-and-files.sh

time rsync -avh --no-perms --delete --partial --info=progress2 --info=name0 \
    "$SSH_USER@$SSH_HOST:/home/*/domains/$SITE_DOMAIN_WEBROOT_FROM/public_html/" \
    "$SITE_DOMAIN_WEBROOT/web/"
...
chown -R "$SITE_DOMAIN_USER:$SITE_DOMAIN_GROUP" "$SITE_DOMAIN_WEBROOT/web"
...
time mysqldump -h"$MYSQL_HOST" -u"$MYSQL_USER" -p"$MYSQL_PASS" "$MYSQL_SRC_BASE" \
    | pv \
    | mysql -h"$MYSQL_DST_HOST" -u"$MYSQL_DST_USER" -p"$MYSQL_DST_PASS" "$MYSQL_DST_BASE"
...
time sed -i "
  s|\(define('DOMAIN_NAME',[[:space:]]*'\)[^']*\(');\)|\1https://$DOMAIN/\2|
  s|\(define('DIR_ROOT',[[:space:]]*'\)[^']*\(');\)|\1/var/www/$DOMAIN/web/\2|
  s|\(define('DIR_SYS_ROOT',[[:space:]]*'\)[^']*\(');\)|\1/usr/share/php/tvs/v5/\2|
  s|\(define('DB_HOSTNAME',[[:space:]]*'\)[^']*\(');\)|\1$MYSQL_DST_HOST\2|
  s|\(define('DB_USERNAME',[[:space:]]*'\)[^']*\(');\)|\1$MYSQL_DST_USER\2|
  s|\(define('DB_PASSWORD',[[:space:]]*'\)[^']*\(');\)|\1$MYSQL_DST_PASS\2|
  s|\(define('DB_DATABASE',[[:space:]]*'\)[^']*\(');\)|\1$MYSQL_DST_BASE\2|
" "$SITE_DOMAIN_WEBROOT/web/config.php" "$SITE_DOMAIN_WEBROOT/web/admin/config.php"

This solution isn’t for everyone, but if you’re facing a one-time mass migration, it can save a huge amount of time — and your sanity.

Human Logic, AI Syntax... Note on Content: I'm a Systems Engineer, not a native English writer. To ensure my technical ideas are clear and accessible, I use AI tools to polish the grammar and style. The workflow is simple: I provide the logic, the code, and the real-world experience. The AI handles the "English-to-Human" translation layer. If you find a bug, that's on me. If you find a perfectly placed comma, that's probably the AI.

Comments

Popular posts from this blog

FreeRadius with Google Workspace LDAP

Fixing pssh (parallel-ssh) Problems on Debian 10 with Python 3.7