Installation

This content was generated with the assistance of AI and should be verified against the official documentation of each platform before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

This page covers every step between an empty server and a Magento store that serves pages, runs cron and queue consumers and is ready for traffic, for the self-hosted editions: Magento Open Source 2.4.9 (general availability 2026-05-12, standard support until 2029-05-31), Adobe Commerce 2.4.9 on your own infrastructure and Mage-OS 3.6.0 (2026-10-14, based on the Magento 2.4.9 code). It was checked on 2026-10-27. It does not cover Adobe Commerce on cloud infrastructure, whose projects are built and deployed from a Git repository by Adobe’s tooling, or Adobe Commerce as a Cloud Service, which is installed by Adobe. The editions are compared on Editions and Ecosystem, the support dates and the full requirements matrix are on Versions, Lifecycle and Requirements, and a laptop setup with Docker is on Local Development Environments. The first screens of the Admin after an install are on Admin and Store Setup.

Everything shown as run was run on 2026-10-27 with Mage-OS 3.6.0 (bin/magento --version prints Mage-OS CLI 3.6.0 (based on Magento 2.4.9)) in Docker containers on an arm64 host: PHP 8.5.12, Composer 2.10.3, Apache 2.4.69, MariaDB 11.8.9 and 12.3.3, OpenSearch 3.9.0, Valkey 9.1.3 and RabbitMQ 4.4.0. Nginx 1.30.6 was used to check the Nginx configuration. The Adobe Commerce and Magento Open Source packages come from repo.magento.com, which needs a personal key pair, so that download was documented and not executed; everything after the download is the same code path. The running example, Pocket Coffee, is a Madrid shop with a Spanish default store view and an English one, priced in euros, installed with --language=es_ES --currency=EUR --timezone=Europe/Madrid.

Five stages from an empty server to a running store: prerequisites, get the code, permissions, setup:install and the steps after the install

Choose a path

Path What it gives you Needs

composer create-project from repo.magento.com, project magento/project-community-edition

Magento Open Source from Adobe, the reference for every Adobe document.

A Marketplace account and an access key pair, even for the open source edition.

composer create-project from repo.magento.com, project magento/project-enterprise-edition

Adobe Commerce on your own servers.

A licence and a key pair that is entitled to the Commerce packages (the Marketplace shows the Access Keys section only to entitled accounts).

composer create-project from repo.mage-os.org, project mage-os/project-community-edition

Mage-OS, a community distribution of the Magento Open Source code with its own release cadence.

Nothing: the repository is public and needs no keys. Run on 2026-10-27 in about one minute with a warm Composer cache.

Cloud project template (magento/magento-cloud) with ece-tools

Adobe Commerce on cloud infrastructure, deployed with a Git push.

A Cloud project. Not covered here.

Git clone of magento/magento2

The code the contributors work on, not a store.

Adobe’s installation guide points developers who contribute to Magento Open Source to the Git-based method; a store installs from Composer.

Adobe Commerce as a Cloud Service

Nothing to install.

An Adobe-provisioned instance.

Which distribution fits a project is a business and ecosystem decision (see Editions and Ecosystem). For the install itself the two repositories behave the same way: the metapackage requires about 360 component packages (magento/... or mage-os/...), Composer resolves them and the result is a project directory with bin/magento.

Requirements

The system requirements page of the 2.4.9 documentation lists PHP 8.5 only, and the 2.4.9 release notes say PHP 8.5 is fully supported and "PHP 8.4 is allowed for upgrade purposes only (not recommended for production)". Treat PHP 8.5 as the production target and 8.4 as a stepping stone while you upgrade. The two older lines that still receive security patches are listed for planning.

Component 2.4.9 (GA 2026-05-12) 2.4.8-p5 2.4.7-p10

PHP

8.5

8.4, 8.3

8.3, 8.2

Composer

2.10

2.x

2.x

MariaDB

12.3 (Cloud: 12.3 recommended, 11.8)

11.4, 11.8

10.11, 11.8

MySQL

8.4 (8.0 reached end of support 2026-04-30)

8.4

not supported

Search engine

OpenSearch 3 (2.x compatibility kept); Elasticsearch is not listed

OpenSearch 3, Elasticsearch 8

OpenSearch 2.19 and 3, Elasticsearch 8

Cache and sessions

Valkey 9 (Redis is not listed)

Valkey 8.1

Valkey 8.1; Redis not supported

Message queue

RabbitMQ 4.3, ActiveMQ Artemis 2

see the requirements page

see the requirements page

Web server and cache

Nginx 1.30, Apache 2.4, Varnish 8

see the requirements page

see the requirements page

Three details matter in practice. The release notes still say RabbitMQ 4.2 in one place while the requirements table says 4.3; use the table. The database line for 2.4.9 is MariaDB 12.3 (the 2.4.9 text says "Upgrade to MariaDB 12.3 before you upgrade to 2.4.9"); MariaDB 11.8 installed and ran Pocket Coffee without a problem in this test, but it is not the combination Adobe tests on premises. And Mage-OS declares a wider PHP range than Adobe: the mage-os/product-community-edition 3.6.0 package requires php ~8.3.0||~8.4.0||~8.5.0, so a Mage-OS store can still run on PHP 8.3 while an Adobe 2.4.9 store cannot.

PHP extensions and php.ini

Composer enforces the extension list when it resolves the project, and composer check-platform-reqs reports it at any time. In the Mage-OS 3.6.0 project the command printed success for every line:

$ composer check-platform-reqs
Checking platform requirements for packages in the lock file
composer-plugin-api 2.9.0  success
composer-runtime-api 2.2.2 success
ext-bcmath 8.5.12          success
ext-ctype *                success
ext-curl 8.5.12            success
ext-dom 20031129           success
ext-fileinfo 8.5.12        success
ext-filter 8.5.12          success
ext-gd 8.5.12              success
ext-hash 8.5.12            success
ext-iconv 8.5.12           success
ext-intl 8.5.12            success
ext-json 8.5.12            success
ext-libxml 8.5.12          success
ext-mbstring *             success
ext-openssl 8.5.12         success
ext-pcre 8.5.12            success
ext-pdo_mysql 8.5.12       success
ext-phar 8.5.12            success
ext-reflection 8.5.12      success
ext-simplexml 8.5.12       success
ext-soap 8.5.12            success
ext-sockets 8.5.12         success
ext-sodium 8.5.12          success
ext-tokenizer 8.5.12       success
ext-xml 8.5.12             success
ext-xmlwriter 8.5.12       success
ext-xsl 8.5.12             success
ext-zip 1.22.8             success
ext-zlib 8.5.12            success
lib-libxml 2.9.14          success
php 8.5.12                 success
php-64bit 8.5.12           success

