Tutorial: Moving live projects from Forge to Envoyer
Published on by Joost
In general, I try to reduce the amount of paid services that I use. If they add significant value, I will integrate them because, as a bootstrapped founder, the costs quickly add up. I also try to reduce the amount of server management work because it makes me a grumpy person.
Recently I switched my live projects from Forge to Envoyer for deployments. And because these projects were live applications and actively used by customers, I wanted the switch to be as seamless and with as little downtime as possible.
In this post, I’d like to share the steps involved in doing this. First, I’ll explain some context about my decision to switch. If you want to dive into the migration steps, please feel free to skip this first part.
Background
Deployments with Forge took about 10 to 20 seconds. My project has an API with more and more traffic, and because I ship changes multiple times a week, this quickly adds up to the point that I started getting downtime notifications from my monitoring services.
I also wanted to bring more steps to the deployment process like npm ci
and npm run production
, which adds even more downtime.
So it was time for zero-downtime deployments, which is the main selling point of Envoyer. But there are a few other advantages that also add a lot of value to the deployment process, and that I didn’t really think about before.
Your application doesn’t stay in maintenance mode if things go wrong (and they will)
It’s common to have php artisan down
at the start of your deployment script to put your application into maintenance mode during the deployment, and then finish with php artisan up
to take it back out of maintenance mode.
But if something goes wrong in between, the application will stay in maintenance mode because it never reaches php artisan up
. So then you have to take your app out of maintenance mode and investigate what went wrong. With Envoyer, if a deployment fails at any point in the build process, the whole process is canceled, and your app is still up and running.
Redeploy deployments
If at any point you introduced a bug in your app and you need to put it back to the state where it was working, you can redeploy a previous deployment with a single click. With Laravel Forge, you’ll have a bug in production with no way to easily revert.
Configure the build and deployment steps
Although with Forge you can do this using the deployment script, it always felt like playing with fire. With Envoyer, things are easy, organized, and drag-and-drop.
Make your life easier
Because deployments are now zero downtime, you can introduce more steps to these deployments that will make your deploy life easier. For instance, I have a few files that I copy over to a CDN. I can now do this by running an artisan command during deployment using Deployment Hooks. Sweet!
There are a few other features like Health Checks and Heartbeats, but these are the main advantages. Let’s move on to the migration.
Step 1: The basics
I won’t cover this in detail as this is really straightforward. Do these things first in Envoyer: connect your source control of choice (like Github) and create a project.
Tip: if you like to have more than four recent deployments, go to your project > settings and increase Deployments to Retain.
Note: the steps below assume that your web directory in Forge is set to /public, and not something else. You can check this by going to your Site in Forge, then to Meta in the side menu, and then Web Directory. If you have anything other than /public just keep this in mind for the next steps.
Step 2: Import your Forge servers
This previously involved multiple steps copying IP addresses and adding SSH keys, but the Laravel team has now simplified this process:
- Go to your account in Forge (top right), go to API from the side menu, and create a token (you could call it Envoyer)
- Then, go to your account in Envoyer (top right), go to Integrations in the side menu and use the token you just generated to connect to Forge
- Go to your project, click on Servers and click on Import Forge server and choose which site to import
Done!
Step 3: First deploy
You can now actually hit deploy. Don’t worry, as this deployment won’t be live yet, and we’ll get to that later. But it will help you understand the underlying principles better because you can look at the old vs. the new file structure. And because nothing will be live yet, it’s an excellent way to test that everything’s working.
We’ll switch our live app from Forge to Envoyer at the last step.
Step 4: Understanding the new file structure
Your site directory
Let’s look at what we have now. SSH into your Forge server and go to your site directory (below shown as example.com). It will look something like this:
drwxrwxr-x 15 forge forge 4096 Dec 21 16:27 ./drwxr-xr-x 11 forge forge 4096 Dec 21 16:26 ../-rw-rw-r-- 1 forge forge 2417 Dec 15 22:04 .envdrwxrwxr-x 8 forge forge 4096 Dec 19 14:07 .git/-rw-rw-r-- 1 forge forge 111 Jul 3 19:12 .gitattributes-rw-rw-r-- 1 forge forge 214 Dec 9 15:30 .gitignore-rw-rw-r-- 1 forge forge 174 Jul 3 19:12 .styleci.yml-rw-rw-r-- 1 forge forge 1607 Nov 9 14:07 README.mddrwxrwxr-x 14 forge forge 4096 Nov 9 14:07 app/-rw-rw-r-- 1 forge forge 1686 Jul 3 19:12 artisan-rw-rw-r-- 1 forge forge 187 Jul 3 19:12 auth.jsondrwxrwxr-x 3 forge forge 4096 Jul 3 19:12 bootstrap/-rw-rw-r-- 1 forge forge 2850 Dec 16 13:32 composer.json-rw-rw-r-- 1 forge forge 396382 Dec 19 14:07 composer.lockdrwxrwxr-x 2 forge forge 4096 Dec 17 11:51 config/lrwxrwxrwx 1 forge forge 46 Dec 21 16:27 current -> /home/forge/example.com/releases/20201221152610/drwxrwxr-x 6 forge forge 4096 Nov 17 13:46 database/drwxrwxr-x 3 forge forge 4096 Jul 3 19:12 nova-components/-rw-rw-r-- 1 forge forge 450772 Dec 15 22:35 package-lock.json-rw-rw-r-- 1 forge forge 1059 Nov 17 13:46 package.json-rw-rw-r-- 1 forge forge 1405 Jul 3 19:12 phpunit.xmldrwxrwxr-x 11 forge forge 4096 Dec 19 14:07 public/drwxrwxr-x 9 forge forge 4096 Dec 21 16:26 releases/drwxrwxr-x 6 forge forge 4096 Jul 3 19:12 resources/drwxrwxr-x 2 forge forge 4096 Dec 19 14:07 routes/-rw-rw-r-- 1 forge forge 563 Jul 3 19:12 server.phpdrwxrwxr-x 7 forge forge 4096 Nov 9 14:39 storage/drwxrwxr-x 4 forge forge 4096 Jul 3 19:12 tests/drwxrwxr-x 67 forge forge 4096 Dec 19 14:07 vendor/-rw-rw-r-- 1 forge forge 683 Jul 3 19:12 webpack.mix.js
What you see here in the site folder (example.com) is your website that was deployed through Forge.
New Envoyer directories
There are two new directories created by Envoyer.
The folder current
is a symlink to the latest release directory. That is how deployments work: a new release directory is created, and the current directory will link to it.
The folder releases
contains all your deployments. The names of the folders are timestamps.
New structure
If you now go into the current directory, you’ll notice two things:
.env links back to /home/forge/example.com/.env
and
/storage links back to /home/forge/example.com/storage
This means that these won’t be overwritten with every deployment, so files and cache that is stored in storage
there will stay as-is and of course, your .env
file will stay there locally used by every deployment.
Step 5: linking the storage folder
That brings us to step 5. Remember that when you deployed via Forge for the first time, you ran php artisan strorage:link
? For your new deployments via Envoyer, this link is needed as well. The problem is that with every new deployment, you would have to rerun it because every deployment is a new copy of your app in a new folder.
Luckily, Envoyer provides a setting to permanently setup this link. Go to your project, Deployment Hooks, and click Manage Linked Folders. Link public/storage
to storage/app/public
.
Step 6: Horizon, schedulers and daemons
If you’ve set these up in Forge, you’ll know what they are and why you have them. They run in a folder, which is still the ‘old’ folder where your Forge deployment lives. Let’s add the schedulers and daemons that you need to the Envoyer folder.
Schedulers
In Forge, go to your server and for every scheduler that has /home/forge/example.com
where example.com is your site, copy these and create a new scheduler where you add current
to the path: /home/forge/example.com/current
.
So for instance
php /home/forge/example.com/artisan schedule:run
becomes
php /home/forge/example.com/current/artisan schedule:run
The reason I’m saying copy is because your current app (deployed via Forge) is still running and using the schedulers. So add the new ones, and once you’ve completed the migration to Envoyer (the last step in this guide), you can remove the old ones.
Note: these schedulers will now run on both applications (the current Forge one and the new Envoyer one). I didn’t encounter any issues, but if you have lots of processes running very often (eg, every minute) then check how you want to coordinate this first. You can, for example, first copy over the schedulers and do the deployment as the last step.
Daemons
The same for daemons, where the Directory value should be /home/forge/example.com/current
Here as well: add new ones, don’t update the existing ones.
Laravel Horizon
If you use Horizon you probably run php artisan horizon:terminate
on deployment. You can add this to Envoyer by going to your project, Deployment Hooks, and then select Add Hook.
Give it a name, run it as Forge, and then add:
cd {{ release }}php artisan horizon:terminate
The first line is to make sure it goes to into the just added directory for the new deployment to run the command there.
Make sure to select the server and click Save Hook.
Step 7: Are we live yet?
No, not yet, but let’s do that now! We can now switch the Web Directory to the new current
directory, which symlinks to the latest release.
In Forge, go to your site, select Meta from the side menu and then go to Web Directory. Update this to /current/public
.
Congrats, you now have zero downtime deployments.
Bonus steps
Removing old files
As you probably figured out, there is now an unused copy of your app in the site folder (example.com) because the Envoyer deployments are in example.com/current. That means you can optionally delete the old files.
You can delete everything except for:
- The folder current
- The folder releases
- The folder storage
- The file .env
Why do my releases folders not have a .git
folder like I had on Forge?
In your previous Forge deployment, there was a .git folder because Forge would pull from your source control repository. Envoyer doesn’t do this. Instead, it downloads a tarball of your repo and then unzips this in the relevant release folder.
Adding deployment hooks
We already covered this briefly for Horizon, but you can add any deployment hooks that you like.
Things like running npm ci
and npm run production
are useful, but you can also create your own custom artisan commands and run them on deployment. For instance, a command to clear your own cached items from your Redis cache. Because you now have zero downtime deployments, it’s really convenient to move things to the deployment process.
Hopefully, this helped you to move your production apps from Forge to Envoyer. Enjoy those sweet zero downtime deployments! If you want to sign up for Envoyer, you can do that here.
Written by Joost from Freddy Feedback