Now Docker-backed! A single containerized stack provides runtime and services (PHP-FPM, MySQL, Redis, MinIO, Mailpit) exposed to the host, while the CLI manages apps, services, and HTTPS certs.
- One Docker container, many applications.
https://<app>.dev.localhostfor every application (no more/etc/hosts).- Xdebug is trigger-only (no idling slow cost).
- Enable/disable applications without deleting them.
- Sail is per-application: each app ships its own
compose.yamland containers. That means duplicated services and rebuild time per application. - Shared container here: one runtime container group serves all apps, so no duplicate services per application.
- Sail CLI depends on containers: if an application’s container fail, Sail commands for that application is blocked, since the devEnvironment is inaccessible.
- Host tools here: Composer, PHP, and NPM run on the host. The container only provides runtime and services.
- Net: faster iteration, fewer moving parts, and no per-application Docker overhead.
2026-02-07.20-03-36.mp4
-
Docker Desktop (which contains Engine and Compose for containerization)
-
macOS (Apple Silicon + Intel)
brew install --cask docker sleep 3 open -a Docker
-
Linux (Ubuntu tested)
set -e uname -m | grep -Eq 'x86_64|amd64' grep -Eq '(vmx|svm)' /proc/cpuinfo sudo apt update sudo apt install -y ca-certificates curl gnupg qemu-kvm qemu-utils libvirt-daemon-system libvirt-clients pass gnome-terminal || true lsmod | grep -q '^kvm' || sudo modprobe kvm if grep -qi intel /proc/cpuinfo; then lsmod | grep -q '^kvm_intel' || sudo modprobe kvm_intel fi if grep -qi amd /proc/cpuinfo; then lsmod | grep -q '^kvm_amd' || sudo modprobe kvm_amd fi getent group kvm | grep -q "$USER" || sudo usermod -aG kvm "$USER" sudo install -m 0755 -d /etc/apt/keyrings if [ ! -f /etc/apt/keyrings/docker.gpg ]; then curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpg fi if [ ! -f /etc/apt/sources.list.d/docker.list ]; then echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \ | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null fi sudo apt update cd ~/Downloads [ -f docker-desktop-amd64.deb ] || wget https://desktop.docker.com/linux/main/amd64/docker-desktop-amd64.deb sudo apt install ./docker-desktop-amd64.deb systemctl --user start docker-desktop systemctl --user enable docker-desktop
-
-
Building tools
-
macOS (Homebrew + NVM)
-
Linux (Ubuntu tested)
- Composer, PHP, and some of its extensions:
sudo apt update sudo apt install -y curl php-cli unzip php-xml php-bcmath php-sqlite3 php8.3-mysql php-redis php-gd composer sudo phpenmod sockets echo "fs.inotify.max_user_watches=524288" | sudo tee /etc/sysctl.d/99-inotify.conf sudo sysctl --system
- Node.js via NVM preferably:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bash
- Composer, PHP, and some of its extensions:
-
-
Certification Tools
-
macOS
brew install mkcert nss mkcert -install
-
Linux (Ubuntu tested)
sudo apt update sudo apt install ca-certificates libnss3-tools golang-go git clone https://github.com/FiloSottile/mkcert && cd mkcert go build -ldflags "-X main.Version=$(git describe --tags)" sudo mv mkcert /usr/local/bin/ cd .. && rm -rf mkcert
-
-
Android Tools (for NativePHP Android apps)
-
macOS
brew install openjdk@17 echo 'export JAVA_HOME=$(/usr/libexec/java_home -v 17)' >> ~/.zshrc echo 'export PATH=$PATH:$JAVA_HOME/bin' >> ~/.zshrc brew install --cask android-studio
-
Linux (Ubuntu tested)
sudo apt update sudo apt install openjdk-17-jdk echo 'export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64' >> ~/.bashrc echo 'export PATH=$PATH:$JAVA_HOME/bin' >> ~/.bashrc sudo snap install android-studio --classic
-
-
iOS Tools (for NativePHP iOS apps, macOS only)
- macOS
open "macappstores://itunes.apple.com/app/id497799835" xcode-select --install || true sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer sudo xcodebuild -runFirstLaunch sudo xcodebuild -license accept
- macOS
- Clone this repo, from the new main
dockerbranch, and navigate to it. - Create
.envfrom.env.exampleand fill the values.cp .env.example .env
- On macOS, set:
HOST_HOME_PATH=/Users/$USERNAMEAPPS_ROOT=/Users/$USERNAME/Code/Laravel
- On macOS, set:
- Run the CLI:
chmod +x ./lara-stacker.sh && ./lara-stacker.sh
Applications:
List— lists folders underAPPS_ROOTand whether they’re enabledCreate— new Laravel app underAPPS_ROOT, wired to Docker servicesImport— copy an existing app intoAPPS_ROOTand wire itRefresh— reinstall deps, clear caches, rewire env (full consistency pass)Rewire— updates an application’s.envand the Vite configuration fileDelete— removes application files and its database, bucket, etc.Enable— removes.disabledmarker and serves the appDisable— adds.disabledmarker and returns 503 blocked response
Tip
The rewire command updates config only (writes host-exposed service addresses and ports). Whereas the refresh command does a full dependency reinstall on the host, clears caches, and then rewires too.
If you manually copy or git clone a project folder into APPS_ROOT (instead of using Create/Import), run Rewire for that app (or Refresh) so lara-stacker recognizes it and syncs runtime wiring for /var/www/html/<app>.
Services:
MySQL > Browse— shows all databases in the container MySQL serviceMySQL > Create— creates a new database by nameMySQL > Delete— deletes a database by name (with confirmation)MinIO > Browse— lists MinIO bucketsMinIO > Create— creates a MinIO bucketMinIO > Delete— deletes a MinIO bucketRedis > Browse— lists Redis keysRedis > Delete— deletes Redis keys by pattern
Container:
Start— boots the Docker container and prepares HTTPS certificationCheck— shows running containers in the container group (the stack)Stop— shuts down all container servicesCertify— installs local HTTPS certs/trust (via mkcert)- Requires
sudoaccess once, in order to write to the system trust store - Requires restarting the browser, in order to pick up the new trust
- Requires
Purge— removes the container and all of its resources (containers, images, volumes, networks, build cache; everything!)
Edit .env (same order as the file):
-
Host
USERNAME— system user that owns application filesDB_PASSWORD— root password for the MySQL container imageHOST_HOME_PATH— host home path mounted into the app container (read-only) to support Composer path-repository symlinks that resolve outsideAPPS_ROOT(for example local packages under~/Code/LaravelPackages).- Linux typical value:
/home/<user> - macOS typical value:
/Users/<user>
- Linux typical value:
APPS_ROOT(default/var/www/html) — host directory where applications live and coded from, locally!- Linux typical value:
/home/<user>/Code/Laravel - macOS typical value:
/Users/<user>/Code/Laravel - The CLI will create
APPS_ROOTif missing and make it owned byUSERNAME.
- Linux typical value:
OPINIONATED— copy opinionated application files (Prettier config)USE_VSC— whentrue,Create,Import,Refresh, andRewiregenerate each app's.vscode/launch.json(plus.vscode/extensions.jsonif missing) for xdebug with VSCodium/VS Code.- Xdebug is run in "trigger-only" mode (
xdebug.start_with_request=trigger). - Install the
xdebug.php-debugextension for VSC. - Use the browser extension and keep it on only when needed.
- Xdebug is run in "trigger-only" mode (
VSC_WORKSPACES_DIR— auto-create.code-workspacefiles (leave empty to disable;nullis also treated as disabled)
-
Container
DOCKER_COMPOSE_FILE— override the compose file pathRESTART_UNLESS_STOPPED— whether to start the container automatically along Docker.PHP_VERSION— changing this triggers a rebuild on nextStart ContainerAPT_MIRROR— Debian main mirror (HTTPS)APT_SECURITY_MIRROR— Debian security mirror (HTTPS)CADDY_HTTP_PORT/CADDY_HTTPS_PORT— host ports for Caddy (it's recommended to use80/443if free)MYSQL_PORT— host port for MySQL (container3306)REDIS_PORT— host port for Redis (container6379)MAILPIT_SMTP_PORT/MAILPIT_UI_PORT— host ports for Mailpit SMTP/UI (container1025/8025)MINIO_PORT/MINIO_CONSOLE_PORT— host ports for MinIO API/Console (container9000/9001)
-
The rewire command writes these host port values into each application’s
.env(withDB_HOST=127.0.0.1,REDIS_HOST=127.0.0.1, etc.). -
After changing any host port variables, restart the container and run the rewire command to refresh each app’s
.env.
Tip
APT_MIRROR and APT_SECURITY_MIRROR are used inside the Debian app container image build (apt-get in Docker), not on the macOS host.
To compare mirrors from Docker Desktop, time the app image build:
time docker compose --env-file ./.env -f ./configurations/compose.yaml --project-name lara-stacker build --no-cache appIf you see xcrun: error: unable to find utility "simctl", not a developer tool or in PATH on macOS, install full Xcode, then run:
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -runFirstLaunchThis container is isolated from host installs (v4-style). Conflicts only happen if a host service already uses these same ports:
- Caddy:
CADDY_HTTP_PORT/CADDY_HTTPS_PORT(default8080/8443, container80/443) - MySQL:
MYSQL_PORT(default3307, container3306) - Redis:
REDIS_PORT(default6380, container6379) - Mailpit SMTP/UI:
MAILPIT_SMTP_PORT/MAILPIT_UI_PORT(default1026/8026, container1025/8025) - MinIO API/Console:
MINIO_PORT/MINIO_CONSOLE_PORT(default9100/9101, container9000/9001)
Service UIs include:
https://minio.dev.localhost:8443https://mailpit.dev.localhost:8443
Note
It's extremely recommended to use 80/443 ports with Caddy. I only made them different by default in order not to conflict with lara-stacker v4. Check the .env file.
- The app container has Xdebug installed and configured via
configurations/xdebug.ini:xdebug.client_host=host.docker.internalxdebug.client_port=9003xdebug.start_with_request=trigger
- Container path mappings are generated as
/var/www/html/<app> -> ${workspaceFolder}so imported/created apps map correctly during debug sessions. - If you enable
USE_VSCafter apps already exist, run Rewire (or Refresh) once per app to generate/update debug files. - For browser debugging: start "Listen for Xdebug" in VS Code/VSCodium, turn on the Xdebug browser trigger, and hit the app URL.
- The container does install and expose the main services (Caddy, MySQL, Redis, MinIO, etc.) ports for you, does runtime stuff in place (PHP, PHP Extensions, PHP-FPM, etc.) too, and finally includes whatever extra packages the local server may need, such as the media's (ImageMagick, Ghostscript, FFmpeg, etc.).
- Application
.envfiles are host-wired (127.0.0.1+ host ports) because all dev tooling (Composer/PHP/Artisan/NPM) runs on the host. - The app container is injected with internal service hosts so runtime still connects to MySQL/Redis/MinIO when serving requests.
- The app container also mounts
HOST_HOME_PATHread-only to keep local Composer path-repository symlinks resolvable at runtime (preventing missing vendor class/provider errors when a dependency points outsideAPPS_ROOT). - The container does NOT contain the development tools themselves that need to exist locally. This includes Java/Android tooling and Xcode/iOS tooling, etc.
TLDR: Docker provides the runtime container group, but there are essential prerequisites that must be installed on the host for ever so many reasons really... All in all, the CLI will DISFUNCTION if those tools are missing.
- Inside the container, applications are always mounted at
/var/www/html(Caddy/PHP-FPM depend on this). - Applications can be disabled via the CLI. This creates a
.disabledfile, and Caddy responds with 503 while keeping files intact. - You can access an application using:
https://<app>.dev.localhost:8443(orhttps://<app>.dev.localhostifCADDY_HTTPS_PORT=443) - Vite HMR is exposed via
https://vite-<app>.dev.localhost:8443. Run on host:cd <app> && npm run dev - mkcert installs the "trust" into the system store, so make sure it's installed back in prerequisites section, of course.
- Certs are generated into
./.certs(which isn't version controlled) and Caddy is restarted to use them from there. DO NOT REMOVE THEM. - There is a small
synchronizersidecar that continuously corrects a few host/container drift issues. It removes stalepublic/hotfiles when Vite is no longer reachable, and it also reloads PHP-FPM when a Composer autoload desync starts surfacing as avendor/composer/autoload_static.phpparse error through Caddy.
Important
This stack uses PHP-FPM (with OPcache), which is fast but can sometimes make changes appear “stuck” due to caching configs, routes, views, or bytecode. If something behaves oddly after a change, this Artisan command should fix it: php artisan optimize:clear.
Support ongoing package maintenance as well as the development of other projects through sponsorship or one-time donations.
- ChatGPT and Codex CLI
- Laravel
- Spatie
- Active Contributors
- All the technologies used to set up this whole development environment, and eventually the apps... Perhaps the browsers too?! -Please help!
والحمد لله رب العالمين