Adobe’s list for the Adobe-hosted packages also names ext-ftp, which the Mage-OS project does not require, and the Docker image used here compiled the other extensions (bcmath gd intl pdo_mysql soap sockets xsl zip sodium pcntl) with docker-php-ext-install. Two optional extensions are worth adding: pcntl, which the indexers need to run in parallel mode, and igbinary, which the installer writes as the cache serialiser ("serializer": "igbinary"), as the installer help says it is 70 percent faster and 58 percent smaller than PHP serialisation. A container without igbinary still served pages in the first test, and the stack built later with the extension installed worked the same way, so install it when you want the smaller, faster cache entries.

The documented php.ini settings are short:

Setting Value and reason

memory_limit

1G to compile code and deploy static files, 2G for debugging, 3G to 4G for tests. The tests here used 2G; setup:di:compile peaked at 514 MiB.

realpath_cache_size and realpath_cache_ttl

10M and 7200, so PHP caches file lookups instead of repeating them on every page.

opcache.save_comments

On. Magento 2.1 and later generate code from PHP doc comments, so OPcache must keep them.

opcache.memory_consumption, opcache.max_accelerated_files

256 MB and 60000 in the tested image, with opcache.validate_timestamps=1.

date.timezone

Set it, or PHP warns that "It is not safe to rely on the system’s timezone settings" during the install and cron can misbehave.

max_execution_time

1800 for the CLI and long admin imports; Nginx’s sample passes 18000 for the web entry point.

session.auto_start

off (the shipped .user.ini already sets it for the web).

The same settings must reach both the command-line PHP and the web server’s PHP (php.ini for Apache’s module, the FPM pool file and the PHP command line’s own ini). Adobe "strongly recommends" the same values in both, because the install and upgrade run in the CLI and the store runs in the web SAPI. In Debian-style systems these are different directories (/etc/php/8.5/cli, /etc/php/8.5/fpm).

; zz-magento.ini used for the tests
memory_limit=2G
max_execution_time=1800
zlib.output_compression=On
opcache.enable=1
opcache.memory_consumption=256
opcache.max_accelerated_files=60000
opcache.validate_timestamps=1
realpath_cache_size=10M
realpath_cache_ttl=7200
date.timezone=UTC

The services

A store needs four network services besides PHP and the web server: a database, a search engine, a key-value store and, optionally, a message broker. The test install ran them as containers on one Docker network.

A request goes from the browser through optional CDN and Varnish to Apache or Nginx and PHP running Magento, which uses MariaDB or MySQL, OpenSearch, Valkey and RabbitMQ; cron starts cron:run every minute

MariaDB or MySQL

Create an empty database with its own user. The installer creates the tables, so the account needs full rights on that schema only.

CREATE DATABASE magento CHARACTER SET utf8mb4;
CREATE USER 'magento'@'%' IDENTIFIED BY '<choose-a-strong-password>';
GRANT ALL PRIVILEGES ON magento.* TO 'magento'@'%';
FLUSH PRIVILEGES;

Replace % with the host the web server connects from. Adobe’s database page adds four requirements that you must handle yourself. Magento creates database triggers when an indexer runs in Update by Schedule mode, so the account needs the right to create triggers (ALL PRIVILEGES includes it). Enable the STRICT_ALL_TABLES SQL mode on a new project. Use only row-based replication, because statement-based replication is not supported and CREATE TEMPORARY TABLE inside transactions breaks GTID-based replication on some managed services. And raise max_allowed_packet above its 16 MB default if you import large catalogues. The installer itself rejects a wrong password early:

SQLSTATE[HY000] [1045] Access denied for user 'magento'@'172.20.0.5' (using password: YES)

In InstallCommand.php line 291:
  Parameter validation failed

OpenSearch

2.4.9 uses OpenSearch 3 (2.x still works). The Mage-OS 3.6.0 installer still accepts --search-engine=elasticsearch8, although Adobe’s 2.4.9 table lists only OpenSearch. The installer stops when it cannot reach the search engine (see the errors table), because the storefront catalogue, layered navigation and search all read from it. A single node is enough for a laptop or a small shop:

docker network create ec-net
docker run -d --name search --network ec-net --memory 1g \
  -e discovery.type=single-node \
  -e DISABLE_SECURITY_PLUGIN=true \
  -e DISABLE_INSTALL_DEMO_CONFIG=true \
  -e OPENSEARCH_JAVA_OPTS="-Xms512m -Xmx512m" \
  opensearchproject/opensearch:3
docker exec search curl -s localhost:9200

On 2026-10-27 the last command answered with the version block ("number": "3.9.0", "distribution": "opensearch"). The security plugin is disabled in this example because the container is reachable only from its Docker network; on a shared network keep it on and give the installer --opensearch-enable-auth=1 --opensearch-username=<user> --opensearch-password=<password>. Give every store sharing a cluster its own index prefix with --opensearch-index-prefix: an index is named <prefix>_product_<store view id>_v<version>, so the installs with prefixes magento2 and pocketcoffee never touch each other.

Valkey

Valkey 9 is the tested key-value store for 2.4.9. It is protocol-compatible with Redis 7.2 and the PHP side still talks to it with Redis commands, which is why some option names still say redis. Magento uses three databases of one server: database 0 for the cache, 1 for the full-page cache and 2 for sessions, "to protect against data loss" when you flush one of them.

docker run -d --name cache --network ec-net --memory 128m valkey/valkey:9-alpine
docker exec cache valkey-cli ping
docker exec cache valkey-cli info server | grep -E "redis_version|valkey_version"

The server reports redis_version:7.2.4 (the compatibility version) and valkey_version:9.1.3. Details of the Magento side, including a trap in the session options, are in the Valkey section below.

RabbitMQ (optional)

Without a broker Magento falls back to a MySQL queue (tables queue, queue_message, queue_message_status), which is acceptable for a small shop. Consumers process asynchronous operations such as REST bulk and asynchronous endpoints and Inventory mass actions; a broker is the scalable transport for them. Adobe lists RabbitMQ 4.3 and ActiveMQ Artemis 2 for 2.4.9; the release notes call Artemis the long-term replacement for RabbitMQ. The test used the public rabbitmq:4 image, which resolved to RabbitMQ 4.4.0 on 2026-10-27, newer than the version in Adobe’s table, and everything worked:

docker run -d --name rabbitmq --network ec-net --memory 512m \
  -e RABBITMQ_DEFAULT_USER=magento \
  -e RABBITMQ_DEFAULT_PASS='<choose-a-strong-password>' \
  rabbitmq:4
docker exec rabbitmq rabbitmqctl version

Composer access

Marketplace access keys

repo.magento.com requires authentication for every package, open source ones included. In the Commerce Marketplace open My Profile, choose Access Keys in the Marketplace tab and select Create a New Access Key. You get two 32-character strings: the public key is the Composer username and the private key is the password. Since August 2022 the account owner needs an Adobe ID linked to the Commerce account before keys can be generated, and keys carry the entitlement of the account (the Primary Contact’s MAGEID), which is why a developer may see no Access Keys section until the account owner grants access or an invoice is settled. You can disable, re-enable or delete keys, for example when a contractor leaves; key one pair per person and per CI system so one can be revoked alone.

