Skip to content
Guides · Starters

How Do I Set Up a PHP Dev Environment in a Cloud Sandbox?

Updated October 27, 2026 .md

Short answer: A new sandbox has no PHP. apt-get install php-cli gave Ubuntu's PHP 8.3.6 in 11 seconds; the ondrej/php PPA that php.net lists gave 8.5.12 in 56. Install Composer with its signature check, serve Laravel on 127.0.0.1, host it privately, and trust the 127.0.0.1 proxy or Laravel builds http:// links.

We ran every command on this page on prod on 2026-10-27 in eu-central, on small sandboxes (2 vCPU, 4 GB). The project is the laravel/laravel application skeleton at tag v13.9.0 (commit 3514078), which pulls 111 Composer packages including Laravel 13.

Results#

Step Time on small
apt install php-cli and 6 extensions (Ubuntu, PHP 8.3.6) 11.2 s
Add the ondrej/php PPA, install PHP 8.5.12 and the same extensions 55.6 to 56.3 s
Install Composer 2.10.3, signature checked 1.3 s
composer install, empty cache (111 packages) 6.8 s
composer install, warm cache, no vendor/ (3 runs) 1.86, 2.10, 1.99 s
composer install, nothing to do 1.06 s
php artisan test 0.32 s
sbx new --from the named snapshot 3.2 s
In the new sandbox: composer install, cache only (3 runs) 1.79, 1.96, 1.95 s
4 parallel 1-second requests, artisan serve with 1 worker 4.2 s
The same with PHP_CLI_SERVER_WORKERS=4 … --no-reload (3 runs) 2.2, 3.2, 2.2 s

The lesson: Composer is quick, so in PHP the snapshot saves you the PHP install, not the packages. The PPA install is the slow part, at almost a minute.

1. There is no PHP yet#

sbx new --name phpdev-1 --type small
sbx exec sb_9btgmurcx0 "which php composer"

which printed nothing: neither php nor composer is in the image on 2026-10-27. Everything else that is there is listed in What's pre-installed in a sandbox. You have passwordless sudo, so you have two choices for PHP.

2. Pick a PHP: Ubuntu's 8.3 or the PPA's 8.5#

php.net's supported versions page shows 8.5 (released November 2025) as the newest branch on 2026-10-27, and 8.3 as security fixes only, until 31 December 2027. The sandbox runs Ubuntu 24.04, whose own package is 8.3.

Ubuntu's PHP 8.3. This is the "OS default" option on php.net's download page. We added the extensions Laravel and Composer need:

sudo apt-get update -qq
sudo apt-get install -y php-cli php-xml php-mbstring php-curl php-sqlite3 php-zip php-intl unzip
php -v | head -1
PHP 8.3.6 (cli) (built: Oct 19 2026 15:16:58) (NTS)

It took 11.2 seconds. Laravel 13 needs PHP ^8.3, so this is enough for the skeleton.

PHP 8.5 from the PPA. For Ubuntu and a newer version, php.net's download page gives the ondrej/php PPA. The page notes that these are third-party builds with patches, including telemetry in the Apache 2 and PHP-FPM modules; the CLI is what this page uses. On a fresh sandbox:

sudo apt-get install -y software-properties-common
sudo LC_ALL=C.UTF-8 add-apt-repository ppa:ondrej/php -y
sudo apt-get update -qq
sudo apt-get install -y php8.5-cli php8.5-xml php8.5-mbstring php8.5-curl php8.5-sqlite3 php8.5-zip php8.5-intl unzip
php -v | head -1; update-alternatives --list php
PHP 8.5.12 (cli) (built: Oct 21 2026 14:37:30) (NTS)
/usr/bin/php8.5

It took 55.6 seconds, most of it adding the repository.

Don't mix the two without checking php -v. In our first sandbox we installed Ubuntu's php-cli first and then the php8.5-* packages. The PPA upgraded the php-cli metapackage, which pulled in PHP 8.4, and php pointed at 8.4, not 8.5:

PHP 8.5.12 (cli) (built: Oct 21 2026 14:37:30) (NTS)     # php8.5 -v
PHP 8.4.27 (cli) (built: Oct 21 2026 14:35:21) (NTS)     # php -v
/usr/bin/php.default
/usr/bin/php8.3
/usr/bin/php8.4
/usr/bin/php8.5

Three PHPs were installed. Pick the default by hand:

sudo update-alternatives --set php /usr/bin/php8.5
php -v | head -1
update-alternatives: using /usr/bin/php8.5 to provide /usr/bin/php (php) in manual mode
PHP 8.5.12 (cli) (built: Oct 21 2026 14:37:30) (NTS)

Any version stays reachable by name (php8.3 artisan test), which is handy for testing a library on two versions.

3. Install Composer with the signature check#

getcomposer.org asks you not to copy its installer snippet, because the hash in it changes with each installer. Its programmatic install fetches the current signature instead, so it works in a script:

