Zero downtime deployment for Laravel apps
Feb 19, 2022
While I was building laravelremote.com (a Laravel remote job aggregator), I was worried that each time I wanted to push some changes to the production server, it might take the website down for a couple of seconds. Which probably isn't a big deal, but I still wanted to avoid it.
Laravel offers a first-party paid product to avoid this, Envoyer it's only $10 bucks a month. But laravelremote.com doesn't generate any revenue right now, and I'm the type of person that likes to do things in-house to learn how it works, and I also like the freedom that it provides.
So with that in mind, I figured out a way to do this for free, and I wanted to share this in case someone else finds it useful.
I searched around the internet to see how Envoyer works under the hood, I don't exactly remember where I saw this, but basically, it works like this:
We have 3 directories that relate to our project:
A new directory will be created in the
releases/ directory each time we need to deploy a change to the website, then we run some "set up" commands (ex. composer install, move the cache routes, views, etc.), and we add a symbolic link from the releases' storage directory to the "permanent"
storage/ directory, since we don't want to forget sessions and stuff like that on each deployment. After all of this, we add a symbolic link to the
current/directory that points to the new release we just created.
This article is not a tutorial on how to deploy a Laravel app, so I'm not going to explain my NGINX configuration. The only thing you need to know is that my NGINX server is configured to read my website from the
/var/www/laravelremote.com/ directory. This directory has another symbolic that points to the
More simply, every new release will be deployed then linked to the current directory, which will make the server show the updates.
Let's put that into practice, first I created a base directory that will contain everything we need for the project. I like to add this directory under the home folder and give it the name of the app
since we are going to be working in this directory, we should
cd into it
Now we need to create the storage directory and the other subdirectories that Laravel uses.
mkdir storage/ mkdir -p storage/cache/data/ mkdir storage/sessions/ mkdir storage/views/
These are the default Laravel directories that exist within the storage directory.
After that, we need to create the releases folder.
Also, we should create and global .env file that we should link to on every release.
Add to this file the production environment variables.
current directory will be created once we deploy the project for the first time, so there is no need to create it right now.
We could create a new directory in the releases' directory, pull the main branch from GitHub, run every other needed command, and link everything at the end to the current directory. But that seems like a lot of steps for simple deployment, so I created a bash script to do that instead:
#!/bin/sh UNIX_TIME=$(date +%s) DEPLOYMENT_DIRECTORY=$UNIX_TIME mkdir -p $DEPLOYMENT_DIRECTORY git clone --depth 1 --branch master [email protected]:username/reponame.git $DEPLOYMENT_DIRECTORY echo removing storage rm -Rf $DEPLOYMENT_DIRECTORY/storage cd $DEPLOYMENT_DIRECTORY cp ~/laravelremote/.env .env echo link master storage ln -s -n -f -T ~/laravelremote/storage ~/laravelremote/releases/$DEPLOYMENT_DIRECTORY/storage composer install --no-dev npm install npm run prod echo view:cache command php artisan view:cache echo route:cache command php artisan route:cache echo config:cache command php artisan config:cache echo migrations php artisan migrate --force cd .. #link to the latest deployment from Live ln -s -n -f -T ~/laravelremote/releases/$DEPLOYMENT_DIRECTORY ~/laravelremote/current echo Done!
This script lives under the
releases/ folder. It creates a new directory for the new release with the current UNIX timestamp, then it pulls the repo from GitHub into that directory. After that, I remove the storage folder from that directory, copy the
.env file that contains the production keys (you might want to create a symbolic link instead), and then link the "permanent" storage file to the current release.
After that, I run some necessary commands for the project to work.
- Install the composer dependencies
- Install the NPM dependencies and build the resources. (I don't commit my build resources, other people might do it differently)
- Cache the views, routes, and config for optimization
- Run the database migrations
Then finally, we link the
current directory to this release, which will publish the changes.
And that's it, whenever I push something to the master branch, I
ssh into the server, and then I run this script, and my changes are deployed with 0 downtime.
I still have some things that I don't like about this approach.
One of them is that I would like to make it, so it automatically deploys the changes when I push them to GitHub without having to
ssh into the server.
Another one is that the old versions are not removed automatically, I currently remove them manually, but I would like to automate this too. And since we are talking about that, I noticed that Envoyer has a way to roll back the changes if something goes wrong, so you can link one of the previous versions if you need to. That would probably need a more complicated setup on my part, but It would be interesting to do.
Regardless, I'm pretty happy with how this turned up, even though I still want to improve it. If you would like to hear about those future improvements follow me on Twitter, I'll probably post any developments there.
You might also like