Post

One Docker image for Bun, PHP 8.5, Laravel, and Node

Harianto van Insulinde

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:

ToolHow it gets in
BunBase image (oven/bun:canary, currently v1.4.0) — Rust runtime on JavaScriptCore
nvm + Node LTS + npmOfficial nvm install, then nvm install --lts
PHP 8.5php.new / Herd Lite for Linux
ComposerComes with php.new; also used for global packages
Laravel installercomposer global require laravel/installer
Basicscurl, 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:

bash
/bin/bash -c "$(curl -fsSL https://php.new/install/linux/8.5)"
composer global require laravel/installer

Two Docker-specific quirks showed up:

  1. clear fails without a TTY — the installer calls clear, and with set -e that aborts the build. I temporarily replace clear with /bin/true during install.
  2. 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 as linux/amd64 so 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:

bash
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:

env
DOCKER_CONTAINER=harianto/bun-php-laravel
bash
./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

bash
cp .env.example .env   # if needed
./docker-build.sh
./docker-run.sh

Inside the container:

bash
php -v
composer -V
laravel --version
bun -v
node -v
npm -v
nvm --version

Or one-shots from the host:

bash
./docker-run.sh php -v
./docker-run.sh laravel new example

What I would change next

  • Drop the linux/amd64 pin once Herd publishes PHP 8.5 for arm64.
  • Optionally split “image tag” and “published ports” further in .env if 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.