EXPECTED="$(curl -fsSL https://composer.github.io/installer.sig)"
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
ACTUAL="$(php -r "echo hash_file('sha384', 'composer-setup.php');")"
[ "$EXPECTED" = "$ACTUAL" ] && echo "installer signature OK" || exit 1
sudo php composer-setup.php --quiet --install-dir=/usr/local/bin --filename=composer
rm composer-setup.php
composer --version; composer config --global cache-dir
installer signature OK
Composer version 2.10.3 2026-08-27 13:34:23
/home/user/.cache/composer

It took 1.3 seconds. Composer 2.10.3 was the latest stable release on 2026-10-27. The cache is under /home/user, so it survives sbx stop and goes into a snapshot.

4. Install a real project, cold and warm#

Long commands run past the 30-second default of sbx exec, so we ran the script with --detach and read its log:

git clone -q https://github.com/laravel/laravel app && cd app && git checkout -q 3514078f98dd
composer install --no-interaction --no-progress
…
6.751 s
packages: 111
75M	vendor
29M	/home/user/.cache/composer
== composer install, warm cache, no vendor (3 runs)
1.859 s
2.095 s
1.992 s
== composer install, nothing to do
1.057 s

A cold install of 111 packages took 6.8 seconds; from the cache, about 2. Even "nothing to do" takes a second, because Composer still runs the skeleton's post-autoload-dump scripts (artisan package:discover). Then the app setup and its test suite:

cp .env.example .env && php artisan key:generate -q
touch database/database.sqlite && php artisan migrate --force
php artisan test
  0001_01_01_000000_create_users_table ........................... 9.72ms DONE
  0001_01_01_000001_create_cache_table ........................... 5.42ms DONE
  0001_01_01_000002_create_jobs_table ............................ 8.44ms DONE
…
  Tests:    2 passed (2 assertions)
  Duration: 0.15s

SQLite needs no database server, so the skeleton runs as cloned. The same tests passed on 8.5 with php8.5 artisan test, and composer check-platform-reqs passed on 8.5.

5. Serve it on a private HTTPS URL#

php artisan serve runs PHP's built-in web server on 127.0.0.1:8000. That is what you want: a sandbox has a public IPv6 address, so don't bind 0.0.0.0. Start it detached and host it with --localhost:

sbx exec sb_9btgmurcx0 --detach "cd app && php artisan serve > serve.log 2>&1"
sbx host sb_9btgmurcx0 8000 --localhost
{"ok":true,"type":"port.hosted","sandboxId":"sb_9btgmurcx0","port":8000,"url":"https://jade-formal-clam-8000.box.sandboxer.com?_token=…","access":"private","isProtected":true,"localhost":true}

Without the token the URL returned 401. With ?_token=… it returned 302, set a cookie and sent us to the same URL without the token; then / returned 200. Inside the sandbox, the pre-installed host 8000 does the same. How private URLs, tokens and cookies work is in Hosting.

We added a route that prints what Laravel thinks of the request:

// routes/web.php
Route::get('/whereami', fn (Illuminate\Http\Request $r) => [
    'secure' => $r->isSecure(),
    'host' => $r->getHost(),
    'url' => url('/dashboard'),
    'xfp' => $r->header('X-Forwarded-Proto'),
]);
Route::get('/go', fn () => redirect('/whereami'));

Through the hosted URL, as cloned:

{"secure":false,"host":"jade-formal-clam-8000.box.sandboxer.com","url":"http:\/\/jade-formal-clam-8000.box.sandboxer.com\/dashboard","xfp":"https"}
location: http://jade-formal-clam-8000.box.sandboxer.com/whereami

The proxy sends X-Forwarded-Proto: https, but Laravel ignores it from an untrusted proxy, so url(), asset() and redirects come out as http://. The proxy answers http:// with a 308 to https://, so plain links still work, but a browser blocks http:// scripts and styles on an HTTPS page as mixed content. Laravel's fix is trusted proxies. With --localhost the proxy reaches the app from 127.0.0.1, so trust only that address in bootstrap/app.php:

->withMiddleware(function (Middleware $middleware): void {
    $middleware->trustProxies(at: '127.0.0.1');
})
{"secure":true,"host":"jade-formal-clam-8000.box.sandboxer.com","url":"https:\/\/jade-formal-clam-8000.box.sandboxer.com\/dashboard","xfp":"https"}
location: https://jade-formal-clam-8000.box.sandboxer.com/whereami

No restart was needed: artisan serve loads the app on every request.

More than one request at a time#

PHP's built-in server runs one single-threaded process by default, so a slow request blocks the next one. We added a route that sleeps one second and sent four requests at once through the hosted URL:

ok 9433
ok 9433
ok 9433
ok 9433
4.226 s

One process, served in turn. PHP's PHP_CLI_SERVER_WORKERS variable forks more workers, but artisan serve ignores it unless you also pass --no-reload. Without the flag we still got one process and 4.25 seconds. With it:

cd app && PHP_CLI_SERVER_WORKERS=4 php artisan serve --no-reload
ok 9481 ok 9477 ok 9479 ok 9479 2.226 s
ok 9479 ok 9480 ok 9479 ok 9479 3.226 s
ok 9479 ok 9480 ok 9481 ok 9479 2.219 s