auth.json and the environment

Composer reads credentials from a global auth.json, a project auth.json or the COMPOSER_AUTH environment variable, so you are not prompted each time. The project ships auth.json.sample; a real auth.json must never be committed.

# global, outside every repository: ~/.composer/auth.json (or $COMPOSER_HOME/auth.json)
composer config -g http-basic.repo.magento.com "<public-key>" "<private-key>"

# CI: inject the secret as an environment variable
export COMPOSER_AUTH='{"http-basic":{"repo.magento.com":{"username":"<public-key>","password":"<private-key>"}}}'

With placeholder values, the first command wrote an auth.json whose http-basic object had the key repo.magento.com with username and password, and composer config -g --list printed [http-basic.repo.magento.com.username] for the second form. No request to repo.magento.com was made. Mage-OS’s repository needs none of this.

Get the code

Work as the account that will own the files (see the next section) and use an empty directory: Composer refuses a directory that already holds anything, including a log file you redirected into it ("Project directory is not empty").

# Magento Open Source from Adobe (prompts for the key pair unless auth.json exists)
composer create-project --repository-url=https://repo.magento.com/ \
  magento/project-community-edition=2.4.9 /var/www/pocketcoffee

# Adobe Commerce on your own infrastructure
composer create-project --repository-url=https://repo.magento.com/ \
  magento/project-enterprise-edition=2.4.9 /var/www/pocketcoffee

# Mage-OS 3.6.0, no keys needed (run on 2026-10-27)
composer create-project --repository-url=https://repo.mage-os.org/ \
  mage-os/project-community-edition=3.6.0 /var/www/pocketcoffee

The version after = pins a release; without it Composer takes the newest stable one. Patch releases use the 2.4.8-p5 naming; Adobe also publishes monthly isolated security builds such as 2.4.9-2026-oct, which are applied to a project on the latest patch line and are described on Versions, Lifecycle and Requirements. The Mage-OS repository listed these project versions on 2026-10-27: 3.6.0, 3.5.0, 3.4.0, 3.3.0, 3.2.0, 3.1.0 and 3.0.0, then 2.3.0 down to 2.0.0 and 1.x.

The Mage-OS run took about 50 seconds to download the 609 packages of the lock file (dev packages included), ended with PHP CodeSniffer Config installed_paths set to ../../mage-os/magento-coding-standard,... and No security vulnerability advisories found., and produced this top-level layout:

app  bin  dev  generated  lib  nginx.conf.sample  phpserver  pub  setup  var  vendor
auth.json.sample  composer.json  composer.lock  .htaccess  .htaccess.sample  .user.ini  ...

The project composer.json is short: three require lines (mage-os/product-community-edition, the two Composer plugins mage-os/composer-root-update-plugin and mage-os/composer-dependency-version-audit-plugin, all 3.6.0), a repositories entry for https://repo.mage-os.org/, an autoload section that maps app/code and generated/code and "extra": {"magento-force": "override"}. Production deployments should install with composer install --no-dev from the committed composer.lock, so the production tree has no test tools.

Ownership, permissions and the document root

Magento has one rule: a single user owns every file and runs both the command line and the web server’s PHP. Adobe calls it the file system owner. The simplest shared-hosting layout is the account that owns the web root; on a server it is usually a deployment user in the web server’s group, with group-writable directories only where Magento writes.

cd /var/www/pocketcoffee
find var generated vendor pub/static pub/media app/etc -type f -exec chmod g+w {} +
find var generated vendor pub/static pub/media app/etc -type d -exec chmod g+ws {} +
chown -R :www-data .      # Ubuntu; use the web server's group
chmod u+x bin/magento

The setgid bit (g+s) on directories makes new files inherit the group. Run bin/magento as the owner only (sudo -u www-data php bin/magento ... or su <owner> -s /bin/bash -c ...): a command run once as root leaves root-owned files in generated/ and var/, and every later command then fails. That is not hypothetical; in this test a php bin/magento --version run as root produced interceptor files that the file system owner could not delete, and deploy:mode:set production answered with a wall of The "/.../generated/code/.../Interceptor.php" file can't be deleted. Warning!unlink(...): Permission denied messages until chown -R repaired the tree.

The document root of the web server must be the pub/ directory. The installer records this as "directories": {"document_root_is_pub": true} in app/etc/env.php; with it, app, vendor, var, setup and the rest are not under the web root at all, so the code and the configuration files cannot be requested by URL. The installer message For security, remove write permissions from these directories: '.../app/etc' reminds you to remove the write bit from app/etc once you are done (see After the install).

setup:install

bin/magento setup:install is the one command that creates the tables, writes the deployment configuration, creates the administrator and reindexes. Every value can be an option, so it works unattended in scripts and containers. The complete command used for Pocket Coffee, with a Madrid timezone, the Spanish locale and euro as the base currency:

bin/magento setup:install \
  --base-url=http://localhost:8093/ \
  --db-host=db --db-name=magento --db-user=magento --db-password='<db-password>' \
  --admin-firstname=Ada --admin-lastname=Lovelace --admin-email=admin@example.test \
  --admin-user=admin --admin-password='<admin-password>' \
  --language=es_ES --currency=EUR --timezone=Europe/Madrid --use-rewrites=1 \
  --search-engine=opensearch --opensearch-host=search --opensearch-port=9200 --opensearch-index-prefix=pocketcoffee \
  --session-save=redis --session-save-redis-host=cache --session-save-redis-db=2 \
  --cache-backend=valkey --cache-backend-valkey-server=cache --cache-backend-valkey-db=0 \
  --page-cache=valkey --page-cache-valkey-server=cache --page-cache-valkey-db=1

The command took 11 seconds in the Docker stack and ended with:

11 indexer(s) are indexed.
[Progress: 1348 / 1348]
[SUCCESS]: Magento installation complete.
[SUCCESS]: Magento Admin URI: /admin_rvpka41
Nothing to import.

The number of progress steps is the count of modules and tasks (1348 here, 1349 in a second install), so it differs between distributions and versions. The same run printed these messages in this order: "Installing admin user", "Caches clearing", "Disabling Maintenance Mode" and "Post installation file permissions check".

The options, by group

setup:install --help of Mage-OS 3.6.0 lists about 180 options. These are the ones that matter, grouped by what they configure. Defaults are from the help output.

Group and options Purpose Notes

--base-url, --base-url-secure, --use-secure, --use-secure-admin, --use-rewrites, --admin-use-security-key

The store URL, HTTPS and URL rewriting.

All six are marked "Deprecated, use config:set with path web/…​" in the 2.4.9 help: set them with bin/magento config:set after the install. --use-rewrites=1 removes index.php from URLs and is the normal value.

--language, --currency, --timezone

Default locale, base and default currency, time zone.

Marked deprecated as well (config:set general/locale/code, currency/options/base, general/locale/timezone). The base currency is awkward to change after orders exist, so decide it now.

