Back to Blog
Lesson 26 of the Advanced Laravel: Architecture, Scaling & Performance course
LaravelJune 28, 20264 min read

Zero-Downtime Deployment Pipelines: Atomic Symlink Switching

Learn to implement atomic deployments using symlink switches in your CI/CD pipeline, ensuring your Laravel SaaS remains available during every release.

LaravelCI/CDDeploymentDevOpsPerformancephpbackend

Previously in this course, we explored High-Availability Infrastructure to ensure our services stay online despite localized failures. Today, we focus on the deployment process itself: how to push new code to production without dropping a single request.

In a standard git pull or rsync deployment, your files are updated in place. If a request hits your server while the files are mid-copy, the application may enter an inconsistent state, leading to 500 errors or missing assets. To achieve true zero-downtime, we must decouple the deployment of files from the activation of the release.

The Atomic Deployment Principle

The core concept of an atomic deployment is that the switch between the "old" code and the "new" code must be instantaneous. We achieve this using a symlink-based directory structure.

Instead of deploying directly to a single public_html folder, we create a structure where the web server points to a symlink. When a new version is ready, we update the symlink to point to the new folder in a single, atomic filesystem operation.

Recommended Directory Structure

TEXT
/var/www/my-saas/
├── current -> /var/www/my-saas/releases/20231027120000
├── releases/
│   ├── 20231027100000/
│   └── 20231027120000/
└── shared/
    ├── .env
    └── storage/
  • releases/: Contains timestamped directories of every deployment.
  • shared/: Holds files that persist across deployments, such as storage/ and the .env file.
  • current/: A symlink that your Nginx/Apache root points to.

Implementing the Symlink Switch

The deployment pipeline follows these steps:

  1. Fetch: Clone the repository into a new timestamped directory inside releases/.
  2. Prepare: Install dependencies (composer install, npm install) and run build tasks inside the new directory.
  3. Link Shared: Symlink the shared/ directories/files into the new release directory.
  4. Switch: Atomically update the current symlink.

Worked Example: The Deployment Script

Here is a simplified Bash implementation of the atomic switch:

Bash
# 1. Generate timestamp
RELEASE_DIR="/var/www/my-saas/releases/$(date +%Y%m%d%H%M%S)"

# 2. Deploy to new directory
git clone --depth 1 git@github.com:org/repo.git $RELEASE_DIR

# 3. Link shared assets
ln -nfs /var/www/my-saas/shared/.env $RELEASE_DIR/.env
ln -nfs /var/www/my-saas/shared/storage $RELEASE_DIR/storage

# 4. Run build steps
cd $RELEASE_DIR
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan view:cache

# 5. Atomic symlink switch
ln -nfs $RELEASE_DIR /var/www/my-saas/current

The ln -nfs command is the secret sauce. By using the -n (no-dereference) and -f (force) flags, the symlink update happens as an atomic system call. The web server process will resolve the path to the old release until the exact millisecond the link is updated, at which point it begins resolving to the new one.

Advancing the Project: Deployment Automation

For our SaaS platform, we are moving away from manual scripts. We are now integrating this logic into our CI/CD pipeline using GitHub Actions. If you’ve followed our previous work on Zero-downtime deploy with GitHub Actions, you can now replace the simple copy steps with this symlink strategy.

Hands-on Exercise: Clean Up Old Releases

An atomic deployment strategy leaves behind old code folders, which will eventually fill your disk. Create a simple cleanup script to run after your deployment:

Bash
# Keep only the last 5 releases
cd /var/www/my-saas/releases && ls -dt */ | tail -n +6 | xargs rm -rf

Add this to your deployment pipeline to ensure disk space remains managed.

Common Pitfalls

  1. Database Migrations: If your migration adds a column that the old code doesn't support, you have a race condition. Always ensure your database changes are additive (e.g., add a nullable column) rather than destructive (e.g., dropping a column) to ensure the previous version of your code can still run during the transition.
  2. Queue Workers: Your workers are likely running the "old" code even after the symlink switch. You must restart them using php artisan queue:restart immediately after the switch to ensure they pick up the new code.
  3. Session State: If you use file-based sessions, they will be lost during the switch. Ensure you are using Redis for session persistence as discussed in our lesson on Session Persistence in Clusters.

Recap

Atomic deployments leverage symlinks to swap out application versions instantly. By separating the build process from the activation process, you eliminate the risk of serving partially-updated code to your users. Combine this with additive database migrations and Redis-backed state to ensure your Laravel SaaS remains rock-solid during deployments.

Up next: We will dive into Advanced OAuth2 Implementation to secure our multi-tenant SaaS architecture.

Similar Posts