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

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;
        }
    }
}

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