--db-host, --db-name, --db-user, --db-password, --db-prefix, --db-engine, --db-model, --db-init-statements, --db-ssl-*

Database connection.

--db-prefix prefixes every table when a schema is shared. --db-ssl-key, --db-ssl-cert, --db-ssl-ca and --db-ssl-verify secure the connection. --skip-db-validation (-s) skips the connection check.

--admin-firstname, --admin-lastname, --admin-email, --admin-user, --admin-password

The first administrator.

The password must be at least 7 characters and include both letters and digits (the installer prints "Your password must be at least 7 characters." and "Your password must include both numeric and alphabetic characters." and stops with "Parameter validation failed").

--backend-frontname

The Admin path.

Omit it. Adobe recommends the generated random URI because it is harder to guess; the installer prints it (/admin_rvpka41) and bin/magento info:adminuri shows it later.

--search-engine and --opensearch-* or --elasticsearch-*

Search engine, host, port, authentication, index prefix and timeout.

opensearch or elasticsearch8 in the Mage-OS 3.6.0 help. --opensearch-timeout=15 is the documented value. The installer validates the connection and stops on failure (see the troubleshooting table).

--session-save and --session-save-redis-* or --session-save-valkey-*

Where sessions are stored: files, db, redis or valkey.

--session-save-redis-max-concurrency, --session-save-redis-break-after-frontend (5 s) and --session-save-redis-break-after-adminhtml (30 s) control the session lock. See Valkey.

--cache-backend and --cache-backend-redis-* or --cache-backend-valkey-*; --page-cache and its *-redis-*, *-valkey-* options

Cache and full-page cache backends and databases.

--cache-backend-redis-compress-data, --cache-backend-redis-compression-lib and --cache-backend-redis-serializer are the tuning knobs; --cache-id-prefix isolates two stores sharing a server; --allow-parallel-generation lets requests build a cache entry without blocking.

--amqp-host, --amqp-port, --amqp-user, --amqp-password, --amqp-virtualhost, --amqp-ssl, --stomp-*, --queue-default-connection, --consumers-wait-for-messages

RabbitMQ (AMQP) or ActiveMQ Artemis (STOMP) and queue behaviour.

--queue-default-connection accepts db, amqp or a custom name. Set the same flags later with setup:config:set.

--lock-provider, --lock-db-prefix, --lock-zookeeper-host, --lock-file-path

The lock service used by cron and indexers.

The default provider is db.

--http-cache-hosts

Varnish hosts that receive purge requests.

Needed in production when Varnish is in front (varnish:80).

--sales-order-increment-prefix

A prefix for order numbers.

Handy when two installs send orders to the same payment account and you must tell them apart.

--enable-modules, --disable-modules

Install with some modules on or off.

Comma-separated module names.

--use-sample-data

Installs with sample data.

Sample data packages are added with sampledata:deploy (see Adobe’s sample data pages). Never use it on a production store.

--cleanup-database, --safe-mode, --data-restore, --dry-run, --interactive (-i)

Repeatable and guided installs.

--cleanup-database drops existing tables first, so it destroys data. --interactive asks for each value.

--key, --id_salt

The encryption key and the GraphQL salt.

Normally generated. Provide --key only when you restore a database that was encrypted with a known key.

--remote-storage-*

Remote media storage (S3-compatible).

--remote-storage-driver is file by default.

Mage-OS adds a helper: a setup:install run that fails parameter validation starts with the line Tip: Run bin/magento install for a guided interactive installation. The command install ("Interactive Mage-OS installation wizard", registered in the list output) asks the same questions in stages (environment, database, admin, store, backend, document root, search engine, Redis or Valkey, RabbitMQ, logging, sample data and theme), detects services it can reach, checks permissions and runs setup:install for you; with -vvv it prints the underlying command. It needs a terminal, so it was listed and read but not driven here.

What the installer writes

  • app/etc/env.php holds what is specific to this environment: the Admin path, the database connection, the cache, session and queue settings, MAGE_MODE, the cache_types switches, the lock provider and the crypt key.

  • app/etc/config.php holds what must be the same in every environment: the module list with its enabled flags (more than 330 entries in the Mage-OS 3.6.0 install) and, once you run app:config:dump, scopes and system settings.

  • The database holds everything else, in core_config_data and the entity tables.

An excerpt of the generated env.php (without the crypt key, the database password and the GraphQL salt, which are secrets and must never be pasted anywhere or committed with a public repository):

return [
    'backend' => ['frontName' => 'admin_rvpka41'],
    'remote_storage' => ['driver' => 'file'],
    'queue' => ['consumers_wait_for_messages' => 1],
    'db' => [
        'table_prefix' => '',
        'connection' => [
            'default' => [
                'host' => 'db', 'dbname' => 'magento', 'username' => 'magento',
                'password' => '<db-password>',
                'model' => 'mysql4', 'engine' => 'innodb', 'active' => '1',
                'driver_options' => [1013 => false],
            ],
        ],
    ],
    'resource' => ['default_setup' => ['connection' => 'default']],
    'x-frame-options' => 'SAMEORIGIN',
    'MAGE_MODE' => 'default',
    'lock' => ['provider' => 'db'],
    'directories' => ['document_root_is_pub' => true],
    'downloadable_domains' => ['localhost'],
    'install' => ['date' => '...'],
];

Three observations from the run. The installer wrote MAGE_MODE as default and left cache types enabled (16 types). downloadable_domains was set from the base URL (localhost); a store behind another domain must add that domain here or with bin/magento downloadable:domains:add (and downloadable:domains:show lists them). All 11 indexers finished Ready with Update On: Schedule, which means index changes are queued by database triggers and applied by cron: with cron not running, a product edit does not reach search or the category pages (see Cron).

Verification after the install, with their 2026-10-27 results in the Docker stack:

bin/magento --version                 # Mage-OS CLI 3.6.0 (based on Magento 2.4.9)
bin/magento setup:db:status           # All modules are up to date.
bin/magento info:adminuri             # Admin URI: /admin_rvpka41
bin/magento indexer:status            # 11 indexers, all Ready, Update On: Schedule
bin/magento deploy:mode:show          # Current application mode: default.
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8093/                # 200
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8093/admin_rvpka41   # 200

Running the installer again

Installing over an installed database is accepted and rerun end to end; in the test it reused the existing Admin URI and finished with [SUCCESS]: Magento installation complete. Use --cleanup-database only when you mean to drop every table: in one test a failed run that included it had already emptied the database when it stopped at the search-engine check, and the store answered HTTP 500 afterwards.

Common errors

What you see Cause and fix

Project directory ".../." is not empty. from composer create-project

The target directory has a file in it. Use an empty directory and write logs elsewhere.

SQLSTATE[HY000] [1045] Access denied for user ... Parameter validation failed

The database credentials or the host part of the grant are wrong. Test with the mariadb or mysql client from the web host.

Could not validate a connection to the OpenSearch. No alive nodes found in your cluster (in SearchConfig.php)

