craft-cloud · git:20260902.cd2ff4a · 2026-09-02 · sha256 debbc3b0b2ea727a
craft-cloud git:20260902.cd2ff4aA
Immutable. This exact content is served forever at /api/v1/blob/debbc3b0b2ea727a.
---
name: craft-cloud
description: "Craft Cloud — Pixel & Tonic's serverless hosting platform for Craft CMS. Covers craft-cloud.yaml configuration, the Build → Migrate → Release deploy pipeline, the craftcms/cloud extension package, edge image transforms via Cloudflare, edge static caching with cache.rules + ESI, Cloud-managed S3 filesystem, MySQL 8 / Postgres 15 databases (no MariaDB, no tablePrefix), Console-based command runner and scheduled cron (once-per-hour minimum), auto-handled queue jobs, custom domains and SSL, preview environments per branch, Cloud limitations (ephemeral filesystem, no SSH, no .htaccess, no built-in mail), plugin development requirements for Cloud compatibility, and self-hosted → Cloud migration. Triggers on: craft-cloud.yaml, craftcms/cloud package, cloud.esi(), php craft cloud/up, php craft cloud/setup, App::isEphemeral(), CRAFT_EPHEMERAL, edge.craft.cloud, preview.craft.cloud, CRAFT_CLOUD_PROJECT_ID, CRAFT_CLOUD_ENVIRONMENT_ID, CRAFT_CLOUD_CDN_BASE_URL, Build → Migrate → Release, Cloud filesystem, Cloud-compatible plugin, Cloudflare Images at edge, AssetsFs, static-caching rules, ESI islands, deploy to Craft Cloud, migrate to Craft Cloud, self-hosted to Cloud, Craft Cloud quotas, Craft Cloud regions, request signing (RFC 9421), CRAFT_CLOUD_SIGNING_KEY, headless 429/503, cloud.artifactUrl(), @artifactBaseUrl. Do NOT trigger for Servd (use the servd skill) or generic Craft deployment on Forge/bare metal (craftcms/deployment.md). Do NOT trigger for general DDEV local dev unrelated to Cloud parity."
---
# Craft Cloud — Serverless Hosting for Craft CMS
Reference for Craft Cloud, Pixel & Tonic's serverless hosting platform for Craft CMS. Covers the `craft-cloud.yaml` config file, the Build → Migrate → Release deploy pipeline, the `craftcms/cloud` extension, edge image transforms and static caching, the Cloud filesystem, plugin Cloud-compatibility requirements, and self-hosted → Cloud migration.
This skill is scoped to **Craft Cloud specifically** — what's different on Cloud vs running Craft yourself on Forge, Servd, or bare metal. For **Servd**, see the `servd` skill. For generic Craft deployment (build artifacts, project config sync, atomic deploys on traditional hosts), see the `craftcms` skill's `deployment.md`.
## Companion Skills — Load When Needed
- **`craftcms`** — When the Cloud topic intersects plugin or module PHP work (e.g. `App::isEphemeral()` guards in services, asset-bundle constraints, queue job design for the 15-minute cap).
- **`craft-site`** — When Cloud intersects front-end templating (ESI islands inside cached pages, edge image transform usage, `{% expires %}` opt-outs for static caching).
- **`ddev`** — For the local-dev parity recipe (matching PHP/DB versions, simulating the ephemeral filesystem locally).
- **`craft-php-guidelines`** — When editing plugin PHP to add Cloud-compatibility checks.
## Documentation
Authoritative sources used to write this skill:
- Craft Cloud docs landing: https://craftcms.com/docs/cloud/
- Configuration reference: https://craftcms.com/docs/cloud/config
- Deployment pipeline: https://craftcms.com/docs/cloud/deployment
- Builds: https://craftcms.com/docs/cloud/builds
- Compatibility & limitations: https://craftcms.com/docs/cloud/compatibility
- Assets & transforms: https://craftcms.com/docs/cloud/assets
- Static caching: https://craftcms.com/docs/cloud/static-caching
- Headless apps: https://craftcms.com/docs/cloud/headless-apps
- Request signing: https://craftcms.com/docs/cloud/request-signing
- Plugin development: https://craftcms.com/docs/cloud/plugin-development
- Cloud extension source: https://github.com/craftcms/cloud-extension-yii2
Per-claim URLs appear in each reference file. Last verified against the docs and `craftcms/cloud-extension-yii2@main` on 2026-05-28; headless/request-signing, artifact URLs, environments, quotas, and backups content verified against `craftcms/docs@main` on 2026-09-02.
## What's Different on Cloud vs Self-Hosted
A quick orientation table. Each row is a place self-hosted habits will mislead you.
| Concern | Self-hosted | Cloud |
|---|---|---|
| Config file | `.env`, `config/general.php`, `config/db.php`, your web-server config | **`craft-cloud.yaml`** at repo root for platform settings; runtime env vars set in Craft Console UI (not `.env`) |
| Deploy | Whatever you've wired (Forge, GitHub Actions, rsync) | Git push → automatic **Build → Migrate → Release** (15-min build cap) |
| Filesystem | Local disk or your own S3/R2 config | **Ephemeral** Lambda filesystem; use `App::isEphemeral()` to gate writes; user assets must use the Cloud-bundled S3-backed filesystem type |
| Database | Whatever you've installed | **MySQL 8.0 or Postgres 15 only** — no MariaDB, no `tablePrefix`, no `db.php` touching, no `CRAFT_DB_*` overrides |
| App / component config | Free to override components in `config/app.php` | The extension **owns several components** (`response`, `session`, `cache`, `mutex`, `queue`, `assetManager`) + the DB connection — overriding them in `app.*.php` breaks the wiring, and it's **not reproducible locally** (the extension only runs on Cloud) |
| Headless / API consumers | Your GraphQL endpoint, your rate limits | Bot detection deprioritizes automated traffic — SSG builds/ISR get **429/503** unless requests are **signed** (RFC 9421, `$CRAFT_CLOUD_SIGNING_KEY`) and retried with backoff |
| Queue jobs | You run a worker (systemd, supervisor, cron) | **Auto-processed** by Cloud — don't schedule the runner; cap each job at 15 minutes |
| Cron | `crontab -e`, any frequency | Craft Console UI only; **once per hour minimum** |
| Image transforms | ImageOptimize / Imager-X / native | **Edge transforms** via Cloudflare Images — no template changes needed, but ImageOptimize is incompatible |
| Page caching | Blitz or similar | **Edge static caching** via `cache.rules` in `craft-cloud.yaml`; tag-based auto-invalidation |
| Dynamic islands in cached pages | Per-cache-driver workaround | First-class **`cloud.esi(...)`** Twig helper |
| Logs | Files in `storage/logs/` | `Craft::info/warning/error()` only — no file-tailing UI; Console command output is the de-facto surface |
| Mail | sendmail or any adapter | **No built-in mail** — bring your own SMTP/Postmark/SES/Resend |
| SSH | `ssh user@server` | None — Console command runner only (255-char cap, 15-min cap) |
| Server rewrites | `.htaccess` / nginx config | `redirects:` and `rewrites:` keys in `craft-cloud.yaml` |
| Custom domains | DNS provider + cert | DNS + ownership TXT + CNAME to `edge.craft.cloud`; auto SSL via Cloudflare |
| Preview environments | Your CI | Per-branch only — **no per-PR previews documented** |
## Common Pitfalls (Cross-Cutting)
- **Writing to disk without an `App::isEphemeral()` guard.** Lambda's filesystem is ephemeral and writes are lost between requests. Plugins, services, and migrations all need to check and use the Path service (`Craft::$app->getPath()->getTempPath()`/`getStoragePath()`/`getCachePath()`) for transient writes. See `references/extension.md` (App::isEphemeral) and `references/plugin-development.md`.
- **Reconfiguring Cloud-owned app components in `config/app.php` / `app.web.php` / `app.console.php`.** The `craftcms/cloud` extension augments the `response` component (header normalization, gzip, binary→S3 offload, all attached on `EVENT_AFTER_PREPARE`) and replaces `session`, `cache`, `mutex`, `queue`, and `assetManager` — plus auto-wires the database connection. Redefining or augmenting any of these in your app config collides with that wiring and can return **HTTP 500 on every request** (and bypass edge caching). This includes the DB: don't configure `db.php` or set `CRAFT_DB_*` env vars. To add response logic (e.g. a `Content-Language` header), attach at runtime to the response *instance* from a module — `Craft::$app->getResponse()->on(\yii\web\Response::EVENT_AFTER_PREPARE, …)` — never redefine the `response` component. **And never validate these changes locally:** the extension only runs on Cloud (`Helper::isCraftCloud()` is false on DDEV), so a local HTTP 200 proves nothing — treat `app.*.php` as prod-only-verifiable. See `references/database.md` and `references/extension.md`.
- **Using `tablePrefix`.** Unsupported on Cloud. Run `php craft db/drop-table-prefix` before migrating. See `references/migration.md`.
- **Choosing MariaDB.** Cloud only supports MySQL 8.0 and Postgres 15.
- **Scheduling your own queue runner.** Cloud auto-processes queue jobs — adding a scheduled command for the queue runner is redundant and may conflict.
- **Flushing the whole cache from a migration.** `Craft::$app->getCache()->flush()` (or any global wipe) during the Migrate phase contends with the still-live old version on a shared cache — and when Redis/Valkey isn't provisioned, that cache is a single MySQL table, so a flush can deadlock (MySQL 1205) and blow the deploy's CLI cap. Invalidate only what changed. See `references/deploy-pipeline.md` (Never flush the whole cache from a migration) and `references/extension.md` (Cache, queue, and session wiring).
- **Synchronous external HTTP in element save hooks.** Blocks every save, and during a resave or `cloud/up` serializes network latency into the deploy's CLI cap. Queue the call instead. See `references/plugin-development.md`.
- **Trying to schedule cron more often than hourly.** Cloud's UI enforces a one-hour minimum. Design recurring tasks around this floor or move the work into queue jobs triggered by other events.
- **`{{ csrfInput() }}` in cacheable templates.** Forces a cookie, busts edge static caching. Use the `csrfInput()` function instead, which renders an async input compatible with `asyncCsrfInputs` (force-enabled on Cloud).
- **Expecting logged-in users to see `currentUser`-gated UI on a cached route.** Cloud does **not** auto-bypass the edge cache for logged-in requests — once a URL's guest copy is cached, editors are served that copy too (`cf-cache-status: HIT`), so server-rendered edit links / member-only blocks silently disappear. Vary the cache key on the session cookie (`cookies: [CraftSessionId]`), not `session:`, under a nested `cache:` → `rules:` key. See `references/caching-and-edge.md`.
- **`.htaccess` rules or nginx config in the repo.** Won't be honored — move to `redirects:` / `rewrites:` in `craft-cloud.yaml`. See `references/config-file.md`.
- **Writing to log files.** No persistent filesystem. Use `Craft::info/warning/error()` and the logger routes to Cloud's log target automatically. See `references/extension.md`.
- **Assuming SSH access.** There is none. The Console command runner is the only way to execute commands, with a 255-character argument cap and 15-minute timeout.
- **Forked Git repos.** Cloud can't deploy from forks — must be the upstream repository.
- **Expecting per-PR preview environments.** Cloud supports per-branch environments only; no automatic ephemeral environment per pull request.
- **Default `sendmail` adapter.** Will not deliver mail. Configure SMTP/Postmark/SES/Resend explicitly. See `references/limitations.md` (Mail).
- **Using ImageOptimize on Cloud.** Cloud handles transforms at the edge via Cloudflare Images — ImageOptimize duplicates the work and can conflict. See `references/assets-and-transforms.md`.
- **Unsigned automated traffic (SSG builds, ISR revalidation, CI) getting `429`/`503`.** Cloud's bot detection correctly flags headless content retrieval as bot traffic. Sign server-side requests with the environment's `$CRAFT_CLOUD_SIGNING_KEY` (RFC 9421) *and* retry `429`/`503` with `Retry-After`-aware backoff — signatures bypass the bot policy, not capacity limits. Never expose the key client-side. See `references/headless.md`.
- **A robots.txt that blocks everything, or slow first requests, on a non-production environment.** Cloud injects `Disallow: /` into every environment not marked Production, and lets non-prod functions sleep after ~15 min idle. Both are by design — check the Production Environment setting before debugging. See `references/limitations.md` (Environments).
- **Changing a Console env var and expecting it to apply.** Variable changes take effect only on the **next deployment**. See `references/deploy-pipeline.md`.
## Reference Files
Read the relevant reference file(s) for your task. Multiple files often apply.
**Task examples:**
- "Set up a new Craft project on Cloud" → `config-file.md` + `deploy-pipeline.md` + `extension.md`
- "Configure `craft-cloud.yaml` for PHP and Node versions" → `config-file.md`
- "Add redirects in `craft-cloud.yaml`" → `config-file.md` (Redirects and Rewrites)
- "What happens during deploy?" → `deploy-pipeline.md`
- "Set build-time env vars" → `deploy-pipeline.md` (Build-time vs runtime variables)
- "Which env vars are reserved / shouldn't I set on Cloud?" → `deploy-pipeline.md` (Reserved runtime variables)
- "What does `php craft cloud/up` do?" → `extension.md` (cloud/up internals)
- "Add an ESI island inside a cached page" → `caching-and-edge.md`
- "Edge static caching rules" → `caching-and-edge.md`
- "Migrate user assets from self-hosted to Cloud" → `assets-and-transforms.md` + `migration.md`
- "Make this plugin Cloud-compatible" → `plugin-development.md`
- "Why does my plugin's file write silently fail on Cloud?" → `plugin-development.md` (Ephemeral filesystem) + `extension.md` (App::isEphemeral)
- "Set up a custom domain" → `domains.md`
- "Schedule a recurring command" → `commands-and-cron.md` (hourly minimum)
- "Run a one-off command on Cloud" → `commands-and-cron.md` (Console command runner)
- "Configure SMTP on Cloud" → `limitations.md` (Mail) + `craftcms/email.md`
- "Set up local DDEV to match Cloud" → `local-dev.md`
- "Move a self-hosted Craft site to Cloud" → `migration.md`
- "What plugins work on Cloud?" → `limitations.md` (Plugin compatibility — community knowledge)
- "My Next.js/Nuxt/Astro build gets 429s from Cloud" → `headless.md`
- "Sign programmatic requests to Cloud" → `headless.md` (Request signing)
- "Link built JS/CSS from templates" → `deploy-pipeline.md` (Artifact URLs)
- "Private Composer packages on Cloud" → `deploy-pipeline.md` (Private Composer packages)
- "robots.txt is blocking my site" → `limitations.md` (Environments)
Load only the reference files your task needs.
| Reference | Scope | ~Lines |
|---|---|---:|
| `references/config-file.md` | Full `craft-cloud.yaml` key reference: `php-version`, `node-version`, `node-path`, `npm-script`, `artifact-path` (webroot-unpublishing caution), `app-path`, `webroot`, `cache:` → `rules:`, `redirects`, `rewrites` | 156 |
| `references/deploy-pipeline.md` | Build → Migrate → Release flow, On Push vs Manual triggers, 15-min build cap, build-time system vars (`CRAFT_CLOUD_*`, `GIT_SHA`), runtime env vars in Console (changes need a redeploy), **reserved/injected vars you must NOT set** (`CRAFT_SECURITY_KEY`, `CRAFT_APP_ID`, `CRAFT_OMIT_SCRIPT_NAME_IN_URLS`, `CRAFT_DB_*`, `REDIS_*`, …), artifact URLs (`cloud.artifactUrl()`, `@artifactBaseUrl`, no cache-busting, manifest/`svg()` CDN reads, white-label CDN rewrite), private Composer packages (Composer Auth), failed-migration rollback semantics | 210 |
| `references/headless.md` | Headless apps + request signing: bot detection and `429`/`503` on SSG/ISR traffic, RFC 9421 HMAC signatures with `$CRAFT_CLOUD_SIGNING_KEY` (5-min validity, exact-URL match, gateway validation falls back silently), the `Retry-After` + backoff retry contract (reads only, throw on exhaustion), Next.js/Nuxt/Astro integration, signing from Craft via `getRequestSigner()` | 107 |
| `references/extension.md` | `craftcms/cloud` package: Yii2 module bootstrap, `App::isEphemeral()`, Path service usage, `cloud.esi()` Twig helper, `cloud/up` internals (verified from source), asset-bundle CDN publisher, binary response auto-upload, log target | 201 |
| `references/assets-and-transforms.md` | Cloud filesystem type (mandatory `assets/` subpath), `aws s3 sync` migration, edge image transforms (70MB / 100MP / 12,000px limits, AVIF 1,600px cap, stretch-mode constraint), no template-side changes required | 87 |
| `references/database.md` | MySQL 8.0 / Postgres 15 only (no MariaDB), `tablePrefix` ban, `php craft db/drop-table-prefix`, auto-wired connection, nightly + on-demand backups (30-day retention, 200GB cap, not part of Cloud's durability architecture), restore commands | 111 |
| `references/caching-and-edge.md` | Static caching rules (nested `cache:` → `rules:`, `pattern` + `query-string` + `cookies`, first-match wins), default ignored tracking params (custom rules **replace** the list), logged-in users vs the cached guest copy (`cookies: [CraftSessionId]`), opt-outs (`{% expires %}`, `setNoCacheHeaders()`), flash-guard pattern, Commerce cart caveat, `asyncCsrfInputs` force-enabled, `cloud.esi(...)` for dynamic islands (scalar vars only, no nesting), tag-based auto-invalidation | 347 |
| `references/domains.md` | DNS setup (`_cf-custom-hostname` TXT, CNAME to `edge.craft.cloud`, apex A records, Orange-to-Orange Cloudflare), preview URL format, auto SSL, `www` not auto-added, extra-domain pricing | 104 |
| `references/commands-and-cron.md` | Console command runner (no SSH, 255-char cap, 15-min cap, no shell interpolation), scheduled commands (hourly minimum, max 5 per env), queue jobs auto-processed (don't schedule a runner), 15-min per-job cap, batched jobs for long work | 143 |
| `references/plugin-development.md` | Cloud-compatible plugin checklist: asset-bundle constraints, `App::isEphemeral()` gating, Path service, `Craft::info()` for logs, queue-job budgeting, cookie-free design, `csrfInput()` function (CSRF-token leakage warning), `sendContentAsFile()` auto-handling, asset-select preference, Craft 4.6+ minimum | 234 |
| `references/migration.md` | Self-hosted → Cloud cutover: audit (table prefix, MariaDB, file writes, `.htaccess`, third-party S3 plugins), `php craft db/drop-table-prefix`, asset migration via `aws s3 sync`, DNS cutover sequence, project-config sync | 132 |
| `references/local-dev.md` | DDEV recipe to mirror Cloud: matching PHP from `craft-cloud.yaml`, DB engine + version, the Cloud extension self-detection and no-op locally, optional `CRAFT_EPHEMERAL` for testing guards, S3 emulation pointers | 145 |
| `references/limitations.md` | Documented unsupported features (MariaDB, tablePrefix, .htaccess, SSH, sendmail, devMode in prod, 6MB response cap incl. front-end uploads, 16KB header cap, 60s request timeout, 200MB upload cap, no env cloning, no region change, no per-PR previews, CP DB-backup utility disabled, forked repos can't deploy) + environments facts (non-prod `robots.txt` Disallow, function sleep, shared asset bucket, deletion is total) + flattened duplicate headers, inert `resourceBase*` + community-knowledge plugin compatibility matrix (ImageOptimize, Imager-X, Blitz, Typesense, etc.) clearly marked as not Pixel & Tonic-blessed + mail strategy + logging gap | 239 |