Zero Downtime Craft CMS 3 Deployments with

Continuous deployment of a website or application with a service like greatly reduces the chance of mistakes that can occur when launching or updating a site.

In this blogpost I'll go over our config at Marbles for how we deploy our Craft CMS 3 websites with Buddy. With some creative thinking, this method will also work on Envoyer, Deploybot or any other continuous delivery service.

Prior reading

Before configuring your own website to run on Buddy I suggest getting up to speed with the concept of Pipelines.

Craft config

First things first, there's a few config settings that need to be set to have a continuous deployment workflow without errors or inconvenience.

You need to give your Craft site a unique ID and override the session location, this is to make sure that sessions and logins are not lost between deployments. It's very annoying to lose access to the backend every time a deployment gets run.

* [
    'appId' => 'marbles',
    'overridePhpSessionLocation' => true,

The next option should only be enabled on a staging or production environment to prevent updates. You don't want to have an update Craft instance with updated database changes being overwritten by a deployment that wasn't updated yet!

    'allowUpdates' => false,
This deployment flow is based on the default “Atomic Deployment” template of Buddy


Buddy can be configured by using either the GUI or a buddy.yml file. I'll go over the settings in the GUI here but know that these can be just as easily configured using the config file. Buddy even allows you to switch at any time and keep your pipelines intact.

Defining a pipeline

It's possible to define multiple pipelines for each project and even start one pipeline from an action within another. For this example I've just defined one pipeline, but we often have a pipeline for each environment (staging, acceptance, live...)


Inside each pipeline you can define a set of actions that need to be run.

The pipeline actions


This is a slightly modified version of the default “Run Webpack” action that Buddy provides, what we've changed here is the Docker image (with one from Yarnpkg) and the basic commands. This runs our Laravel Mix script to compile our assets, generate critical css and run some optimizations.

The default “Run Webpack” action reconfigured

Composer install

To install Craft CMS and all your plugins, a composer install needs to be run. Make sure you commit your composer.lock to speed things up and keep everything consistent!

For this action we use the laratools/laravel-ci docker image which is a great base that easily allows us to install everything without having to customise too much.

Install dependencies

Rsync files

This action is pretty straight forward, after everything is installed and compiled it uses rsync to upload the files from the pipeline filesystem to a deploy-cache folder on the server.

I've set up a few environment variables for the working directory and SSH settings in the pipeline settings to make this configuration easier to reuse on other projects or pipelines.

The “Rsync files” action

Activate release

This is the most complicated part of our pipeline, the action is set to "Run as a script" which means the context is saved between each line of code, we're doing a few things: 

  • Creating the release folder for the current revision (or deleting it and creating anew when we want to refresh)
  • Creating and symlinking the persistence folders (storage and assets)
  • Linking the latest release to a current link in the working directory.
  • Reloading FPM to make new code available (we have some strict opcache settings)
  • Running any Craft CMS or plugin migrations that might be needed
  • Clearing caches
  • Deleting stale releases

Below the image is the full code with extra comments to clarify what we're doing.

# If it exists and we want to refresh
if [ -d "releases/${execution.to_revision.revision}" ] && [ "${execution.refresh}" = "true" ];
# Then delete the revision
rm -rf releases/${execution.to_revision.revision};
# If there is no directory for this release yet
if [ ! -d "releases/${execution.to_revision.revision}" ];
# Copy the deploy cache to the new release directory
cp -dR deploy-cache releases/${execution.to_revision.revision};
# Make sure we have a storage/assets folder
mkdir -p storage/assets
# Make sure the release doesn't have a storage or web/assets folder
rm -rf releases/${execution.to_revision.revision}/storage
rm -rf releases/${execution.to_revision.revision}/web/assets
# Link the storage and web/assets folders
ln -s ~/httpdocs/storage releases/${execution.to_revision.revision}/storage
ln -s ~/httpdocs/storage/assets releases/${execution.to_revision.revision}/web/assets
# Remove the link to the current release
rm -f current
# Symlink to the new release
ln -s releases/${execution.to_revision.revision} current
# Reload FPM to make new code available
sudo /bin/systemctl reload plesk-php71-fpm
# Move into the current release
cd current
# Run any migrations we might have
./craft migrate/all
# Clear caches
./craft cache/flush-all
# Only keep 2 releases
cd ../releases && ls -t | tail -n +3 | xargs rm -rf

Further reading

Most of this setup was done with the help of the following resources:

If you have any questions or comments, or you find that something in this blogpost doesn't work (anymore), don't hesitate to send me a Tweet or any other way of contact!