Laravel Zero Downtime Deployment
When it comes to updating a live application, there are two fundamentally different ways of going about this.
In the first approach, we make incremental changes to the state of our system. For example, we update files, modify environment properties, install additional necessities, and so on. In the second approach, we tear down whole machines and rebuild the system with new images and declarative configurations (for example, using Kubernetes).
Laravel Deployment Made Easy
This article mainly covers relatively small applications, which may not be hosted in the cloud, although I will mention how Kubernetes can greatly help us with deployments beyond the “no-cloud” scenario. We will also discuss some general problems and tips for performing successful updates that might be applicable in a range of different situations, not just with Laravel deployment.
For the purposes of this demonstration, I will be using a Laravel example, but bear in mind that any PHP application could use a similar approach.
For starters, it is crucial that we know the version of code currently deployed on production. It can be included in some file or at least in the name of a folder or file. As for the naming, if we follow the standard practice of semantic versioning, we can include more information in it than just a single number.
Looking at two different releases, this added information could help us easily understand the nature of changes introduced between them.
Versioning the release begins with a version control system, such as Git. Let’s say we have prepared a release for deployment, for example, version 1.0.3. When it comes to organizing these releases and code flows, there are different development styles such as trunk-based development and Git flow, which you can choose or mix based on your team’s preferences and the specifics of your project. In the end, we will most likely end up with our releases tagged correspondingly on our main branch.
After the commit, we can create a simple tag like this:
git tag v1.0.3
And then we include tags while executing the push command:
git push <origin> <branch> --tags
We can also add tags to old commits using their hashes.
Getting Release Files to Their Destination
Laravel deployment takes time, even if it’s simply copying files. However, even if it doesn’t take too long, our goal is to achieve zero downtime.
Therefore, we should avoid installing the update in-place and should not change files which are being served live. Instead, we should deploy to another directory and make the switch only once the installation is completely ready.
Actually, there are various tools and services which can assist us with deployments, such as Envoyer.io (by Laravel.com designer Jack McDade), Capistrano, Deployer, etc. I have not used all of them in production yet, so I cannot make recommendations or write a comprehensive comparison, but let me showcase the idea behind these products. If some (or all) of them cannot meet your requirements, you can always create your custom scripts to automate the process the best way you see fit.
For the purposes of this demonstration, let’s say our Laravel application is served by an Nginx server from the following path:
First, we need a directory to place release files every time we make a deployment. Also, we need a symlink which will point to the current working release. In this case,
/var/www/demo will serve as our symlink. Reassigning the pointer will allow us to quickly change releases.
In case we are dealing with an Apache server, we might need to allow following symlinks in the configuration:
Our structure can be something like this:
/opt/demo/release/v0.1.0 /opt/demo/release/v0.1.1 /opt/demo/release/v0.1.2
There might be some files which we need to persist through different deployments, e.g., log files (if we are not using Logstash, obviously). In the case of Laravel deployment, we might want to keep the storage directory and .env configuration file. We can keep them separated from other files and use their symlinks instead.
In order to fetch our release files from the Git repository, we can either use clone or archive commands. Some people use git clone, but you cannot clone a particular commit or tag. That means the whole repository is fetched and then the specific tag is selected. When a repository contains many branches or a large history, its size is considerably larger than the release archive. So, if you don’t specifically need the git repo on production, you could use
git archive. This allows us to fetch just a file archive by a specific tag. Another advantage of using the latter is that we can ignore some files and folders which should not be present in the production environment, e.g., tests. For this, we just need to set the export-ignore property in the
.gitattributes file. In the OWASP Secure Coding Practices Checklist you can find the following recommendation: “Remove test code or any functionality not intended for production, prior to deployment.”
If we are fetching the release from source version control system, git archive and export-ignore could help us with this requirement.
Let’s take a look at a simplified script (it would need better error handling in production):
#!/bin/bash # Terminate execution if any command fails set -e # Get tag from a script argument TAG=$1 GIT_REMOTE_URL='here should be a remote url of the repo' BASE_DIR=/opt/demo # Create folder structure for releases if necessary RELEASE_DIR=$BASE_DIR/releases/$TAG mkdir -p $RELEASE_DIR mkdir -p $BASE_DIR/storage cd $RELEASE_DIR # Fetch the release files from git as a tar archive and unzip git archive \ --remote=$GIT_REMOTE_URL \ --format=tar \ $TAG \ | tar xf - # Install laravel dependencies with composer composer install -o --no-interaction --no-dev # Create symlinks to `storage` and `.env` ln -sf $BASE_DIR/.env ./ rm -rf storage && ln -sf $BASE_DIR/storage ./ # Run database migrations php artisan migrate --no-interaction --force # Run optimization commands for laravel php artisan optimize php artisan cache:clear php artisan route:cache php artisan view:clear php artisan config:cache # Remove existing directory or symlink for the release and create a new one. NGINX_DIR=/var/www/public mkdir -p $NGINX_DIR rm -f $NGINX_DIR/demo ln -sf $RELEASE_DIR $NGINX_DIR/demo
For deploying our release, we could just execute the following:
Note: In this example, v1.0.3 is the git tag of our release.
Composer on Production?
You might have noticed that the script is invoking Composer to install dependencies. Although you see this in many articles, there can be some problems with this approach. Generally, it is a best practice to create a complete build of an application and advance this build through various testing environments of your infrastructure. In the end, you would have a thoroughly tested build, which can be safely deployed to production. Even though every build should be reproducible from scratch, it does not mean that we should rebuild the app on different stages. When we make composer install on production, this is not genuinely the same build as the tested one and here is what can go wrong:
- Network error can interrupt downloading dependencies.
- Library vendor might not always follow the SemVer.
A network error can be easily noticed. Our script would even stop executing with an error. But a breaking change in a library might be very difficult to pin down without running tests, which you cannot do in production. While installing dependencies, Composer, npm, and other similar tools rely on semantic versioning–major.minor.patch. If you see ~1.0.2 in the composer.json, it means to install version 1.0.2 or the latest patch version, such as 1.0.4. If you see ^1.0.2, it means to install version 1.0.2 or the latest minor or patch version, such as 1.1.0. We trust the library vendor to bump the major number when any breaking change is introduced, but sometimes this requirement is missed or not followed. There have been such cases in the past. Even if you put fixed versions in your composer.json, your dependencies might have ~ and ^ in their composer.json.
If it is accessible, in my opinion, a better way would be to use an artifact repository (Nexus, JFrog, etc.). The release build, containing all necessary dependencies, would be created once, initially. This artifact would be stored in a repository and fetched for various testing stages from there. Also, that would be the build to be deployed to production, instead of rebuilding the app from Git.
Keeping Code and Database Compatible
The reason I fell in love with Laravel at first sight was how its author paid close attention to details, thought about the convenience of developers, and also incorporated a lot of best practices into the framework, like database migrations.
Database migrations allow us to have our database and code in sync. Both of their changes can be included in a single commit, hence single release. However, this does not mean that any change can be deployed without downtime. At some point during the deployment, there will be different versions of the application and database running. In case of problems, this point might even turn into a period. We should always try to make both of them compatible with the previous versions of their companions: old database–new app, new database–old app.
For example, let’s say we have an
address column and need to split it into
address2. To keep everything compatible, we might need several releases.
- Add two new columns in the database.
- Modify the application to use new fields whenever possible.
addressdata to new columns and drop it.
This case is also a good example of how small changes are much better for deployment. Their rollback is also easier. If we are changing the codebase and database for several weeks or months, it might be impossible to update the production system without downtime.
Some Awesomeness of Kubernetes
Even though the scale of our application might not need clouds, nodes, and Kubernetes, I would still like to mention what deployments look like in K8s. In this case, we do not make changes to the system, but rather declare what we would like to achieve and what should be running on how many replicas. Then, Kubernetes makes sure that the actual state matches the desired one.
Whenever we have a new release ready, we build an image with new files in it, tag the image with the new version, and pass it to K8s. The latter will quickly spin up our image inside a cluster. It will wait before the application is ready based on the readiness check that we provide, then unnoticeably redirect traffic to the new application and kill the old one. We can very easily have several versions of our app running which would let us perform blue/green or canary deployments with just a few commands.
If you are interested, there are some impressive demonstrations in the talk “9 Steps to Awesome with Kubernetes by Burr Sutter.”
Understanding the basics
What is Laravel and why it is used?
Laravel is an opinionated PHP framework with MVC architecture, providing modern tools and expressive syntax for rapid development, maintenance, testing, and, consequently, shorter release cycles.
Is Laravel popular?
According to the charts of 2019, Laravel is at the top of the list as the most popular PHP framework.
What are advantages of Laravel?
Eloquent ORM, fluent query builder, great documentation, and always up-to-date Laracasts are definitely making the framework stand out. Laravel embraces automation and rapid development with providing tools like Artisan.
Located in Tbilisi, Georgia
Member since November 2, 2018
About the author