Releases without downtime, fully under control
Two identical production environments, a controlled traffic switch and compatible database migrations turn a risky Magento release into something customers never notice. This article explains the complete architecture of Blue-Green Deployment for Magento 2, from the infrastructure all the way to automated rollback.
Table of Contents
- 1. What Blue-Green Deployment means and why classic Magento deployment causes downtime
- 2. Infrastructure prerequisites: two identical production environments and the traffic switch
- 3. Keeping database migrations compatible: the expand-contract pattern in db_schema.xml
- 4. Shared state between Blue and Green: Redis sessions, cache backend, message queue
- 5. Switching the search index without downtime: the Elasticsearch/OpenSearch index alias strategy
- 6. Static content deployment and the build pipeline for both environments in parallel
- 7. The traffic switch mechanism: load balancer/Nginx upstream cutover and health checks before switching
- 8. Rollback strategy: switching straight back to the previous environment on failure
- 9. CI/CD integration: an automated Blue-Green workflow in the pipeline
- 10. Summary
- 11. FAQ
1. What Blue-Green Deployment means and why classic Magento deployment causes downtime
Blue-Green Deployment is an architecture pattern in which two fully identical production environments run in parallel, usually labeled "Blue" and "Green". At any given moment exactly one of the two environments serves real customer traffic, while the other is being prepared for the next release. Once the new version is deployed, tested and verified through health checks, a load balancer flips all traffic from the old environment to the new one in a single, near-atomic step. For the customer sitting in a browser there is no visible transition, no maintenance page and no dropped session.
Classic Magento deployment, by contrast, runs sequentially on the same environment: bin/magento setup:upgrade locks tables during the schema migration, setup:di:compile and setup:static-content:deploy take several minutes during which the existing generated code is partially overwritten, and restarting PHP-FPM or Nginx interrupts requests already in flight. Even with maintenance mode enabled (bin/magento maintenance:enable), customers still see visible downtime ranging from a few minutes to half an hour, depending on shop size and module count. For shops with round-the-clock international traffic, every maintenance window is direct lost revenue.
Blue-Green Deployment solves exactly this problem by completely separating the risky phase, the deployment itself, from the visible traffic cutover. The new version is installed, compiled and tested at leisure on the inactive environment, without a single live request ever touching it. Only once everything is demonstrably working does the switch happen, which in the successful case takes milliseconds. That makes the approach fundamentally different from a rolling deployment, where individual servers are updated one after another and two versions briefly answer requests at the same time.
| Strategy | Downtime | Rollback Speed | Complexity | Resource Cost |
|---|---|---|---|---|
| Standard Deployment | 5-30 minutes | Slow, requires a fresh deployment | Low | 1x environment |
| Rolling Deployment | None, but mixed operation | Medium, server by server | Medium | 1x to 1.x environment |
| Canary Deployment | None, partial traffic exposed | Medium, staged rollback | High | 1.x environment |
| Blue-Green Deployment | None | Instant, a single switch back | Medium to high | 2x environment |
2. Infrastructure prerequisites: two identical production environments and the traffic switch
The basic prerequisite for Blue-Green Deployment is genuine parity between both environments: the same PHP version, the same Nginx configuration, the same PHP extensions, the same resource limits and ideally the same container base. Differences between Blue and Green are the most common source of trouble in practice, because a bug then only shows up in one of the two environments and becomes hard to reproduce. Teams usually solve this with infrastructure as code, for example identical Docker images or Ansible playbooks that build both environments from the same definition.
Each environment needs its own application servers, but both typically share the same database and the same Redis cluster, since duplicate databases for orders and customer data would immediately create inconsistencies. The traffic switch itself is usually implemented at the load balancer or reverse proxy layer: Nginx upstream blocks, a HAProxy backend swap, or DNS based routing on a cloud load balancer. The important part is that the switch happens atomically, not server by server, but as a single configuration change applied with nginx -s reload without dropping connections.
One often underestimated detail: both environments need to reach the same external dependencies, such as payment provider webhooks, fulfillment APIs, or the ERP system. If a webhook only knows the IP address of the currently active environment, that configuration has to move along with the Blue-Green Deployment switch, otherwise incoming callbacks vanish into nothing after the cutover.
# /etc/nginx/conf.d/upstream.conf
# Blue-Green upstream definition: only the active block is enabled
upstream magento_blue {
server 10.0.1.11:8080 max_fails=2 fail_timeout=10s;
server 10.0.1.12:8080 max_fails=2 fail_timeout=10s;
keepalive 32;
}
upstream magento_green {
server 10.0.2.11:8080 max_fails=2 fail_timeout=10s;
server 10.0.2.12:8080 max_fails=2 fail_timeout=10s;
keepalive 32;
}
# The active pointer: swapped by the deploy script, then reloaded
map $host $active_upstream {
default magento_green; # currently live environment
}
server {
listen 443 ssl http2;
server_name shop.example.com;
location / {
proxy_pass http://$active_upstream;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_next_upstream error timeout http_502 http_503;
}
}
3. Keeping database migrations compatible: the expand-contract pattern in db_schema.xml
Because Blue and Green share the same database, the biggest technical challenge in Blue-Green Deployment is not the traffic switch, it is the database migration. A db_schema.xml update that renames a column or drops an existing one would instantly break the old environment that is still active, since its code still expects the old column layout. The solution is the expand-contract pattern, also known as parallel change: every schema change is split into additive and destructive steps that are rolled out in separate releases.
During the expand phase, only additions happen: a new column, a new index, a new table. The old code simply ignores the new column, while the new code writes to both the old and the new column, or reads from the new one first with a fallback to the old. Only once the old environment is reliably no longer going to be switched active again, after several successful Blue-Green Deployment cycles, does the contract phase follow, in which the old column is removed via a separate db_schema.xml update. There should be at least one or two full release cycles between expand and contract.
Data type changes follow the same pattern: instead of changing a column directly from varchar to int, a new column with the target type is created, a data patch synchronizes the values, and only once the code has fully switched to the new column is the old one removed. bin/magento setup:db-declaration:generate-whitelist helps document every schema change traceably in db_schema_whitelist.json, which gives extra confidence during a Blue-Green Deployment with several parallel release branches.
<!-- app/code/Mironsoft/CustomerLoyalty/etc/db_schema.xml -->
<!-- Expand phase: add new column, keep old one untouched -->
<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
<table name="customer_loyalty_points">
<!-- Legacy column, still read by the currently active environment -->
<column xsi:type="int" name="points_balance" nullable="false" default="0"
comment="Legacy points balance column, scheduled for removal"/>
<!-- New column added in the expand step, additive only -->
<column xsi:type="decimal" name="points_balance_v2" scale="4" precision="12"
nullable="false" default="0.0000"
comment="New decimal points balance, dual-written during migration"/>
<index referenceId="CUSTOMER_LOYALTY_POINTS_V2" indexType="btree">
<column name="points_balance_v2"/>
</index>
</table>
</schema>
<!-- Contract phase: separate release, only after all traffic
has run against the new column for at least one full cycle -->
<!--
<table name="customer_loyalty_points">
<column xsi:type="int" name="points_balance" nullable="false" default="0" disabled="true"/>
</table>
-->
4. Shared state between Blue and Green: Redis sessions, cache backend, message queue
A common misconception about Blue-Green Deployment is assuming both environments must be fully isolated. For session data the opposite is true: Magento stores sessions in Redis (session.save = redis), and that Redis cluster must be reachable from both Blue and Green equally. If each environment ran its own session store, every traffic switch would log out all signed-in customers and drop every shopping cart, which would defeat the entire purpose of a zero-downtime approach.
The cache backend calls for a more nuanced rule. The full page cache and the object cache can share the same Redis server as long as cache keys can be invalidated per version, since a new code version sometimes serializes objects differently than the old one. Magento handles this through the id_prefix configuration value in env.php, which should be set differently per environment or per deployment version so that Blue and Green never read the same cache entry with an incompatible structure. After every Blue-Green Deployment switch, a targeted bin/magento cache:flush on the newly active environment is a sensible precaution against stale fragments.
Message queue consumers, for example for asynchronous order processing over RabbitMQ, keep running independently of the currently active web environment and should ideally be able to consume on both environments at once, provided message handling is implemented idempotently. If that is not the case, the consumer process needs to be part of the traffic switch: the old consumer is shut down cleanly first (bin/magento queue:consumers:stop, or signal handling inside the consumer itself), then the new one starts on the freshly activated environment.
5. Switching the search index without downtime: the Elasticsearch/OpenSearch index alias strategy
The product catalog search index is one of the most complex pieces of a Blue-Green Deployment setup, because a full reindex can take anywhere from several minutes to hours, and a half-finished index must never go live during that time. Magento already solves this in the standard schedule indexer mode with its own alias strategy: every full reindex builds a completely new physical index, for example magento2_product_1_v123, while the alias magento2_product_1 keeps pointing at the old, working index.
Only once the new index is fully populated and validated does Magento move the alias atomically via the Elasticsearch or OpenSearch API onto the new physical index, and the old one is deleted afterward. For a Blue-Green Deployment this means: as long as both environments use the same Elasticsearch cluster and therefore the same alias, index cutover works independently of the web traffic switch. It only gets tricky when a new release changes the search schema, for example new facet attributes or a changed mapping. Then the expand-contract principle has to apply here too, keeping the old code compatible with the old mapping until the contract phase is reached.
In practice it pays to consistently leave the indexer mode at schedule and let reindexing run through the cron of bin/magento cron:run, rather than triggering it manually in the deploy script with reindex in realtime mode. That way the old environment stays fully functional right up to the actual Blue-Green Deployment switch, while the index for the new version is already being built in the background.
6. Static content deployment and the build pipeline for both environments in parallel
Static assets such as compiled CSS, JavaScript and the generated di.xml interceptor classes must be built completely and independently for each environment before the Blue-Green Deployment switch happens. The command bin/magento setup:static-content:deploy de_DE -t Mironsoft/default -f runs entirely on the inactive environment, while the active one keeps serving its own unchanged static files the whole time. This separation is exactly what makes the deployment downtime free: there is never a moment where pub/static on the active environment is half old, half new.
For Hyvä themes there is also the Tailwind build, which likewise needs to be fully finished before the switch: bin/npm --prefix app/design/frontend/Mironsoft/default/web/tailwind run build produces the final CSS, which then becomes part of the setup:static-content:deploy run. Since both environments share the same codebase but have separate generated directories (var/view_preprocessed, generated/code), it is worth fully clearing these directories on the target environment before every Blue-Green Deployment, to rule out leftovers from an older build.
A CDN sitting in front of both environments should serve assets through versioned paths or query strings, so a browser cache from the Blue phase does not accidentally hand stale assets to customers who just got switched to the Green environment. Magento's Magento_Deploy module already sets a content version hash in the static content path by default, which changes with every deployment and thereby takes care of clean cache invalidation automatically.
7. The traffic switch mechanism: load balancer/Nginx upstream cutover and health checks before switching
The actual traffic switch is the most critical moment in the entire Blue-Green Deployment process, because it marks the point of no return after which real customers see the new version. That is why the switch must never happen blindly, only after an automated health check endpoint on the new environment has answered successfully several times in a row. Such an endpoint should check more than just "PHP responds": database connectivity, Redis reachability, Elasticsearch status and the existence of the compiled di.xml classes all belong in any serious health check.
<?php
declare(strict_types=1);
namespace Mironsoft\DeployHealth\Controller\Health;
use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\App\ResponseInterface;
use Magento\Framework\Controller\Result\JsonFactory;
use Magento\Framework\Controller\ResultInterface;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\App\Cache\Type\FrontendPool;
/**
* Health check endpoint used to gate the blue-green traffic switch.
* Verifies database, cache backend and compiled code before a deploy
* script is allowed to flip the active upstream.
*/
final class Check implements HttpGetActionInterface
{
/**
* @param JsonFactory $resultJsonFactory Factory for JSON result responses.
* @param ResourceConnection $resourceConnection Database connection resource.
* @param FrontendPool $cacheFrontendPool Cache frontend pool for backend checks.
*/
public function __construct(
private readonly JsonFactory $resultJsonFactory,
private readonly ResourceConnection $resourceConnection,
private readonly FrontendPool $cacheFrontendPool,
) {
}
/**
* Executes all readiness checks and returns a JSON status payload.
*
* @return ResultInterface JSON response with overall status and per-check details.
*/
public function execute(): ResultInterface
{
$checks = [
'database' => $this->checkDatabase(),
'cache' => $this->checkCache(),
'generated_code' => $this->checkGeneratedCode(),
];
$healthy = !in_array(false, $checks, true);
$result = $this->resultJsonFactory->create();
$result->setHttpResponseCode($healthy ? 200 : 503);
return $result->setData(['healthy' => $healthy, 'checks' => $checks]);
}
/**
* Verifies the primary database connection responds to a trivial query.
*
* @return bool True when the connection is reachable.
*/
private function checkDatabase(): bool
{
try {
$connection = $this->resourceConnection->getConnection();
return (bool) $connection->fetchOne('SELECT 1');
} catch (\Throwable) {
return false;
}
}
/**
* Verifies the configured cache backend accepts read and write operations.
*
* @return bool True when the cache backend is reachable.
*/
private function checkCache(): bool
{
try {
$frontend = $this->cacheFrontendPool->get('default');
$frontend->getBackend()->save('ok', 'deploy_health_probe', [], 30);
return $frontend->getBackend()->load('deploy_health_probe') === 'ok';
} catch (\Throwable) {
return false;
}
}
/**
* Verifies that compiled DI classes exist for the new deployment.
*
* @return bool True when the generated code directory is populated.
*/
private function checkGeneratedCode(): bool
{
return is_dir(BP . '/generated/code/Magento') && (count(scandir(BP . '/generated/code/Magento') ?: []) > 2);
}
}
Only once this health check has been stably green across several iterations does the deploy script perform the actual Nginx upstream cutover. The switch itself only changes the value of the map directive and then runs nginx -s reload, which lets existing connections drain gracefully instead of cutting them off. This exact health-check-gate mechanism is what separates a Blue-Green Deployment from a risky, manual cutover.
#!/usr/bin/env bash
# deploy-switch.sh: health-check-gated blue-green traffic switch
set -euo pipefail
readonly TARGET_ENV="${1:?Usage: deploy-switch.sh blue|green}"
readonly HEALTH_URL="http://${TARGET_ENV}-internal.example.com/health/check"
readonly MAX_ATTEMPTS=10
readonly SLEEP_SECONDS=5
echo "[INFO] Gating switch to ${TARGET_ENV} on health check success"
attempt=1
while (( attempt <= MAX_ATTEMPTS )); do
status_code="$(curl -s -o /dev/null -w '%{http_code}' "$HEALTH_URL" || echo '000')"
if [[ "$status_code" == "200" ]]; then
echo "[OK] Health check passed on attempt ${attempt}"
break
fi
echo "[WAIT] Attempt ${attempt}/${MAX_ATTEMPTS} returned ${status_code}"
attempt=$((attempt + 1))
sleep "$SLEEP_SECONDS"
done
if (( attempt > MAX_ATTEMPTS )); then
echo "[ABORT] ${TARGET_ENV} never reported healthy, switch cancelled" >&2
exit 1
fi
echo "[INFO] Flipping active upstream to magento_${TARGET_ENV}"
sed -i "s/default magento_.*/default magento_${TARGET_ENV};/" /etc/nginx/conf.d/upstream.conf
nginx -t && nginx -s reload
echo "[DONE] Traffic switched to ${TARGET_ENV}, previous environment kept warm for rollback"
8. Rollback strategy: switching straight back to the previous environment on failure
The biggest practical advantage of Blue-Green Deployment over any other deployment strategy is rollback speed. Because the previous environment is not shut down immediately after the switch but kept warm for a defined period, a rollback is simply a second traffic switch in the opposite direction. There is no new deployment to run, no restore from a backup and no manual debugging under time pressure required to bring the shop back online.
For this rollback to actually work, two conditions have to be met. First, the contract phase of a database migration may only happen once several Blue-Green Deployment cycles have passed without a rollback, because switching back to code that expects an already-deleted column would fail instantly. Second, monitoring and alerting need to be tuned tightly enough to catch errors within seconds of the switch, for example through error rate thresholds in New Relic or Grafana, combined with synthetic checkout transactions that automatically walk through the entire order process.
In practice, teams define a time window, typically 15 to 60 minutes after every Blue-Green Deployment, during which the old environment is actively monitored and not shut down. Only once that window passes without any issues is the old environment reused for the next release or deliberately reset. Orders placed on the new environment during that window remain correctly stored in the shared database even after a rollback, since the database and Redis are never part of the switch.
9. CI/CD integration: an automated Blue-Green workflow in the pipeline
Without automation, Blue-Green Deployment stays a theoretical concept that fails in practice due to manual mistakes. A CI/CD pipeline, for example in GitLab CI, should model the complete flow: determine the target environment, deploy the code, run migrations, wait for the health check, switch traffic, and roll back automatically on failure. Each of these steps should be modeled as its own repeatable pipeline job, so a failed step stops the pipeline cleanly instead of leaving things in an inconsistent in-between state.
A key building block is determining which environment is currently inactive, so the pipeline automatically knows where to deploy. This can be solved with a simple comparison against the current Nginx configuration, or with a small state file in a shared object storage bucket. Only after a successful deployment to the inactive environment and a passed health check does the pipeline call the same deploy-switch.sh used for manual Blue-Green Deployment cutovers, so that automation and manual intervention always take exactly the same code path.
Post-deployment jobs such as automated smoke tests against the newly active environment and a Slack notification with a rollback link round out the workflow. That way every Blue-Green Deployment stays traceably documented, and a rollback is always a single click or pipeline trigger away for the team, without anyone needing to remember the exact sequence of commands.
# .gitlab-ci.yml: automated blue-green rollout for Magento
stages:
- determine-target
- deploy
- migrate
- health-check
- switch
- smoke-test
determine-target:
stage: determine-target
script:
- ./ci/determine-inactive-environment.sh > target.env
artifacts:
reports:
dotenv: target.env
deploy-to-target:
stage: deploy
needs: ["determine-target"]
script:
- ssh deploy@${TARGET_ENV}-host "cd /var/www/magento && git pull origin main"
- ssh deploy@${TARGET_ENV}-host "bin/composer install --no-dev -o"
- ssh deploy@${TARGET_ENV}-host "bin/magento setup:di:compile"
- ssh deploy@${TARGET_ENV}-host "bin/magento setup:static-content:deploy de_DE -f"
run-migrations:
stage: migrate
needs: ["deploy-to-target"]
script:
- ssh deploy@${TARGET_ENV}-host "bin/magento setup:upgrade --keep-generated"
wait-for-health:
stage: health-check
needs: ["run-migrations"]
script:
- ./ci/wait-for-health.sh "${TARGET_ENV}"
switch-traffic:
stage: switch
needs: ["wait-for-health"]
script:
- ./ci/deploy-switch.sh "${TARGET_ENV}"
when: on_success
smoke-test:
stage: smoke-test
needs: ["switch-traffic"]
script:
- ./ci/checkout-smoke-test.sh
after_script:
- if [ "$CI_JOB_STATUS" == "failed" ]; then ./ci/rollback-switch.sh; fi
10. Summary
Blue-Green Deployment for Magento solves the downtime problem of classic releases by running two identical production environments in parallel and letting a controlled traffic switch move customer traffic between them in milliseconds. The real technical work is not the switch itself but compatibility: database migrations follow the expand-contract pattern in db_schema.xml, sessions and cache live in shared Redis, and the Elasticsearch search index is updated through alias cutover independently of the web traffic switch.
Static content deployment and the Tailwind build for Hyvä themes run entirely on the inactive environment before a health check endpoint clears the switch. This exact health-check-gate mechanism, combined with a previous environment kept warm, turns rollbacks during a Blue-Green Deployment into a matter of seconds instead of hours. An automated CI/CD pipeline captures the entire flow reproducibly, from determining the target environment to automatically switching back on failed smoke tests, and turns zero-downtime releases for Magento shops into routine instead of the exception.
Blue-Green Deployment for Magento: the key points at a glance
Two environments, one switch
Blue and Green are set up identically. The traffic switch happens atomically through Nginx upstream or a load balancer, without dropping connections.
Expand-contract for the database
Schema changes in db_schema.xml are rolled out additively, destructive steps only follow after several stable release cycles.
Shared state
Redis sessions, cache backend and message queue consumers are used by both environments, so no customer gets logged out during the switch.
Instant rollback
The previous environment stays warm. A rollback is a second traffic switch, not a new deployment.
11. FAQ: Blue-Green Deployment for Magento
1What exactly is Blue-Green Deployment for Magento?
2Do Blue and Green need their own databases?
3How do database migrations stay compatible?
4What happens to sessions during the switch?
5How does search index cutover work?
6How fast is a rollback?
7Which health checks are needed before the switch?
8How long should the old environment stay warm?
9Can the workflow be fully automated?
10What extra resources does the setup cost?
Mironsoft
Zero-downtime infrastructure and CI/CD for Magento shops
Ready for Blue-Green Deployment without a maintenance window?
We build Blue-Green infrastructure for your Magento shop, automate the traffic switch including health checks, and set up a CI/CD pipeline that rolls out releases with no visible downtime.
Blue-Green infrastructure setup
Two identical production environments, load balancer configuration and a shared Redis/Elasticsearch cluster.
CI/CD pipeline development
An automated Blue-Green workflow from health check to rollback, integrated into GitLab CI or GitHub Actions.
Zero-downtime release consulting
Expand-contract strategy for db_schema.xml and migration planning for existing Magento releases.