Wrong --opensearch-host or port, the node is down or the security plugin needs credentials. The check runs after --cleanup-database has dropped the tables.

Your password must be at least 7 characters. and ...numeric and alphabetic characters.

The administrator password policy, enforced before anything is created.

This account isn't confirmed. Verify and try again. when requesting a customer token

Customer account confirmation is on (customer/create_account/confirm), which is a setting, not an installer problem.

Please ask an administrator with sufficient access to configure 2FA first. when requesting an admin token

The two-factor module is on and the admin has no second factor. Configure it, or on a local machine disable Magento_TwoFactorAuth (see the local environment page).

Permission denied and file can't be deleted during mode changes

Files created by another user. Run chown -R from the file system owner’s side and run every command as that one account.

Valkey and Redis

Adobe’s 2.4.9 documentation describes dedicated Valkey flags: --session-save=valkey with --session-save-valkey-<parameter>, and --cache-backend=valkey and --page-cache=valkey with their own option families, and says the options of 2.4.8 and earlier keep the redis names. The Mage-OS 3.6.0 installer accepts both families, and the tests separated three cases:

Option set Effect in the test Verified by

--cache-backend=valkey --cache-backend-valkey-server=cache --cache-backend-valkey-db=0

The cache used the Valkey server, backend name valkey, database 0; --page-cache=valkey used database 1 the same way.

env.php showed 'backend' => 'valkey' with the given server and database; valkey-cli -n 0 dbsize returned 230 keys after the first page loads (cache:id_tags:..., ...SYSTEM_SCOPES).

--session-save=redis --session-save-redis-host=cache --session-save-redis-db=2 against the Valkey server

Sessions were stored in Valkey database 2.

valkey-cli -n 2 --scan listed keys named sess_....

--session-save=valkey --session-save-valkey-host=cache --session-save-valkey-db=5

The installer wrote 'save' => 'valkey' and a valkey block with the default host 127.0.0.1 and database 2, ignoring the host and database given, and the storefront wrote its sessions to var/session/sess_* files.

The session.valkey block in env.php, an empty database 5 and session files on disk.

So on Mage-OS 3.6.0, use the redis session flags against a Valkey server (the same flags Adobe says to use on 2.4.8 and earlier), and verify with valkey-cli that sess_ keys appear. The Mage-OS source explains it: the installer’s session option class tests a constant instead of the chosen handler, so the Valkey input options are never mapped to the configuration, and the runtime session handler only reads the session/redis block. I did not run the Adobe-hosted 2.4.9 packages, so the same check on your own install is the only reliable answer for that edition.

The same flags work after the install with setup:config:set, which rewrites the matching part of env.php. It accepted --session-save=redis --session-save-redis-host=cache --session-save-redis-db=5 and removed the stale valkey block, and it printed "We saved default values for these options: …​", a reminder that it also writes the defaults for options you did not pass.

bin/magento setup:config:set --session-save=redis \
  --session-save-redis-host=cache --session-save-redis-port=6379 --session-save-redis-db=2
docker exec cache valkey-cli -n 2 dbsize
docker exec cache valkey-cli -n 2 --scan | head -3

Production advice from Adobe’s pages: keep the three databases distinct, set the session max_concurrency to at least 10 percent of the number of PHP processes (the default is 6 and disable_locking defaults to 0, which keeps session locking on), and read the L2 cache page when you size the key-value memory. Cache databases can be rebuilt after a restart; session data cannot, so a non-persistent session database logs every customer out when it restarts.

OpenSearch settings after the install

The installer stores the connection in core_config_data (catalog/search/engine, catalog/search/opensearch_server_hostname, ..._port, ..._index_prefix and so on), so you change it with config:set and then reindex:

bin/magento config:show catalog/search/engine             # opensearch
bin/magento config:show catalog/search/opensearch_server_hostname   # search
bin/magento indexer:reindex catalogsearch_fulltext
docker exec search curl -s "localhost:9200/_cat/indices?h=index,docs.count&s=index"

On the fresh Pocket Coffee install the last command listed pocketcoffee_product_1_v2 0: one product index per store view, with a version suffix, and no documents yet because the catalogue was empty. On a store with two products another install’s index showed magento2_product_1_v5 2 after indexer:reindex. A cluster health of yellow on a single node is normal: replicas have nowhere to go.

RabbitMQ settings and consumers

Configure the broker with the installer options or afterwards, which writes the queue block of env.php:

bin/magento setup:config:set --amqp-host=rabbitmq --amqp-port=5672 \
  --amqp-user=magento --amqp-password='<choose-a-strong-password>' --amqp-virtualhost=/
bin/magento setup:upgrade

setup:upgrade declares the exchanges and queues: 23 queues appeared in RabbitMQ (async.operations.all, product_action_attribute.update, catalog_product_generate_urls, inventory.reservations.update, media.storage.catalog.image.resize, export, codegenerator, product_alert.queue and others), and bin/magento queue:config:status answered Queue config files are up to date. The resulting env.php block:

'queue' => [
    'consumers_wait_for_messages' => 1,
    'amqp' => [
        'host' => 'rabbitmq', 'port' => '5672',
        'user' => 'magento', 'password' => '<choose-a-strong-password>', 'virtualhost' => '/',
    ],
],

An end-to-end check of the whole chain: an asynchronous REST call stores a message instead of doing the work, a consumer does the work. The administrator token comes from POST /rest/V1/integration/admin/token; here it is in the variable ${TOKEN}.

curl -s -X POST http://localhost:8093/rest/async/V1/products \
  -H "Authorization: Bearer ${TOKEN}" -H "Content-Type: application/json" \
  -d '{"product":{"sku":"PC-ASYNC-1","name":"Async test","price":1.5,"type_id":"simple","attribute_set_id":4,"status":1}}'
# {"bulk_uuid":"564e003a-569a-496d-8aed-647dd2c9331b","request_items":[{"id":0,"data_hash":"...","status":"accepted"}],"errors":false}

docker exec rabbitmq rabbitmqctl list_queues name messages | grep async     # async.operations.all  1
bin/magento queue:consumers:start async.operations.all --max-messages=1
docker exec rabbitmq rabbitmqctl list_queues name messages | grep async     # async.operations.all  0

After the consumer ran, the queue was empty, the product PC-ASYNC-1 existed in catalog_product_entity and the row in magento_operation had status 1 (complete). With the MySQL fallback the same flow needs consumers but no broker; that variant was not run.

Consumers must be running for any of this to work. bin/magento queue:consumers:list printed 23 consumer names in the test. In production the cron job consumers_runner (group consumers) starts them, and its behaviour is configured in env.php; the defaults come from the framework code, and are listed here from the Mage-OS source:

env.php key Meaning

cron_consumers_runner/cron_run

Whether cron starts consumers at all; default true. Set it to false when a process manager such as systemd or Supervisor runs them.

cron_consumers_runner/max_messages

Messages a consumer handles before it exits and is restarted; default 10000.

cron_consumers_runner/consumers

A list of consumer names to run; empty means all.

