Slot 42 Field fix

DirectAdmin cPanel Import: Why Your Addon Domains Vanish

A DirectAdmin cPanel import reports success, the primary domain works, and every addon domain is gone with no error. The cause is one line in the user file.

The import finishes. DirectAdmin says the account restored. The primary domain loads, the databases are there, the mailboxes are there, and every addon domain on the account is gone. Not broken, not misconfigured. Gone. There is no error in the interface, nothing in errortaskq.log, and nothing in the restore summary to suggest anything was skipped.

The cause is a single value in the cPanel user file, and once you know to look for it the fix takes two minutes.

The symptom, precisely

You will see all of these together:

  • The restore completes with no warning or error.
  • The account exists and the primary domain resolves and serves.
  • /usr/local/directadmin/data/users/<user>/domains.list contains one line.
  • The addon domains have no directory under /home/<user>/domains/.
  • The files for those sites are still present, sitting inside the primary domain’s document root where cPanel kept them.

That last point matters. The data came across. Only the domain objects were never created.

The cause: MAXADDON

cPanel stores per-account limits in a plain key-value file at /var/cpanel/users/<user>. One of those keys is MAXADDON, the addon domain limit for the account. A copy of that same file travels inside the move archive at cp/<user>, and that copy is what DirectAdmin reads during the restore.

If MAXADDON is missing, or set to 0, DirectAdmin treats the account as not permitted to have addon domains and skips the domain-creation step entirely. It does not warn you, because from its point of view nothing went wrong: it honoured the limit it was given.

Accounts end up in this state often enough that it is now the first thing I check. A package created before addon domains were enabled, an account hand-edited years ago, a reseller plan where the limit was never set, or a pkgacct run against an account whose package was deleted. Any of those leave you with a 0 or an absent key.

Diagnosing it before you import

Check the source first. This is the whole diagnosis:

grep -E '^(MAXADDON|MAXSUB|MAXPARK|PLAN)=' /var/cpanel/users/olduser

Healthy output looks like MAXADDON=unlimited or MAXADDON=10. If you get MAXADDON=0, or the line is not printed at all, you have found it.

If you only have the archive, look inside it. You do not need to extract the whole thing:

tar -xzf cpmove-olduser.tar.gz -O cpmove-olduser/cp/olduser | grep -E '^MAX'

Now compare that against the domains the account actually has. The archive carries a userdata/main file listing every domain by type:

tar -xzf cpmove-olduser.tar.gz -O cpmove-olduser/userdata/main

That file is YAML and reads roughly like this:

main_domain: example.com
addon_domains:
  secondsite.com: addon.example.com
  thirdsite.net: third.example.com
parked_domains: []
sub_domains:
  - addon.example.com
  - third.example.com
  - mail.example.com

Two addon domains listed, MAXADDON=0 in cp/olduser. That is the mismatch, and it tells you exactly how many domains are about to disappear.

You can also list the per-domain userdata files, which is a useful cross-check when main looks incomplete:

tar -tzf cpmove-olduser.tar.gz | grep 'userdata/' | grep -v 'cache\|_SSL'

Fix A: correct it on the source, then re-export

This is the clean version and the one I use when the cPanel server is still available.

# raise the limit on the account
whmapi1 modifyacct user=olduser MAXADDON=unlimited

# confirm it landed in the user file
grep '^MAXADDON=' /var/cpanel/users/olduser

# rebuild cPanel's domain mapping, then re-export
/scripts/updateuserdomains
/scripts/pkgacct olduser /home/backups

Use a real number instead of unlimited if you are moving to a plan that caps addon domains. What matters is that the value is greater than the number of addon domains on the account.

Fix B: patch the archive and repack

When the source is already decommissioned, or you are working from an archive somebody handed you, edit the file in place inside the tarball.

The critical detail is that the top-level directory name inside the archive must stay cpmove-<user>, and the final file must stay cpmove-<user>.tar.gz. DirectAdmin keys on both. Get either wrong and the import will not be recognised as a cPanel move file at all.

cd /home/admin/admin_backups

# 1. extract, preserving the top-level directory
tar -xzf cpmove-olduser.tar.gz

# 2. patch the user file
grep -q '^MAXADDON=' cpmove-olduser/cp/olduser \
  && sed -i 's/^MAXADDON=.*/MAXADDON=unlimited/' cpmove-olduser/cp/olduser \
  || echo 'MAXADDON=unlimited' >> cpmove-olduser/cp/olduser

# 3. verify
grep '^MAXADDON=' cpmove-olduser/cp/olduser

# 4. repack under the original name
mv cpmove-olduser.tar.gz cpmove-olduser.tar.gz.orig
tar -czf cpmove-olduser.tar.gz cpmove-olduser
rm -rf cpmove-olduser

