Migrating a Magento Store to a VPS: Orders, Stock & Reindexing
WordPress migrations are mostly copy-paste-and-pray. Magento migrations punish that approach. A live Magento store has search indexes, a crypt key tied to every encrypted field in the database, and a cron-driven queue that quietly breaks if it goes stale for even a day. Move the files and database without handling those three things and you'll have a store that loads fine, takes orders fine, and then silently fails to send order emails or show correct stock. Here's how to move a live Magento store to a VPS without any of that happening.
Why a Magento Move Is Not "Just Another WordPress Migration"
If you've migrated WordPress or WooCommerce before, the instinct is to zip the files, export the database, import it, and repoint DNS. That gets a Magento store maybe 60% of the way there. The rest trips people up because Magento keeps state outside the obvious places:
- The crypt key. Magento encrypts payment tokens, some customer data, and API credentials using a key stored in
app/etc/env.php. Copy the database without the matchingenv.php, and every encrypted value becomes unreadable garbage - saved cards stop working, some admin settings blank out. - Search indexes live outside MySQL. Since Magento 2.4, product search runs on Elasticsearch or OpenSearch, not MySQL full-text search. If the new server doesn't have a matching search engine configured, your storefront search returns nothing - the site looks broken even though the database imported cleanly.
- Generated code and static assets aren't meant to be copied. The
var/generation,var/cache, andpub/staticfolders are build output, not source. Copying them from an old PHP version or Magento build onto a new server is a common cause of white screens after migration - regenerate them instead.
Before You Start: The Checklist
- Confirm the target VPS has a PHP version Magento actually supports for your version (Magento 2.4.6+ needs PHP 8.1-8.3) and the required extensions:
bcmath,intl,soap,sodium,xsl. - Have Elasticsearch or OpenSearch installed and reachable on the new server, matching the version your Magento release requires.
- Copy
app/etc/env.phpfrom the old server - don't regenerate it. The crypt key inside it must match the one used to encrypt existing data. - Note your last order ID, current stock counts for top SKUs, and active payment gateway credentials (Razorpay, PayU, Stripe, etc.) for a post-migration sanity check.
- Composer access on the new server so you can reinstall dependencies cleanly instead of copying
vendor/wholesale.
Step 1: Freeze the Store
Put the old store into maintenance mode for the final sync window so nothing changes mid-transfer:
php bin/magento maintenance:enable
Note the last order ID and current stock levels for your top SKUs before you go further - this is your reconciliation checkpoint once the new server is live.
Step 2: Move Code the Right Way
Don't rsync the whole document root. Pull the source from version control (or the deployed archive) and skip build artifacts entirely:
rsync -avz --exclude 'var/' --exclude 'generated/' --exclude 'pub/static/' \
--exclude 'vendor/' /path/to/old/site/ user@new-vps:/path/to/new/site/
Media - product images, uploaded files under pub/media - is real data, not build output, so sync that folder in full:
rsync -avz /path/to/old/site/pub/media/ user@new-vps:/path/to/new/site/pub/media/
Then, on the new server:
composer install --no-dev
Step 3: Migrate the Database
Magento's database is bigger and slower to move than a typical WordPress one, mostly because of the EAV (Entity-Attribute-Value) tables that store product data as thousands of narrow rows instead of a few wide ones. Dump with compression, and exclude log tables you don't need:
mysqldump -u dbuser -p --single-transaction \
--ignore-table=magento.customer_log \
--ignore-table=magento.report_event \
magento | gzip > magento-db.sql.gz
On the new server, create the database and user first, then import:
gunzip < magento-db.sql.gz | mysql -u dbuser -p magento
For very large catalogs (50k+ products), this import can take well over an hour. Run it inside screen or tmux so a dropped SSH session doesn't kill it halfway through.
Step 4: Point the App at Its New Home
Copy the original app/etc/env.php into place (keep that crypt key), then update the base URLs to match the new environment - either directly in core_config_data via the CLI:
php bin/magento setup:store-config:set --base-url="https://yourdomain.com/"
php bin/magento setup:store-config:set --base-url-secure="https://yourdomain.com/"
Run the upgrade script to apply any schema/data patches, then set production mode:
php bin/magento setup:upgrade
php bin/magento deploy:mode:set production
Finally, re-register cron - Magento's own queue for order emails, indexers, and scheduled price rules depends on it, and it does not survive a file copy:
php bin/magento cron:install
Step 5: Rebuild Indexes and Cache Before You Cut Over
This is the step people skip, and it's the one that causes "the store works but search/stock/prices are wrong" reports the next morning. Indexes are derived data - they don't travel with a database dump the way you'd expect, and stale ones serve stale storefront data.
php bin/magento indexer:reindex
php bin/magento cache:flush
Check indexer status before you consider the migration done:
php bin/magento indexer:status
Every row should read Ready, not Reindex required. If Elasticsearch/OpenSearch isn't reachable yet, the catalog search indexer will fail silently here - fix that connection before moving on, not after customers start complaining that search returns nothing.
Step 6: DNS Cutover and the First 30 Minutes After
Lower your domain's DNS TTL to 300 seconds a day or two before the cutover so the switch propagates fast. Once the new server is verified working end-to-end, point the A record at the new VPS and take the old store out of maintenance mode only after DNS has fully moved - keeping both live briefly with the old server in maintenance mode prevents orders splitting between two databases.
For the first half hour after cutover, place a test order yourself with a real (small) payment, and confirm: the order appears in the admin panel, the confirmation email sends, stock decrements, and the payment gateway's dashboard shows the transaction matching the order ID in Magento.
Common Migration Failures
| Symptom | Cause | Fix |
|---|---|---|
| Checkout completes but no confirmation email | Cron wasn't re-registered on the new server | Run php bin/magento cron:install and verify with crontab -l |
| Product images 404 on the frontend | pub/media wasn't synced, or permissions are wrong | Re-sync the media folder; chown -R www-data:www-data pub/media |
| "Invalid Form Key" on every form submit | Session/cookie domain mismatch after the URL change | Clear var/session and browser cookies; confirm base URLs match the live domain exactly |
| Storefront search returns nothing | Elasticsearch/OpenSearch not installed or unreachable on the new server | Install the matching search engine version, set it in Stores > Configuration > Catalog Search, then reindex |
| Saved cards/API keys fail after login | env.php was regenerated instead of copied, so the crypt key changed | Restore the original env.php from the old server; a mismatched key can't be recovered after the fact |
Prevention: Keep the Next Migration Boring
- Keep a documented copy of
env.php's crypt key in your password manager, separate from routine backups, so a future move never depends on finding the right old server. - Schedule a full
mysqldumpand media backup nightly via cron, not just relying on hosting-panel snapshots - a plain SQL dump is what actually restores cleanly onto a different server. - Pin your PHP, Elasticsearch/OpenSearch, and Magento versions in a short README in the repo, so the next migration starts from a known-good target instead of guesswork.
- Test the full indexer and cron setup on a staging copy before every major migration - reindexing failures are much cheaper to catch there than on the live store.
Frequently asked questions
Can I just copy the vendor and generated folders from the old server instead of rebuilding them?
You can, but it's risky if the PHP version or Magento patch level differs even slightly between servers. Running composer install --no-dev and letting Magento regenerate var/generation and pub/static avoids mismatched compiled code that causes white screens after migration.
Do I need Elasticsearch or OpenSearch on the new VPS before I import the database?
Yes. Since Magento 2.4, catalog and storefront search depend on Elasticsearch or OpenSearch rather than MySQL. If it's not installed and configured under Stores > Configuration > Catalog Search before you reindex, search will silently return no results even though the rest of the store works.
Why did my saved customer data or API keys break after migration?
Almost always because app/etc/env.php was regenerated on the new server instead of copied from the old one. Magento encrypts sensitive fields with the crypt key stored in that file - a new key can't decrypt data that was encrypted with the old one, and there's no way to recover it after the fact.
How long should I keep the old server running after cutover?
At least a few days. Keep it in maintenance mode (not powered off) so you can pull the original env.php, media files, or a fresh database dump if you spot a discrepancy during reconciliation. Lowering DNS TTL to 300 seconds beforehand keeps the actual cutover window short.
My storefront loads but stock counts and prices look outdated - what's wrong?
Indexes are derived data that don't travel with a database dump. Run php bin/magento indexer:reindex followed by php bin/magento cache:flush, then confirm with php bin/magento indexer:status that every indexer shows Ready rather than Reindex required.