cron_consumers_runner/multiple_processes

A map of consumer name to process count. --multi-process on the command line only works together with this key.

queue/consumers_wait_for_messages

1 keeps a consumer polling for messages; 0 makes it exit when the queue is empty.

The ActiveMQ Artemis path uses the --stomp-* options and the STOMP framework in 2.4.5 and later, with ANYCAST addressing (point-to-point delivery to one consumer). The Mage-OS project includes mage-os/framework-stomp, but no Artemis broker was started here.

Cron

Magento does everything that is not a page request in cron: indexer updates in schedule mode, reindexing of invalid indexers, sitemap generation, currency rate updates, transactional emails queued for sending, catalogue price rule application, session and cache cleanup, newsletter queues, the queue consumers and more. There is one entry point:

bin/magento cron:install        # Crontab has been generated and saved
crontab -l
#~ MAGENTO START <64-character hash>
* * * * * /usr/local/bin/php /var/www/pocketcoffee/bin/magento cron:run >> /var/www/pocketcoffee/var/log/magento.cron.log 2>&1
#~ MAGENTO END <64-character hash>

cron:install writes that block into the crontab of the user who ran it (so run it as the file system owner), tagged with a hash so that bin/magento cron:remove removes exactly it ("Magento cron tasks have been removed"). cron:run accepts --group=<name> and --exclude-group=<name>; the groups defined in the Mage-OS install were default, index, consumers, translate_products and translate_select_attributes.

Cron behaves in two phases, which surprises people. The first cron:run only schedules jobs for the next period by writing rows to the cron_schedule table; later runs execute them. After four manual runs in a row the test table held 299 pending and 19 success rows across 29 distinct job codes, for example catalog_product_outdated_price_values_cleanup, sales_grid_order_async_insert and sales_send_order_emails, each with scheduled_at, executed_at and finished_at timestamps. SELECT status, COUNT(*) FROM cron_schedule GROUP BY status is the quickest health check. The Mage-OS 3.6.0 Admin has no grid for the table; its Stores > Configuration > Advanced > System > Cron (Scheduled Tasks) section sets, for each cron group (default, index, consumers and the two translation groups), Generate Schedules Every, Schedule Ahead for, Missed if Not Run Within, History Cleanup Every, Success History Lifetime, Failure History Lifetime and Use Separate Process. In a container remember that the crontab only fires while a cron daemon runs, so start one in the image or run a sidecar container that calls cron:run every minute.

Application modes and production deployment

Default, developer and production modes with the commands that switch them, what production mode did in the test install and the maintenance switch

The mode lives in MAGE_MODE of env.php (or an environment variable, which wins: deploy:mode:show says "Environment variables may override this value"). Default mode is what the installer leaves and is not meant for production. Developer mode shows exceptions and generates code and static files when they are requested. Production mode serves precompiled code and prebuilt static files and hides errors. Adobe on cloud infrastructure supports only production and maintenance modes.

bin/magento deploy:mode:set developer       # Enabled developer mode.
bin/magento deploy:mode:set production      # compile, deploy static content, maintenance on during the work
bin/magento deploy:mode:set production --skip-compilation   # use when generated/ was built earlier (CI)
bin/magento deploy:mode:show

Switching clears var/cache, generated/metadata, generated/code, var/view_preprocessed and pub/static (keeping .htaccess files and the static version file). In the test, on a separate Mage-OS install with the Spanish and English locales, deploy:mode:set production took 1 minute 14 seconds: "Enabling maintenance mode", compilation in 24 seconds with a peak of 514 MiB (all 9 generators: proxies, repositories, service data attributes, application code generator, interceptors, area configuration, interception cache, application action list, plugin list), static content with the quick strategy for the themes Magento/blank, Magento/luma, Magento/backend and the Mage-OS admin theme in es_ES and en_US (about 1800 files per storefront theme, 2550 to 2800 per admin theme), then "Disabling maintenance mode" and "Enabled production mode." Afterwards pub/static held 302 MB and generated/ 119 MB, MAGE_MODE was production, and the storefront answered 200 with asset URLs of the form static/version1793120232/frontend/Magento/luma/es_ES/css/styles-m.css. The number after version is the content of pub/static/deployed_version.txt and changes with every static deployment, so browsers refetch changed files while a CDN can cache them for a year.

A production deployment is therefore a build step, not a runtime step: build once in CI (composer install --no-dev, bin/magento setup:di:compile, bin/magento setup:static-content:deploy -f en_US es_ES), ship the tree and run bin/magento setup:upgrade --keep-generated plus bin/magento cache:flush on the server. Without --keep-generated the upgrade deletes the generated classes and prints the instruction to run setup:di:compile again (module:disable printed "Generated classes cleared successfully. Please run the 'setup:di:compile' command to generate classes." in the test).

Maintenance mode is independent of the three modes. bin/magento maintenance:enable made every request answer HTTP 503 in the test (curl printed 503), maintenance:status printed "Status: maintenance mode is enabled" and "List of exempt IP-addresses: none", and maintenance:disable restored 200. Add --ip=<address> to maintenance:enable, or run maintenance:allow-ips <address>, to let your own address through while you test.

A production deployment on one host is what this section describes. Running the same code as an immutable image on several Kubernetes nodes, with the database, OpenSearch, Valkey and RabbitMQ as managed services, is described in Cloud: Reference Architecture and Cloud: Container Image and Pipeline Build.

Web server recipes

The web server’s job is small: serve pub/ directly, send PHP requests to Magento and cache static files for a long time. Choose one of the two supported servers.

Apache

The test image used php:8.5-apache with PHP as an Apache module (mod_php). It needs mod_rewrite, mod_headers and mod_expires, a virtual host whose DocumentRoot is pub/ and AllowOverride All so the shipped .htaccess files (pub/.htaccess, pub/static/.htaccess, pub/media/.htaccess) take effect.

<VirtualHost *:80>
    ServerName pocketcoffee.example.test
    DocumentRoot /var/www/pocketcoffee/pub
    <Directory /var/www/pocketcoffee/pub>
        AllowOverride All
        Require all granted
    </Directory>
    ErrorLog ${APACHE_LOG_DIR}/pocketcoffee-error.log
    CustomLog ${APACHE_LOG_DIR}/pocketcoffee-access.log combined
</VirtualHost>

Source: Adobe: Apache

apachectl -M listed rewrite_module, headers_module, expires_module and deflate_module in the test image. For anything beyond a laptop put PHP-FPM behind Apache (proxy_fcgi) instead of mod_php, so web workers and PHP workers can be sized separately; that variant was not run here. The shipped .htaccess also carries a commented SetEnv MAGE_MODE line for choosing the mode from the server configuration.

Nginx with PHP-FPM

