Blue-Green Deployment for Magento: Releases Without Downtime
AI generated
M2
di.xml
Magento 2 · PHP 8.4 · DevOps
Blue-Green Deployment for Magento
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.

18 min read Blue-Green Deployment · Zero-Downtime · Traffic Switch Magento 2.4.8-p4 · PHP 8.4

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?
Two identical production environments run in parallel. One serves live traffic, the other is prepared. The load balancer switches atomically once health checks pass.
2Do Blue and Green need their own databases?
No, both share the database and Redis cluster. Separate databases would cause inconsistencies for orders.
3How do database migrations stay compatible?
Through the expand-contract pattern in db_schema.xml. Additive changes first, destructive steps only after several stable release cycles.
4What happens to sessions during the switch?
Sessions live in shared Redis. Customers stay logged in and carts are preserved, because the session store is never part of the switch.
5How does search index cutover work?
A new physical index gets built, the alias is only moved atomically once it is fully populated. The old index stays active until then.
6How fast is a rollback?
A few seconds. A rollback is just a second traffic switch back to the warm-kept previous environment, not a new deployment.
7Which health checks are needed before the switch?
Database connectivity, Redis reachability, Elasticsearch status and compiled di.xml classes. A plain HTTP 200 check is not enough.
8How long should the old environment stay warm?
Common practice is 15 to 60 minutes of active monitoring after the switch, before the old environment is reused or reset.
9Can the workflow be fully automated?
Yes, a CI/CD pipeline can fully cover target environment determination, deployment, migrations, health check waiting, switch and rollback.
10What extra resources does the setup cost?
Double the application server capacity. Database, Redis and Elasticsearch typically still only run once.

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.