Deploy with zero-downtime
In the previous episode, we installed Deployer and defined our deployment flow by configuring a deploy.yaml
file at the root of our project. Enough theory, it's about time we ship this thing to our server.
Ship it!
In our deploy.yaml
file, we configured our deployment flow inside the deploy
task. Therefore, all we need to do to start a deployment is to run:
dep deploy
If everything went well, we should have the following output.
What happened?
Our application was successfully deployed to our server using a technique that causes no downtime to your users. We call this “zero-downtime”.
Concretely, the following happens during each deployment:
- A brand new folder is created and the entire project is cloned into it. We call that folder: a release. Deployer does that efficiently by caching things from one release to another.
- Our deployment tasks are being executed inside that release. For example,
deploy:vendor
which executescomposer install
. - Once all tasks have been successfully executed, that release is marked as the new "current" release. It does that by creating a
current
symlink that points to the release folder we’ve just created.
Since all our deployment tasks are being executed before we symlink the directory to the new release, the current
folder always acts as the latest stable release without ever experiencing any downtime.
This is why, when we configured Nginx on our server, we told it to serve our application inside that current
folder by defining our root
as /var/www/jollygood.app/current/public
.
After this first deployment, if we look at the folder structure of our deploy_path
inside our server — in our case /var/www/jollygood.app
— we should see something like this.
As you can see our current
folder which is responsible for representing the active release is pointing to the releases/1
folder — our very first release 🎉.
If we were to deploy again, then we would see the following folder structure during the deployment process.
At this point, the second release — located in releases/2
— is being built but any user accessing our application will still be using releases/1
.
Once all tasks have been successfully executed, the current
symlink is updated to point to that new release.
In case you're wondering, the .dep
folder exists for internal usage of the Deployer library. It is used to store some metadata and cache things such as our git repository so we don't clone it from scratch at every deployment.
If anything goes wrong during the deployment process, the user is none the wiser as they will continue to use the first release until we are sure the deployment was a success.
On top of that, if the deployment was successful but we later find out there is a bug in the latest release, we can roll back to a previous release by simply updating the current
symlink again. Deployer allows you to do this via the dep rollback
command.
Additionally, you may use the dep status
command to list your latest available releases and their status — i.e. if they were successful or not and which one is the current
release.
Now you might be wondering: what about our .env
file and our storage
directory? Do they also get created from scratch after each deployment?
Thankfully not. Instead, they are put in a special shared
folder. During the deployment of each release, we create symlinks that point inside that shared folder so that every release uses the same .env
file and the same storage
directory.
By default, only .env
and storage
are configured as shared by the Laravel recipe but you may explicitly configure which files and folders should be shared via the shared_files
and shared_dirs
options respectively.
However, on the very first deployment, we don’t have a .env
file on our repository. Therefore, a brand new empty one is created and added to our shared
folder.
This is why, if you remember the output of our first deployment, we were not able to migrate our database.
Additionally, without a .env
file, our application is lacking the mandatory APP_KEY
environment variable which is why, if you try to visit your application, you’ll see a 500
error.
So let’s fix that.
Setting up our environment
We’ll start by running the following command to SSH into our server.
dep ssh
Now that we have deployed our application, we arrive directly in the current
release.
Now, if we run ls -la
, you’ll notice that the .env
file exists and is symlinked to a .env
file on our shared folder as we’ve mentioned above.
However, that file is empty so let’s fill it. We’ll run the following commands to start from our .env.example
file and to generate the APP_KEY
environment variable randomly.
cp .env.example .env
php artisan key:generate
Note that running the cp
command will keep the symlink to the shared folder which is exactly what we want.
Next, vim into your .env
file to configure your production environment variables.
- Ensure
APP_NAME
is set to your application name. - Ensure
APP_ENV=production
in production - Ensure
DEBUG=false
in production - Ensure
APP_URL
is set to your production base URL — usinghttps://
if you set up a SSL certificate as we did in episode 2. - Ensure your
DB_*
variables are pointing to your production database otherwise theartisan:migrate
task will fail on our next deployment.
Now, if you save and exit — :wq
— you’ll still see a 500
when you try to view your application. That’s because we included the following task on our deployment flow: artisan:config:cache
. Caching our configuration is good for performance but we’ve got to remember to regenerate the cache after manually editing the .env
file.
If you’re already inside your server, simply run php artisan config:cache
, otherwise, you may run dep artisan:config:cache
to run this on your server from your local machine.
# Inside the current release.
php artisan config:cache
# In your local machine.
dep artisan:config:cache
If you're still seeing a 500
at this point, it could either be a cache issue from PHP-FPM or it could be that the page you're requesting requires access to the database which we haven't yet migrated at this point. Both of these will be automatically fixed by our next deployment so bear with me before entering panic mode. 😊
Finally, let’s start a brand new deployment. This will ensure our deployment flow works properly and that the database is migrated now that we’ve set up our .env
file. Back to your local machine run dep deploy
again. You should now see the following console output.
And that’s it! You’re now fully equipped to deploy your application at any time with two simple words!
Okay now you can start to panic if you're still seeing a 500. 😅 Check the logs using dep logs
or by manually checking the log files on your server. Feel free to comment your problem below so others can help you and so you can help others facing the same issue in the future.
Running tasks after deploying
Now that we have a better understanding of "zero-downtime", there's one last thing I'd like to mention before wrapping up this article.
You might have noticed that tasks can be executed both during and after a deployment. For example, if you want to regenerate your configuration cache after updating your .env
file, you can run:
dep artisan:config:cache
Since we are not in the middle of a deployment, the task will be executed directly inside the current
directory — as opposed to inside the release that is being built.
When creating custom tasks in Deployer you have access to many paths but here are the most important ones you’ll want to know:
-
current_path
. This is the path of the active release. The one that users interact with when they go on your application. -
release_path
. This is the path of the release which is being built. It has not yet been symlinked so users do not see it yet but at the end of the deployment flow — if everything goes well — it will become the newcurrent_path
. -
release_or_current_path
. This will always be equal to therelease_path
unless it does not exist — meaning we are not deploying. In this case, it will equal thecurrent_path
instead. This path represents the "best of both worlds" and I recommend using this one when possible.
Therefore, we could update our npm:run:prod
custom task inside our deploy.yaml
file to make sure it can also be executed outside a deployment if we need to.
tasks:
...
npm:run:prod:
- - run: 'cd {{release_path}} && npm run prod'
+ - run: 'cd {{release_or_current_path}} && npm run prod'
All artisan commands provided by the Laravel recipe are using this release_or_current_path
variable which means you can use them inside and outside deployments.
The only exceptions to this rule are the artisan:up
and artisan:down
tasks which will always use the current_path
even when they are inside a deployment. This is by design since, even when deploying, we’ll want maintenance mode changes to apply to our active release.
Conclusion
Congratulations, We did it! 🥳
It took us 4 episodes but we went from nothing to having our Laravel application deployed in a fully configured server using only 2 words: dep deploy
.
You can find the updated deploy.yaml
for this episode here — or the equivalent deploy.php
here.
Where do we go from here? Well, having a set of predefined recipes and tasks is super helpful but there's no doubt you'll want to add more custom logic to your deployment flow either now or in the future. Fortunately, Deployer allows great flexibility by letting us code our own recipes in PHP which is exactly what we'll do in the next episode. See you there!