Load balanced environments with Laravel Forge and Envoyer

September 24, 2021

Here's how I set up a load balanced environment on Laravel Forge and deployed with Envoyer.

What the heck are Forge and 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.

Set up and configure servers at Laravel Forge

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.

Load Balancer

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.

Create your site

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.

Point DNS & configure SSL

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.78
2CNAME 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.

Web Servers

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.

Create your site

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:

  1. The standard Web Directory is /public, but Envoyer will create a file system like this:
1domainname.com
2 - releases
3 - 202301021234
4 - public
5 - 202301011234
6 - public
7 - 202301010842
8 - public
9 - 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.

  1. You need to keep your PHP version synchronized between Forge and Envoyer. I've shot myself in the foot by updating the PHP version on Forge, which changes the version of PHP that is serving your site, but forgetting to update the PHP version on Envoyer. What will happen is that Envoyer will restart the old version of PHP on your server, leaving the new version happily serving an outdated copy of your site until that release is deleted and your entire site goes down without the ability to even generate logs. Ask me how I know!

Restrict access with Firewall Rules

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.

Database server

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.

Configure network on all servers

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:

Configure SSH access on all servers

You should add your public key to each server on the SSH Keys screen.

  • Load balancer can communicate with web servers
  • Web servers can communicate with database server and load balancer
  • Queue workers can communicate with database server
  • Database server can communicate with web servers and queue workers.

Set up Laravel .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=redis
10SESSION_DRIVER=redis
11 
12REDIS_HOST=10.12.14.16
13REDIS_PASSWORD=password-here
14REDIS_PORT=6379
15 
16SCOUT_DRIVER=meilisearch
17SCOUT_QUEUE=true
18MEILISEARCH_HOST=http://10.12.14.16:7700
19MEILISEARCH_KEY=key-here

Popular Posts

Recent Posts

View all