Magento deployments done wrong mean minutes of downtime, broken shopping carts, and unhappy customers. Done right, they're invisible — customers never notice. Here's how to get there.
Why Magento deployments are hard
A typical Magento deployment touches:
- PHP files (application code)
- Generated code (
var/generation/,generated/) - Static files (
pub/static/) - Database schema and data
- Cache (must be flushed)
The challenge: each of these operations takes time, and during that time your store is in an inconsistent state. New code referencing old database schema. New static files not yet deployed. Old cached content serving.
The naive approach (and why it fails)
# Bad: everything happens in-place
git pull origin main
composer install
php bin/magento setup:upgrade
php bin/magento setup:di:compile
php bin/magento setup:static-content:deploy
php bin/magento cache:flush
During setup:di:compile (2–5 minutes), your store serves requests without compiled DI — intermittent 500 errors. During setup:static-content:deploy, JS/CSS files may be missing. Classic downtime.
Approach 1: Maintenance mode (simple, has downtime)
php bin/magento maintenance:enable --ip=YOUR_IP
git pull origin main
composer install --no-dev --optimize-autoloader
php bin/magento setup:upgrade
php bin/magento setup:di:compile
php bin/magento setup:static-content:deploy -f en_US
php bin/magento cache:flush
php bin/magento maintenance:disable
Maintenance mode shows a "temporarily unavailable" page. Downtime: 3–10 minutes depending on catalog size. Acceptable for small stores, not for high-traffic production.
Approach 2: Atomic symlink deployment (zero downtime)
The industry-standard approach: deploy to a new directory, then atomically switch the webserver symlink.
Directory structure:
/var/www/
├── releases/
│ ├── 20260419_143000/ ← current deployment
│ └── 20260419_120000/ ← previous deployment
├── shared/
│ ├── pub/media/ ← shared across deployments
│ ├── var/ ← shared var directory
│ └── app/etc/env.php ← shared config
└── current -> releases/20260419_143000/ ← symlink
Deployment script:
#!/bin/bash
set -e
RELEASE_DIR="/var/www/releases/$(date +%Y%m%d_%H%M%S)"
SHARED_DIR="/var/www/shared"
CURRENT_LINK="/var/www/current"
# 1. Create new release directory
mkdir -p "$RELEASE_DIR"
git clone --depth=1 git@github.com:yourorg/magento-store.git "$RELEASE_DIR"
cd "$RELEASE_DIR"
# 2. Install dependencies
composer install --no-dev --optimize-autoloader --no-interaction
# 3. Link shared directories
rm -rf pub/media var app/etc/env.php
ln -s "$SHARED_DIR/pub/media" pub/media
ln -s "$SHARED_DIR/var" var
ln -s "$SHARED_DIR/app/etc/env.php" app/etc/env.php
# 4. Build static assets and DI (while old release is still live)
php bin/magento setup:di:compile
php bin/magento setup:static-content:deploy -f en_US de_DE
# 5. Run database upgrades (safe to run before switch)
php bin/magento setup:upgrade --keep-generated
# 6. ATOMIC SWITCH — this is instant
ln -sfn "$RELEASE_DIR" "$CURRENT_LINK"
# 7. Flush caches (new code is now live)
php bin/magento cache:flush
# 8. Clean up old releases (keep last 3)
ls -t /var/www/releases | tail -n +4 | xargs -I{} rm -rf "/var/www/releases/{}"
echo "Deployment complete: $RELEASE_DIR"
The key is step 6: ln -sfn is an atomic operation on Linux. The webserver symlink switches from old to new in a single filesystem operation — no window where the symlink is broken.
Approach 3: Blue-green deployment
For maximum safety: run two identical environments (blue and green), load balancer switches between them.
Load Balancer
├── Blue environment (magento-blue.internal) ← currently live
└── Green environment (magento-green.internal) ← deploy here
Process:
- Deploy to green while blue serves traffic
- Run smoke tests on green
- Switch load balancer to green (instant, no downtime)
- Blue becomes the new staging environment
Works best with containerized deployments (Docker/Kubernetes) or if your hosting provider supports it (AWS, GCP, Azure).
Database migrations without downtime
setup:upgrade runs database migrations. Running migrations while the store is live risks:
- Old code reading new schema columns that don't exist yet
- New code failing on old schema
Backward-compatible migration pattern:
Phase 1 (current deployment): add new column as nullable with default
ALTER TABLE sales_order ADD COLUMN new_field VARCHAR(255) DEFAULT NULL;
Phase 2 (next deployment): make the column required, backfill data
UPDATE sales_order SET new_field = 'default_value' WHERE new_field IS NULL;
ALTER TABLE sales_order MODIFY COLUMN new_field VARCHAR(255) NOT NULL DEFAULT '';
Phase 3 (later): remove old compatibility code
This "expand and contract" pattern lets you run migrations without the risk of old code breaking on new schema.
CI/CD pipeline example (GitHub Actions)
name: Deploy to Production
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.PROD_HOST }}
username: deploy
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /var/www
./scripts/deploy.sh
- name: Smoke test
run: |
sleep 10
curl -f https://your-store.com/ || exit 1
curl -f https://your-store.com/catalog/product/view/id/1 || exit 1
Post-deployment checklist
# Verify indexers aren't invalid
bin/magento indexer:status
# Verify caches are warmed
curl -s -o /dev/null -w "%{http_code} %{time_total}s" https://your-store.com/
# Check error logs
tail -50 var/log/system.log | grep -i error
tail -50 var/log/exception.log
# Verify static files deployed
curl -I https://your-store.com/static/frontend/Magento/luma/en_US/requirejs/require.js
Zero-downtime deployments require upfront setup but pay off immediately — no more stress about deployment windows, no more customer complaints during releases.
Originally published on magevanta.com
Top comments (0)