Exploring Laravel Cloud: Comprehensive Guide to Its Features, Data Modeling, and Architecture
Draft Disclaimer: Please note that this article is currently in draft form and may undergo revisions before final publication. The content, including information, opinions, and recommendations, is subject to change and may not represent the final version. We appreciate your understanding and patience as we work to refine and improve the quality of this article. Your feedback is valuable in shaping the final release.
Language Mismatch Disclaimer: Please be aware that the language of this article may not match the language settings of your browser or device.
Do you want to read articles in English instead ?
Laravel Cloud is a fully managed platform, where Laravel takes care of all the infrastructure, maintenance, and optimizations for you. In contrast, Laravel Forge and Vapor are self-managed services that helps you set up and configure infrastructure with external providers like AWS or DigitalOcean, but you are responsible for maintaining and managing that infrastructure yourself.
From the Laravel Cloud page
The goal with this write up is to walk through how I would build Laravel Cloud
Main features
- one click deployment from source control
- deployment will provision
- dns record for :app-:hash:8.example.coom
- server with all standard services
- Payment: subscription, plan, invoice, etc
- Usage metrics per environment. payment per usage ? Probably easier to start by having a set amount of resoures and charged a fix amount
Data modeling
- Application
- name
- size: enum of support sizes, with matching prices, etc
- replicas_count
- has one source repository
- has many environments
- Source Repository
- name
- url
- sso ? access token + refresh token from source
- has many branches
- Source Repository Branch
- name
- commits as computed from http api (ie Github)
- latest commit
- belongs to Source Repository
- has one/many environments
- Environment
- name
- branch_id
- Belongs to application
- Has many deployments
- Has many databases
- Has many services
- Has many workers
- Database
- type: enum of supported one
- Belongs to environment
- Deployment
- Belongs to environment
- Service
- install commands = f(config)
- has many dependencies with pivot order
- User
- Has many teams
- Has many applications
- Team
- Has many users
- Has many applications
- Source Repository
- name
- url
- sso ? access token + refresh token from source
- Worker
- name
- processes_count
- command ie
php artisan queue:work sqs --timeout 3600
App requirement
- AWS account
- role for each user + app + environment ?
- overall architecture
- availability zone
- subnets
- private
- DB
- public
- actual server
- private
Services and configs
- services has dependencies
- server:
- memory
- cpu
- storage
- bandwidth
- php
- version
- install php and php fpm
- composer
- npm
- config has
- web server
- nginx config
- ssl less with usuage of cloudflare
- replica
- database
- mysql as default
- database options: postgresql, sqlite,
- DB backup
- write instance vs read replicas
- queue
- supervisor
- laravel scheduler
- queue workers = f(environment)
PHP config
; total_ram = 1000MB (free -h)
; used_ram = 200MB (sudo service php8.3-fpm stop, free -h)
pm. max_children = 10
; avail_ram = 800MB
; ram_per_child = 60MB
; avail_ram / ram_per_child = 800/60 = 13
pm.start_servers = 4; num_cpus * 4
pm.min_spare_servers = 2 ; num_cpus * 2
pm.max_spare_servers = 2 ; num_cpus * 4
pm.max_requests = 1000 ; restart to free memory in case of memory leaks
Infrastructure as a code
{
"Resources": {
"VPC": {
"Type": "AWS::EC2::VPC",
"Properties": {
"CidrBlock": "10.1.0.0/16",
"EnableDnsSupport": true,
"EnableDnsHostnames": true,
"InstanceTenancy": "default",
"Tags": [
{
"Value": "app_name_vpc",
"Key": "Name"
}
]
}
},
"InternetGateway": {
"Type": "AWS::EC2::InternetGateway",
"Properties": {
"Tags": [
{
"Value": "app_name_igw",
"Key": "Name"
}
]
}
},
"InternetGatewayAttachment": {
"Type": "AWS::EC2::VPCGatewayAttachment",
"Properties": {
"VpcId": {
"Ref": "VPC"
},
"InternetGatewayId": {
"Ref": "InternetGateway"
}
}
},
"PublicSubnet1": {
"Type": "AWS::EC2::Subnet",
"Properties": {
"VpcId": {
"Ref": "VPC"
},
"AvailabilityZoneId": "use1-az4",
"CidrBlock": "10.1.0.0/20",
"MapPublicIpOnLaunch": true,
"Tags": [
{
"Value": "app_name_public_subnet_1",
"Key": "Name"
}
]
}
},
"PrivateSubnet1": {
"Type": "AWS::EC2::Subnet",
"Properties": {
"VpcId": {
"Ref": "VPC"
},
"AvailabilityZoneId": "use1-az4",
"CidrBlock": "10.1.128.0/20",
"MapPublicIpOnLaunch": false,
"Tags": [
{
"Value": "app_name_private_subnet_1",
"Key": "Name"
}
]
}
},
"PublicSubnet2": {
"Type": "AWS::EC2::Subnet",
"Properties": {
"VpcId": {
"Ref": "VPC"
},
"AvailabilityZoneId": "use1-az6",
"CidrBlock": "10.1.16.0/20",
"MapPublicIpOnLaunch": true,
"Tags": [
{
"Value": "app_name_public_subnet_2",
"Key": "Name"
}
]
}
},
"PrivateSubnet2": {
"Type": "AWS::EC2::Subnet",
"Properties": {
"VpcId": {
"Ref": "VPC"
},
"AvailabilityZoneId": "use1-az6",
"CidrBlock": "10.1.144.0/20",
"MapPublicIpOnLaunch": false,
"Tags": [
{
"Value": "app_name_private_subnet_2",
"Key": "Name"
}
]
}
},
"PublicRouteTable": {
"Type": "AWS::EC2::RouteTable",
"Properties": {
"VpcId": {
"Ref": "VPC"
},
"Tags": [
{
"Value": "app_name_rtb_public",
"Key": "Name"
}
]
}
},
"PublicRoute": {
"Type": "AWS::EC2::Route",
"Properties": {
"RouteTableId": {
"Ref": "PublicRouteTable"
},
"DestinationCidrBlock": "0.0.0.0/0",
"GatewayId": {
"Ref": "InternetGateway"
}
}
},
"PublicSubnet1RouteTableAssociation": {
"Type": "AWS::EC2::SubnetRouteTableAssociation",
"Properties": {
"RouteTableId": {
"Ref": "PublicRouteTable"
},
"SubnetId": {
"Ref": "PublicSubnet1"
}
}
},
"PublicSubnet2RouteTableAssociation": {
"Type": "AWS::EC2::SubnetRouteTableAssociation",
"Properties": {
"RouteTableId": {
"Ref": "PublicRouteTable"
},
"SubnetId": {
"Ref": "PublicSubnet2"
}
}
},
"PrivateRouteTable": {
"Type": "AWS::EC2::RouteTable",
"Properties": {
"VpcId": {
"Ref": "VPC"
}
}
},
"PrivateSubnet1RouteTableAssociation": {
"Type": "AWS::EC2::SubnetRouteTableAssociation",
"Properties": {
"RouteTableId": {
"Ref": "PrivateRouteTable"
},
"SubnetId": {
"Ref": "PrivateSubnet1"
}
}
},
"PrivateSubnet2RouteTableAssociation": {
"Type": "AWS::EC2::SubnetRouteTableAssociation",
"Properties": {
"RouteTableId": {
"Ref": "PrivateRouteTable"
},
"SubnetId": {
"Ref": "PrivateSubnet2"
}
}
},
"SecurityGroupSshHttpHttps": {
"Type": "AWS::EC2::SecurityGroup",
"Properties": {
"VpcId": {
"Ref": "VPC"
},
"GroupDescription": "Allow SSH, HTTP, HTTPS access.",
"SecurityGroupIngress": [
{
"CidrIp": "0.0.0.0/0",
"IpProtocol": "tcp",
"FromPort": 22,
"ToPort": 22
},
{
"CidrIp": "0.0.0.0/0",
"IpProtocol": "tcp",
"FromPort": 80,
"ToPort": 80
},
{
"CidrIp": "0.0.0.0/0",
"IpProtocol": "tcp",
"FromPort": 443,
"ToPort": 443
}
],
"Tags": [
{
"Value": "app_name_sg",
"Key": "Name"
}
]
}
},
"EC2Instance": {
"Type": "AWS::EC2::Instance",
"Properties": {
"ImageId": "ami-080e1f13689e07408",
"InstanceType": "t2.micro",
"KeyName": "app_name",
"SubnetId": {
"Ref": "PublicSubnet1"
},
"SecurityGroupIds": [
{
"Ref": "SecurityGroupSshHttpHttps"
}
],
"Tags": [
{
"Value": "app_name_ec2_instance",
"Key": "Name"
}
],
"UserData": {
"Fn::Base64": {
"Fn::Join": [
"",
[
"#!/bin/bash -ex\n",
"git clone https://github.com/rcravens/devops-laravel.git /usr/local/bin/devops\n",
"/usr/local/bin/devops/common/create_aliases.sh\n",
"source ~/.bashrc\n",
]
]
}
}
}
}
}
}
Supervisor conf
[unix_http_server]
file=/assets/supervisor.sock
[supervisord]
logfile=/var/log/supervisord.log
logfile_maxbytes=50MB
logfile_backups=10
loglevel=info
pidfile=/assets/supervisord.pid
nodaemon=false
silent=false
minfds=1024
minprocs=200
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
[supervisorctl]
serverurl=unix:///assets/supervisor.sock
[include]
files = /etc/supervisor/conf.d/*.conf
Laravel Dockerfile
# FROM ubuntu 22 alpine
mkdir -p /etc/supervisor/conf.d/
cp /assets/worker-*.conf /etc/supervisor/conf.d/
cp /assets/supervisord.conf /etc/supervisord.conf
## Start supervisor
supervisord -c /etc/supervisord.conf -n
PHP FPM
[www]
listen = 127.0.0.1:9000
user = www-data
group = www-data
listen.owner = www-data
listen.group = www-data
pm = dynamic
pm.max_children = 50
pm.min_spare_servers = 4
pm.max_spare_servers = 32
pm.start_servers = 18
clear_env = no
Nginx
/etc/nginx/nginx.conf
user www-data www-data;
worker_processes 5;
daemon off;
worker_rlimit_nofile 8192;
events {
worker_connections 4096; # Default: 1024
}
http {
include $!{nginx}/conf/mime.types;
index index.html index.htm index.php;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] $status '
'"$request" $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx-access.log;
error_log /var/log/nginx-error.log;
sendfile on;
tcp_nopush on;
server_names_hash_bucket_size 128; # this seems to be required for some vhosts
server {
listen ${PORT};
listen [::]:${PORT};
server_name localhost;
root /app;
add_header X-Content-Type-Options "nosniff";
client_max_body_size 35M;
index index.php;
charset utf-8;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
error_page 404 /index.php;
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include $!{nginx}/conf/fastcgi_params;
include $!{nginx}/conf/fastcgi.conf;
fastcgi_param PHP_VALUE "upload_max_filesize=30M \n post_max_size=35M";
}
location ~ /\.(?!well-known).* {
deny all;
}
}
}
Workers
Definition
- program name
- command
- auto start
- auto restart
- stdout log file
- stderr log file
- maximum second per job / maximum execution time
- rest seconds when empty
- failed job delay in seconds
- maximum tries (optional)
- has many environments
- if none defined will be applied to all environments
- maximum memory
- number of processes
- working directory
Queue as a special worker
- connection
- queue names
Default workers
Worker Nginx
worker-nginx.conf
[program:worker-nginx]
process_name=%(program_name)s_%(process_num)02d
command=nginx -c /etc/nginx.conf
autostart=true
autorestart=true
stdout_logfile=/var/log/worker-nginx.log
stderr_logfile=/var/log/worker-nginx.log
Worker PHP FPM
worker-php-fpm.conf
[program:worker-phpfpm]
process_name=%(program_name)s_%(process_num)02d
command=php-fpm -y /assets/php-fpm.conf -F
autostart=true
autorestart=true
stdout_logfile=/var/log/worker-php-fpm.log
stderr_logfile=/var/log/worker-php-fpm.log
Worker Laravel Default Queue
worker-laravel-default-queue.conf
[program:worker-laravel-default-queue]
process_name=%(program_name)s_%(process_num)02d
command=bash -c 'exec php /app/artisan queue:work --sleep=3 --tries=3 --max-time=3600'
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
numprocs=12
startsecs=0
stopwaitsecs=3600
stdout_logfile=/var/log/worker-laravel.log
stderr_logfile=/var/log/worker-laravel.log
Worker Laravel Notification
worker-laravel-notification.conf
[program:worker-laravel-notification]
process_name=%(program_name)s_%(process_num)02d
command=bash -c 'exec php /app/artisan queue:work sqs-notification --sleep=3 --tries=3 --max-time=3600'
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
numprocs=12
startsecs=0
stopwaitsecs=3600
stdout_logfile=/var/log/worker-laravel.log
stderr_logfile=/var/log/worker-laravel.log
Pricing
- DB compute
- EC2 compute
- 75c for 1GB DB storage
Audience
- Solopreneurs
- Indihackers
- side projects you are tinkering with
Customization
- Bring your own database
References
https://bcd.dev/post/scaling-your-laravel-app-130