What is bin/magento setup:upgrade ?
bin/magento setup:upgrade is the Magento 2 CLI command that applies module install/upgrade data. It walks every enabled module under Setup/Patch/Schema and Setup/Patch/Data, runs each unapplied patch in dependency order, updates the setup_module and patch_list tables, rebuilds app/etc/config.php with the current enabled-modules array, and invalidates the config cache. Required after composer require, module:enable, module:disable, or a Magento core version bump. In production mode you must follow it with setup:di:compile and setup:static-content:deploy — and wrap the whole thing in maintenance:enable / maintenance:disable.
Five steps from maintenance:enable to cache:flush
setup:upgrade is not a one-liner on production — it’s the middle of a five-command pipeline. Skip any step and your storefront either fatals or serves stale assets. Here is the wiring, end to end.
-
01
Enable maintenance mode before you touch the DB
Run
bin/magento maintenance:enablefirst. This drops a flag file atvar/.maintenance.flag; Magento’spub/index.phpbootstrapper checks for it on every request and returns the503 errors/503.phtmlpage until you re-enable. Skip this step on production and you will have customers hitting a half-migrated schema mid-checkout — the user table is the new shape, the order table is the old shape, the storefront fatals. Wrap your full deploy in the maintenance window: enable, upgrade, di:compile, static-content:deploy, disable. Optionally pass--ip=<your-ip>so you can verify the site yourself while customers see the 503. -
02
Run setup:upgrade — the actual work
Run
bin/magento setup:upgrade. Internally Magento walks every enabled module underapp/code/<Vendor>/<Module>/Setup/Patch/Schema/*.phpandSetup/Patch/Data/*.php, sorts them by theirgetDependencies()return, and applies each unapplied patch in order. Each applied patch writes a row to thepatch_listtable (patch_name = FQCN). Thesetup_moduletable’s schema/data version pointers move forward,app/etc/config.phpis rewritten with the current enabled-modules array, and the config cache is invalidated. In default-mode installs this step also re-generatesgenerated/code/proxies/interceptors; in production mode it should not (see Step 03 + the di:compile FAQ below). -
03
(Production mode) setup:di:compile — pre-generate proxies
On a production-mode install, run
bin/magento setup:di:compilenext. This walks everydi.xmland pre-generates theProxy,Interceptor, andFactoryclasses undergenerated/code/, plus the dependency-injection plan caches undergenerated/metadata/. Production mode refuses to auto-generate these on-the-fly (default mode does); without di:compile your storefront throwsProxy.php: No such file or directorywithin seconds of the first request. Run thissetup:upgradefirst, thensetup:di:compile, never the other way around — di:compile needs the new DB schema to walk the new module’s di.xml correctly. -
04
(Production mode) setup:static-content:deploy — build the asset bundle
Run
bin/magento setup:static-content:deploy -f en_US <other-locales>. This compiles every theme’s LESS to CSS, bundles RequireJS modules, hashes filenames for cache-busting, and writes the result topub/static/frontend/<theme>/<locale>/. The-f(force) flag wipes the existingpub/staticdirectory before redeploying — necessary because Magento’s static-content cache will otherwise reuse stale CSS even after a LESS edit (seefeedback_static_content_force_rebuild). Run for every locale your stores expose, not justen_US, or non-default-locale storefronts will 404 on stylesheets. -
05
Disable maintenance + flush caches — back online
Run
bin/magento maintenance:disablefollowed bybin/magento cache:flush. The maintenance flag is removed, Redis/Varnish caches are invalidated, and the storefront serves the new schema + new static assets to live customers. Tailvar/log/system.logandvar/log/exception.logfor the first 10 minutes — any patch-related fatal will surface here. If you seeArea code not setorlog_bin_trust_function_creatorserrors at this stage, the upgrade itself failed mid-flight; re-enable maintenance, fix the underlying issue (see FAQs below), and re-run from Step 02.
Four scenarios where setup:upgrade is the next command you must run
Most Magento developers only think to run setup:upgrade after composer require. There are three more situations where forgetting it produces hours of debugging.
-
After composer require of any new Magento module
Any time you run
composer require vendor/module-foo, the new module ships its ownSetup/Patch/Schema/*.phpandSetup/Patch/Data/*.phpfiles expecting to be applied. Until you runbin/magento module:enable Vendor_Foofollowed bybin/magento setup:upgrade, the module’s tables don’t exist, its seed data isn’t in the DB, and any controller it ships will fatal on first request because its declared DI dependencies don’t resolve. This is the most common reason to run setup:upgrade and the only one most Magento developers think of. -
After a Magento core version bump (e.g. 2.4.7 to 2.4.9)
A Magento core upgrade ships dozens of Adobe-authored data and schema patches under
vendor/magento/module-*/Setup/Patch/. Thecomposer updatebrings the new code;setup:upgradebrings the DB shape. Without it,setup_module.schema_versionis still on the old Magento version, the newquoteandsales_ordercolumns don’t exist, and the new admin pages fatal on missing tables. Adobe’s release notes for every minor version list which patches run — on 2.4.7→2.4.9 you’re looking at 40+ schema patches and 60+ data patches plus a fresh declarative schema diff. -
After enabling or disabling a module via module:enable / module:disable
When you toggle a module’s enabled state, Magento rewrites the
modulesarray inapp/etc/config.php— but only the nextsetup:upgradeactually picks up the change end-to-end and re-syncssetup_module, runs any pending patches the newly-enabled module ships, and (in default mode) refreshesgenerated/code/for the new DI plan. Disabling a module without re-running setup:upgrade leaves stale proxies pointing at classes that no longer load; symptom isClass Vendor\Foo\Block\X does not existintermittently invar/log/exception.log. -
On a fresh clone / staging slot loaded from a prod DB dump
When you bootstrap a new dev box or staging environment from a production DB dump, the
patch_listtable comes pre-seeded with every patch that has already run on prod.bin/magento setup:upgradeon the fresh box compares the dump’spatch_listagainst the code’s patches, finds them all already applied, and exits cleanly. The reverse path (fresh DB fromsetup:install) only works if your code state is exactly the same Magento version that the install command targets — otherwise the schema patches expect a starting state that doesn’t exist and fatal mid-migration.
Three setup:upgrade mistakes that produce hours of incident response
Every "Magento broke after deploy" call I’ve been pulled into started with one of these three. Audit your deploy pipeline before the next release.
-
Running setup:upgrade as the root user
If you SSH in as root and run
bin/magento setup:upgradedirectly, every file Magento writes undergenerated/,var/cache,var/page_cache,var/view_preprocessed, andpub/staticends up owned byroot:rootwith mode0644/0755. Your PHP-FPM web user (typicallywww-data,nginx, ormagento_www) then can’t write to those paths on subsequent requests, and you get intermittent 500s withPermission deniedfillingvar/log/system.log. Alwayssudo -u <web-user> bin/magento setup:upgradeor SSH in as the web user directly. The fix after the fact ischown -R <web-user>:<web-user> var/ generated/ pub/static/. -
Shipping an edited data patch without clearing patch_list
Magento’s
DataPatchInterfaceis fire-once. Once a patch’s FQCN lands inpatch_list,setup:upgradewill silently skip it forever — even if you edit the body and re-deploy. The fix is to either (a) write a brand-new patch class with a new FQCN, or (b)DELETE FROM patch_list WHERE patch_name = 'Vendor\Module\Setup\Patch\Data\Foo'before running setup:upgrade again. This is documented infeedback_data_patch_idempotencyand bites every Magento dev exactly once. Design data patches to be idempotent (insert-or-update, not insert-only) so re-runs are safe. -
Running setup:upgrade on production without maintenance mode
Without
bin/magento maintenance:enable, customer requests keep hittingpub/index.phpwhile the schema is mid-migration. Thequotetable has the new column but thecheckoutblock still reads the old DI plan; carts fatal, orders place against the old schema, and the resulting data corruption takes hours to unpick from the order log. Even worse: on a multi-store install you can end up with one store view seeing the new schema and another seeing a half-cached old plan becausecache:flushhasn’t happened yet. Always:maintenance:enable→setup:upgrade→di:compile→static-content:deploy→cache:flush→maintenance:disable. Never skip the wrapper.
bin/magento setup:upgrade — frequently asked questions
-
setup:upgrade vs setup:db-schema:upgrade vs setup:db-data:upgrade — what’s the difference?
<code>bin/magento setup:upgrade</code> is the umbrella command — it internally runs both <code>setup:db-schema:upgrade</code> (applies every unapplied <code>Setup/Patch/Schema/*.php</code> and processes <code>db_schema.xml</code> declarative schema diffs) and then <code>setup:db-data:upgrade</code> (applies every unapplied <code>Setup/Patch/Data/*.php</code>). The two split commands exist so you can apply schema-only changes during the maintenance window and run data-only patches later out-of-band on a smaller window — useful when the data patch is expensive (millions of rows) but the schema change is trivial. 95% of the time you just run <code>setup:upgrade</code> and let Magento orchestrate both. The split-command pattern is documented in Adobe’s DevDocs but underused in practice; worth knowing about for big-table data migrations. -
Why does setup:upgrade fail with "Area code not set" or "log_bin_trust_function_creators"?
"Area code not set" almost always means a data patch is calling a class that tries to resolve a store context before Magento has bootstrapped a store — the patch is running in <code>adminhtml</code> area but the class wants <code>frontend</code>. Wrap the offending call in <code>$this->appState->emulateAreaCode(Area::AREA_FRONTEND, fn() => ...)</code>. "log_bin_trust_function_creators" is a MySQL replication-safety guard — Magento’s declarative schema diff creates stored functions, and binary-log-replicated DBs refuse them unless the flag is on. Fix is <code>SET GLOBAL log_bin_trust_function_creators = 1</code> as root, but the value resets on MySQL restart — bake it into <code>my.cnf</code> or your docker compose (see <code>feedback_mysql_log_bin_trust_persistent</code>) so it survives restarts. -
Can setup:upgrade run without maintenance mode?
In a local dev environment, yes — nobody is hitting your storefront, the schema change risk is zero, and you can iterate fast. In a staging environment with a small test team, yes if you have a Slack heads-up culture and accept that any QA session in flight will fatal. In production, never. Customers will hit a half-migrated schema between the moment <code>setup:upgrade</code> alters the first table and the moment it finishes the last data patch — that window is anywhere from 10 seconds (small store, no schema diff) to 20 minutes (multi-GB tables, big declarative-schema delta). Production deploys without <code>maintenance:enable</code> are how stores end up with corrupt orders, half-imported customers, and angry support tickets. Wrap every production upgrade in the maintenance window, every time. -
How do I re-run a data patch that already applied?
Magento sees a patch as applied when its FQCN exists as a row in <code>patch_list</code>. To force a re-run: <code>DELETE FROM patch_list WHERE patch_name = 'Vendor\Module\Setup\Patch\Data\YourPatchName';</code> followed by <code>bin/magento setup:upgrade</code>. Magento will see the patch as unapplied and execute it again. Important: design data patches to be idempotent — use <code>INSERT … ON DUPLICATE KEY UPDATE</code> or check <code>SELECT</code>-then-<code>INSERT</code>, never plain <code>INSERT</code> — otherwise the re-run produces duplicate rows. The full rationale is in <code>feedback_data_patch_idempotency</code>. The alternative is to write a brand-new patch class with a new FQCN that performs the corrected change; less surgical but lower risk on production. -
Why does setup:upgrade re-compile generated/code on production after I already ran di:compile?
It shouldn’t — and on a properly-configured production-mode install, it doesn’t. The trick is the <code>--keep-generated</code> flag and the deploy mode. In default mode, <code>setup:upgrade</code> wipes <code>generated/code/</code> at the start so Magento can regenerate proxies on-the-fly during request handling. In production mode (<code>bin/magento deploy:mode:set production</code>), Magento refuses to regenerate at runtime — you must run <code>setup:di:compile</code> after <code>setup:upgrade</code>. If you’ve already pre-built <code>generated/code/</code> as part of an immutable deploy artifact and just want <code>setup:upgrade</code> to apply DB patches without touching the generated tree, pass <code>--keep-generated</code>. Then run di:compile only if a module’s di.xml actually changed. -
What does setup:upgrade do under the hood, end to end?
Five phases. (1) Magento reads <code>app/etc/config.php</code> for the enabled-modules array and walks every enabled module’s <code>Setup/Patch/</code> directories. (2) It computes the dependency DAG from each patch’s <code>getDependencies()</code> and sorts patches topologically. (3) For each unapplied schema patch (FQCN not in <code>patch_list</code>), it runs <code>apply()</code>, then INSERTs the FQCN into <code>patch_list</code>. The same for declarative <code>db_schema.xml</code> diffs — Magento computes the diff between current DB state and the declared XML and runs the ALTER TABLEs. (4) For each unapplied data patch, same loop. (5) <code>setup_module.schema_version</code> and <code>data_version</code> for each module are updated, <code>app/etc/config.php</code> is rewritten with the current enabled-modules list (in case <code>module:enable</code>/<code>module:disable</code> changed it), and the config cache is invalidated so the next request re-reads the new module list and applied patches. In default mode there’s a sixth phase: wipe and regenerate <code>generated/code/</code>. Production mode skips that phase — you run di:compile separately.
Want a zero-downtime deploy pipeline on your Magento store?
Send your repo URL and prod host details — I will audit your maintenance-mode wrapper, setup:upgrade timing, di:compile parallelism, static-content:deploy locale list, and patch_list idempotency, then reply with a written tuning plan, fixed-price quote, and earliest start date. 24-business-hour turnaround.