Four workers took 2.2 to 3.2 seconds, not 1: the workers share one listening socket and the kernel doesn't spread requests evenly. With --no-reload, restart the server after you change .env. Inside the sandbox, the welcome page took 5 to 11 ms on 127.0.0.1.

6. Check code over SSH#

With sbx ssh-config --install done once on your laptop, any SSH tool reaches the sandbox by id. In the sandbox we later started from the snapshot (step 7), we linted a broken file and a good one:

ssh sb_pqvf8vbouy.sbx 'cd app && php -l /tmp/bad.php; php -l routes/web.php'
Errors parsing /tmp/bad.php
PHP Parse error:  Unclosed '[' on line 2 in /tmp/bad.php on line 3
No syntax errors detected in routes/web.php

In VS Code or Cursor, Remote-SSH to sb_pqvf8vbouy.sbx and open /home/user/app; PHP extensions installed on the remote side use the sandbox's php. We tested the ssh command, not the editor UI. Setup and troubleshooting are in SSH into a cloud sandbox from VS Code or Cursor.

7. Save it as a named snapshot and start from it#

sbx snapshot sb_9btgmurcx0 phpdev-cache
sbx new --name phpdev-2 --type small --from phpdev-cache

The save took 2.1 seconds and the new sandbox was ready in 3.2. It came with everything on the disk: the PHPs, the update-alternatives choice, Composer, its cache, the clone with vendor/ and .env, and the trusted-proxy edit:

PHP 8.5.12 (cli) (built: Oct 21 2026 14:37:30) (NTS)
Composer version 2.10.3 2026-08-27 13:34:23
== composer install, as saved
1.926 s
== composer install, cache only (rm -rf vendor), 3 runs
1.789 s
1.956 s
1.952 s

We started PHP_CLI_SERVER_WORKERS=4 php artisan serve --no-reload again and hosted port 8000. Hosting the new sandbox gave a new URL and token, and /whereami reported "secure":true at once. The server process itself is not in a named snapshot; start it again, or run it as a systemd unit. For naming the snapshot by a hash of composer.lock and rebuilding it when that changes, see Cache dependencies in a named snapshot.

FAQ#

Which PHP version does a Sandboxer sandbox have?#

None on 2026-10-27; PHP isn't in the image. sudo apt install php-cli gives Ubuntu 24.04's PHP 8.3.6, and php.net's ondrej/php PPA option gave 8.5.12. Check php -v after installing.

The hosting proxy terminates HTTPS and sends X-Forwarded-Proto: https, which Laravel ignores until the proxy is trusted. Add $middleware->trustProxies(at: '127.0.0.1') in bootstrap/app.php when the port is hosted with --localhost.

Why does php -v show 8.4 after I installed php8.5?#

Installing PPA packages over Ubuntu's php-cli upgraded that metapackage, which pulled in PHP 8.4 as the default. Run sudo update-alternatives --set php /usr/bin/php8.5, or install only the php8.5-* packages on a fresh sandbox.

Is it worth snapshotting vendor and the Composer cache?#

For the time, barely: a cold composer install of 111 packages took 6.8 seconds and a warm one about 2. The snapshot pays off for the PHP install, which took 56 seconds from the PPA, and for your edits and database.

Can artisan serve handle parallel requests?#

Only with PHP_CLI_SERVER_WORKERS set and --no-reload. Without the flag, artisan serve runs one process. It is a development server either way; php.net says not to use it on a public network.

More guides

Top AI sandbox platforms for code execution in 2026, ranked

For running AI-written code we rank Sandboxer, Freestyle and E2B first: each gives code its own VM with an API or SDK and can pause with memory. Pick Daytona for Windows or GPUs, Modal or Northflank for GPUs at scale, E2B or Northflank to run in your own cloud, and Vercel Sandbox if your app lives on Vercel.

Read the guide
Best sandboxes for running coding agents (Claude Code, Codex, Gemini CLI) in 2026, ranked

For coding agents we rank Sandboxer first: a full Ubuntu VM with Claude Code and Codex preinstalled, Docker, and stop-and-resume that keeps the agent's session in memory. Freestyle and Amp Orbs also keep memory across pauses. Pick E2B for prebuilt agent templates on open source, Docker Sandboxes for free local runs, and Codespaces if you live in GitHub.

Read the guide
Best Daytona sandbox alternatives in 2026, ranked

The best Daytona alternatives depend on why you're switching. For a full Linux microVM on every sandbox with memory-saving stop and Docker, we rank Sandboxer first. E2B suits open source and self-hosting, Modal massive short-job fleets, Northflank bring-your-own-cloud with GPUs, and Vercel Sandbox apps on Vercel.

Read the guide
Best E2B alternatives in 2026, ranked

If you're leaving E2B for longer sessions or a fuller machine, we rank Sandboxer first, then Freestyle: both give each sandbox a full Linux VM that keeps memory across pauses, with Docker and no short session cap. Pick Daytona for Windows or GPUs, Northflank for bring-your-own-cloud, and stay on E2B if you need open source.

Read the guide