Nginx is the web server that the shipped sample configuration targets. The project root contains nginx.conf.sample, a complete set of location blocks (255 lines in 3.6.0): the PHP entry points index.php, get.php, static.php, health_check.php and the error pages, the /static/ rewrite that strips the version123456 segment, long cache headers, denial of /media/customer/, /media/downloadable/, /media/import/, /media/custom_options/ and /media/customer_address/, the setup and update entry points (denied unless you open them), a gzip block and a final deny rule for .php, .phtml, .htaccess, .htpasswd and .git anywhere else. You include it from your own server block:

upstream fastcgi_backend {
    server unix:/run/php/php8.5-fpm.sock;   # or 127.0.0.1:9000
}

server {
    listen 80;
    server_name pocketcoffee.example.test;
    set $MAGE_ROOT /var/www/pocketcoffee;
    set $MAGE_MODE production;
    include /var/www/pocketcoffee/nginx.conf.sample;
}

Source: Adobe: Nginx

nginx -t with this file, with the sample copied next to it and the upstream pointing at 127.0.0.1:9000, answered the configuration file /etc/nginx/nginx.conf syntax is ok and configuration file /etc/nginx/nginx.conf test is successful on nginx/1.30.6. No PHP-FPM pool was attached, so request handling through Nginx was not exercised. Two points need your attention. The line add_header Cache-Control "public, max-age=31536000, immutable" in the /static/ block is commented out in the sample and should be enabled in production mode only (static URLs carry the version segment, so a change in content changes the URL). And set $MAGE_MODE production only has an effect when you also pass fastcgi_param MAGE_MODE $MAGE_MODE; in the PHP location blocks; the CLI-written env.php value is the safer source.

A PHP-FPM pool for a small store. Size pm.max_children by memory, not by guesswork: measure the resident size of a worker under realistic load and divide the memory you can spare by it.

; /etc/php/8.5/fpm/pool.d/pocketcoffee.conf
[pocketcoffee]
user = www-data
group = www-data
listen = /run/php/php8.5-fpm.sock
listen.owner = www-data
listen.group = www-data
pm = dynamic
pm.max_children = 12
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
pm.max_requests = 500
php_admin_value[memory_limit] = 756M
php_admin_value[max_execution_time] = 600
request_terminate_timeout = 600s

The pool is a recipe assembled from Adobe’s PHP settings and the Nginx sample’s PHP_VALUE memory_limit=756M; it was not run in this test. pm.max_requests recycles workers to bound memory growth, and request_terminate_timeout should be at least the fastcgi_read_timeout of 600 s that the sample sets.

HTTPS and the proxy

Terminate TLS at the web server, a load balancer or a CDN, and tell Magento about it:

bin/magento config:set web/secure/base_url https://shop.example.com/
bin/magento config:set web/unsecure/base_url https://shop.example.com/
bin/magento config:set web/secure/use_in_frontend 1
bin/magento config:set web/secure/use_in_adminhtml 1
bin/magento config:set web/secure/offloader_header X-Forwarded-Proto
bin/magento config:set web/secure/enable_hsts 1
bin/magento config:set web/secure/enable_upgrade_insecure 1

Six of the seven commands were run (all but the unsecure base URL) and each answered Value was saved. With use_in_adminhtml on, a plain HTTP request to the Admin path returned 302 to an https URL, and a request carrying X-Forwarded-Proto: https was accepted with 200, which is what offloader_header is for: a TLS-terminating proxy that talks plain HTTP to PHP must announce the original scheme, or Magento treats the request as plain HTTP. HSTS and upgrade-insecure-requests should be switched on only when HTTPS works everywhere, because browsers remember HSTS. The install options --use-secure and --base-url-secure are deprecated for the same reason as the other URL options.

Varnish

Magento’s full-page cache has two backends: the built-in one (Valkey or files) and Varnish. Varnish is faster for anonymous traffic and is the production recommendation. Set the application to Varnish, generate the VCL and give the purge hosts:

bin/magento config:set system/full_page_cache/caching_application 2
bin/magento setup:config:set --http-cache-hosts=varnish:80
bin/magento varnish:vcl:generate --backend-host=web --backend-port=80 \
  --access-list=localhost --grace-period=300 --export-version=6 \
  --output-file=/var/www/pocketcoffee/var/default.vcl

