Magento Release Structure with releases/, current/ and shared/ from Scratch
AI generated
CI/CD
.yml
GitLab · Magento · Release Structure · Symlink · Rollback
Magento Release Structure
releases/, current/ and shared/ from Scratch

A Magento shop that gets deployed straight into a single directory cannot be rolled back in seconds. The symlink model with releases/, current/ and shared/ is the foundation of every zero-downtime deployment, and it needs to be built correctly once, before the first pipeline ever runs.

14 min read releases/ · current/ · shared/ · symlink · retention · rollback Magento 2.4 · Linux · GitLab CI · Nginx/Apache

The symlink model is the most widely used deployment pattern for PHP applications and the standard approach in tools like Deployer, Capistrano and Ansistrano. The idea is simple: every release gets its own timestamped directory. The web server points to a symlink named current, which in turn points to the active release directory. A deployment atomically switches the symlink to the new directory. A rollback resets the symlink back to an older directory.

For Magento this model matters even more, because Magento has plenty of files that must be shared across releases while being kept strictly separate from release-specific artifacts. env.php holds database credentials and must never be copied per release. pub/media holds uploaded images and must never be reverted during a rollback. vendor/ and generated/, on the other hand, belong exactly to a given code state and must never be shared.

The model itself is not complicated, but it needs to be set up correctly, once. Mistakes in the initial structure, wrong symlink targets, missing permissions, unprepared shared/ directories, lead to deployment failures that are hard to debug because they only surface during the first real deploy. That is why the setup of the release structure has to be documented, scripted and tested on staging before production gets configured.

2. The complete directory structure

The following directory layout shows what a Magento server looks like after two successful deployments. The releases/ directory holds timestamped subdirectories for every deploy. The current symlink points to the active release. The shared/ directory holds all the persistent files that are shared across releases.

# Server directory structure after two successful deployments
# /var/www/magento/: base deployment path
#
# current -> releases/20260509-143022   (symlink to active release)
# releases/
#   20260509-143022/                    (latest release, current)
#     app/
#       code/
#       design/
#       etc/
#         config.php                    (versioned module config)
#         env.php -> shared/app/etc/env.php   (symlink to shared)
#     generated/                        (DI factories from build)
#     pub/
#       media -> shared/pub/media       (symlink to shared media)
#       static/                         (built CSS/JS, release-specific)
#     var/
#       cache/                          (runtime cache, gitignored)
#       log -> shared/var/log           (symlink to shared logs)
#       session -> shared/var/session   (symlink to shared sessions)
#     vendor/                           (composer dependencies)
#   20260509-120500/                    (previous release, keep for rollback)
#     [same structure as above]
# shared/
#   app/
#     etc/
#       env.php                         (environment-specific config)
#   pub/
#     media/                            (persistent uploaded files)
#   var/
#     log/                              (persistent log files)
#     session/                          (persistent sessions)

This layout shows at a glance which files are release-specific and which are shared. The current symlink is the only pointer the web server needs to know about; it only has to be defined once in the web server configuration and then applies automatically to every future release. That is the key benefit: switching between releases requires no web server configuration change.

3. releases/: the release archive

The releases/ directory is the archive of every deployable release. Each deploy job creates a new subdirectory with a timestamped name, typically in the format YYYYMMDD-HHMMSS, which guarantees automatic sortability by date. This timestamp also serves as the release ID used for rollbacks.

It is important that every release directory is complete and self-contained. That means vendor/, generated/, pub/static/ and the code itself are all fully present inside the release directory. Only the shared paths are mounted in as symlinks. When a release directory is invoked, Magento must work without any further configuration steps: every required file is either directly present in the directory or reachable through a symlink.

The current symlink is the heart of the symlink deployment model. It is a plain symbolic link pointing to the active release directory. The command ln -sfn /var/www/magento/releases/20260509-143022 /var/www/magento/current switches the symlink to the new release, and this operation is atomic at the Linux filesystem level. That means no HTTP request ever sees an intermediate state where current points to a half-finished release.

The web server must be configured with follow_symlinks or its equivalent so it follows the current symlink and serves the actual release directory. Nginx needs the disable_symlinks off option (the default) or the use of an absolute path. Apache needs Options +FollowSymlinks. A misconfigured web server that does not follow symlinks results in 403 errors, a mistake that shows up during initial setup rather than on every deploy.

