Reinstalling a Server That Has Customers On It
INSTALL.sh was written for an empty machine. Running it again on a server
that already has hosting accounts used to be unsafe: it regenerated the MySQL
root password, copied the stock configuration over the operator's own,
rm -rf'd directories the running server had written to, and — because it saw
its own panel holding port 2083 — usually refused to start at all.
It is now safe to point at a server that matters, and it will say what it is about to do before it does it.
The three ways to run it
bash INSTALL.sh --dry-run # report what would change; change nothing
bash INSTALL.sh --adopt # update an existing installation in place
bash INSTALL.sh --reinstall # re-run every step, keeping the data
Running plain bash INSTALL.sh on a machine that already has HiTechCloud on it
now stops with an error that names those three, rather than treating the
machine as empty.
Other flags:
| Flag | What it does |
|---|---|
--force | Proceed past the refusals: a checkout with local changes, a version downgrade, a MySQL root password that no longer authenticates. Read what is being refused first. |
--no-backup | Skip the pre-flight backup. On a server with customer data this is not advisable and the run says so. |
Start with the dry run
bash INSTALL.sh --dry-run
It changes nothing at all — no packages, no services, no files. It reports the version, account, site and database counts it found, every configuration file a real run would create or rewrite, and a unified diff of each one it would replace. That report is what makes it reasonable to point the installer at a production server.
The pre-flight backup
Before an --adopt or --reinstall writes anything, it runs
deploy/adopt-server.sh, which copies the three things a reinstall cannot
regenerate:
- the panel database — accounts, domains, sites and plans, as a compressed SQL dump. Everything else on the server is derived from it; it is derived from nothing.
/etc/hitechcloud— credentials, the per-site records undernative/sites, the issued certificates, DNS zone files, and the operator's ownhitechcloud.config.- the account list — every hosting account with its uid and gid. An account recreated with a different uid leaves every file in its home directory owned by nobody, which is the failure here that is hardest to undo.
It lands in /var/backups/hitechcloud/preflight-<timestamp>/, the path is
printed, and /var/backups/hitechcloud/latest points at the most recent one.
Set HITECHCLOUD_BACKUP_ROOT to put it somewhere else.
Customer content — /home, the customers' own MySQL databases, the mail
spool — is deliberately not copied: it is far too large for a pre-flight
tarball, and nothing in the installer writes to any of it. What happens instead
is that it gets counted, before and after.
Nothing was lost
The backup includes a manifest.env with the account, site, domain, database,
zone, certificate and home-directory counts as they were before the run. At the
end, the installer re-counts and compares:
bash deploy/adopt-server.sh --compare /var/backups/hitechcloud/latest/manifest.env
A count that went up is somebody using the server while you worked. A count that went down fails the run and names what went missing, so "nothing was lost" is something you can check rather than something you have to assume.
You can run adopt-server.sh on its own at any time:
bash deploy/adopt-server.sh --survey # report only
bash deploy/adopt-server.sh --backup # take a backup, print its path
Running it on the demo server
The demo server is redeployed through CI/CD, not from a shell on the box. In
GitHub Actions, run the Deploy to demo workflow with workflow_dispatch
and pick a mode:
| Mode | What happens |
|---|---|
deploy | The normal path, and what every push to main does: build here, copy there, re-render the sites, restart. It does not run INSTALL.sh. |
dry-run | Surveys the server and runs INSTALL.sh --dry-run on it. Changes nothing. The report goes into the run summary. |
reinstall | The data-safe INSTALL.sh path: pre-flight backup, every step idempotent, then the count comparison. The backup path appears in the run summary. |
Run dry-run first and read its summary. Then run reinstall.
Restoring
Everything the run replaced is in the backup directory, under replaced/ with
its original path. To go further back:
cd /var/backups/hitechcloud/latest
gzip -dc panel.sql.gz | mysql # the panel database
tar -C / -xzf etc-hitechcloud.tar.gz # /etc/hitechcloud
systemctl restart hitechcloud hitechadmin
What changed, and why
Each of these was a step that assumed the machine was empty.
| Step | Before | Now |
|---|---|---|
| Port check | 2083 and 2087 held by the panel itself counted as a conflict, so the installer refused to run on any working server | Ports held by hitechcloud and hitechadmin are treated as ours |
| Repository update | git reset --hard discarded local changes in /opt/hitechcloud | Stops and names the changed files unless --force |
| Credentials | A missing credentials.env meant a new MySQL root password on a live database | The working password is recovered from the option files; if none works, the run refuses |
| MySQL root password | ALTER USER ... || true, then the new password was written to the client files whether or not it had worked | Only set when it is not already in force, and proven to authenticate before anything that depends on it is written |
/root/.my.cnf, mysql.cnf | Rewritten every run | Written only when the content differs; a socket-authenticated root no longer gets a password line that breaks it |
| Feature sets, stock config | cp -a over the operator's files | Copied through a comparison, with the previous content kept |
hitechcloud.config | Carefully preserved, then overwritten a few lines later by the shipped copy | Excluded from both copies |
services.json | The shipped copy replaced the one generated from this host's units, putting every admin Services row back to "Inactive" | Excluded |
hitechcloudcli | rm -rf took the generated aliases.txt and the ImunifyAV scripts with it | rsync --delete with the same exclusions the deploy pipeline uses |
panel.env, admin.env | Rewritten wholesale, deleting anything an operator had added | Merged: our keys are set, everything else is kept |
| systemd units | Overwritten every run | Written only when they differ, previous copy kept |
| Panel binary | The mirror's "latest" installed over whatever was running, downgrade included | Staged, questioned, and refused if it is older than the running build |
| Service restart | enable --now does nothing to an already-running service, so a new binary was never picked up | Restarted when the binary on disk actually changed |
| Redis | Restarted on every run, dropping every panel session | Restarted only when its configuration changed |
| Administrator account | A missing sqlite3 made the count read 0 and added a second administrator | Refuses to guess when the table cannot be read |
| Hosting account | Checked /etc/passwd only | Checks the panel's users table too, so a half-created account is reported rather than duplicated |
| Multi-WebServer Hosting | A failure was a warning | Fatal on a server with websites — a site whose configuration did not regenerate is a site that is down |
| Web configuration | Nothing checked it afterwards | Both nginx configurations are tested; if either is rejected the previous set is restored from a snapshot and the run stops |
What it still will not do
- It does not migrate the panel database schema. It loads the schema when there is none, and leaves an existing one alone.
- It does not back up
/homeor the customers' own databases. Usehitechcloudcli user-backupfor those. --forceturns off every refusal listed above. There is a reason for each of them; read the message before you reach for it.