laserstack

Documentation

Everything you need to run it.

This page is the practical reference. The repository holds the deep documentation: the README for users and CONTRIBUTING.md for the internals, the tests and the release process.

Requirements

  • Docker with Compose v2 (docker compose or docker-compose). That is the only heavy dependency;
  • git and bash on the host. No PHP, no Node, no database client, no package from a language ecosystem — the containers carry those;
  • macOS (Intel or Apple silicon), Linux, or Windows through WSL2/WSLg — on WSL keep the projects inside the Linux filesystem (~/src), not under /mnt/c, and the bind mounts stay fast;
  • a directory where your projects live (this is what the stack mounts and serves), and the stack cloned next to it.

Install

curl -fsSL https://localdev.it/laserstack/install | bash
# or clone it yourself; the installer only does these steps for you:
git clone https://github.com/sdbruder/laserstack ~/src/laserstack
cd ~/src/laserstack
cp env.example .env          # laser also copies it on first use
# edit .env: PROJECTS_DIRECTORY, PHP_VERSION, PROFILES, credentials, SUFFIX
laser up -d
laser scan
laser dashboard              # the stack in your browser
laser help                   # the commands; laser dockerhelp for the compose ones

Make laser reachable from your projects: an alias or a $PATH entry to the cloned directory. laser always works from its own directory, so a project can call laser artisan migrate from anywhere.

The .env reference

variabledefault in env.examplewhat it does
PROJECTS_DIRECTORY./..the directory bind-mounted into the app container at /var/www and scanned for projects
SUFFIXlocaldev.itthe domain suffix of a project URL: projecta.localdev.it. Every name under localdev.it resolves to 127.0.0.1 and ::1 for anyone, so the default needs no setup at all; a suffix of your own, test for instance, needs the names in /etc/hosts
PHP_VERSION8.4the app image series, laserstack/app:<version>: 7.4, 8.0–8.6
MYSQL_VERSION8.0the mysql profile image and its config directory
MARIADB_VERSION10.11the mariadb profile image and its config file
POSTGRES_VERSION15.1the postgres profile image (-alpine is appended) and its config directory
REDIS_VERSION8.10the redis profile image and redis.conf
MEMCACHED_VERSION1.6the memcached profile image
ELASTICSEARCH_VERSION7.14.2the elasticsearch profile image and elasticsearch.yml
DB_DATABASE, DB_USERNAME, DB_PASSWORD, DB_ROOT_PASSWORDlaser, laser, laser, rootthe database and the accounts the mysql profile creates
MARIADB_*same shapethe same for the mariadb profile: database, user, password, root password
POSTGRES_PASSWORDpostgresthe postgres password. POSTGRES_USER, POSTGRES_DB, POSTGRES_INITDB_ARGS, POSTGRES_HOST_AUTH_METHOD are also read when set
PROFILESmysql,redisthe additional containers to run, comma separated: mysql, mariadb, postgres, redis, memcached, elasticsearch

The versions each engine supports are on the features page. A variable that names a version without a config directory gives you docker's own "bind source path does not exist" error: there is no validation layer in between.

Which container answers to which name

Inside the docker network, a project talks to the services by name; from the host, by the published port:

servicename inside the networkhost port
mysqldb3306
mariadbmariadb and the alias db3306
postgrespostgres5432
redisredis6379
memcachedmemcached11211
elasticsearchelasticsearch9200 and 9300

Because the mariadb service carries the alias db, a project whose .env says DB_HOST=db works with the mysql profile and with the mariadb profile. Run one of them at a time: they both want port 3306.

Serving your projects

  • Every subdirectory of PROJECTS_DIRECTORY with a public/ directory is a candidate site; laser scan decides what each one is and writes its vhost;
  • the drivers are laravel, symfony, wordpress and generic. The generic driver installs a laserstack-nginx.conf from the project root as the vhost of that project;
  • the name a request must use is <directory>.<SUFFIX>, so projecta.localdev.it for a directory called projecta with the default suffix;
  • those names must resolve to 127.0.0.1, and the default suffix needs nothing: env.example sets SUFFIX=localdev.it. Every name under localdev.it already resolves to 127.0.0.1 (and ::1) for anyone, so projecta.localdev.it works on this machine the moment the stack is up — no hosts file, no DNS server, nothing to install. The notes behind that domain are at localdev.it/catch-all-dns.html;
  • with a suffix of your own, add the names to /etc/hosts or point a local DNS at them. Installing and maintaining that DNS is on the roadmap;
  • projects live at /var/www/<directory> in the app container, so a project's own paths, relative URLs and artisan invocations stay ordinary;
  • after adding or removing a project: laser scan. It restarts nginx only when the vhosts really changed;
  • after changing SUFFIX: laser up -d first, so the containers read the new value, and then laser scan for the vhosts. Until the containers are recreated the stack keeps answering on the old suffix.

