Tutorial: Moving live projects from Forge to Envoyer

Published on by

Tutorial: Moving live projects from Forge to Envoyer image

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.


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


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 .env
drwxrwxr-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.md
drwxrwxr-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.json
drwxrwxr-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.lock
drwxrwxr-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.xml
drwxrwxr-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.php
drwxrwxr-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


/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.


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


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.


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

Joost photo

Product manager turned solo founder at Freddy Feedback.


Laravel Newsletter

Join 40k+ other developers and never miss out on new tips, tutorials, and more.


Version 4 of Tinkerwell is available now. Get the most popular PHP scratchpad with all its new features and simplify your development workflow today.

Visit Tinkerwell
Laravel Forge logo

Laravel Forge

Easily create and manage your servers and deploy your Laravel applications in seconds.

Laravel Forge
Tinkerwell logo


The must-have code runner for Laravel developers. Tinker with AI, autocompletion and instant feedback on local and production environments.

No Compromises logo

No Compromises

Joel and Aaron, the two seasoned devs from the No Compromises podcast, are now available to hire for your Laravel project. ⬧ Flat rate of $7500/mo. ⬧ No lengthy sales process. ⬧ No contracts. ⬧ 100% money back guarantee.

No Compromises
Kirschbaum logo


Providing innovation and stability to ensure your web application succeeds.

Shift logo


Running an old Laravel version? Instant, automated Laravel upgrades and code modernization to keep your applications fresh.

Bacancy logo


Supercharge your project with a seasoned Laravel developer with 4-6 years of experience for just $2500/month. Get 160 hours of dedicated expertise & a risk-free 15-day trial. Schedule a call now!

Lucky Media logo

Lucky Media

Bespoke software solutions built for your business. We ♥ Laravel

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar: Laravel E-Commerce

E-Commerce for Laravel. An open-source package that brings the power of modern headless e-commerce functionality to Laravel.

Lunar: Laravel E-Commerce
LaraJobs logo


The official Laravel job board

Larafast: Laravel SaaS Starter Kit logo

Larafast: Laravel SaaS Starter Kit

Larafast is a Laravel SaaS Starter Kit with ready-to-go features for Payments, Auth, Admin, Blog, SEO, and beautiful themes. Available with Vue and Livewire stacks.

Larafast: Laravel SaaS Starter Kit
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS Starter Kit

SaaSykit is a Laravel SaaS Starter Kit that comes with all features required to run a modern SaaS. Payments, Beautiful Checkout, Admin Panel, User dashboard, Auth, Ready Components, Stats, Blog, Docs and more.

SaaSykit: Laravel SaaS Starter Kit
Rector logo


Your partner for seamless Laravel upgrades, cutting costs, and accelerating innovation for successful companies


The latest

View all →
New Proposed Array Find Functions in PHP 8.4 image

New Proposed Array Find Functions in PHP 8.4

Read article
Laracon AU 2024 tickets are now on sale image

Laracon AU 2024 tickets are now on sale

Read article
Dash UI is a Laravel Blade Component Library Inspired by Shopify Polaris image

Dash UI is a Laravel Blade Component Library Inspired by Shopify Polaris

Read article
A Look at What's Coming to PHP 8.4 image

A Look at What's Coming to PHP 8.4

Read article
Is class instantiation without extra parenthesis coming to PHP 8.4? image

Is class instantiation without extra parenthesis coming to PHP 8.4?

Read article
Validation Errors Card for Laravel Pulse image

Validation Errors Card for Laravel Pulse

Read article