# 5. confirm the structure survived
tar -tzf cpmove-olduser.tar.gz | head -5

Keep cpmove-olduser.tar.gz.orig until the import is verified. Repacking a large archive is the kind of step where a full disk truncates the output and you would rather still have the original.

Then remove the half-imported account if one exists, and re-run the restore. Watch it in the foreground:

/usr/local/directadmin/dataskq d200
tail -f /var/log/directadmin/errortaskq.log

Verifying the addon domains actually landed

Do not trust the interface alone. Check the three places DirectAdmin writes a domain:

# 1. the domain list for the account
cat /usr/local/directadmin/data/users/newuser/domains.list

# 2. per-domain config
ls /usr/local/directadmin/data/users/newuser/domains/

# 3. the generated Apache or Nginx vhosts
grep -l 'secondsite.com' /usr/local/directadmin/data/users/newuser/httpd.conf

And confirm the document roots exist with content in them:

ls -la /home/newuser/domains/secondsite.com/public_html | head

If the vhost is there but the site 404s, rebuild the web server configuration:

echo "action=rewrite&value=httpd" >> /usr/local/directadmin/data/task.queue
/usr/local/directadmin/dataskq d200

If you already imported without the fix

You do not have to start over. The files came across; only the domain objects are missing. Add each domain, then move its document root into place.

Create the domain from the command line through DirectAdmin’s API, using the account’s own login since domain creation is a user-level command:

curl -sk --user 'newuser:ACCOUNTPASSWORD' \
  'https://127.0.0.1:2222/CMD_API_DOMAIN' \
  -d action=create \
  -d domain=secondsite.com \
  -d php=ON -d cgi=ON -d ssl=ON \
  -d ubandwidth=unlimited -d uquota=unlimited

Then move the content. On cPanel the addon domain’s files lived inside the primary document root, typically at /home/<user>/public_html/<foldername>. After the import that path is under the primary domain:

SRC=/home/newuser/domains/example.com/public_html/secondsite
DST=/home/newuser/domains/secondsite.com/public_html

rsync -a "$SRC"/ "$DST"/
chown -R newuser:newuser "$DST"

rsync -a already carried the modes across, so ownership was the only thing wrong. Correct what is actually broken and normalise nothing: a blanket chmod 755 on directories reopens the 0700 ones somebody locked on purpose, and a blanket chmod 644 on files republishes a 0660 config to every account on the server.

If anything under the copy is group or world writable, take those bits off symbolically and leave the rest of each mode alone:

find "$DST" \( -type d -o -type f \) -perm /go=w -printf '%m %p\n' | head -50
find "$DST" \( -type d -o -type f \) -perm /go=w -exec chmod go-w {} +

find "$DST" -maxdepth 3 -type f \( -name '.env' -o -name 'wp-config.php' \) \
  -exec chmod 600 {} +

Leave the source directory alone until the site is verified, then remove it. If you do not, the same content stays reachable at example.com/secondsite/ and you have duplicate content problems on top of everything else.

Check each moved site for absolute paths that still point at the old layout:

grep -rIl "/home/olduser/public_html" "$DST" | head

Finally, reissue SSL for each domain you added, since the certificates in the archive covered the primary only:

cd /usr/local/directadmin/scripts
./letsencrypt.sh request_single secondsite.com 4096

FAQ

Why does DirectAdmin not report an error?

Because from its side no error occurred. It read a limit of zero addon domains from the user file and complied with it. The restore did what it was told; the instruction was wrong.

Does the same thing affect subdomains or parked domains?

The equivalent keys are MAXSUB and MAXPARK, and they behave the same way. Check all three at once when you are diagnosing, since an account with a zeroed MAXADDON frequently has the others unset too.

Can I just set MAXADDON on the DirectAdmin side after the import?

No. The value is consumed during the restore, so raising the account’s limit afterwards changes nothing about domains that were never created. You either fix it before the import or add the domains manually.

Is unlimited a valid value in the cPanel user file?

Yes. cPanel accepts unlimited as a value for these limit keys, and DirectAdmin reads it as no cap. A plain integer works equally well and is the better choice if you want a real limit on the destination.

How do I check this across every account before a bulk migration?

Loop the user files and print anything that would fail. Run it on the cPanel source before you generate a single archive:

for f in /var/cpanel/users/*; do
  u=$(basename "$f")
  m=$(grep -m1 '^MAXADDON=' "$f" | cut -d= -f2)
  [ -z "$m" ] || [ "$m" = "0" ] && echo "$u: MAXADDON='${m:-unset}'"
done

Where this fits

This is the kind of failure that makes people conclude cross-panel migration does not work, when it is one line in one file. It is a routine part of server migration work, and it belongs on the pre-flight list alongside the mail format and the archive naming rules. The rest of that list is in the cPanel to DirectAdmin gotchas post.