September 24, 2021
Here's how I set up a load balanced environment on Laravel Forge and deployed with Envoyer.
Forge is a service run by the Laravel crew that provisions and configures servers. They don't run the actual servers - you'll pay Amazon or DigitalOcean for that - but they provide a management layer that makes your life easy. You can host Laravel projects, general PHP sites, static HTML, or Symphony sites very quickly and easily.
Envoyer is a separate service that handles code deployment. Typically Envoyer will wait for you to publish to the main
branch of your Github repo, and when you do, it will execute some predefined steps to deploy the latest version of your code on your server. It does not create or manage servers - it just connects to them and runs shell commands at the right time in the right place. It's built around the idea of a zero or extremely low downtime deployment, where almost all of the deployment steps are done in a separate directory and only if it's successful will it be symlinked into your main document root and served to your users.
During our deployment process, we clone the latest version of the repo, install composer dependencies, build CSS + JS bundles, run database migrations, clear caches, and reload queue workers. Our deployment runs on four different servers and executes some unique commands on certain servers.
Using these tools together, you can roll out and manage a set of database, web, and queue servers and deploy new code with virtually no downtime for users, very little hassle for yourself, and quite a bit of control when things go wrong.
For smaller projects where a minute or two of downtime during deployment might be acceptable, you can skip Envoyer and just run on a single Forge server. This guide is not for that use case.
We're going to set up a load balancer that will be the primary endpoint of your domain name, two web servers that will handle requests, a database server, and a queue worker.
Create a new server in your Forge account and select the Load Balancer option. This server will proxy all requests to your website and pass them to your web servers. I'm using a 2 vCPU / 2 GB RAM droplet that is handling ~300k HTTP requests/day with ease.
Add a new site to the load balancer and include any aliases you might use. Since my project was in pre-launch, I chose preview.mydomain.com.
Your SSL certificate should be installed on this load balancer, but before you do that you'll need to point your DNS to your new load balancer. You can do this with an A record & an IP address, or a CNAME. I like to use CNAMEs, like so:
1A load-balancer-name.domainname.com 12.34.56.782CNAME preview.domainname.com load-balancer-name.domainname.com
Once you have DNS pointing at the server, you may need to wait a few minutes for DNS to propagate. Then it's time to order a free Let's Encrypt certificate, which you can do in Forge under your site config > SSL > Let's Encrypt.
Your SSL will terminate at the load balancer. This means that browsers will connect to the load balancer via HTTPS on port 443, and the load balancer will proxy that traffic to the web servers via HTTP on port 80. The web servers will not use SSL to communicate with the load balancer on the internal network.
Create two or more servers in your Forge account and select the Web Server option. These are the servers that will run your Laravel application. I am running two 2 vCPU / 2 GB RAM droplets that are handling ~300k HTTP requests/day.
You will create a duplicate site configuration on both servers. In Forge, visit the server page for each server. Click the arrow by New Site, then set the Root Domain to domainname.com, include any aliases (optional), the Web Directory to /current/public
, and pick the appropriate PHP version. Click Add to set it up.
Two notes on site config:
/public
, but Envoyer will create a file system like this:1domainname.com2 - releases3 - 2023010212344 - public5 - 2023010112346 - public7 - 2023010108428 - public9 - current -> ../releases/202301021234
Envoyer maintains the releases
folder with the last 3-5 releases, so you can quickly roll back to the previous release. It then symlinks domainname.com/current
to the most recent release in the releases
folder.
Your web servers should be accessible via SSH on port 22, and HTTP on port 80 from your load balancer. I also opened up port 80 to a range of IPs at my monitoring service, but otherwise the web servers are locked away from the world and only accessible to the public through the load balancers.
Create another server in Forge and select the Database server option. Both of your web servers will communicate with this single database server, so regardless of which server handles the request, they'll return the same information from the database.
I'm currently using a 8 vCPU / 16 GB RAM / 320 GB SSD server. It's running MySQL, redis, and Meilisearch, and at times is CPU bound under intense load. I plan to migrate redis and Meilisearch to individual servers as needed in the future.
Unfortunately I don't have good notes from the database server setup process, so I can't share them here.
Once you have all of your servers provisioned, they'll need to communicate with each other. Think about what servers actually need to be able to reach one another - for example, should your load balancer be able to reach your queue worker? Should your load balancer be able to reach your database server? Probably not, so restrict it as much as you can! Here's how I've chosen to set it up:
You should add your public key to each server on the SSH Keys screen.
.env
Envoyer will maintain a single .env
file across all servers. You should create a production .env
file that configures your database, redis, and meilisearch as appropriate:
1DB_CONNECTION=mysql 2DB_HOST=10.12.14.16 3DB_PORT=3306 4DB_DATABASE=somename 5DB_USERNAME=forge 6DB_PASSWORD=password-here 7 8CACHE_DRIVER=redis 9QUEUE_CONNECTION=redis10SESSION_DRIVER=redis11 12REDIS_HOST=10.12.14.1613REDIS_PASSWORD=password-here14REDIS_PORT=637915 16SCOUT_DRIVER=meilisearch17SCOUT_QUEUE=true18MEILISEARCH_HOST=http://10.12.14.16:770019MEILISEARCH_KEY=key-here