All documentation
  1. Home
  2. Docs
  3. From a Linux server

A Linux server, over SSH

No panel on the source and nothing installed on it: KLYRN reads the machine with the tools it already has, copies a document root and its databases while the site keeps serving, and takes the difference at cut-over. Read what to expect first.

What you need before you start

An ordinary SSH login
A normal shell is enough for one site. Root, or a login that can sudo without a password, is what a whole-server reading needs. A restricted shell fails the connection check with “The login worked but no command could be run.”
A password or a private key
Paste the whole PEM block. Encrypted private keys are not supported yet.
The document root
Absolute, or relative to the SSH user's home. public_html is the default. It cannot be the home directory itself, and that is refused rather than guessed around.
The website's domain name
Required: this route has no panel to ask what the site is called.
Database credentials, usually none
KLYRN reads wp-config.php and .env under the document root, so a WordPress or Laravel site normally needs nothing typed. Give them by hand when it cannot: --db name:user:password[@host[:port]], repeatable.
Room here
A migration refuses to start with “less than 1 GB free on this server; free space before migrating”.
# One site and its databases, with the two-phase engine
klyrn import ssh --host old.example.com --user deploy --key ~/.ssh/id_ed25519 \
  --mode files --domain example.com --path public_html

# Credentials given rather than discovered
klyrn import ssh --host old.example.com --user deploy --password '…' \
  --mode files --domain example.com --db acme_wp:acme_wp:secret

# Choose the strategy yourself instead of letting it be measured
klyrn import ssh … --transfer rsync --compress zstd-3

--transfer takes auto, rsync, tar or manifest; --compress takes auto, none, zstd[-N] or gzip[-N]; --port defaults to 22 and --fingerprint SHA256:… pins the source's host key on a first connection. --engine legacy is the older path: one tar stream into a work directory, rather than two passes straight into the site.

Reading the whole machine first is not in this build. 0.3.5‑beta.1 migrates one website at a time, from the CLI or from Migrate in the panel, which asks for a single host and a single document root. A Migration Center that reads a plain Linux server is in the next release and is not published yet. It lists the web server's own loaded configuration, the PHP versions installed, the databases the server admits to, the certificates on disk and the crontabs. Until it ships, walk the source yourself and migrate a site at a time.

What migrates

The document root
Everything under it. Files land owned by the destination account, setuid and setgid bits are stripped, and symlinks are copied as links and never followed: the number pointing outside the tree is counted and reported.
The databases the site actually uses
Found in wp-config.php and .env under the document root (and one level above it, where WordPress and Laravel both allow them), or given with --db. Each dump is --single-transaction --quick --skip-lock-tables --default-character-set=utf8mb4 --routines --triggers --events, streamed through a compressor into a file here and then fed to the database.
The application's own credentials
The database user is recreated with the same password, so the configuration file that came across keeps working without being edited. A WordPress wp-config.php is re-pointed at the new database name and the original is kept as wp-config.php.klyrn-import.bak, which later passes will not overwrite.
The crontab of the login you connected with
Read with crontab -l, rewritten to run as the new account's user, and re-read at the final sync so a job added since the pre-copy is not lost. Root's crontab is not read (it belongs to the machine, not to an account), so connect as the site's own user if its scheduled tasks are to come across.
The shape of the site
WordPress, Laravel, plain PHP or static, decided by what is in the document root rather than by asking.
The PHP version
Asked of the source's own php binary, and used only when the answer looks like a version. Otherwise the site is created on this server's default.

The strategy is measured, not assumed, and the reasons are recorded on the migration. rsync on both ends means files already here are skipped and a half-copied file resumes where it stopped. Without rsync on the source, a size-and-mtime manifest from the source's own find is compared with a walk here and only the difference travels, as one tar stream, which needs nothing on the source but find and tar. Compression is chosen from a measured link: over roughly 200 MB/s a compressor cannot keep up and none is used; below that the level rises as the link gets slower, and it is capped when either side has fewer than three threads. An rsync copy runs at lower CPU and IO priority on this server, because this server has other tenants too.

