Need a better deployment process for Magento 2? Zero downtime
The original video this piece is based on.
This is rebuilt from a run of videos I recorded between early 2021 and the summer of 2022, when I spent a lot of time trying to deploy Magento 2 stores without taking them offline in front of paying customers. The tools have moved on, so treat this as the thinking rather than a copy-paste script. The awkward bit of Magento deployment is the same now as it was then.
Let me start with the honest part, because I got this wrong for a long time. There is no such thing as a truly zero-downtime Magento deploy, not for any deploy that changes the database schema. Magento needs setup:upgrade to run for those changes to land, and you should not run setup:upgrade against a live database with customers on the site. So what people sell as zero downtime is really minimum downtime. Once you accept that, you can build a sane process. Chase the myth instead and you end up with a clever pipeline that quietly corrupts a checkout.
The three stages most stores go through
Almost every Magento store moves through the same three stages of deployment, each one adopted because the last one hurt enough.
- Manual FTP. You drag changed files onto the server by hand. It works for one developer on a tiny site, and falls apart the first time two of you touch the same file or you forget to upload one.
- A deploy script with downtime. You put the site in maintenance mode, run composer install, setup:upgrade, di:compile, static content deploy, flush the cache, then drop maintenance. This is fine. It is also five to twenty minutes of the shop being shut, every single deploy, even when you only changed a label.
- Release folders with a symlink switch. You build the new version in a fresh folder, then flip a symlink so it goes live in one atomic move. This is the one worth setting up, and it is what the rest of this is about.
How the release-folder approach actually works
The idea is simple even if the wiring takes an afternoon. Every deploy creates a new timestamped folder on the server. Your code and dependencies build inside it while the live site keeps serving customers from a different folder. Nothing the customer sees changes until the end, when you point a current symlink at the new release. That switch is instant.
The order you run things in is where the savings come from. You want all the slow work done before you flip the symlink, because it is invisible to the live site. So inside the new release folder, before it goes live, you run composer install, dump-autoload optimised, static content deploy and di:compile. Those are the commands that take real time, and the customer is on the old release the whole time they run.
The trick that makes most deploys genuinely zero downtime
Here is the part that took me a colleague's prompting to get right. Not every deploy needs setup:upgrade. If you have only changed a template, some CSS, or a bit of front-end logic, the database schema has not moved and there is nothing to upgrade. So before you blindly run it, check whether the database actually needs it.
Magento can tell you. Run setup:db:status and it reports whether a schema or data upgrade is pending. If nothing is, you skip setup:upgrade, flip the symlink, and that deploy has zero downtime. A large share of day-to-day deploys fall into this bucket, which is the whole win. You only pay the downtime cost on deploys that genuinely change the database.
When it does need an upgrade, then and only then do you put the site into maintenance mode, run setup:upgrade with the keep-generated flag, drop maintenance, and flush the cache. That flag matters more than it looks. Without it, setup:upgrade wipes the static content you just spent minutes building, and you rebuild it from scratch behind the maintenance page. With it, setup:upgrade does the database work and leaves your built files alone. On a real store that took my schema-changing deploys down to about ninety seconds of maintenance instead of fifteen or twenty minutes.
The OPcache gotcha nobody warns you about
This one cost me an embarrassing amount of time. PHP's OPcache holds the old file paths in memory, so even after you switch the symlink, the server can keep serving the previous release until the cache clears. The nasty version of this is that your maintenance page sometimes does not appear, because OPcache is still pointing at the old code that says the site is up. You stand there convinced your deploy is broken when it is just stale bytecode.
The fix is to flush OPcache as part of the deploy. The catch is that on most servers you can only flush it as root, and you do not want to deploy as root. The workaround I landed on was a tiny opcache-flush script that only runs when hit via a front-end URL, so the deploy can trigger it as part of the run. Ugly, but reliable.
The pipeline around all this
None of this should be run by hand from your laptop. The release-folder dance belongs in a pipeline: your code lives in a git repository, a deployment service watches the branch you deploy from, and on a push it runs your build commands in a new release folder and flips the symlink. I used Bitbucket with DeployBot, and later DeployHQ. The specific tool matters far less than the shape. Container-based hosts can get closer to true zero downtime by running setup:upgrade in a separate container while the live nodes keep serving.
One thing that is easy to underrate: your .gitignore is part of your deployment. The vendor folder, var, generated and pub/static are all build output and have no business in the repository. Get it wrong and your first push is tens of thousands of generated files, and your deploys are slow and fragile from then on. Spend time on the ignore rules before your first commit, not after.
On rollbacks, briefly
People assume release folders give you easy rollbacks. They half do. You can flip the symlink back in a second, and for a pure code change that is fine. But the moment a deploy ran setup:upgrade, your database has moved, and pointing the symlink at old code that expects an old schema is its own kind of broken. The honest move is usually to roll forward: fix the thing and deploy again. So I keep a couple of old release folders, not dozens.
Why I do not miss any of this
Reading this back, the amount of care it takes to ship a small change to a Magento store safely is the thing that stands out. Release folders, conditional upgrades, keep-generated flags, OPcache scripts, a pipeline, careful ignore rules, and you still cannot honestly call it zero downtime. That weight is a big part of why I moved most of my work to a headless setup, where shipping a change is a static rebuild and a CDN swap with nothing to take offline. I wrote about that in why I moved from Magento to headless. If you are still on Magento, the process above is the one I would run. I just would rather not have to.