laserstack

the localdev.it stack · macOS · Linux · WSL2 · docker-compose · one shell script · BSD-3-Clause

Many PHP projects.
One small stack.

Laser Stack serves all of your Laravel, Symfony and WordPress projects from a single set of Alpine containers — one nginx, one PHP-FPM, one database server — with the PHP and database versions chosen by a variable. No VM, no per-project containers, no dashboard application to install.

https on every project, by default — a real wildcard certificate your browser already trusts, so there is no CA to install, no warning page and no -k in your tests. How it works.

curl -fsSL https://localdev.it/laserstack/install | bash

Runs on macOS, Linux and WSL2/WSLg — Intel and Apple silicon, any docker with compose v2 (Docker Desktop, OrbStack, Colima, the engine). It installs nothing on the host: no VM, no system package, no daemon of its own.

Terminal recording: laser up -d starts two containers, laser scan identifies every project of the projects directory, one project answers 200, and the same two containers serve them all

https, out of the box

Every project answers on 443 as well as on 80, from a wildcard certificate a browser already trusts — no CA to install, no warning page, no browser exception.

Nothing to install, nothing to trust

No mkcert step, no CA in the keychain, no re-trust after an update. The certificate is a real one, it covers every name under the suffix — including the project you create tomorrow — and HTTPS=off is one line if you want plain http back.

A secure context, because it is not localhost

Service workers, Secure cookies, SameSite=None, navigator.clipboard, WebAuthn and camera or microphone permissions all require a secure context. projecta.localdev.it is not localhost, so over http they refuse — here they work, on 443, by default.

What your tools and your CI expect

curl, Postman, PHP, node and the browser validate the chain normally: no -k, no --insecure, no exception to remember. OAuth, SAML and payment callbacks that demand an https redirect URI work locally without a tunnel.

How https works here, and what a certificate of your own suffix takes →

a first run, end to end
$ curl -fsSL https://localdev.it/laserstack/install | bash
  bash 5.3.20   git 2.56.0   docker 29.9.2   compose 5.7.0
  system macOS, arm64
  wrote .env from env.example in ~/src/laserstack
$ cd ~/src/laserstack && laser up -d
Container laserstack-app-1     Started
Container laserstack-db-1      Started
Container laserstack-redis-1   Started
$ laser scan
SCAN: /Users/you/src/projecta identified as laravel.
SCAN: /Users/you/src/projectb identified as wordpress.
$ laser ps
NAME                  IMAGE                 STATUS         PORTS
laserstack-app-1      laserstack/app:8.4    Up 4 seconds   0.0.0.0:80->80/tcp
laserstack-db-1       mysql:8.0             Up 4 seconds   0.0.0.0:3306->3306/tcp
laserstack-redis-1    redis:8.10-alpine     Up 4 seconds   0.0.0.0:6379->6379/tcp
$ # http://projecta.localdev.it and http://projectb.localdev.it are both served

What it costs

Measured on a MacBook (Docker 29, arm64), warm images and data volumes — the state you are in from the second day. The method is on the numbers page.

2containers serving every project of the projects directory
113 MiBidle memory for the app and the database together
2.9 sfrom laser up -d to nginx and php-fpm serving
297 MBthe PHP 8.2 app image, 244–327 MB across 7.4–8.6
7.4 – 8.6PHP versions, one variable away
nightlythe app images are rebuilt in CI, newest PHP patch included
6database engines, each with selectable versions

Built for a small footprint

The whole stack is a compose file, a config directory and a shell script you can read in an afternoon.

Many projects, one stack

One nginx with a vhost per project, rendered by laser scan from the projects directory. Laravel, Symfony and WordPress drivers are included, and any project can ship its own nginx config in laserstack-nginx.conf.

Versions are variables

PHP_VERSION, MYSQL_VERSION, MARIADB_VERSION, POSTGRES_VERSION, REDIS_VERSION, MEMCACHED_VERSION, ELASTICSEARCH_VERSION. Each one selects the image tag and the matching config directory, so two installs can run different versions from the same repository.

Config lives in git

php.ini, php-fpm, supervisord, nginx and every database config are files of this repository, one directory per version. Change a value, restart one service, keep the change in a commit.

Alpine, no VM

The images are Alpine-based and pulled from public registries. There is no virtual machine, no custom orchestrator and no daemon: docker-compose does the work.

A CLI that stays out of the way

