A simple, secure and fluent PHP API for running untrusted code in isolated
Docker containers. It talks directly to the Docker Engine API over a unix
socket or TCP/TLS — no shelling out to the docker CLI — and makes the
easiest way to run code also the most locked-down one.
The clip above is Code Arena, a playground built on this package: run PHP, Python or Node in a sandbox and watch it block a network call, refuse a write to its filesystem, and grade HackerRank-style challenges — all in throwaway containers.
composer require sharryy/docker-sandboxRequires PHP 8.3+, the cURL extension, and access to a Docker daemon (Docker Desktop, Colima, Lima, or a remote daemon over TCP/TLS).
Note: The package is developed and tested on Linux and macOS. Windows is not supported or tested — the test suite requires a Linux Docker daemon.
use Sharryy\Docker\Sandbox;
$sandbox = new Sandbox();
$result = $sandbox->php('<?php echo 2 + 2;');
$result->output(); // "4"
$result->exitCode(); // 0
$result->successful(); // true
$result->duration(); // secondspython and node presets ship out of the box:
$sandbox->python('print(6 * 7)')->output(); // "42"
$sandbox->node('console.log(6 * 7)')->output(); // "42"use Sharryy\Docker\{Sandbox, Preset};
// Preset(image, filename, interpreter)
Sandbox::register('ruby', new Preset('ruby:3.3-slim', 'main.rb', 'ruby'));
$sandbox->run('ruby', 'puts "hello"')->output(); // "hello"A custom preset with the same name overrides a built-in one.
Every Sandbox/run() execution is locked down. The container has no
network, runs as a non-root user, on a read-only root filesystem
with only a small writable /tmp tmpfs, with all Linux capabilities
dropped, no-new-privileges, no swap, and a process limit (so a
fork bomb can't take down the host). Missing images are pulled automatically,
and a timeout kills runaway code:
use Sharryy\Docker\Exceptions\ProcessTimeoutException;
try {
$sandbox->php('<?php while (true) {}', timeout: 5);
} catch (ProcessTimeoutException $e) {
// the container was killed and removed
}ExecutionResult exposes output(), errorOutput(), exitCode(),
successful(), failed(), timedOut(), oomKilled() and duration().
use Sharryy\Docker\{Docker, ConnectionOptions};
// Auto-discovers the socket (DOCKER_HOST, default, Colima, Docker Desktop)
$docker = new Docker();
// Or be explicit:
$docker = new Docker(ConnectionOptions::fromSocket('/var/run/docker.sock'));
$docker = new Docker(ConnectionOptions::fromTcp('127.0.0.1', 2375));
$docker = new Docker(ConnectionOptions::fromTls('docker.example.com', 2376,
caCert: '/certs/ca.pem', clientCert: '/certs/cert.pem', clientKey: '/certs/key.pem'));The API version is negotiated with the daemon automatically.
For long-lived or custom containers, use the fluent builder:
$container = $docker->containers()
->from('redis:alpine')
->withName('cache')
->withCommand(['redis-server'])
->withPort(6379, 6379)
->withMemoryLimit('256m')
->withCpuLimit(0.5)
// security hardening (all opt-in here)
->withUser('1000:1000')
->asReadOnly()
->withTmpfs('/tmp')
->withPidsLimit(128)
->dropCapabilities()
->withoutNewPrivileges()
->withoutSwap()
->withUlimit('nofile', 1024)
->create();
$container->start();$container->status(); // 'created' | 'running' | 'exited' | 'paused' | ...
$container->isRunning();
$container->logs(); // combined stdout + stderr
$container->inspect(); // full inspect payload
$container->stats(); // one-shot CPU/memory/network snapshot
$container->pause();
$container->unpause();
$container->restart();
$container->rename('new-name');
$container->stop()->remove();
// Run a command and read its result
$result = $container->exec(['redis-cli', 'ping']);
$result->output(); // "PONG"
$result->exitCode(); // 0
// Follow output in real time
$container->streamLogs(function (string $text, string $stream) {
// $stream is "stdout" or "stderr"
echo $text;
});use Sharryy\Docker\Support\Tar;
$container->putFiles([
'app/main.php' => '<?php echo "hi";',
'app/lib.php' => '<?php /* ... */',
]);
$tar = $container->getArchive('/app'); // raw tar of the directory$images = $docker->images();
$images->exists('php:8.2-cli');
$images->pull('php:8.2-cli'); // optional registry auth arg
$images->inspect('php:8.2-cli'); // low-level image details
$images->tag('php:8.2-cli', 'myapp:latest'); // add another tag
$images->list(); // repo tags
$images->remove('php:8.2-cli', force: true);
$images->prune(); // remove dangling images$networks = $docker->networks();
$network = $networks->create('app-net', internal: true); // also: driver, attachable, labels, options
$network->connect($container->id()); // optional aliases
$network->disconnect($container->id(), force: true);
$network->inspect();
$networks->find('app-net'); // by id or name, or null
$networks->list(); // array of Network objects
$networks->remove('app-net');
$networks->prune(); // remove unused networks$volumes = $docker->volumes();
$volume = $volumes->create('app-data'); // also: driver, labels, driverOpts
$volume->mountpoint(); // host path
$volume->inspect();
// Mount a named volume into a container:
$docker->containers()->from('php:8.2-cli')->withVolume('app-data', '/data');
$volumes->exists('app-data');
$volumes->find('app-data'); // Volume object, or null
$volumes->list(); // array of Volume objects
$volumes->remove('app-data', force: true);
$volumes->prune(); // remove unused volumes$container = $docker->containers()->find('cache'); // by id or name, or null
$all = $docker->containers()->list(all: true); // array of Container objectsThe test suite runs against a real Docker daemon:
composer testNetwork-heavy tests (pulling images in run(), and the python/node presets)
are skipped unless DOCKER_PULL_TESTS=1 is set.
Please see CHANGELOG for more information on what has changed recently.
Please see CONTRIBUTING for details.
If you discover a security vulnerability, please review our security policy for how to report it responsibly.
The MIT License (MIT). Please see License File for more information.