On Mage-OS 3.6.0 the command is provided by the bundled elgentos/magento2-varnish-extended 2.0.7 module, whose options are --access-list, --backend-host, --backend-port, --export-version (default 6), --grace-period (default 300), --input-file, --output-file and --enable-bfcache. With only the defaults it failed on a fresh install (explode(): Argument #2 ($string) must be of type string, null given from the access-list reader); passing the values explicitly produced a 10,523-byte VCL starting vcl 4.1;, with import cookie; and import std;, a backend default with a /health_check.php probe and a ban(...) on X-Magento-Tags. The official varnish:stable container could not compile it because the cookie VMOD is not in that image, so the VCL was generated and read but not run through Varnish. Install varnish-modules (or the distribution’s package that contains libvmod_cookie) before trying it, and test with varnishd -C -f default.vcl.

CDN, HTTP/2 and images

A CDN in front of pub/static and pub/media is a configuration change, not a code change: set the Base URL for Static View Files and Base URL for User Media Files under Stores > Configuration > General > Web (the paths web/unsecure/base_static_url, web/unsecure/base_media_url and their secure twins), keep the version123 segment in the static URL so the CDN can use year-long cache headers. HTTP/2 is a web server feature (listen 443 ssl; http2 on; in Nginx 1.25.1 and later), and it reduces the benefit of bundling and merging files; Magento’s built-in bundling and merging are off by default, keep them off unless you have measured a gain, and they are disabled for Hyvä store views, as Hyvä’s documentation advises (see Frontend: Hyvä, PWA Studio and Edge Delivery). For images, catalog:images:resize pregenerates product image sizes after a bulk import, and the sample’s commented image_filter block shows how Nginx can resize catalogue images instead of PHP when its image-filter module is loaded. These items are summarised from the Adobe pages and the configuration sample; none of them was run here.

After the install

  • Take the write bit off app/etc. The installer asks for it. chmod -R a-w app/etc after you finish; temporarily restore it only when you change env.php or config.php by hand.

  • Keep the secrets out of Git. env.php contains the encryption key (crypt), the database password and the GraphQL salt. Commit config.php (settings that are the same in every environment) and keep env.php per environment, outside the repository or encrypted. Rotate the encryption key after any compromise with bin/magento encryption:key:change, and re-encrypt stored data with encryption:data:re-encrypt when you change it.

  • Use a random Admin URI and two-factor authentication. Both are on by default; two-factor authentication for the Admin is provided by the Magento_TwoFactorAuth module and is why an admin API token cannot be created until a second factor is set (the endpoint answered "Please ask an administrator with sufficient access to configure 2FA first." on a default install). Disabling the module is a local-development shortcut; see Security.

  • Patch. Adobe has published seven security bulletins for Commerce and Magento Open Source in 2026 so far, one of them a CVSS 10 flaw exploited in the wild (APSB26-146, 2026-09-07). Install the newest patch or isolated build before launch and subscribe to the bulletins; the Security Scan Tool is free and covers Magento Open Source. Mage-OS publishes its own security releases (3.6.0 is one).

  • Back up. bin/magento setup:backup --code --db writes code and database archives under var/backups, which is a convenience, not a backup strategy: dump the database with your database tools and copy pub/media, and test a restore.

  • Skip sample data in production. bin/magento sampledata:deploy and the --use-sample-data option exist for demonstrations. sampledata:remove and sampledata:reset undo them in composer.json.

  • Do not run setup:install against a store with data unless you mean to, and never with --cleanup-database on a production database.

Moving from Magento 1 with the Data Migration Tool

Magento 1 reached end of life in June 2020. Adobe still documents and maintains the Data Migration Tool (DMT), a command-line tool that copies a Magento 1 database into a fresh Magento 2 database. It moves products, customers, orders, stores and configuration, promotions and more; it does not move code, extensions or themes. Those are rebuilt for Magento 2 (the Code Migration Tool helps convert extension code, and the Marketplace has Magento 2 versions of most extensions). The tool supports migrating from Magento Open Source 1.6.x to 1.9.x and from Adobe Commerce 1.11.x to 1.14.x, and from Open Source 1.x to Adobe Commerce 2. The newest release on GitHub on 2026-10-27 was 2.4.8.1 (2025-06-03); no 2.4.9 tag existed yet, and the package version must match the Magento 2 codebase, so check the releases page before you plan a migration to 2.4.9.

Data Migration Tool: settings, data and delta modes, each with integrity check, data transfer and volume check, followed by the manual cutover

Terms

  • A mode is an ordered set of operations: settings, data, delta, run in this order.

  • A step is a task inside a mode that decides what kind of data is migrated (the settings mode has a Stores step and a Settings step).

  • A stage is the unit inside a step: Integrity Check (compare field names and types between the two schemas), Data Transfer (copy table by table) and Volume Check (compare record counts).

  • Map files are XML files that describe differences between the Magento 1 and 2 tables. When a difference is not declared the tool shows an error and does not start; tables created by third-party extensions are copied as they are when nothing differs.

Prepare

  1. Install Magento 2 normally (this page) with a topology at least equal to the Magento 1 system, then take a database dump immediately so you can restore the initial state.

  2. Do not start Magento 2 cron jobs. Stop the Magento 1 cron jobs.

  3. Do not create products, categories or attributes in Magento 2 first: the tool overwrites them with the Magento 1 ones.

  4. Test on a copy of the Magento 1 database, not on the production one, and delete logs, quotes, recently viewed and compared products, visitors, event categories and old promotional rules first. Open ports between the tool host and both databases; with binary logging on in Magento 1, set log_bin_trust_function_creators=1 or grant SUPER.

  5. During the whole run: no changes in the Magento 1 Admin except order management (shipping, invoices, credit memos), no code changes, no changes in the Magento 2 Admin or storefront. Operations in the Magento 1 storefront are allowed.

Install and configure

The tool is a Composer package whose version must match the Magento 2 version:

composer config repositories.data-migration-tool git https://github.com/magento/data-migration-tool
composer require magento/data-migration-tool:<version>
bin/magento --version        # the tool version must match this

Configuration lives under vendor/magento/data-migration-tool/etc/opensource-to-opensource, opensource-to-commerce and commerce-to-commerce, with one subfolder per supported version. Adobe recommends keeping your copy in a module so it is under source control, with Magento_DataMigrationTool in the module’s sequence. Copy config.xml.dist to config.xml and set the two databases and the encryption key:

<source>
    <database host="127.0.0.1" name="magento1" user="root" password="<magento1-db-password>"/>
</source>
<destination>
    <database host="127.0.0.1" name="magento2" user="root" password="<magento2-db-password>"/>
</destination>
<options>
    <source_prefix>magento1</source_prefix>
    <crypt_key><key-from-magento1-local-xml></crypt_key>
    <direct_document_copy>1</direct_document_copy>
</options>

crypt_key must be filled: it is the value of the <key> element in app/etc/local.xml of Magento 1, needed to re-encrypt stored values. The optional password, port and <source_prefix>, <dest_prefix> handle non-default connections and table prefixes, and TLS attributes can be added to the database element. direct_document_copy is Adobe’s performance tip, and it requires both databases on the same MySQL server with an account that can read both.

Run the three modes

bin/magento migrate:settings [-r|--reset] [-a|--auto] /path/to/config.xml
bin/magento migrate:data     [-r|--reset] [-a|--auto] /path/to/config.xml
bin/magento migrate:delta    [-r|--reset] [-a|--auto] /path/to/config.xml

--auto keeps going when an integrity check fails; --reset starts again from the beginning (restore the Magento 2 database dump first). The tool saves its progress and resumes from the last good state. The data mode adds tables named m2_cl_<table> and INSERT, UPDATE, DELETE triggers to the Magento 1 database so the delta mode can find what changed; the delta mode then runs continuously, restarting every 5 seconds, until you stop it with Ctrl-C. The delta mode covers customer and order activity from the storefront and Admin operations on orders, products and categories; it does not carry attributes or CMS pages created in the Magento 1 Admin after the data run, and data from extension tables needs an extra deltalog.xml entry and a delta class that extends Migration\App\Step\AbstractDelta.

Adobe’s benchmark on a 2.5 GB, single-core virtual machine with 177,000 products, 355,000 orders and 214,000 customers took about 10 minutes for settings and about 9 hours for data (all data except URL rewrites), with a downtime of "a few minutes to reindex and change DNS" afterwards.

Cutover

After you have tested the new store thoroughly: put Magento 1 into maintenance mode and stop all Admin activity, let the delta catch up, start the Magento 2 cron jobs, run bin/magento cache:flush and bin/magento indexer:reindex, and change DNS and the load balancer to the Magento 2 servers. Plan for a cold full-page cache. Keep redirects from the old URLs when the URL structure changes (see Admin and Store Setup for URL rewrites).

The Data Migration Tool was not run for these pages, because it needs a Magento 1 database and the tool package from repo.magento.com or a GitHub tag that matches the Magento 2 codebase. The commands, options and behaviour above are from Adobe’s documentation, last updated 2026-10-08, and from the repository’s tags.

What could not be run

Everything described as run in this page was run on 2026-10-27. The following was documented from the official sources and not executed: the download from repo.magento.com (needs a personal key pair) and every Adobe Commerce package; Adobe’s 2.4.9 installer code, which differs from Mage-OS 3.6.0 in places you might meet (see the Valkey session finding, and the removal of the webapi.xsd and csp_whitelist.xsd schemas that Mage-OS 3.6.0 still ships); a PHP-FPM pool and Nginx serving traffic (only nginx -t was run); Varnish serving the generated VCL (the cookie VMOD was missing from the image); ActiveMQ Artemis; a CDN; HTTP/2; the guided bin/magento install wizard; the Data Migration Tool; a real production server with systemd or Supervisor consumers; and a TLS certificate from a certificate authority.