What does not migrate

  • Mail. KLYRN has no mail stack. If the machine runs one, the reading says so: “This machine runs a mail server. KLYRN has no mail stack, so mailboxes and mail routing are not migrated and must be dealt with separately.” Your MX records are not touched.
  • Anything outside the document root. A generic source has no manifest of an account, so what is copied is the tree you named and nothing else. Code that lives above it (a Laravel application whose public/ is the document root, for instance) is not carried by naming public/ alone.
  • The web server's configuration. nginx or Apache virtual hosts are read to find sites; they are not translated. KLYRN generates its own. .htaccess is a separate, explicit step: every directive is classified as converted, needs review, or not possible, and nothing is silently dropped.
  • Certificates. Existing certificates on the source are listed in the reading and not copied. KLYRN issues its own once the name resolves here.
  • DNS. KLYRN is not a nameserver. The import result ends with the exact records to change.
  • System-level cron. /etc/cron.d and other users' crontabs are listed by the whole-machine reading, with the file each line came from, and not imported. The crontab of the login you migrate with is.
  • Credentials the reading does not need. The whole-machine reading takes the database name out of wp-config.php and leaves the password where it is: the name is what makes the mapping, and KLYRN has no reason to hold the rest. A migration is different and does read it, because it has to dump the database, and it holds it in memory only.

The reading says what it did not look at. A machine with more than 250 virtual hosts has the first 250 examined in detail and the rest listed without their application or size, and the report says so. A configuration that could not be dumped, a database server that would not answer, a directory that could not be read with the login given: each is a line in the report rather than an absence. An inventory that quietly omits half a machine is worse than one that is visibly partial, because you plan against it.

What downtime to expect

Two passes. The pre-copy runs while the source serves its customers; nobody is waiting on it, and when it finishes the site is live here with nothing pointing at it. The final sync is the second pass, and its wall time is the downtime.

klyrn import apply <id>      # pass 1: the pre-copy. The source stays live.
klyrn import state <id>      # every phase, with its duration
klyrn import sync <id>       # pass 2: the delta. This is the downtime.

What decides its length:

  • How much changed since the pre-copy. rsync sends the difference and deletes what is gone on the source; the manifest strategy sends only the files whose size or mtime differ. The tar strategy has no delta at all, and warns at the second pass that it cannot remove files deleted on the source since.
  • The databases, in full. InnoDB has no safe incremental strategy without binlog access on the source, which a migration never has, so the final sync takes a fresh snapshot and re-applies it.
  • The link, measured before anything moves with 16 MiB of incompressible bytes over SSH.

Both numbers are recorded rather than predicted: klyrn import state prints the pre-copy and the final sync with their seconds, their bytes and their file counts, and the final line says (this is the downtime).

A WordPress source can be frozen for the final sync with klyrn import sync <id> --freeze, which writes a .maintenance file on the source and removes it afterwards, including when the pass fails. Anything else has no standard switch and the job warns that writes during the final sync may be lost. A MyISAM table cannot be in a transactional snapshot, and that is warned about by name at analysis time.

The checks, and what each refusal means

The connection check writes nothing (no migration row, no job, no history entry) and it identifies a machine by reading files that exist, never by running anything the source's applications own. A machine with no panel comes back as “a Linux server with no control panel”, with what it has: nginx or Apache, PHP, MySQL or MariaDB, and the operating system's own name for itself.

“no directory /home/deploy/public_html on the source”
Give the website's document root, absolute or relative to the SSH user's home. The hint says so, and nothing is guessed.
“the document root cannot be /home/deploy itself”
Copying a whole home directory into a website's public tree is how a private file becomes a public URL. Name the folder that holds the website.
“document root is empty on the source”
A warning on the plan, not a refusal, but it is almost always the wrong path.
“no database credentials found in wp-config.php or .env”
The site will be migrated without databases unless you add them with --db. Said at analysis time, before anything moves.
“access denied for acme_wp@localhost; check the credentials” / “no database named acme_wp”
The database is sized during the analysis, so a credential that does not work is found before the copy rather than during it.
“mysqldump is not available on the source”
The files can still be migrated; the databases need a source that can dump them.
“The login worked but no command could be run.”
A restricted shell. A migration needs to run read-only commands, so use a login that has a normal one.
“The database server would not answer with this login”
In a whole-machine reading. The files can still be copied; the databases need a login that can read them.
“nginx is installed but its configuration could not be dumped”
Usually because the login is not root. The reading uses nginx -T, which is the running configuration with every include resolved, and the includes an operator forgot about are exactly the sites that go missing in a migration.

Pin the source's host key with --fingerprint SHA256:…, or in the connection form. A key that does not match stops the connection during the handshake, before the password is offered, and the refusal names both fingerprints. On 0.3.5‑beta.1 an unpinned connection trusts whatever key answers: recording the key on the first connection, refusing it when it changes, approving it, and the page that lists the record are built and arrive in a later beta.

