Post
One Docker image for Bun, PHP 8.5, Laravel, and Node
One Docker image for Bun, PHP 8.5, Laravel, and Node
I got tired of juggling separate toolchains for Laravel front-end work. Bun for speed, PHP for the app, Composer and the Laravel installer for scaffolding, Node/npm when a package still expects them. So I put them in one image: harianto/bun-php-laravel.
This post is a walkthrough of what is inside, how I run it, and a few sharp edges I hit while building it.
What it is
A Docker image based on oven/bun:canary, extended into a full Laravel-friendly shell:
| Tool | How it gets in |
|---|---|
| Bun | Base image (oven/bun:canary, currently v1.4.0) — Rust runtime on JavaScriptCore |
| nvm + Node LTS + npm | Official nvm install, then nvm install --lts |
| PHP 8.5 | php.new / Herd Lite for Linux |
| Composer | Comes with php.new; also used for global packages |
| Laravel installer | composer global require laravel/installer |
| Basics | curl, git, zip, unzip, tar |
After a build I can open a container and run bun, php, composer, laravel, node, npm, and nvm without installing any of that on the host.
Why Bun as the base
I already use Bun a lot. Starting from oven/bun:canary means I get a current Bun runtime for free, then layer the PHP world on top instead of bolting Bun onto a PHP image later. At the time of writing, that canary image ships Bun v1.4.0.
Bun used to be written in Zig. From v1.4.0 onwards it is rewritten in Rust (still on Apple’s JavaScriptCore) — see Rewriting Bun in Rust. That is a big part of why canary feels like the right base for a personal toolbox image: I get the new Rust build without pinning an old LTS. I rebuild when I need a refresh.
PHP via php.new
Laravel’s php.new installer (Herd Lite) downloads PHP, Composer, and a Laravel binary into ~/.config/herd-lite/bin. In the Dockerfile that looks like:
/bin/bash -c "$(curl -fsSL https://php.new/install/linux/8.5)"
composer global require laravel/installer
Two Docker-specific quirks showed up:
clearfails without a TTY — the installer callsclear, and withset -ethat aborts the build. I temporarily replaceclearwith/bin/trueduring install.- No PHP 8.5 binary for linux/arm64 yet — Herd’s CDN returns 403 for
linux/arm64/8.5. On Apple Silicon I therefore build aslinux/amd64so the x64 PHP 8.5 binary works (under emulation). When arm64 8.5 lands, that platform pin can go.
Symlinks into /usr/local/bin keep php, composer, and laravel available even when a login shell resets PATH.
Node via nvm (not a one-off Node install)
I wanted real version management inside the container, not a single hardcoded Node package:
curl -fsSL "https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh" | bash
. "$NVM_DIR/nvm.sh"
nvm install --lts
nvm alias default 'lts/*'
That pulls the current Node LTS (and npm with it). Binaries are linked into /usr/local/bin, and /etc/profile.d/nvm.sh loads nvm in interactive shells so I can still nvm install other versions later.
Bun stays for day-to-day JS speed; nvm/npm covers the ecosystem corners that still assume Node.
Helper scripts
Build and run are thin wrappers around .env:
DOCKER_CONTAINER=harianto/bun-php-laravel
./docker-build.sh # docker build -t "$DOCKER_CONTAINER" .
./docker-run.sh # interactive shell (or pass a command)
docker-run.sh mounts the project’s app/ directory at /app, publishes 80 and 443, and only adds -it when a real TTY is present (so the same script works from CI or a plain pipe).
Image tags like harianto/bun-php-laravel are valid. Docker container names are not allowed to contain /, so I do not pass --name from that value — the image tag is the source of truth.
Everyday use
cp .env.example .env # if needed
./docker-build.sh
./docker-run.sh
Inside the container:
php -v
composer -V
laravel --version
bun -v
node -v
npm -v
nvm --version
Or one-shots from the host:
./docker-run.sh php -v
./docker-run.sh laravel new example
What I would change next
- Drop the
linux/amd64pin once Herd publishes PHP 8.5 for arm64. - Optionally split “image tag” and “published ports” further in
.envif I start running more than one stack. - Add a small sample Laravel app under
app/so a fresh clone has something to hit on port 80.
Closing
harianto/bun-php-laravel is not a production runtime. It is a disposable, rebuildable workshop: Bun canary, PHP 8.5 from php.new, Laravel’s installer, and nvm-managed Node in one shell. That is exactly what I wanted on my Mac without polluting the host.