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:

  1. The deploy sends the release's service folder to the hosting's manager.
  2. 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.
  3. It starts a container from the image on the tenant's own network, and gives it a credential of its own.
  4. 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" }
  }
}
KeyMeaning
sourceThe bundle folder with the service's code, outside public/ and server/. Default service.
baseThe image the service runs on. php8.4, the default and only choice for now, is PHP 8.4 with Apache, pinned by the platform.
docrootThe folder inside source the web server serves, default public. A request without a matching file goes to its index.php. "" serves source itself.
memoryMbMemory for the container, 64–2048, default 256.
cpusCPU share, 0.1–4, default 0.5.
healthA 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, maxBodyBytesAs 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 its composer.lock), the platform runs composer install --no-dev with 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 in composer.json under autoload.
  • 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, with mod_rewrite and .htaccess available. Requests without a file go to index.php.
  • PHP. PHP 8.4 comes with the extensions of the official image: among them ctype, curl, dom, fileinfo, mbstring, openssl, pdo_sqlite, sodium, xml and zlib. 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) and JETSTACK_APP_CREDENTIAL (its credential). Secrets and environment variables come from the platform through runtime.secret and runtime.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:

StatusWhat happens
buildingThe manager installs the service's dependencies and builds its image, in the background.
startingThe container runs, and the platform waits until it answers its health path.
activatedThe release is live: its routes go to the new container.
  • jetstack deploy follows these states and ends when the release is live or the deploy failed. deployments.get moves 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> --runtime shows 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 false to 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:

VariableWhereMeaning
MANAGER_APPS_TOKEN_KEYmanagerSwitches managed runtimes on (at least 32 characters). Each tenant's token is derived from it.
MANAGER_APPS_INTERNAL_URLmanagerHow tenants reach the internal listener, default http://manager:8081.
MANAGER_APPS_TENANT_RUNTIMESmanagerThe runtimes tenant containers are told the hosting offers, default managed.
MANAGER_APPS_TENANT_CONTAINERSmanagerWhich 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_NETWORKmanagerThe network Composer runs on; give it access to the package registries only. Default bridge.
MANAGER_APPS_NETWORK_INTERNALmanager1 cuts the services' internet access entirely.
MANAGER_APPS_WORK_DIRmanagerWhere builds are prepared.
MANAGER_APPS_BASE_PHP84, MANAGER_APPS_COMPOSER_IMAGEmanagerMove the pinned images, after vetting a new digest.
JETSTACK_MANAGER_URL, MANAGER_TENANT_ID, MANAGER_TENANT_TOKEN, JETSTACK_APP_RUNTIMES, JETSTACK_RUNTIME_PLATFORM_URLtenantSet 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.