# GitLab CI deploy job: symlink switch is the zero-downtime moment
deploy:production:
  stage: deploy
  script:
    - |
      RELEASE_ID="$(date +%Y%m%d-%H%M%S)"
      BASE="${DEPLOY_PATH}"
      RELEASE="${BASE}/releases/${RELEASE_ID}"
      SHARED="${BASE}/shared"
      CURRENT="${BASE}/current"

      echo "Deploying release: ${RELEASE_ID}"

      # Step 1: Create and populate release directory
      ssh "${DEPLOY_USER}@${DEPLOY_HOST}" "mkdir -p ${RELEASE}"
      rsync -az --delete \
        --exclude="pub/media" \
        --exclude="var/log" \
        --exclude="var/session" \
        --exclude="var/cache" \
        --exclude="app/etc/env.php" \
        ./ "${DEPLOY_USER}@${DEPLOY_HOST}:${RELEASE}/"

      # Step 2: Link shared paths inside new release
      ssh "${DEPLOY_USER}@${DEPLOY_HOST}" bash -s << REMOTE
        set -euo pipefail
        ln -sfn "${SHARED}/app/etc/env.php" "${RELEASE}/app/etc/env.php"
        ln -sfn "${SHARED}/pub/media"       "${RELEASE}/pub/media"
        ln -sfn "${SHARED}/var/log"         "${RELEASE}/var/log"
        ln -sfn "${SHARED}/var/session"     "${RELEASE}/var/session"
REMOTE

      # Step 3: Run Magento release steps before activation
      ssh "${DEPLOY_USER}@${DEPLOY_HOST}" \
        "cd ${RELEASE} && bin/magento setup:upgrade --keep-generated"
      ssh "${DEPLOY_USER}@${DEPLOY_HOST}" \
        "cd ${RELEASE} && bin/magento cache:flush"

      # Step 4: Atomic symlink switch, zero-downtime activation
      ssh "${DEPLOY_USER}@${DEPLOY_HOST}" \
        "ln -sfn ${RELEASE} ${CURRENT}"

      echo "Release ${RELEASE_ID} activated successfully"

5. shared/: persistent runtime data

The shared/ directory holds all the files that are shared across releases: pub/media with the uploaded images, var/log with the logs, var/session with the session data, and app/etc/env.php with the environment-specific configuration. These files live outside the release model; they are neither release-specific nor versioned.

The shared/ directory must never be part of a deployment or a rollback. Its contents change through user actions (uploads into pub/media) and through the system itself (logs in var/log), not through deployments. One important consequence: if the env.php in shared/ needs to be updated, for example because database connection details changed, that has to happen outside the deploy pipeline, either directly on the server or through a separate configuration management system.

6. Server initialization: once, and done right

The initialization of the release structure has to be performed once on the server before the first deploy job runs. It covers creating all the necessary directories, setting the correct permissions and placing the initial env.php inside shared/. A scripted initialization procedure beats manual SSH commands because it is documented and reproducible.

The env.php for shared/ must exist before the first deploy: the deploy job only sets the symlink, it never creates an env.php itself. A missing symlink target results in a deployment failure. A script that checks for the presence of env.php before deploying prevents this failure and produces a clear error message instead of a cryptic symlink error.

# One-time server initialization script
# Run this before the first pipeline deployment
initialize:server:
  stage: deploy
  when: manual
  script:
    - |
      ssh "${DEPLOY_USER}@${DEPLOY_HOST}" bash << 'REMOTE'
        set -euo pipefail

        BASE="${DEPLOY_PATH:-/var/www/magento}"
        SHARED="${BASE}/shared"

        echo "=== Initializing Magento release structure ==="

        # Create directory structure
        mkdir -p "${BASE}/releases"
        mkdir -p "${SHARED}/app/etc"
        mkdir -p "${SHARED}/pub/media"
        mkdir -p "${SHARED}/var/log"
        mkdir -p "${SHARED}/var/session"

        # Set permissions (deploy user: rwx, www-data: rx)
        chmod 755 "${BASE}"
        chmod 755 "${BASE}/releases"
        chmod 755 "${SHARED}"
        chmod 775 "${SHARED}/pub/media"   # web server needs write for uploads
        chmod 770 "${SHARED}/var/log"
        chmod 770 "${SHARED}/var/session"
        chmod 750 "${SHARED}/app/etc"

        # Guard: env.php must be placed manually before first deployment
        if [ ! -f "${SHARED}/app/etc/env.php" ]; then
          echo ""
          echo "ERROR: ${SHARED}/app/etc/env.php is missing."
          echo "Copy your production env.php to this path before deploying."
          echo ""
          exit 1
        fi

        echo "=== Initialization complete ==="
        echo "Base path: ${BASE}"
        echo "Releases:  ${BASE}/releases/ (empty)"
        echo "Shared:    ${SHARED}/"
        ls -la "${SHARED}/"
      REMOTE

7. Web server configuration for the symlink model

Nginx must set its document root to /var/www/magento/current/pub, never to the releases/ directory or the absolute path of a specific release. The current symlink then leads nginx to the active release. The disable_symlinks off directive is the nginx default and normally does not need to be set explicitly, but on some hosted environments or with restrictive nginx configurations it has to be enabled explicitly.

PHP-FPM should also point to the current directory as its root. One important detail with PHP-FPM: the open_basedir setting must include the absolute paths of both possible locations, the releases/ directory as well as the shared/ directory. Otherwise access to shared files through the symlink fails, because PHP cannot find the symlink's target path within the allowed directory tree.

