Day-to-day Laravel Cloud operations with the `cloud` CLI and the Cloud REST API, plus the conventions for setting up a new Cloud app. Use when asked to create or set up a new Cloud app or environment, look up a Cloud app or environment, read or set env vars, deploy or check a deployment, run tinker or an artisan command on Cloud, fix a stuck or failed domain, purge the edge cache, attach a bucket, check usage, cost or bandwidth, report which apps sleep or stay awake, change hibernation or instance size, configure a database cluster (suspend, backups, public endpoint), or set up deploy notifications.
72
90%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Low
Low-risk findings worth noting
Two tools:
cloud CLI (cloud list, cloud <command> --help). Most commands take an environment ID or name and support --json.https://cloud.laravel.com/api, bearer token. Use it for things the CLI lacks.cloudapi() { # usage: cloudapi METHOD PATH [non-secret JSON]
if [ -n "${3:-}" ]; then
printf '%s' "$3" | "$HOME/.dotfiles/bin/spatie-ops" request cloud "$1" "$2" --input -
else
"$HOME/.dotfiles/bin/spatie-ops" request cloud "$1" "$2"
fi
}Read spatie-ops first for private authentication and safe API output. The helper uses the Cloud CLI login or a private 1Password reference; its response is wrapped in status and response. For sensitive payloads, construct JSON in memory and send it on stdin rather than passing secret values as shell arguments. Use --private-output or the in-memory client when automation needs unredacted data.
cloud ... --json masks env var values. --show-sensitive reveals them: only use it piped into a file (chmod 600) or a script, never into the conversation.GET /buckets/{id}/keys and /bucket-keys/{id} return access_key_secret. Only print id, name, permission.The defaults we settled on after moving ~40 apps. Deviate only with a reason. For moving an existing site, follow the move-site-to-laravel-cloud skill; this section is the target setup.
Naming and source
example.com). Name the GitHub repo after the domain too. If you rename the repo later, point the app at it: PATCH /applications/<id> with {"repository":"org/example.com","source_control_provider_type":"github"}, then trigger a deploy to prove it works.production, deploying main with push to deploy. No staging unless asked.eu-central-1 for Belgian sites).cloud app:update <app> --avatar=/absolute/path/icon.png --force -n --fields=id,name,avatarUrl. If the CLI returns 422 for a valid PNG, use POST /applications/<id>/avatar with multipart field avatar=@/absolute/path/icon.png;type=image/png; explicitly setting the MIME type fixed the CLI upload failure. Keep the API token in RAM, pass headers through stdin, capture the response, and emit only the app ID/avatar URL because application responses can include environment values.Runtime
composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader, then npm ci && npm run build, then php artisan optimize. Deploy command: php artisan migrate --force.node_modules after the build. If PHP shells out to a Node tool at runtime (for example Shiki), install it into a separate directory during the build.flex-512mb for plain sites. Apps that process images (Glide, media conversions on large originals) or run Statamic need flex-1gb; 512 MB runs out of memory. Hibernation on with a 1 minute idle timeout (Freek's current preference, backend acceptance verified 6 Oct 2026). See the size identifiers and upgrade procedure below.cloud command:run runs inside the web container. Heavy one-offs (cache warming, image generation, big imports) compete with web traffic and can OOM the app: dispatch them to the queue, and never run two in parallel./browser-rendering/content) instead.Database
suspend_seconds: 120, daily backups kept 7 days, public endpoint off (on only during an import).utf8mb4 and turn MySQL strict mode on. With strict mode off, values longer than a column (for example a 191-char string column) are silently truncated.Storage
filesystem_keys), so it shows on the canvas and Cloud injects credentials. Restore throw => true and any root/visibility/url in a service provider.public/ assets if stale CSS/JS after deploys matters.Edge cache
Cache-Control: public, max-age=60, s-maxage=300 for client sites. No stale-while-revalidate (ignored).POST /environments/<env>/purge-edge-cache. Note the token's expiry date somewhere you will see it.private, no-store there.Domains
wildcard_enabled: false, www_redirect: www_to_root (apex) and verification_method: real_time, and add the pre-verification records for both apex and www, even if www does not resolve today. Without the www records the canvas shows "Not connected" although the apex works.Observability and mail
minimal_log_level error), deploy notifications to Slack (dashboard setting).env:variables, never print them.Oh Dear
Every request Oh Dear makes can wake a hibernating app, and an edge-cached page can hide an outage. Per app:
https:// and the real domain, so the certificate check sees Cloud's certificate./up (Laravel's health route, never edge cached). Checking an edge-cached homepage only notices downtime after the cache expires. For apps meant to sleep, use a long interval (720 or 1440 minutes); busy apps that never sleep can stay at 1 minute. Look for extra monitors on other paths of the same site, they wake it too.spatie/laravel-health): Oh Dear polls the endpoint every few minutes, so the app never sleeps. Use it only on apps that are awake anyway, otherwise turn it off. Drop checks that make no sense on Cloud (Redis, Horizon, used disk space).spatie/laravel-schedule-monitor): the app pings Oh Dear after each task, so it wakes nothing. Set the monitor id (the package reads monitor_id), add php artisan schedule-monitor:sync to the deploy commands, and raise grace times to about 5 minutes for cold starts.OH_DEAR_API_TOKEN actually works (a 401 means cron sync and maintenance windows silently fail).Node apps
X-Forwarded-Proto is http; use CF-Visitor for the scheme and CF-Connecting-IP for the client IP.php artisan schedule:run. For Node, expose secret-protected job endpoints and trigger them from a GitHub Actions schedule (or a Cloudflare cron trigger).cloud app:list --json
cloud env:list <app> --json
cloud env:get <env> --json # branch, deploy commands, settings
cloud instance:list <env> --json
cloudapi GET '/applications?per_page=100'
cloudapi GET /applications/<app-id>/environments
cloudapi GET /environments/<env-id>/instancescloud env:variables <env> --action=set --key=FOO --value=bar --force # one var
cloudapi POST /environments/<env>/variables '{"method":"set","variables":[{"key":"FOO","value":"bar"}]}'
cloudapi POST /environments/<env>/variables/delete '{"keys":["FOO"]}'--action=replace replaces all variables. Avoid unless that is the intent.AWS_* names for your own settings; they collide with the managed queue's SQS config.cloud deploy # from a repo configured with `cloud repo:config`
cloudapi POST /environments/<env>/deployments '{}'
cloud deployment:list <env> --json
cloud deployment:get <deployment-id> --json # status: pending, ..., deployment.succeeded / failed
cloudapi GET /deployments/<deployment-id>/logs
cloud env:update <env> --branch=main --forcePoll deployment:get every 10 to 15 s until the status contains succeeded, failed or cancel. Run waits in a background agent.
cloud deploy:monitor <app> <env> -n immediately while a deployment is active. The CLI may wait for a future deployment if the latest one already finished. If it hangs, check that deployment's actual status and stop only the local monitor when a terminal result is confirmed.cloud command:run <env> --cmd="php artisan migrate:status"
cloud command:run <env> --cmd='php artisan tinker --execute="echo App\Models\User::count();"'
cloud tinker # interactive
cloudapi POST /environments/<env>/commands '{"command":"php artisan about"}' # then GET /commands/<id>For larger scripts: base64-encode locally and echo <b64> | base64 -d > /tmp/x.php && php /tmp/x.php inside --cmd.
cloud env:logs <app> <env> --from=2026-01-01T10:00:00Z --to=2026-01-01T11:00:00Z --json # max ~100 lines per callcloud domain:list <env> --json # hostname_status, ssl_status, origin_status, dns_records
cloud domain:get <domain-id> --json
cloud domain:verify <domain-id>
cloud domain:create <env> --name=example.com --wildcard-enabled=false \
--www-redirect=www_to_root --verification-method=real_time
cloudapi DELETE /domains/<domain-id>www_redirect (www_to_root, or the reverse if the site lives on www). Without it the www host is dropped.wildcard_enabled: false, no www.verification_method. Delete and re-add instead.verification_method: pre_verification, then add the _cf-custom-hostname TXT and _acme-challenge CNAME records shown in dns_records (also for www). For an existing domain, use cloud domain:update <id> --verification-method=pre_verification --force -n. real_time waits for origin DNS to point at Cloud first. Pre-verification is unavailable when the hostname is already proxied by Cloudflare. Verify TLS before switching DNS. See https://laravel.com/cloud/docs/domains.pending forever or flip to failed even with correct DNS. Fix: save the domain JSON, delete, re-add with the same options, re-add the verification records. Audit all domains periodically.cloudapi POST /environments/<env>/purge-edge-cache '{}'origin_status is still pending (DNS not yet at Cloud). A redeploy does not purge either.s-maxage but ignores stale-while-revalidate.public/ get about a 2-hour edge TTL. Serve versioned assets from a bucket if that is a problem. Bucket CDNs also cache 404s for hours.X-Forwarded-Proto: http; use CF-Visitor to detect https.cloud bucket:list --json
cloud bucket-key:list <bucket-id> --json # contains secrets, filter to id/name/permission
cloudapi PATCH /environments/<env> '{"filesystem_keys":[{"id":"<bucket-key-id>","disk":"media","is_default_disk":false}]}'
cloudapi GET '/environments/<env>?include=buckets'filesystem_keys is the full set: include buckets that are already attached, or they get detached.throw false, custom root/visibility/url/options lost. Restore what you need in a service provider. Needs league/flysystem-aws-s3-v3.php artisan tinker --execute='foreach (config("filesystems.disks") as $n => $d) { echo $n, " ", $d["driver"] ?? "", " ", $d["bucket"] ?? "", " throw=", var_export($d["throw"] ?? null, true), PHP_EOL; }'cloudapi GET /usage # data.summary, resources, application_totals, environment_usage
cloudapi GET '/usage?period=1' # older billing periods; meta.available_periods lists themNo per-app bandwidth breakdown. For bandwidth per path, use the access logs (data.bytes_sent in env:logs).
cloudapi GET '/environments/<env>/metrics?period=7d' # 6h, 24h, 3d, 7d, 30d
cloudapi GET '/databases/clusters/<cluster-id>/metrics?period=7d'Environment metrics: cpu_usage, memory_usage, http_response_count, replica_count, web_workers_count. A replica_count of 0 means the app is hibernating; the share of non-zero points is its awake time. Cluster metrics include compute_hours. Common wake-up causes: uptime monitors, frequent scheduled tasks (Cloud wakes apps for cron expressions), crawlers, pollers.
cloud instance:sizes
cloudapi PATCH /instances/<inst-id> '{"hibernation_timeout":1}'
cloudapi PATCH /instances/<inst-id> '{"size":"flex-512mb"}'
cloudapi POST /environments/<env>/stop # or /start
cloudapi POST /instances/<inst-id>/pause # pause a queue instance, /resume to undoflex-512mb, flex-1gb, and flex-2gb (verified 6 Oct 2026). The fixed-CPU flex.c-*, flex.g-*, and flex.m-* sizes are deprecated legacy compute. The dots in their names do not indicate a newer generation.visibilityStatus (public or deprecated). GET /instances/sizes supplies identifiers, CPU, and RAM, but has no generation/deprecation or pricing flags.flex-512mb, 1 GB to flex-1gb, 2 GB to flex-2gb. A legacy 256 MB instance needs the current minimum of 512 MB. Evaluate larger or always-on workloads separately. Compare current regional prices; the increase is not uniformly $1 across the old compute profiles, and sleeping compute is billed only while awake.size, then redeploy its existing branch. Preserve scheduler, replica/scaling settings, hibernation, attached resources, and env vars. Start with the owner's named pilot, confirm the deployed commit and unchanged configuration, check health and natural sleep/wake behavior, then continue the authorized rollout.mq.* sizes. An App compute upgrade does not require changing queue sizes. Distinguish live environments from rehearsal/default environments before a fleet rollout.cloud db-cluster:list --json
cloud db:list <cluster-id> --json
cloudapi PATCH /databases/<cluster-id> '{"config":{"suspend_seconds":120}}'
cloudapi PATCH /databases/<cluster-id> '{"config":{"retention_days":7}}'
cloudapi PATCH /databases/<cluster-id> '{"config":{"is_public":true}}' # only during an import, then false
cloud db:open <cluster> <database> # local clientDELETE /databases/clusters/<cluster>/databases/<schema-id>), then the cluster. Needs the owner's OK.Slack deploy notifications are configured in the Cloud dashboard (organization or application settings), not via the API. They cover deploy success and failure only: no "deploy started", and there are no generic outgoing webhooks.
freekmurze/dotfiles
fe89482
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.