Two invariants refuse rather than half-work, and both stop before a byte is written. The tree a pass copies into has to be inside the home of the account whose ownership it stamps on the files, and a migration will not write into a site that belongs to a different account: “example.com already exists under account other, and this migration is for acme”. Break either and files land in one tenant's home owned by another.

The cut-over

  1. Pre-copy and look at the siteklyrn import apply <id>. The site is live here; nothing points at it. Use the curl --resolve and hosts-file lines from the report to see it as a visitor will.
  2. Fix what the verification namesEleven checks against this server. It does not connect to the source and cannot tell you the two pages render the same: a blank page returns HTTP 200, so compare them yourself.
  3. Lower the TTLHours before the cut-over. The checklist warns when a record's TTL is over 300 s, and says for how long visitors may keep reaching the old server afterwards.
  4. Run the final syncklyrn import sync <id>, with --freeze if the source is WordPress. This pass is the downtime.
  5. Read the cut-over checklistklyrn import cutover <id>: files, databases, runtime, HTTP, SSL, cron, DNS and the TTL, each with evidence, then the records to set. It will not call itself ready while files, databases, runtime or HTTP say action required.
  6. Change the recordsBy hand, or --apply-dns with a connected provider. Applying DNS before the final sync is refused: it would send visitors to the pre-copy.
  7. WatchEvery 30 seconds for 15 minutes by default, 60 at most: the page as a visitor gets it, new 5xx lines in the access log, the certificate and each database. It reports; it never rolls DNS back.
  8. Keep the old server for a weekLong enough for every cached record to expire and for anything you missed to surface while you can still look it up.

Mail, if the old machine carries it. KLYRN runs no mail server and never changes an MX record. If MX points at the machine you are migrating, leave that machine running and leave MX alone, or move the mailboxes to a mail provider before the websites move. Pointing MX here would mean every message refused.

How to roll back

Nothing is installed on the source and nothing is deleted from it. KLYRN connects, runs read-only commands the machine already has, and reads. The one thing it ever writes is the WordPress maintenance file you asked for with --freeze, and that is removed again even when the pass fails. The old server is exactly as it was, and still serving.

  1. Point DNS backThe old machine has every file and every row it had before.
  2. Take the site out of maintenance if a frozen sync stopped badlyDelete .maintenance in the source document root. KLYRN removes it on its way out of a failed pass, and a link that dropped at the wrong moment can leave it behind.
  3. Remove what landed here when you are readyklyrn import delete <id> removes the migration record and its working files, and refuses while a job for it is still running. The site and databases it created are deleted separately.

Troubleshooting

The pass stopped part way
klyrn import resume <id> continues from the phase it stopped in. Files already here are not sent again; with the manifest strategy only what is still missing goes.
“the SSH login for … is no longer held (klyrn-core restarted)”
Credentials are held in memory for the life of the migration and never written to the database. Give them again with --password or --key on resume or sync.
“no password is held for acme_wp@localhost”
A password KLYRN discovered rather than one you typed. It re-reads the file the analysis found it in when it can: the same path, and only when the credential names the same database. When it cannot, the message carries the command: klyrn import resume <id> --db acme_wp:acme_wp:PASSWORD.
“the dump ended early (no ‘Dump completed’ trailer)”
The compressor's exit status hides mysqldump's, so the dump is checked for its own closing line before it is imported. A truncated dump is a failed database, reported as one, and retried on its own.
“3 of 7 database(s) failed; the others are in place”
Each database has its own record and its own retry. Resuming copies only the ones that failed.
“rsync is not on both ends; using the manifest strategy”
A warning, not a failure. The delta and the confinement are the same; what is lost is resume within a single very large file.
“zstd is not on both ends; using gzip -1”
Also only a warning. The choice is recorded on the migration with its reason.
“some files changed while they were being read”
tar's exit code 1 on a live source. The copy is complete otherwise. It is one of the reasons the final sync exists.
The source dropped the connection during the handshake
Usually sshd's MaxStartups under load. KLYRN waits 1 s, 3 s and 7 s and tries again before giving up, then says which it was.
The site's PHP files are served as text
The runtime check catches exactly this. It is what the verification's Runtime row is for.

All the routes in one place: Migration. What “verified” proves: What to expect.