Skip to content

Magento to WooCommerce migration on cPanel

This guide shows how to plan and run a Magento to WooCommerce migration on a cPanel server, for Magento 2 stores and older Magento 1 sites. You will move products, variations, customers, images and order history, turn old Magento addresses into 301 redirects, and learn when staying on Magento is the better choice.

This guide is for store owners and administrators who want to move a Magento store to WooCommerce on a cPanel server without losing products, customers or search traffic. It covers Magento 2, sold as Adobe Commerce or free as Magento Open Source, and Magento 1 stores still running after its end of life in June 2020. The usual trigger is cost, because Magento needs specialist developers, heavy hosting and regular upgrade projects that many mid-sized stores cannot justify. The practical route is to restore a copy of the Magento database on the cPanel account, import from that copy into WooCommerce, and build redirects from Magento’s own URL table. By the end, WooCommerce serves the store, every old product and category address answers with a 301 redirect, and you have a plan for customer logins.

What you’ll accomplish

  • Decide whether WooCommerce fits your store, or whether Magento or Mage-OS is the better call.
  • Understand how Magento stores a product and where each piece lands in WooCommerce.
  • Restore a copy of the Magento database on cPanel, readable through a SELECT-only user.
  • Import products, variations, images, customers and past orders, checking every count.
  • Turn the Magento url_rewrite table into 301 redirects, switch DNS, and keep a rollback path ready.

Prerequisites

  • Shell access to the Magento server, or a database dump and a copy of pub/media.
  • A cPanel account with SSH access on the destination server, and WP-CLI, the command-line tool for WordPress. Check with wp --info.
  • The mysql client and the uapi command, which every cPanel server includes.
  • PHP 8.2 or 8.3 for the WordPress site, set per domain in cPanel’s MultiPHP Manager.
  • The pre-flight audit for a WooCommerce migration, done first, which scopes gateways, shipping, tax and email.
  • For Magento 1 stores, the ideas are the same but a few table names differ. Each step points them out.

How it works

Magento stores products with an EAV model, short for entity-attribute-value. Each product is a row in catalog_product_entity holding only its ID (entity_id), its SKU and its type. Everything else, such as the name or price, sits in value tables grouped by data type, like catalog_product_entity_varchar and catalog_product_entity_decimal. Each value row also carries a store_id, so a product can have a different name in each store view. A store view is Magento’s word for one language or storefront within a website.

WooCommerce is flatter. A product is a post in wp_posts, and its SKU, price and stock are rows in wp_postmeta. The diagram shows where each Magento concept lands.

Diagram mapping Magento products, configurable children, attributes, categories, store views, images, customers and orders to their WooCommerce equivalents

Three rows need care. A configurable product is Magento’s parent product, such as a hoodie, and each size and colour is a separate simple product linked to it in catalog_product_super_link. In WooCommerce this becomes a variable product with one variation per child. The options that pick a child, such as color and size, become global attributes, stored as taxonomies named like pa_color.

Passwords do not carry over. Magento 2 stores each customer’s password as a salted hash, a one-way fingerprint of the password, made with SHA-256 or Argon2id. WordPress cannot check either format, so plan a password reset for every customer. Orders come across as history, not as live payments. Your new gateway has no record of charges taken by Magento, so refunds for old orders stay in the old gateway’s dashboard. Before you start, read the last section of this guide to confirm WooCommerce is the right destination.

