Managed Runtime
What It Is
With the managed runtime, a Custom App's routes are served by a PHP service that the platform itself builds and runs. The service's code travels in the app's bundle, and a deploy turns it into a container:
- The deploy sends the release's service folder to the hosting's manager.
- The manager installs the service's Composer dependencies and builds an image: PHP 8.4 with Apache, the service's files, and the platform's own web-server configuration.
- It starts a container from the image on the tenant's own network, and gives it a credential of its own.
- Once the service answers, the release goes live. The platform forwards the app's handler-less routes to the container exactly as it does for an external runtime: after every access check, with a signed assertion of the caller.
The service calls back the same way an external service does: it exchanges its credential for runtime tokens and uses the REST collections and the runtime.* commands. The container holds no database password, secret or platform key.
Why It Matters
Use the managed runtime when a service should ship with the app, the way PHP handlers do, without operating a server for it:
- the service is versioned, reviewed and deployed with the app, and rolls back with it
- releases, environments, approvals and promotion work as for any app
- nobody provisions, patches or monitors a host for the service
It suits the same work as the external runtime: Composer libraries, longer-running requests, code shared with other PHP projects. Choose the external runtime instead when the service is not PHP, runs elsewhere, or is operated by its own team.
The managed runtime needs a hosting that offers it: a containerized installation with the manager. The deploy plan reports missing.runtime where it is not offered, and the deploy is refused.
Declaring The Service
{
"manifestVersion": 2,
"runtime": { "type": "managed", "source": "service", "docroot": "public", "memoryMb": 256, "cpus": 0.5, "health": "/health" },
"routes": {
"GET /notes": { "auth": "user" },
"POST /notes": { "auth": "user" },
"GET /ping": { "handler": "server/ping.php" }
}
}
| Key | Meaning |
|---|---|
source | The bundle folder with the service's code, outside public/ and server/. Default service. |
base | The image the service runs on. php8.4, the default and only choice for now, is PHP 8.4 with Apache, pinned by the platform. |
docroot | The folder inside source the web server serves, default public. A request without a matching file goes to its index.php. "" serves source itself. |
memoryMb | Memory for the container, 64–2048, default 256. |
cpus | CPU share, 0.1–4, default 0.5. |
health | A path the platform requests to see that the service is up, default /. Any answer below 500 counts, so a service that refuses requests without an assertion is still healthy. |
timeoutMs, maxBodyBytes | As for the external runtime. |
As with the external runtime, a route without a handler goes to the service and a route with one runs in the sandbox. Events, schedules and MCP tools can target either kind.
The Service Folder
- Composer. With a
composer.json(and ideally itscomposer.lock), the platform runscomposer install --no-devwith scripts and plugins switched off. Dependencies come from the package registries the hosting allows. A dependency that is not published there must travel inside the folder, mapped incomposer.jsonunderautoload. - What never ships. The CLI leaves out
vendor/,node_modules/,.git/and.env*files; the platform installs dependencies itself, and environment files do not belong in an image. The manager refuses links and special files. - Front controller. Apache serves
docroot, withmod_rewriteand.htaccessavailable. Requests without a file go toindex.php. - PHP. PHP 8.4 comes with the extensions of the official image: among them
ctype,curl,dom,fileinfo,mbstring,openssl,pdo_sqlite,sodium,xmlandzlib. Extensions cannot be added. - Files. The container's file system is read-only apart from
/tmp(64 MB, lost with the container). Keep data in records, the key-value store or files attached to records, never on disk. - Configuration. The container's environment has
JETSTACK_URL(where it reaches the tenant),JETSTACK_APP(the app's slug) andJETSTACK_APP_CREDENTIAL(its credential). Secrets and environment variables come from the platform throughruntime.secretandruntime.env, and only the ones the manifest declares.
With the PHP SDK, a service needs no configuration of its own:
$verifier = Jetstack\Sdk\AssertionVerifier::forApp(getenv('JETSTACK_URL'), getenv('JETSTACK_APP'), sys_get_temp_dir());
$platform = Jetstack\Sdk\PlatformClient::fromEnvironment();
A complete example is in packages/examples/managed-runtime-php.
Deploying
A deploy of a release with a managed runtime passes through two extra states before it goes live:
| Status | What happens |
|---|---|
building | The manager installs the service's dependencies and builds its image, in the background. |
starting | The container runs, and the platform waits until it answers its health path. |
activated | The release is live: its routes go to the new container. |
jetstack deployfollows these states and ends when the release is live or the deploy failed.deployments.getmoves a waiting deploy on, and so does the scheduler.- If the build or the start fails, the deploy fails with the reason (for example Composer's output). The previous release stays live.
- The previous release's container keeps serving the requests it already has for 90 seconds, then stops. Its image stays, so a rollback to it starts a container again without building.
- A deploy of a release whose service already runs goes live at once.
- If the tenant's configuration changes while the service is starting in a way that changes the deploy's plan, the deploy fails and must be made again, as with approvals.
- Each container gets a credential of its own, revoked when it stops. The credential on the app's page is only for running the service yourself, for example while developing it.
The newest three images of an app are kept for rollbacks. Deleting the app removes its containers, images and builds.
Watching The Service
jetstack runtime --env <env>lists the app's containers: which is live, which are starting, failed or stopped.jetstack logs --env <env> --runtimeshows the live container's own output: Apache's access log and PHP's errors.- The app's page shows the same containers under Service.
- Every forwarded call is a line in the app's log (
jetstack logs), with its status and duration.
Isolation
Each tenant's services run on a network of their own, which only that tenant's containers join:
- A service reaches its tenant (at
JETSTACK_URL) and the internet the hosting allows. - It cannot reach the platform's databases or caches, other tenants or their services, the manager, or cloud metadata endpoints.
- A service container runs as an unprivileged user, on a read-only file system, with every capability dropped and privilege escalation blocked. Its memory, CPU, process count and logs are capped.
- Nothing from the app runs while its image is built. Composer runs in a separate throw-away container, without scripts or plugins, and the image is assembled from the pinned base by copying files only.
Tenant Settings
This key is not on the Application settings screen; set it in the tenant configuration.
customAppsManagedRuntime
What it controls:
- whether apps on this tenant may have the platform build and run their services
Default:
true, where the hosting offers the managed runtime
How to configure it:
- set it to
falseto stop every managed service of the tenant at once
What it affects:
- while it is off, forwarded routes of managed apps answer
503, their containers' credentials cannot be exchanged, and deploy plans report the runtime as missing - running containers are not removed, so turning it on again restores the service at once
For Hosting Operators
The manager builds and runs the services; tenants reach it on its internal listener (port 8081), which the public router never routes. These variables configure it:
| Variable | Where | Meaning |
|---|---|---|
MANAGER_APPS_TOKEN_KEY | manager | Switches managed runtimes on (at least 32 characters). Each tenant's token is derived from it. |
MANAGER_APPS_INTERNAL_URL | manager | How tenants reach the internal listener, default http://manager:8081. |
MANAGER_APPS_TENANT_RUNTIMES | manager | The runtimes tenant containers are told the hosting offers, default managed. |
MANAGER_APPS_TENANT_CONTAINERS | manager | Which of a tenant's containers join its app network, and which answers to the tenant's host name there. Default tenant-<slug>=alias. |
MANAGER_APPS_BUILD_NETWORK | manager | The network Composer runs on; give it access to the package registries only. Default bridge. |
MANAGER_APPS_NETWORK_INTERNAL | manager | 1 cuts the services' internet access entirely. |
MANAGER_APPS_WORK_DIR | manager | Where builds are prepared. |
MANAGER_APPS_BASE_PHP84, MANAGER_APPS_COMPOSER_IMAGE | manager | Move the pinned images, after vetting a new digest. |
JETSTACK_MANAGER_URL, MANAGER_TENANT_ID, MANAGER_TENANT_TOKEN, JETSTACK_APP_RUNTIMES, JETSTACK_RUNTIME_PLATFORM_URL | tenant | Set on each tenant container by the manager. |
The host's firewall should keep the app networks (jsapps-*) away from private address ranges and cloud metadata endpoints. Tenant containers must expose nothing but HTTP on those networks: PHP-FPM listens on the loopback interface only.