Step by Step
The atomic symlink switch is the technical core of zero downtime deployments for Magento. Instead of overwriting files directly, which creates inconsistency, the new release is fully prepared and then activated as current in a single kernel system call. Rollback is therefore not an emergency plan but a millisecond operation.
Table of Contents
- 1. The Symlink Switch Concept Explained
- 2. Preparing the Directory Structure on the Server
- 3. Shared Paths: What Gets Shared Between Releases
- 4. The Atomic Symlink Switch: ln -sfn in Detail
- 5. Preparing a New Release: Step by Step
- 6. Integrating the Symlink Switch into the GitLab Pipeline
- 7. Symlink Switch vs. Direct Overwrite Compared
- 8. Rollback: Back to the Previous Release in Seconds
- 9. Release Retention: Cleaning Up Old Releases
- 10. Summary
- 11. FAQ
1. The Symlink Switch Concept Explained
With symlink switch deployment, also known as Capistrano style or release based deployment, every release is stored as its own directory on the server. A symlink named current always points to the currently active release. The web server (nginx, apache) is configured so that its document_root or root points to the current directory. Activating a new version only requires a single command: ln -sfn /var/www/magento/releases/20260509-120000 /var/www/magento/current. This command is atomic: from the kernel's point of view it is a single operation that either succeeds completely or fails completely.
Atomicity is the decisive factor. Overwriting files directly, for example with rsync --delete straight into the web server root, creates a window of time in which some files already have the new version while others still have the old one. During this inconsistency window, PHP classes can be loaded that are incompatible with other classes from a different release. The symlink switch eliminates this inconsistency window entirely: the change happens at kernel level in a single operation, and all subsequent requests see exclusively the new release.
For Magento this deployment model is particularly well suited because Magento has a complex dependency structure between PHP classes, generated code, static content and configuration files. An inconsistency window between these components leads to errors that are hard to reproduce and hard to debug. The symlink switch closes this window completely.
2. Preparing the Directory Structure on the Server
Before the first deployment can take place, the directory structure on the target server needs to be initialized. This setup is a one time step performed either manually or via a script. The structure consists of three main components: the releases directory, the shared directory and the current symlink. The releases directory contains numbered or timestamp based subdirectories, one per release. The shared directory contains all files and directories that are shared between releases; they should not be overwritten with every deployment. The current symlink points to the currently active release.
The shared directories are populated with their initial contents during the very first deployment. In every subsequent deployment only symlinks pointing into the shared directories are created in the new release; the content itself stays untouched. This means uploaded images in pub/media persist across all releases, logs accumulate across releases, and the database specific env.php only needs to be created once.
# One time server initialization, run manually or via a dedicated init job
# This sets up the directory structure before the first deployment
initialize:server:
stage: deploy
when: manual
before_script:
- eval $(ssh-agent -s)
- ssh-add "$SSH_PRIVATE_KEY"
- mkdir -p ~/.ssh && echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
script:
- |
ssh "$DEPLOY_USER@$DEPLOY_HOST" bash -s <<'REMOTE'
set -euo pipefail
# Create base directory structure
mkdir -p "$DEPLOY_PATH/releases"
mkdir -p "$DEPLOY_PATH/shared/app/etc"
mkdir -p "$DEPLOY_PATH/shared/pub/media"
mkdir -p "$DEPLOY_PATH/shared/var/log"
mkdir -p "$DEPLOY_PATH/shared/var/session"
mkdir -p "$DEPLOY_PATH/shared/var/cache"
# Set correct permissions for shared directories
chmod 775 "$DEPLOY_PATH/shared/pub/media"
chmod 775 "$DEPLOY_PATH/shared/var/log"
chmod 775 "$DEPLOY_PATH/shared/var/session"
echo "[OK] Server initialized at $DEPLOY_PATH"
echo "[INFO] Next step: place env.php in $DEPLOY_PATH/shared/app/etc/"
REMOTE
environment:
name: production
3. Shared Paths: What Gets Shared Between Releases
Deciding which paths are shared and which are kept separate per release is central to the symlink switch model. As a rule of thumb: anything that changes with the deployment process (code, generated files, static content) is per release. Anything that is environment dependent or influenced by users is shared. For Magento this results in the following split.
Shared are: app/etc/env.php (database specific credentials and environment configuration), pub/media (user generated content such as product images and CMS uploads), var/log (log files that accumulate across releases), var/session (if file based sessions are used) and optionally var/cache (if cache contents should be reused between releases, though it is often more efficient to flush the cache after the switch rather than share it). Per release are: vendor/, generated/, pub/static/, app/code/, app/design/, app/etc/config.php and all other source code files.
4. The Atomic Symlink Switch: ln -sfn in Detail
The command ln -sfn /path/to/new/release /path/to/current is the core operation of symlink switch deployment. The flags mean: -s (symbolic link), -f (force: replace an existing link), -n (no dereference: treat an existing symlink as a file rather than as the link's target). The -n flag is crucial: without it, ln -sf on an existing symlink would try to create the link inside the linked directory instead of replacing the link itself. With -n, the existing symlink is replaced directly, and that is the atomic operation.
On Linux systems, ln -sfn is internally a rename(2) syscall operation that is guaranteed to be atomic: the directory entry changes from one inode pointer to the next with no intermediate state. From the perspective of every running process, including nginx worker processes handling requests, the switch is instantaneous. Requests that are already in progress and accessing the old release can finish running. Every new request after the switch sees the new release. There is no situation in which a request points at a directory that is mid transition.
5. Preparing a New Release: Step by Step
Preparing a new release follows a fixed order that must be fully completed before the symlink switch. Only once every preparation step has succeeded is the switch allowed to happen. This principle prevents a half finished release from being activated. In practice this means: the artifact is unpacked into the release directory, shared paths are wired in as symlinks, database migrations run (if compatible with the old release, otherwise maintenance mode is required), and every other server side step runs through, and only once all of that has succeeded does the ln -sfn command follow.
Maintenance mode in Magento sets a file called var/.maintenance.flag. When this file exists, Magento shows a maintenance page to all IPs that are not allow listed. For true zero downtime, maintenance mode needs to be as short as possible or avoided entirely. With expand and contract database migrations (backward compatible schema changes) it is possible to run setup:upgrade without maintenance mode, since the new schema is compatible with the old code. Only after the symlink switch does the new code access the new schema.
deploy:production:
stage: deploy
before_script:
- eval $(ssh-agent -s)
- ssh-add "$SSH_PRIVATE_KEY"
- mkdir -p ~/.ssh && echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
script:
# Transfer artifact to server
- scp -o StrictHostKeyChecking=yes \
"$ARTIFACT_NAME" \
"$DEPLOY_USER@$DEPLOY_HOST:/tmp/$ARTIFACT_NAME"
- |
ssh -o StrictHostKeyChecking=yes "$DEPLOY_USER@$DEPLOY_HOST" bash -s <<REMOTE
set -euo pipefail
# Generate unique release ID from timestamp
readonly RELEASE_ID="\$(date +%Y%m%d-%H%M%S)"
readonly RELEASE_PATH="\$DEPLOY_PATH/releases/\$RELEASE_ID"
readonly SHARED="\$DEPLOY_PATH/shared"
echo "[INFO] Preparing release \$RELEASE_ID"
mkdir -p "\$RELEASE_PATH"
# Unpack artifact into release directory
tar -xzf "/tmp/$ARTIFACT_NAME" -C "\$RELEASE_PATH"
rm -f "/tmp/$ARTIFACT_NAME"
# Link shared files, env.php is environment specific and must not be in the artifact
ln -sfn "\$SHARED/app/etc/env.php" "\$RELEASE_PATH/app/etc/env.php"
# Link shared directories, media persists across releases
rm -rf "\$RELEASE_PATH/pub/media"
ln -sfn "\$SHARED/pub/media" "\$RELEASE_PATH/pub/media"
rm -rf "\$RELEASE_PATH/var/log"
ln -sfn "\$SHARED/var/log" "\$RELEASE_PATH/var/log"
rm -rf "\$RELEASE_PATH/var/session"
ln -sfn "\$SHARED/var/session" "\$RELEASE_PATH/var/session"
# Run database migrations, use expand and contract for zero downtime
cd "\$RELEASE_PATH"
bin/magento setup:upgrade --keep-generated --no-interaction
# Atomic symlink switch, this is the zero downtime moment
ln -sfn "\$RELEASE_PATH" "\$DEPLOY_PATH/current"
echo "[OK] Symlink switched to \$RELEASE_ID"
# Flush cache after switch
bin/magento cache:flush
echo "[OK] Cache flushed"
# Cleanup: keep only last 5 releases
ls -1dt "\$DEPLOY_PATH/releases"/*/ | tail -n +6 | xargs --no-run-if-empty rm -rf
echo "[OK] Old releases cleaned up"
REMOTE
environment:
name: production
needs: [verify:staging]
when: manual
only:
- tags
6. Integrating the Symlink Switch into the GitLab Pipeline
In GitLab, the symlink switch deployment process is implemented as a deploy job that runs a script on the target server over SSH. The script contains every step from unpacking the artifact to the symlink switch within a single SSH connection. This matters: if every step opens its own SSH connection, there is no guarantee that the steps run atomically with respect to other processes on the server. A single SSH connection with a bash heredoc ensures that all steps run sequentially inside a controlled subshell.
A common mistake in GitLab integration is when the deploy job fails halfway through, after the symlink has already switched but before the cache flush has completed. To prevent this, use set -euo pipefail in the remote script and make sure the ln -sfn command sits as late as possible in the script, that is, only after every preparation step has succeeded. If the job then fails, the symlink still points to the old, working release.
7. Symlink Switch vs. Direct Overwrite Compared
Comparing the symlink switch model directly against the classic approach of copying files straight into the web server root makes it clear why the symlink model is the only reasonable choice for production systems.
| Criterion | Direct Overwrite | Symlink Switch | Advantage |
|---|---|---|---|
| Atomicity | None: inconsistency window | Kernel atomic (rename syscall) | Symlink eliminates inconsistency |
| Rollback | Requires a fresh deployment | ln -sfn to previous release | Symlink in milliseconds |
| Deploy Risk | Active directory during rsync | New release prepared, then switched | Symlink is lower risk |
| Parallel Releases | Not possible | Several releases on the server at once | Symlink enables parallelism |
| Storage Needs | One release on the server | Several releases (configurable) | Direct overwrite is leaner |
The only real downside of the symlink model is the increased storage requirement from keeping several releases around. With five retained releases and a 500 MB Magento package, that is 2.5 GB of storage reserved for release history. On modern servers this is not a serious obstacle and is far outweighed by the gains in safety, rollback capability and deployment quality.
8. Rollback: Back to the Previous Release in Seconds
Rollback in the symlink switch model is the most elegant feature of the entire deployment approach. The previous release already sits fully prepared inside the releases directory. A single ln -sfn command pointing at the previous release directory makes it active again. That takes milliseconds. A cache flush follows, which takes seconds. The entire rollback completes in under a minute, regardless of the size of the Magento installation.
In GitLab, a rollback job is set up with when: manual. It automatically determines the previous release by sorting the release directories by date and picking the second newest. The job can also accept a variable ROLLBACK_RELEASE, which lets a specific release be named explicitly, useful when you want to roll back not to the immediately previous release but to an older one. After the rollback, a verify job should run to confirm that the old release actually works correctly.
9. Release Retention: Cleaning Up Old Releases
Without automatic cleanup, the releases directory grows with every deployment. With daily deployments there would be 30 release directories after a month. That is not just a storage problem, it is also a clarity problem. The recommended retention is five releases: the current release plus four historical releases that can be rolled back to in an emergency. This number is configurable and should be tuned to your own rollback strategy.
Cleanup happens at the end of the deploy script, after the symlink switch has succeeded. The command ls -1dt "$DEPLOY_PATH/releases"/*/ | tail -n +6 | xargs --no-run-if-empty rm -rf lists all release directories sorted by date (newest first), skips the first five and deletes everything after that. The active current directory is not endangered by the symlink even if the underlying directory were deleted, but retention should always be configured so that the currently active release never ends up on the deletion list. With correct date ordering this is guaranteed.
rollback:production:
stage: deploy
when: manual
allow_failure: false
before_script:
- eval $(ssh-agent -s)
- ssh-add "$SSH_PRIVATE_KEY"
- mkdir -p ~/.ssh && echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
script:
- |
ssh -o StrictHostKeyChecking=yes "$DEPLOY_USER@$DEPLOY_HOST" bash -s <<REMOTE
set -euo pipefail
# Determine rollback target, use ROLLBACK_RELEASE variable if set, else previous
if [[ -n "${ROLLBACK_RELEASE:-}" ]]; then
TARGET_RELEASE="\$DEPLOY_PATH/releases/$ROLLBACK_RELEASE"
else
# Auto detect previous release (second newest by date)
TARGET_RELEASE="\$(ls -1dt "\$DEPLOY_PATH/releases"/*/ | sed -n '2p' | tr -d '/')"
fi
if [[ -z "\$TARGET_RELEASE" ]] || [[ ! -d "\$TARGET_RELEASE" ]]; then
echo "[ERROR] No rollback target found at: \$TARGET_RELEASE" >&2
exit 1
fi
CURRENT_RELEASE="\$(readlink -f "\$DEPLOY_PATH/current")"
echo "[INFO] Rolling back from: \$CURRENT_RELEASE"
echo "[INFO] Rolling back to: \$TARGET_RELEASE"
# Atomic symlink switch to previous release
ln -sfn "\$TARGET_RELEASE" "\$DEPLOY_PATH/current"
echo "[OK] Symlink switched to: \$TARGET_RELEASE"
# Flush cache after rollback
cd "\$DEPLOY_PATH/current"
bin/magento cache:flush
echo "[OK] Cache flushed after rollback"
REMOTE
environment:
name: production
variables:
ROLLBACK_RELEASE: ""
only:
- tags
- main
10. Summary
The symlink switch deployment model is the foundation of zero downtime deployments for Magento. The release directory structure with releases/, shared/ and the current symlink is set up once on the server and then stays stable. Every new deployment creates a new release directory, prepares it fully, including shared symlinks, database migrations and cache preparation, and then activates it with a single ln -sfn command. This command is atomic, takes milliseconds and creates no inconsistency window.
The biggest lever lies in rollback capability: because every release sits fully present on the server, a rollback is not a second deployment but a millisecond operation. This fundamentally changes decision quality within a team: deployments can happen more boldly and more often, because the way back is always clear and fast. Once a team has adopted this model, nobody wants to go back to direct overwriting.
Symlink Switch Deployment for Magento: The Essentials at a Glance
Atomicity
ln -sfn is a kernel rename operation with no inconsistency window. Every new request after the switch sees exclusively the new release.
Shared vs. Per Release
env.php, pub/media, var/log and var/session are shared. vendor/, generated/, pub/static/ and all code files are per release inside the artifact.
Deploy Order
Unpack the artifact, link shared paths, run migrations, only then switch. Never switch before every preparation step has finished.
Rollback
ln -sfn to the previous release plus cache:flush. Under a minute in total, regardless of installation size.