Magento zero downtime deployment: how to run the Magento 2 deploy process without maintenance mode

Last updated July 31, 2026
# the short answer

Magento zero downtime deployment works by splitting the deploy into a build phase and a deploy phase. Everything slow and safe happens first in a fresh release directory while the old release keeps serving: composer install, setup:di:compile and setup:static-content:deploy. The live switch is then a single symlink repoint followed by a PHP FPM reload. The catch is setup:upgrade: if a release contains schema or data patches, Magento needs maintenance mode unless those changes are written to be backward compatible. Run bin/magento setup:db:status before every deploy to find out which kind of release you are shipping.

Skip the manual setup. DeployManage provisions your server and ships zero-downtime deploys on any cloud.

$ get started free

Out of the box, a Magento 2 deploy is a maintenance page. The standard sequence puts the store into maintenance mode, pulls code, runs composer install, compiles dependency injection, deploys static content, runs setup:upgrade and flushes cache. On a real catalogue that is anywhere from two minutes to the better part of an hour, and on a multi server setup it is worse. For a store doing meaningful revenue, that window is the whole problem.

The good news is that most of that sequence does not need the store to be down at all. It only looks that way because the default process does the work in the live directory.

Why does Magento 2 need maintenance mode?

Magento needs maintenance mode because the default deploy modifies the running installation in place. While composer install is swapping vendor files and setup:static-content:deploy is regenerating pub/static, requests hitting the store can load a half updated codebase. Maintenance mode is protection against serving that inconsistent state, not a requirement of the framework itself.

Once you stop deploying in place, most of the reason for the maintenance page disappears. That is exactly what Adobe's pipeline deployment model, introduced in Magento 2.2, is built around.

The build phase and the deploy phase

Pipeline deployment splits the work into two phases with very different risk profiles.

The build phase produces a complete, ready to run copy of the application without touching the live site or the database. It runs in a new release directory (or on a separate build machine entirely):

  • composer install --no-dev --optimize-autoloader
  • bin/magento setup:di:compile
  • bin/magento setup:static-content:deploy -f <locales>

None of that reads or writes the production database, and none of it is visible to customers. It is also the slow part, which is the useful bit: the expensive minutes are spent while the old release is still happily serving traffic.

The deploy phase is what actually changes the live system:

  • bin/magento setup:upgrade --keep-generated applies schema and data changes
  • bin/magento app:config:import imports configuration from app/etc/config.php
  • the symlink switch, then cache flush and a PHP FPM reload

The --keep-generated flag matters. Without it, setup:upgrade will discard the generated code and static content you just built, and you are back to compiling on a live server.

The release directory and symlink model

The structure is the same one used by zero downtime deployment tools generally, and it is worth setting up even if you script it yourself:

/var/www/store/
  releases/
    20260731093000/
    20260730141500/
  shared/
    app/etc/env.php
    pub/media/
    var/log/
  current -> releases/20260731093000

Your web server document root points at current/pub. A deploy builds into a new folder under releases/, symlinks the shared paths into it, and only at the end repoints current. Because repointing a symlink is a single filesystem operation, there is no moment where some requests get old code and others get new code.

Three things must be shared rather than rebuilt: app/etc/env.php (the environment specific config), pub/media (customer and catalogue uploads, which must never be tied to a release), and var/log so history survives. Everything else should be produced by the build.

How do you know if a deploy can be zero downtime?

Run bin/magento setup:db:status before you deploy. It compares the schema declared by your modules against the live database and returns exit code 0 when everything is up to date, 1 when module versions and the database disagree, and 2 when setup:upgrade is required. Exit code 0 means this release can go out with no maintenance window at all.

That single check is the honest dividing line, and it is worth wiring into the pipeline as a gate rather than running by hand:

What the release containsNeeds maintenance mode?Why
Template, layout, CSS or JS changes onlyNoBuilt entirely in the build phase, switched by symlink
New PHP classes, plugins, observersNoCompiled during the build, no database involvement
Config changes via app/etc/config.phpUsually noapp:config:import is fast, though it flushes cache
Additive schema change (new column or table)Usually noSafe if old code ignores the new column and it is nullable or defaulted
Destructive schema change (drop or rename)Yes, or split the releaseOld code still running will break the moment the column disappears
Large data patch backfilling rowsDepends on table sizeLong running writes can lock; batch it outside the deploy instead

Schema changes are the real constraint

Magento 2.3 and later use declarative schema, so db_schema.xml describes the target state and setup:upgrade works out the difference. That makes schema changes easier to write and does nothing to make them safe to deploy, because the problem is not the migration mechanism. The problem is that for a few seconds, code from the old release and code from the new release are both talking to the same database.

The way out is the expand and contract pattern: add the new column in one release while old code ignores it, write to both old and new in the next, backfill, then remove the old column in a later release once nothing reads it. It takes three or four deploys instead of one, which is the price of never taking the store down. The mechanics generalise across frameworks and are worked through in detail in our guide to zero downtime database migrations.

Do not forget opcache