Working inside a project

Run these from the project directory; the current directory decides which project the command applies to:

cd ~/src/myapp
laser artisan migrate
laser composer require laravel/sanctum
laser npm run dev
laser test            # php artisan test
laser tinker
laser shell           # bash in the app container, in this project

# from the repository root, laser appexec talks to the container itself:
laser appexec supervisorctl status

The images carry composer, node, npm, yarn, phpunit, xdebug and the mysql and postgres clients, so the tool version of a project is the tool version of its PHP series — the same for everyone on the team.

Databases

  • laser mysql and laser psql open the client of the database container of the active profile, with the credentials of .env, and pass extra arguments through: laser mysql -e "select version()", laser psql -c '\dt';
  • from the host, connect to 127.0.0.1 on the published port (3306, 5432, 6379, 11211, 9200) with the credentials in .env — for a GUI client, a dump/restore or a script;
  • a project reaches its database by name: DB_HOST=db for mysql and mariadb, DB_HOST=postgres, REDIS_HOST=redis. Those host names are what laser scan and the drivers assume;
  • the elasticsearch profile runs with security disabled (xpack.security.enabled: false) so a local application can talk to it without certificates;
  • each database version has its own data volume (<project>_dbdata_<version>, and the same shape for mariadb and postgres), so changing a version keeps every other version's data and needs no laser down -v. A data directory is still readable only by the server version that created it, which is why the version is part of the volume name;
  • the app image's mysql client is built on MariaDB Connector/C and cannot authenticate against MySQL 8.4 and newer, whose accounts use caching_sha2_password. Use laser mysql, which runs the client of the database container.

Testing your setup

test/smoke.sh starts the stack for real and checks it: nginx and php-fpm, the php.ini of the repository, an HTTP response, and the PDO path from the app container to the database. It runs once per PHP version and once per database profile, and a version variable drives it:

test/smoke.sh                                   # every version, every profile
test/smoke.sh 8.4 8.5                           # only these PHP versions
SMOKE_DB_PROFILES=mariadb test/smoke.sh 8.2     # one database profile
SMOKE_MYSQL_VERSION=9.7 SMOKE_DB_PROFILES=mysql test/smoke.sh 8.5
SMOKE_PULL=0 test/smoke.sh 8.4                  # use the local images

The test stops the stack at the end and leaves .env as it was. It is also the fastest way to find out whether a problem is yours or the stack's.

Staying up to date

The images of the PHP series are rebuilt every night in laserstack_app, so a series tag such as laserstack/app:8.4 carries the newest 8.4 patch available. Taking what is new is one command:

laser update --check    # fetch the code and the images, change nothing
laser update            # the same, then restart the containers whose image changed
  • it fast-forwards the checkout first (git pull --ff-only, when the directory is a git working tree). An edit of yours to a tracked file stops that pull and it says so, instead of merging behind your back; .env and untracked files are never touched;
  • then it pulls every image of the active profiles, the app image included, and prints the PHP version the image carries next to the one the container is running;
  • then it brings the stack up, which recreates exactly the containers whose image changed, and prints the version that is now running;
  • a new PHP series is the one manual step: a version directory in laserstack_app plus config/php<VV> and config/supervisord.<VV> here.

Known limits

  • one PHP version per stack. Every project of a stack shares the app container. Per-project PHP versions are planned;
  • one version per database engine. mysql, mariadb, postgres, redis, memcached and elasticsearch can run side by side, but each engine runs the version you selected;
  • a suffix of your own needs /etc/hosts. The default suffix, localdev.it, resolves every name under it to loopback for anyone, so it needs nothing at all; a local dnsmasq that laser installs and maintains, for a suffix of your own, is planned;
  • memcached is a cache store from 7.4 to 8.5: the images carry the memcached extension there. PHP 8.6 is a release candidate and has neither an Alpine package nor a pecl build of the extension yet, so that series has the server without the client;
  • no Windows-native support: use WSL2. macOS and Linux run as they are;
  • no environment validation: by design. A missing image, an unknown config path or a stopped container gives you docker's own error.

Read more

  • README.md — the user documentation in the repository;
  • CONTRIBUTING.md — how the stack is put together, how to add a version, how to run the tests, how to release;
  • CHANGELOG.md — what changed in every release, newest first;
  • laser help and laser dockerhelp — the command list, from the script itself.