Step-by-step

  1. Read the database settings and count what you have. On the Magento server, from the Magento root folder, print the connection settings from app/etc/env.php:

    php -r '$c = include "app/etc/env.php"; print_r($c["db"]["connection"]["default"]); echo $c["db"]["table_prefix"], "\n";'

    Expected: an array showing [host], [dbname], [username] and [password], then the table prefix, which is usually an empty line. Magento 1 keeps these in app/etc/local.xml. Now take an inventory:

    mysql -u magento -p magento -e "SELECT type_id, COUNT(DISTINCT entity_id) AS products FROM catalog_product_entity GROUP BY type_id; SELECT store_id, code, website_id FROM store; SELECT COUNT(*) AS customers FROM customer_entity; SELECT COUNT(*) AS orders FROM sales_order;"

    Expected, on a mid-sized store:

    +--------------+----------+
    | type_id      | products |
    +--------------+----------+
    | configurable |      184 |
    | simple       |     2317 |
    +--------------+----------+
    +----------+---------+------------+
    | store_id | code    | website_id |
    +----------+---------+------------+
    |        0 | admin   |          0 |
    |        1 | default |          1 |
    +----------+---------+------------+

    Customer and order counts follow. DISTINCT matters on Adobe Commerce, where scheduled updates can store several rows per product. Store 0 holds default values, so this store has one real store view. On Magento 1 the orders table is sales_flat_order. Write every number down for the final checks.

  2. Copy the database to the cPanel account. On the Magento server, take a consistent dump without locking the live store:

    mysqldump --single-transaction --no-tablespaces -u magento -p magento | gzip > ~/magento-copy.sql.gz

    Copy the file to your cPanel home folder with scp, then create a database and restore it. cPanel database names start with the account name and an underscore, shown here as exmpl_:

    uapi Mysql create_database name=exmpl_mage
    gunzip < ~/magento-copy.sql.gz | mysql -u exmpl -p exmpl_mage

    Expected: status: 1 from uapi, then a password prompt and no other output. The dump holds customer addresses and password hashes, so keep it outside public_html and delete it when the project ends.

  3. Create a read-only database user. The importer only needs to read Magento data, and a user limited to SELECT cannot change anything by mistake:

    uapi Mysql create_user name=exmpl_mageread password='REPLACE_ME'
    uapi Mysql set_privileges_on_database user=exmpl_mageread database=exmpl_mage privileges=SELECT
    mysql -u exmpl_mageread -p exmpl_mage -e "DELETE FROM sales_order LIMIT 1;"

    Expected: status: 1 twice, then ERROR 1142 (42000): DELETE command denied to user. That error proves the limit works. If you ever read a live Magento database instead of a copy, this user is essential.

  4. Check the password hash format. Magento 2 writes each hash as hash:salt:version. The last part tells you the algorithm:

    mysql -u exmpl_mageread -p exmpl_mage -e "SELECT SUBSTRING_INDEX(password_hash, ':', -1) AS version, COUNT(*) AS customers FROM customer_entity GROUP BY version;"

    Expected: rows such as 1 for SHA-256 and 2 or a value starting with 3_ for Argon2id. WordPress cannot verify any of them, and the same goes for Magento 1’s salted MD5 or SHA-256 hashes. Step 8 covers the resets.

  5. Build WordPress and WooCommerce on a staging subdomain. In cPanel, create new.example.com with a document root of ~/new.example.com, point its DNS at this server, and create an empty database and user for WordPress. Then install:

    cd ~/new.example.com
    wp core download
    wp config create --dbname=exmpl_wp --dbuser=exmpl_wp --dbpass='REPLACE_ME'
    wp core install --url=https://new.example.com --title='Example Store' \
      --admin_user=admin --admin_email=you@example.com --skip-email
    wp plugin install woocommerce --activate
    wp rewrite structure '/%postname%/' --hard

    Expected: Success: WordPress installed successfully., then Success: Installed 1 of 1 plugins. and Success: Rewrite structure set. Settle the product and category bases in Settings > Permalinks now, because the redirect map depends on them. This guide uses the defaults, /product/ and /product-category/.

  6. Create the global attributes. List the attributes your configurable products use:

    mysql -u exmpl_mageread -p exmpl_mage -e "SELECT DISTINCT a.attribute_code, a.frontend_label FROM catalog_product_super_attribute s JOIN eav_attribute a ON a.attribute_id = s.attribute_id;"

    Expected: one row per attribute, such as color | Color and size | Size. Create a matching global attribute for each:

    wp wc product_attribute create --name=Color --slug=color --user=1
    wp wc product_attribute create --name=Size --slug=size --user=1

    Expected: Success: Created product_attribute 1. and then 2. Our guide to variable products and attributes explains how WooCommerce uses them.

  7. Export and import the catalogue. You have two routes.

    The first is the FG Magento to WooCommerce plugin by Frédéric Gilles, which reads the Magento database directly. Install it with wp plugin install fg-magento-to-woocommerce --activate, open Tools > Import > Magento, and enter localhost, the exmpl_mageread user and any table prefix. The free version moves categories, simple products, images, stock and CMS pages. The plugin’s page lists variations, customers, orders and URL redirects as paid Premium features, so check the current split there.

    The second route is Magento’s own export. In the admin, open System > Data Transfer > Export, choose Products and CSV, then select Continue. Magento 2.4 queues the job, so if no file appears after a few minutes, start the queue consumer from the Magento root:

    bin/magento queue:consumers:start exportProcessor --max-messages=1
    ls var/export/

    Expected: a file such as catalog_product_20260915_140512.csv. On Magento 1, use System > Import/Export > Export. Convert the columns with a spreadsheet or a short script:

    Magento columnWooCommerce column
    skuSKU
    product_type (configurable, simple)Type (variable, simple, variation)
    name, description, short_descriptionName, Description, Short description
    price, special_priceRegular price, Sale price
    qty, is_in_stockStock, In stock?
    categories (Default Category/Men/Tops)Categories (Men > Tops)
    url_keyslug, set after import
    base_image, additional_imagesImages, as full URLs

    Keep only rows where store_view_code is empty, because the others are store view overrides that create duplicates. The configurable_variations column lists each child as sku=MH01-S-Black,size=S,color=Black, with a pipe between children. Use it to set each variation’s Parent to the configurable SKU. Import parents first, then variations, with Attribute 1 global set to 1. Our Shopify to WooCommerce guide walks through this two-pass import.

    For images, copy the media folder without Magento’s resized cache:

    rsync -a --exclude=cache magento-host:/var/www/magento/pub/media/catalog/product/ ~/new.example.com/wp-content/uploads/magento/
    find ~/new.example.com/wp-content/uploads/magento -type f | wc -l

    Expected: a count close to your number of product images. In the Images column, /m/h/mh01-black_main_1.jpg becomes https://new.example.com/wp-content/uploads/magento/m/h/mh01-black_main_1.jpg, and the importer adds each one to the Media Library.

  8. Import customers and plan the resets. Export Customers Main File from the same Export screen and reshape it to the columns user_login, user_email, first_name, last_name and role, with customer as the role:

    wp user import-csv magento-customers.csv --skip-update
    wp user list --role=customer --format=count

    Expected: one line per created user, then a count matching step 1. FG Premium says it can accept Magento passwords at login, so test one real account if you use it. Otherwise, send resets after cutover in batches of 200, because many cPanel servers cap how many emails a domain may send per hour. The first line splits the customer IDs into files of 200, and the second sends the first file:

    wp user list --role=customer --field=ID | split -l 200 - ~/reset-batch-
    xargs wp user reset-password < ~/reset-batch-aa

    Expected: one Reset password for line per user in the first batch, then a Success: line with the total. An hour later, run the second line again with reset-batch-ab, then reset-batch-ac, and so on until ls ~/reset-batch-* shows no batch left to send. Warn customers a day ahead so the email is not mistaken for phishing.

  9. Bring over order history as records. First, disable Completed order under WooCommerce > Settings > Emails, or every past customer gets an email. Completed orders also reduce stock, so import them before your final stock sync. Pull the orders with the read-only user:

    mysql -u exmpl_mageread -p exmpl_mage -N -e "SELECT increment_id, created_at, customer_email, status, grand_total, order_currency_code FROM sales_order ORDER BY entity_id;" > ~/magento-orders.tsv
    wc -l ~/magento-orders.tsv

    Expected: a line count equal to your order total. FG Premium can import these, or a script can create each order from these rows and sales_order_item, keeping the Magento order number in meta:

    wp wc shop_order create --user=1 --status=completed --customer_id=42 \
      --billing='{"email":"jane@example.com"}' \
      --line_items='[{"product_id":318,"quantity":1,"subtotal":"49.00","total":"49.00"}]' \
      --meta_data='[{"key":"_magento_increment_id","value":"000000145"}]' --porcelain

    Expected: the new order ID alone, such as 2051. Then count the completed orders and compare:

    wp eval 'echo count( wc_get_orders( array( "status" => "completed", "limit" => -1, "return" => "ids" ) ) ), "\n";'

    Expected: a single number matching the line count of ~/magento-orders.tsv.

  10. Build the redirect map. Magento’s url_rewrite table lists every public address in request_path. Rows with redirect_type set to 0 are live URLs, and entity_type says what each one points to.

    Flow from Magento url_rewrite rows, matched by SKU or category slug, to WooCommerce permalinks and Redirect 301 lines in .htaccess

    Export product paths with their SKU, and the WooCommerce SKU-to-slug list. Replace 1 with your store view’s store_id, and wp_ with your WordPress table prefix if it differs:

    mysql -u exmpl_mageread -p exmpl_mage -N -e "SELECT DISTINCT u.request_path, p.sku FROM url_rewrite u JOIN catalog_product_entity p ON p.entity_id = u.entity_id WHERE u.entity_type = 'product' AND u.redirect_type = 0 AND u.store_id = 1;" > ~/product-paths.tsv
    wp db query "SELECT pm.meta_value, p.post_name FROM wp_postmeta pm JOIN wp_posts p ON p.ID = pm.post_id WHERE pm.meta_key = '_sku' AND p.post_type = 'product';" --skip-column-names > ~/woo-skus.tsv
    awk -F'\t' 'NR==FNR {slug[$1]=$2; next} ($2 in slug) {printf "Redirect 301 /%s https://example.com/product/%s/\n", $1, slug[$2]}' ~/woo-skus.tsv ~/product-paths.tsv > ~/magento-redirects.conf
    wc -l ~/product-paths.tsv ~/magento-redirects.conf

    Expected: two matching line counts. A product often has several paths, such as mh01-hoodie.html and men/tops/mh01-hoodie.html, and each gets a line. A gap is a SKU that did not import. Category slugs usually match the Magento url_key, so those paths map directly:

    mysql -u exmpl_mageread -p exmpl_mage -N -e "SELECT request_path FROM url_rewrite WHERE entity_type = 'category' AND redirect_type = 0 AND store_id = 1;" | awk '{p=$1; sub(/\.html$/, "", p); printf "Redirect 301 /%s https://example.com/product-category/%s/\n", $1, p}' >> ~/magento-redirects.conf
    head -3 ~/magento-redirects.conf

    Expected:

    Redirect 301 /mh01-hoodie.html https://example.com/product/mh01-hoodie/
    Redirect 301 /men/tops/mh01-hoodie.html https://example.com/product/mh01-hoodie/
    Redirect 301 /radiant-tee.html https://example.com/product/radiant-tee/

    Paste the file into ~/new.example.com/.htaccess above # BEGIN WordPress, so the block stays outside the section WordPress rewrites when permalinks are saved. If the store used a suffix other than .html, adjust the sub() pattern. On Magento 1, the table is core_url_rewrite, with products linked by product_id. Beyond a few thousand lines, load the list into the Redirection plugin instead.

  11. Switch over, with a rollback ready. Lower the DNS TTL (time to live, how long resolvers cache the record) to 300 seconds two days ahead. On the day, put Magento in maintenance mode with bin/magento maintenance:enable. Import orders and customers created since your dump, sync final stock, then move WordPress to the main domain:

    rsync -a ~/new.example.com/ ~/public_html/
    cd ~/public_html
    wp search-replace 'https://new.example.com' 'https://example.com' --all-tables --skip-columns=guid
    wp option get siteurl

    Expected: a replacement count, then https://example.com. Point the domain’s A record at the cPanel server. To roll back, point the record back to the Magento server and run bin/magento maintenance:disable. Orders placed on WooCommerce in between must be re-entered by hand, so set a rollback deadline in advance.