This is the step that catches people out on their first symlink based Magento deploy. PHP's opcode cache keys entries by resolved file path. After you repoint current, PHP FPM can keep serving the compiled bytecode from the previous release, so the switch appears to do nothing, or worse, mixes old and new classes.

Reload PHP FPM (systemctl reload php8.3-fpm or equivalent) as the step immediately after the symlink flip. A reload is graceful: in flight requests finish on the old workers while new ones start against the new release. Do not use a hard restart, which drops connections and undoes the point of the exercise.

Blue green for multi server Magento

The symlink approach works well on a single application server. Once you are running several behind a load balancer, the release has to land on all of them and the switch has to be coordinated, otherwise nodes serve different versions for as long as the rollout takes.

Two options. Roll the release out node by node, draining each from the load balancer first, which is simple but means mixed versions during the rollout and therefore demands backward compatible database changes. Or run two full environments and cut the load balancer across in one move, which avoids mixed versions and costs you a second set of servers. The tradeoffs are compared in blue green versus rolling deployment.

Verify more than the HTTP status

A health check that only asks for a 200 on the homepage will happily approve a release where the cart page renders and the add to cart button silently does nothing. For a store, check something transactional: add a product to a cart, hit the checkout step, confirm the price is right. Those are also the pages worth auditing for copy and layout problems separately from deploys, since a checkout can be technically healthy and still leak revenue.

What a full Magento deploy pipeline looks like

  1. Run bin/magento setup:db:status and record the exit code.
  2. Create a new release directory and clone the target commit into it.
  3. Symlink shared paths: app/etc/env.php, pub/media, var/log.
  4. Build: composer install --no-dev --optimize-autoloader, then setup:di:compile, then setup:static-content:deploy for your locales.
  5. If step 1 returned 0, skip ahead. If not, decide: backward compatible patch, or an announced maintenance window.
  6. Run bin/magento setup:upgrade --keep-generated.
  7. Run bin/magento app:config:import if configuration changed.
  8. Health check the new release directly, including one transactional path.
  9. Repoint current to the new release.
  10. Reload PHP FPM, flush cache, and warm the pages that matter.
  11. Keep the previous release on disk. That is your rollback.

Steps 2 through 11 are the same shape for every PHP application, which is why deployment management software exists rather than every team maintaining its own version of this script. DeployManage provisions the server, runs this release model with health gated switching, and keeps previous releases ready so a rollback is a symlink repoint rather than an emergency rebuild.

Rolling back a Magento release

Code rollback is easy in this model: point current at the previous release directory and reload PHP FPM. Dependencies and static content are already built there, so it takes seconds.

The database is the part that does not roll back. If the release you are reverting applied a schema change, the older code has to be able to run against the newer schema, which is precisely what expand and contract buys you. Teams that skip that discipline discover during an incident that their rollback path does not actually exist.

Ready to stop managing servers by hand? DeployManage provisions, deploys and monitors your fleet from one dashboard.

$ get started free

Frequently asked questions

Can Magento 2 be deployed with zero downtime?

Yes, for most releases. Split the deploy into a build phase that runs composer install, setup:di:compile and setup:static-content:deploy in a new release directory, then switch the live symlink and reload PHP FPM. Releases containing destructive schema changes are the exception and need backward compatible migrations instead.

Why does Magento go into maintenance mode during deployment?

Because the default deploy modifies the live installation in place. While vendor files and static content are being regenerated, incoming requests can hit a partially updated codebase. Maintenance mode prevents customers seeing that inconsistent state. Building in a separate release directory removes the need for it.

What is the difference between the build phase and the deploy phase in Magento?

The build phase creates a ready to run copy of the application without touching the database or the live site: composer install, dependency injection compilation and static content deployment. The deploy phase applies database changes with setup:upgrade, imports config, and switches traffic to the new release.

Do I still need to run setup:upgrade on every deploy?

No. Run bin/magento setup:db:status first. Exit code 0 means the database already matches what your modules declare, so setup:upgrade has nothing to do and can be skipped. Exit codes 1 and 2 mean the release does change the schema and needs handling.

What does setup:upgrade --keep-generated do?

It stops setup:upgrade from deleting the generated code and static content produced during the build phase. Without the flag, Magento regenerates those artifacts on the live server, which is slow and defeats the purpose of building them ahead of time.

Which Magento directories should be shared between releases?

Three at minimum: app/etc/env.php for environment configuration, pub/media for catalogue and customer uploads which must never belong to a single release, and var/log so log history survives deploys. Everything else should be produced fresh by the build.

Why does my Magento site serve old code after switching the symlink?

PHP's opcode cache keys compiled files by resolved path, so PHP FPM can keep serving bytecode from the previous release after the symlink moves. Reload PHP FPM immediately after the switch. A graceful reload lets in flight requests finish on the old workers.

How do you roll back a Magento deployment?

Repoint the current symlink at the previous release directory and reload PHP FPM. That takes seconds because the older release still has its dependencies and static content built. Database changes are not reversed, so schema migrations must be written so the previous code still runs against the new schema.

# related

Provision and deploy from one dashboard.

Connect a provider and ship your first zero-downtime deploy in minutes.

$ get started free

# free plan · no credit card required