Server Migration Runbook
A copy-pasteable checklist for moving an already-installed application to a new server without losing encrypted settings or 2FA data. Read docs/encryption.md first if any step here is unfamiliar.
The one rule that matters most: carry the existing .env file, and do not run php artisan key:generate on the target server.
Before you start
- [ ] Take a full database backup on the source server.
- [ ] Confirm you have a copy of the source server's
.envfile, in full — not a regenerated one, not a.env.examplefilled in from memory. - [ ] Schedule a maintenance window if the source app has active traffic.
1. Copy the environment file
- [ ] Copy the source server's
.envfile to the target server verbatim, including both:APP_KEYDATA_ENCRYPTION_KEYandDATA_ENCRYPTION_PREVIOUS_KEYS(even ifDATA_ENCRYPTION_KEYis empty — that emptiness is itself meaningful and must be carried, not "fixed")
- [ ] Do not create a fresh
.envfrom.env.exampleand paste values in by hand. Copy the actual file.
2. Do NOT run these on the target server
- [ ] Do not run
php artisan key:generate. It overwritesAPP_KEY. IfDATA_ENCRYPTION_KEYis empty on this install,APP_KEYIS the primary data-encryption key — overwriting it makes every encrypted setting and every user's 2FA secret permanently unreadable, with no error at the point of failure. - [ ] Do not run
php artisan encryption:keyon the target server as part of this migration. That command generates a new key; it is for rotation, not migration. Migration carries the existing key across, unchanged. - [ ] Do not run
migrate:fresh,migrate:refresh,migrate:reset, ordb:wipeon the target server against data you intend to keep.
3. Deploy the application code and run migrations
- [ ] Deploy the codebase (same version as source, or your intended upgrade target).
- [ ] Install dependencies (
composer install --no-dev,npm ci && npm run buildas applicable to your deploy process). - [ ] Run
php artisan migrate(schema only — no encryption keys are touched by migrations). - [ ] Restore the database backup from step "Before you start" if this is a fresh database on the target server, rather than a replicated/streamed one.
4. Verify BEFORE serving traffic
Do not point DNS/load balancer traffic at the target server until both of these pass:
-
[ ]
php artisan encryption:healthExpect
safe-to-clear(exit 0) if the source server had already runencryption:rekey/cleared its previous-keys list, orrekey-required(exit 1) if it had pending previous keys — both are fine at this stage, they mean data is readable.unreadable(exit 2) orkey-error(exit 2) means STOP — do not serve traffic; see the recovery section below. -
[ ]
php artisan sk:doctorConfirm no failing checks, in particular any encryption-related check (
Data Encryption Key). A warning may be acceptable depending on your environment; a failure is not. -
[ ] Spot-check manually: open the admin panel's Settings screen and confirm a previously-configured sensitive value (e.g. mail password, a configured storage secret) still shows as configured rather than empty. Log in as a user with 2FA enabled and confirm the 2FA challenge still accepts a code.
Only after both commands pass and the spot-check succeeds, cut traffic over to the target server.
5. After cutover
- [ ] Keep the source server's database and
.envavailable, untouched, until you are confident the target server is stable (rollback path). - [ ] If you plan to rotate the data encryption key on the new server, do it as a separate, later, deliberate operation — see
docs/encryption.md→ "Rotating an already-adopted key". Do not combine key rotation with the server migration itself; verify the migration succeeded first.
Recovery: the old key is genuinely gone
If .env was not carried correctly and the key that encrypted existing data (APP_KEY and/or DATA_ENCRYPTION_KEY, whichever was primary on the source server) is truly lost — not backed up anywhere, not recoverable from any copy of the old .env — then:
- No command in this kit can recover that data.
encryption:healthwill reportunreadableorkey-errorand stay that way; there is no repair path. - What is unrecoverable:
- Every sensitive setting encrypted under the lost key (
mail.password,storage.spaces_secret,storage.aws_secret,turnstile.secret_key,postman.api_key,apidog.access_token) — these must be re-entered by hand in the Settings screen once the target server is otherwise healthy. - Every user's 2FA secret and recovery codes encrypted under the lost key — each affected user must disable and re-enrol two-factor authentication. They cannot self-recover through the existing challenge flow if the secret itself cannot be decrypted.
- Every sensitive setting encrypted under the lost key (
- What is not affected: anything encrypted under a key you do still have (e.g. if only
DATA_ENCRYPTION_PREVIOUS_KEYSwas lost but the current primary key was carried correctly, only rows still on the old key are affected — runencryption:healthto see exactly which). - Do not attempt to work around this by clearing
DATA_ENCRYPTION_PREVIOUS_KEYSor regeneratingAPP_KEY/DATA_ENCRYPTION_KEY"to make the error go away" — that does not restore the data and forecloses any chance of finding the real key later.
Placeholder values used in this document
Any key shown in this document or docs/encryption.md (e.g. in an .env.example block) is an obviously fake placeholder such as base64:REPLACE_ME_WITH_A_GENERATED_KEY= — never a real key. Never paste a real APP_KEY or DATA_ENCRYPTION_KEY value into a ticket, chat, or commit message either; treat both the same way you treat any other secret.
See also
docs/encryption.md— what is encrypted, the key-resolution contract, adoption and rotation procedures.docs/artisan-commands.md— full command reference includingsk:doctor.