Skip to main content

Laravel Cloud

Laravel Cloud is our deployment platform, replacing Laravel Vapor for hosting Laravel services. Not all projects are on Cloud yet - we're migrating incrementally.

Current State​

Services​

ServiceStatusNotes
farfallaVaporCore monolith
farfalla-https-guardCloudFirst migrated service
farfalla-integrationsCloudStaging and production on Cloud
medusaVapor
coniglioCloudStaging and production on Cloud
castoroVapor

How We Deploy​

Deployments are triggered via deploy hooks from CI. GitHub Actions example:

deploy-production:
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
needs: [test, phpstan, lint]
runs-on: depot-ubuntu-24.04
concurrency:
group: deploy-production
cancel-in-progress: false
environment:
name: production
url: https://<project>.publica.la
steps:
- name: Deploy to Laravel Cloud
run: |
curl --fail-with-body -sS \
--retry 3 --retry-all-errors \
--connect-timeout 10 --max-time 30 \
-X POST "${{ secrets.LARAVEL_CLOUD_PRODUCTION_DEPLOY_HOOK }}?commit_hash=${{ github.sha }}"

Deploy hooks are async: the curl returns immediately and the actual deployment happens in the background. No post-deploy health checks.

Curl flags matter. A bare curl -X POST "$HOOK" silently swallows non-2xx responses (expired hook, revoked token, Cloud outage), so CI stays green while the deploy never fires. Keep the flags:

  • --fail-with-body exits non-zero on 4xx/5xx
  • --retry 3 --retry-all-errors handles transient network blips
  • --connect-timeout 10 --max-time 30 prevents the job from hanging if the hook URL stalls
  • ?commit_hash=${{ github.sha }} makes the Cloud dashboard display the real SHA for each deploy

CI secrets per project (GitHub environment-scoped):

  • LARAVEL_CLOUD_STAGING_DEPLOY_HOOK (staging environment)
  • LARAVEL_CLOUD_PRODUCTION_DEPLOY_HOOK (production environment)
warning

Choose one deploy trigger. If GitHub Actions calls the deploy hook, disable Cloud's Push-to-deploy in the environment settings. Otherwise every push fires two deploys.

Build & Deploy Commands​

Configured in the Laravel Cloud dashboard per environment.

Build commands (typical):

composer install --no-dev --optimize-autoloader
php artisan event:cache
php artisan config:cache
php artisan route:cache
php artisan horizon:publish

horizon:publish republishes the Horizon dashboard assets to public/vendor/horizon. Required on every build for projects using Horizon, otherwise the dashboard UI fails to render.

warning

Don't track public/vendor/horizon/ in git. If the directory was previously committed, git rm -r public/vendor/horizon and add /public/vendor/horizon to .gitignore. The build will regenerate it. Leaving stale assets in the repo makes Horizon appear to work until the first time someone upgrades and the committed assets diverge from the installed version.

Deploy commands:

php artisan migrate --force
php artisan optimize

Custom Domains​

We point custom domains to Laravel Cloud via Cloudflare CNAME (proxy ON), then disable the default .laravel.cloud domain.

Monitoring​

Server-side PHP monitoring uses Laravel Nightwatch (auto-injected env vars by Cloud). Keep Sentry JS SDK for frontend monitoring.


Configuration Reference​

Repository linking (.cloud/config.json)​

Every Cloud-hosted repo should commit a .cloud/config.json at the root, pinning the repo to a specific Cloud organization and application. The cloud CLI uses it to pick the right API token (we have multiple) and to default --application for commands like cloud deploy, cloud env:list, cloud tinker.

Format:

{
"organization_id": "org-9fa1cfe2-12f3-4a06-b6f9-b41ee2866be1",
"application_id": "app-..."
}

The official command is cloud repo:config, but it always prompts for the application (no --application flag) and exits 1 silently under --no-interaction. For scripted setup (CI bootstrap, kickoff, batch-linking) use cloud-link.sh, which lives next to this doc and hardcodes the Publica.la org id:

# from inside the target repo
cloud-link.sh <app-slug-or-name>

# or pointed at a specific dir
cloud-link.sh medusa /path/to/medusa

It merges into any existing .cloud/config.json (preserving keys like environment_id) and matches cloud repo:config byte-for-byte on a fresh repo. The org id is hardcoded (and the script seeds a scratch dir before calling application:list) because the CLI needs organization_id to disambiguate when multiple API tokens are stored locally; the comments at the top of the script explain the chicken-and-egg.

Queue Connection​

warning

Cloud does NOT auto-set QUEUE_CONNECTION. Add QUEUE_CONNECTION=redis to env vars manually, otherwise jobs run synchronously. Symptom: Jobs not appearing in Horizon despite worker being active.

Horizon​

warning

Cloud does NOT auto-protect Horizon (Vapor did). You must add HTTP Basic Auth middleware yourself. Also: move laravel/horizon to require (not require-dev), configure supervisors per environment, create a Worker Cluster in Cloud dashboard with php artisan horizon, and schedule horizon:snapshot for non-local environments.

Persistent DB Connections​

Config and test caveat

Enabled for better performance, but disabled in tests (breaks parallel execution and Bus::batch()):

// config/database.php
'options' => [
PDO::ATTR_PERSISTENT => app()->environment('testing') ? false : true,
],

Valkey (Redis-compatible)​

ACL auth config

Cloud uses Valkey with ACL auth. The Redis connection needs both username and password:

// config/database.php, redis connection
'username' => env('REDIS_USERNAME'),

REDIS_USERNAME is auto-injected by Cloud when the managed Valkey resource is created. No manual env var setup needed.

Symptom: Horizon can't connect to Valkey. Cause: Vapor didn't need a username. Cloud uses Valkey ACL requiring both, and the connection config was missing the username key.

artisan optimize​

Views directory requirement

Requires resources/views directory to exist. Create an empty one if your project doesn't have views.

Symptom: php artisan optimize fails with view:cache error.


Resources​

X

Graph View