8. Retention policy: cleaning up old releases

Without a retention policy, the releases/ directory grows by one full Magento directory with every single deploy, which quickly adds up to hundreds of gigabytes under active deployment operations. A retention policy defines how many old releases are kept and automatically deletes older ones after every successful deployment. Five releases is a reasonable recommendation, which allows rollbacks to any of the last four deployments.

Cleaning up old releases has to happen after the new release gets activated, but before the deploy job finishes. It must never delete the currently active release, so the cleanup logic has to exclude the current symlink target from the list of releases to remove. The simplest approach is to keep the most recent N releases sorted by date and delete everything else, which works reliably with the timestamp-based naming scheme.

9. Release models compared

The difference between deploying straight into a single directory and the symlink model with releases/, current/ and shared/ becomes most visible in exceptional situations: a failed deployment, an urgent rollback, or a server restart in the middle of an ongoing deployment.

Scenario Flat Deployment Symlink Release Model Impact
Rollback needed Restore an old backup (minutes) Reset the symlink (seconds) Rollback time: seconds instead of minutes
Deploy aborts Shop shows an inconsistent state current still points to the old release No shop outage on a broken deploy
Multiple deployments at once File conflicts possible Each deploy in its own directory No conflict, the last symlink switch wins
Release history No overview of past deploys ls releases/ shows every past deploy Traceability and an audit trail
Downtime during deploy Inconsistent state while rsync is active Atomic symlink switch, no intermediate state A genuine zero-downtime deployment moment

The table shows that the symlink release model is not only better for the rollback case, but for every exceptional scenario. The atomic symlink switch makes the actual deployment moment safe. Every release living in its own directory prevents conflicts and enables a clear release history. The flat deployment model has none of these properties: it is simpler to set up, but noticeably riskier in production.

10. Summary

The release structure with releases/, current/ and shared/ is the foundation of every zero-downtime-capable Magento deployment. releases/ holds timestamped release directories, each containing a complete, self-contained Magento installation. current/ is a symlink that switches atomically to the active release. shared/ holds all the persistent files that are shared across releases, and are never part of a rollback.

Building this structure is a one-time investment that pays off with every deployment that follows. Rollbacks take seconds instead of minutes. Deployments that abort during the transfer phase leave the shop in a working state. The release history stays visible on the server and enables audits and diagnostics. And most importantly: the foundation for zero downtime is in place without needing a load balancer or a blue-green infrastructure.

Magento Release Structure: the essentials at a glance

releases/

Timestamped directories, one per deploy. Each one complete and consistent. Keep at least 3 to 5 for rollback capability.

current/

Atomic symlink to the active release. No web server restart on switch. ln -sfn is the actual zero-downtime operation.

shared/

pub/media, var/log, var/session, app/etc/env.php. Persistent across all releases, never part of a rollback. Set up once during server setup.

Initialization

Once, before the first deploy. Create the shared/ structure, set permissions, place env.php. After that every deploy job runs automatically.

11. FAQ: Magento Release Structure with releases/, current/ and shared/

1How many releases should be kept?
At least 3, ideally 5. Allows rollback to any of the last four deployments. More than 10 wastes storage unnecessarily, since each release has its own vendor and generated directories.
2Is ln -sfn really atomic?
Yes. The Linux kernel replaces the symlink in a single operation, so no HTTP request ever sees an intermediate state. That is the actual zero-downtime guarantee.
3What happens if a deploy aborts during rsync?
current still points to the old release, so the shop keeps running unaffected. The half-finished release directory can be deleted safely.
4Does the web server need to restart after the symlink switch?
No. Nginx and Apache follow the symlink on every request. A PHP-FPM reload is recommended for OPcache invalidation, but a hard restart is not required.
5Invalidating OPcache after a symlink switch?
A php-fpm reload or an OPcache reset script right after the symlink switch. Without invalidation, PHP-FPM may keep serving cached code from the old release.
6Release structure across multiple servers?
Each server gets its own release structure. The deploy job transfers and switches on every server. pub/media can be shared through NFS or S3.
7What naming scheme for release directories?
YYYYMMDD-HHMMSS is proven in practice: automatically sortable chronologically, retention cleanup becomes trivial. A Git tag or pipeline ID loses that sortability.
8open_basedir and symlinks in PHP-FPM?
open_basedir must include releases/ and shared/. Otherwise PHP-FPM fails to access shared files through symlinks, resulting in 403 or permission errors.
9How to provide env.php for the first deployment?
Manually via SSH or a provisioning script outside the deploy pipeline. Never as a pipeline artifact, since env.php contains secrets (database passwords, Redis keys).
10releases/ and shared/ on different filesystems?
No. Symlinks do not work across filesystem boundaries. releases/, current/ and shared/ must all live on the same filesystem.