laser exposes every docker-compose command unchanged and adds the ones you type all day: artisan, composer, php, npm, shell, mysql, psql, share, scan.

Tested, not vibes

test/smoke.sh starts the stack once per PHP version and once per database profile and checks nginx, php-fpm, the php.ini of the repository, an HTTP response, an https response with a trusted certificate, and the PDO path from the app container to the server. The 0.13.0 release was verified against every version of every engine; 0.14.0 was verified on PHP 8.4 with mysql 8.0, mariadb 10.11 and postgres 15.1.

Always up to date

The app images are rebuilt every night, so laserstack/app:8.4 always carries the newest PHP 8.4 patch and a newer series appears as its own tag. laser update pulls the newest code and images and restarts only what changed.

Project names with no DNS setup

localdev.it is the default suffix, so every project answers as projecta.localdev.it: every name under that domain already resolves to 127.0.0.1 and ::1 for anyone. No hosts file, no dnsmasq, nothing to install.

A dashboard that describes the stack

The checkout is one of the projects, so the stack serves a read-only dashboard for itself on laserstack.<SUFFIX>: the PHP series and its config directory, every project with the driver laser scan found and the code its front page answered, the services, the database version, the certificate it serves with the days left, and the head of the changelog. It writes nothing and prints no credential. See what it shows.

No sshd, so nothing to log into

The containers run nginx and php-fpm under supervisord and nothing else: no sshd and no ssh client, in any image. That is why the OpenSSH-on-musl authentication bypass (CVE-2026-123158, fixed upstream in 10.6p2 on 2026-11-05) has no target here, and why the images are rebuilt every night from Alpine rather than patched by hand.

No magic, no lock-in

Nothing rewrites your compose file and nothing guesses. When a container is missing you get docker's own error, which is the error you can act on.

macOS, Linux, WSL2

Official support for macOS (Intel and Apple silicon), Linux and WSL2/WSLg, with Docker Desktop, OrbStack, Colima or the docker engine. The projects directory is bind-mounted, so your editor keeps working on your own files.

Quick start

Three commands and a hosts entry. The clone belongs next to your projects, because that directory is what the stack mounts and serves.

Clone it next to your projects

git clone https://github.com/sdbruder/laserstack ~/src/laserstack
alias laser=~/src/laserstack/laser        # or add it to $PATH

Write your .env

cd ~/src/laserstack && cp env.example .env
# PHP_VERSION=8.4
# PROFILES=mysql,redis
# DB_PASSWORD=…  MYSQL_VERSION=8.0  REDIS_VERSION=8.10
# SUFFIX=localdev.it   # the default, no setup: projecta.localdev.it

Start it and scan

laser up -d
laser scan          # writes one nginx vhost per project
# nothing else: http://projecta.localdev.it and http://projectb.localdev.it answer
# with a suffix of your own (SUFFIX=test): 127.0.0.1 projecta.test projectb.test

Day to day: laser artisan migrate, laser npm run dev, laser mysql, laser shell, laser share, laser stop. Anything else goes straight to docker-compose: laser logs app --tail 50.

What it does not do (yet)

The stack is opinionated and honest about the edges. Everything below is on the roadmap or documented as a limit.

One PHP version at a time

All projects of one stack share the app container, so they share its PHP version. Per-project PHP versions, one container per version in use, are the next feature.

One version per database engine

You can run mysql and postgres and redis side by side, but each engine runs the version you selected. Several versions of the same engine in parallel is part of the same roadmap item.

A suffix of your own needs /etc/hosts

The default suffix, localdev.it, resolves every name under it to loopback for anyone and needs nothing. A local dnsmasq that laser installs, configures and maintains on macOS, Linux and WSL2 is planned for a suffix of your own.

No GUI, no native Windows

The CLI is the interface, and Windows runs through WSL2; macOS and Linux run as they are. Per-project PHP versions are planned, a desktop app is not.

Where to go next

Features in detail →

What the drivers, the profiles, the versions and the CLI actually do.

How it compares →

Valet, Herd, Sail, DDEV, Lando and Laradock, on the axis that decides the bill: one stack for all projects, or one per project.

Numbers and method →

Image sizes, idle memory, startup time, and how each figure was produced.

Documentation →

The .env reference, the profiles, the versions and the laser commands, with links to the repository docs.

Examples →

Copy-paste recipes: many projects, a custom vhost, another database, another database version, artisan and npm.