Runtime

How to run PHP code in a sandbox

Install php-cli in a Linux microVM, write the script there, and run it with php under a time limit and with the network off.

On Runtime, PHP is one apt command away and every script gets its own kernel. Each sandbox is a Firecracker microVM running Ubuntu 24.04.5, where sudo apt-get install -y php-cli installs PHP 8.3 from Ubuntu's archive. A new sandbox took 351 ms from the create request to its first Python result at the median, measured on 24 September 2026 (speed); bake PHP into an image and that is all the wait there is.

PHP packages on Ubuntu 24.04

PHP is not in the default image. These are the Ubuntu 24.04 (noble) packages, read on packages.ubuntu.com on 25 September 2026:

Package Version What it adds
php-cli 8.3 (php8.3-cli 8.3.6) The php command
php8.3-sqlite3 8.3.6 PDO and SQLite3 for SQLite
php8.3-pgsql 8.3.6 PDO and pgsql for PostgreSQL
composer 2.7.1 Composer, the dependency manager

For another PHP version, start a custom image from the official php image on Docker Hub, such as php:8.4-cli (a tag listed there on 25 September 2026).

Run a script

Install once, turn the internet off, then run what you were given. The script goes in as a file and runs with an array, so nothing passes through a shell.

TypeScriptimport { Sandbox } from "withruntime";const script = `<?php$words = str_word_count("the quick brown fox jumps over the lazy dog", 1);echo json_encode(array_count_values(array_map('strlen', $words))), PHP_EOL;`;await using sbx = await Sandbox.create({ timeoutSeconds: 600, onLeaseEnd: "stop" });await sbx.exec("sudo apt-get update -q && sudo apt-get install -y -q php-cli", {  check: true,  timeoutMs: 300_000,});await sbx.network.set({ internet: false });await sbx.files.write("/workspace/job.php", script);const lint = await sbx.exec(["php", "-l", "job.php"]);if (lint.exitCode !== 0) throw new Error(lint.stdout);const run = await sbx.exec(["php", "-d", "memory_limit=256M", "job.php"], { timeoutMs: 20_000 });console.log(run.exitCode, run.timedOut, run.stdout);
Pythonfrom withruntime import Sandboxscript = """<?php$words = str_word_count("the quick brown fox jumps over the lazy dog", 1);echo json_encode(array_count_values(array_map('strlen', $words))), PHP_EOL;"""with Sandbox.create(timeout_seconds=600, on_lease_end="stop") as sbx:    sbx.exec("sudo apt-get update -q && sudo apt-get install -y -q php-cli", check=True, timeout_ms=300_000)    sbx.network.set(internet=False)    sbx.files.write("/workspace/job.php", script)    lint = sbx.exec(["php", "-l", "job.php"])    if lint.exit_code != 0:        raise SystemExit(lint.stdout)    run = sbx.exec(["php", "-d", "memory_limit=256M", "job.php"], timeout_ms=20_000)    print(run.exit_code, run.timed_out, run.stdout)
  • php -l checks syntax without running the code and reports on stdout. It does not find errors that only show at run time, such as an undefined function (PHP command-line options).
  • -d sets any php.ini directive for one run, so each job can get its own memory limit.
  • timeoutMs stops the command from outside PHP. An endless loop returns timedOut: true with the output so far.

Why not disable_functions?

PHP's own settings can hide functions such as exec or system, but the code still runs in your process, on your kernel, next to your files and network. A sandbox moves the whole interpreter into a microVM with its own kernel. Leave shell_exec on if the job needs it: it reaches only a throwaway machine whose network, CPU, memory and cost are enforced on the host (security).

Install Composer packages, then cut the network

Composer needs Packagist and the package hosts while it installs. Let it install with the web on, then switch the network off before the project's code runs:

TypeScriptimport { Sandbox } from "withruntime";await using sbx = await Sandbox.create({ timeoutSeconds: 900, onLeaseEnd: "stop" });await sbx.exec("sudo apt-get update -q && sudo apt-get install -y -q php-cli composer unzip", {  check: true,  timeoutMs: 300_000,});await sbx.files.upload("./app", "/workspace/app");await sbx.exec(["composer", "install", "--no-interaction", "--no-progress"], {  cwd: "/workspace/app",  check: true,  timeoutMs: 300_000,});await sbx.network.set({ internet: false });const tests = await sbx.exec(["php", "vendor/bin/phpunit"], {  cwd: "/workspace/app",  timeoutMs: 300_000,});console.log(tests.exitCode, tests.stdout.slice(-3000));

See a PHP page in your browser

PHP's built-in web server (php -S) serves a folder. Start it with spawn so it keeps running, then share the port as a private HTTPS preview:

TypeScriptimport { Sandbox } from "withruntime";await using sbx = await Sandbox.create({ image: "php", timeoutSeconds: 1800 });await sbx.files.write("/workspace/site/index.php", "<?php echo 'Hello from PHP ', PHP_VERSION;");await sbx.spawn(["php", "-S", "0.0.0.0:8000", "-t", "/workspace/site"]);const preview = await sbx.previews.create(8000);console.log(preview.urlWithToken);

This uses the php image built below. See previews of agent-built apps for the whole flow.

Start every sandbox with PHP

Pythonfrom withruntime import Runtimeruntime = Runtime()runtime.images.build(name="php", recipe={"apt": ["php-cli", "php8.3-sqlite3", "composer", "unzip"]})with runtime.sandboxes.create(image="php", network={"internet": False}) as sbx:    print(sbx.exec(["php", "-v"]).stdout)
Terminalruntime image build --apt php-cli,php8.3-sqlite3,composer,unzip --name php

Building an image is free, and the free trial stores your first three images free (custom images).

What it costs

Runtime bills the CPU a script uses at $0.025 per vCPU-hour, with a floor of a twentieth of a vCPU, and memory at $0.0075 per GiB-hour. A 2 vCPU, 4 GiB sandbox waiting between requests costs $0.03125 an hour (pricing). New accounts get 50 free sandbox hours, no card:

Terminalnpx withruntime sandbox run --trial -- bash -c 'sudo apt-get update -q && sudo apt-get install -y -q php-cli && php -v'

For code a model wrote, see running untrusted LLM code and turning off a sandbox's internet.

Sources

Checked 25 September 2026.

Facts on this page were checked on 25 September 2026.