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.
Table of Contents
- 1. Why the symlink model matters for Magento
- 2. The complete directory structure
- 3. releases/: the release archive
- 4. current/: the atomic activation point
- 5. shared/: persistent runtime data
- 6. Server initialization: once, and done right
- 7. Web server configuration for the symlink model
- 8. Retention policy: cleaning up old releases
- 9. Release models compared
- 10. Summary
- 11. FAQ
1. Why the symlink model matters for Magento
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.
4. current/: the atomic activation point
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.