Validate it works

Test a random sample of old product addresses:

shuf -n 25 ~/product-paths.tsv | cut -f1 | while read -r p; do
  printf '%s  /%s\n' "$(curl -s -o /dev/null -w '%{http_code} %{redirect_url}' "https://example.com/$p")" "$p"
done

Expected: every line shows 301 and a /product/ address. A 404 is a path with no rule. Then confirm a redirect lands in one hop:

curl -sIL -o /dev/null -w '%{http_code} %{num_redirects} %{url_effective}\n' https://example.com/mh01-hoodie.html

Expected: 200 1 https://example.com/product/mh01-hoodie/. Finally, compare catalogue counts with step 1:

wp post list --post_type=product --format=count
wp post list --post_type=product_variation --format=count

Expected: the first equals configurable products plus standalone simple products, and the second equals simple products linked to a parent. Then place one real test order and confirm it reaches the gateway dashboard.

Troubleshooting

  • The restore stops with an error about the SUPER privilege. Magento 2 triggers carry a DEFINER naming the original database user, which a cPanel user cannot set. Drop and recreate the database, then restore with gunzip < ~/magento-copy.sql.gz | sed -E 's/DEFINER=[^*]*\*/\*/g' | mysql -u exmpl -p exmpl_mage.
  • Variable products show no options. The attribute was created per product instead of globally. Check with wp term list pa_size --fields=name,count. An empty result means Attribute 1 global was missing, so delete those products and re-import both passes.
  • Products appear two or three times. Store view rows were imported. Delete the duplicates and re-import from rows where store_view_code is empty. WooCommerce runs one catalogue per site, so serve other languages with a multilingual plugin, or run one WordPress site per Magento website.
  • Redirects return 404. The path has no line in the file. Check with grep -c 'mh01-hoodie.html' ~/public_html/.htaccess, using the failing path; 0 means it is missing. The usual causes are a path from another store view, so re-run the export with that store_id, or a SKU that changed on import, which you can look up in ~/woo-skus.tsv. If the line is present, confirm it was not pasted inside the # BEGIN WordPress block, where saving permalinks erases it.

When this is the wrong approach

Stay on Magento, or move to Mage-OS, the community-run distribution of Magento Open Source, when the store depends on features WooCommerce lacks out of the box. That includes B2B tools such as company accounts, shared catalogues and negotiated quotes. It also includes several websites with their own prices run from one admin, and MSI, Magento’s Multi-Source Inventory, which splits stock across warehouses. Custom modules tied to an ERP (enterprise resource planning software) each become a plugin you commission from scratch. A catalogue of tens of thousands of SKUs with many filters can run on WooCommerce, but it needs careful database and search tuning.

Magento 1 has had no official patches since June 2020, so plan this migration or a move to Magento 2 or OpenMage soon. Adobe documents the admin export in its data export guide, and WooCommerce describes every import column in the product CSV importer documentation.

Running WordPress that earns real money deserves an operator — Stack Harbor runs that stack under Enterprise WordPress; book an infrastructure consult.