laserstack

Numbers and method

What it costs, measured.

Marketing pages usually say "lightweight". Here is the arithmetic instead: a default install is two containers, 607 MB of images, about 113 MiB of memory, and it serves every project of your projects directory. Every figure below comes from a command you can run on your own machine.

The machine and the versions

MachineApple M3, 8 cores, 16 GB, macOS 27.0.1 (arm64)
DockerDocker 29.8.2 with Compose v2 (5.7.0), Linux containers, no VM of this project involved
Laser Stackrelease 0.13.0 when measured; later releases change documentation, defaults and volume names, not the measured behaviour. Images and data volumes present — the state you are in from the second day
Measured2026-11-05, with docker images, docker stats --no-stream, time/date around laser commands, and curl

The cost of a stack

2containers for app and database — whatever the number of projects
607 MBimages of a default install (app 8.2 + mariadb 10.11)
113 MiBidle memory of both containers, after 60 s of idling
1.0 sfrom laser up -d to the database answering
3 sto nginx and php-fpm serving again
26 msa Laravel 12 request, steady state

Numbers 1 and 2 come from the same stack: seventeen Laravel projects in the projects directory, one app container, one mariadb container.

Images, as docker reports them

Size on disk, arm64. The app image carries nginx, php-fpm, composer, node, npm, yarn, phpunit, xdebug and the mysql and postgres clients, so there is nothing else to install for a project.

imageversionssize
laserstack/app7.4 (272 MB), 8.0 (244 MB), 8.1 (327 MB), 8.2 (297 MB), 8.3 (302 MB), 8.4 (305 MB), 8.5 (314 MB), 8.6 (325 MB)244–327 MB
mysql8.0 (795 MB), 8.4 (809 MB), 9.7 (954 MB), 26.10 (776 MB)776–954 MB
mariadb10.11 (310 MB), 11.4 (355 MB), 11.8 (357 MB), 12.3 (361 MB), 13.0 (386 MB)310–386 MB
postgres15.1 (241 MB), 16 (288 MB), 17 (291 MB), 18 (298 MB) — the -alpine series241–298 MB
redis7.4 (39.5 MB), 8.2 (70.1 MB), 8.4 (95.9 MB), 8.6 (97.2 MB), 8.8 (115 MB), 8.10 (119 MB)40–119 MB
memcached1.6-alpine (13.6 MB)13.6 MB
elasticsearch7.14.2 (1.12 GB), 8.19.23 (992 MB), 9.5.5 (1.2 GB)992 MB–1.2 GB

Two of these are worth a sentence: mariadb is the lighter SQL server (310 MB against 795 MB for mysql 8.0, both with the data directory empty), and elasticsearch is the one heavy profile — everything else in the stack fits in a few hundred megabytes.

Idle memory

docker stats --no-stream, after the stack was up and then left alone for 60 s:

containeridle memorynote
laserstack-app-1 (laserstack/app:8.2)35.5 MiBnginx and php-fpm, one idle worker
laserstack-mariadb-1 (mariadb:10.11)77.9 MiBInnoDB pool allocated lazily
laserstack-postgres-1 (postgres:15.1-alpine)16.2 MiBright after start, no queries yet
laserstack-redis-1 (redis:8.10-alpine)6.5 MiBempty dataset
laserstack-memcached-1 (memcached:1.6-alpine)1.3 MiBempty cache
mysql:8.0375.3 MiBmeasured alone, with the config directory of the repository mounted
elasticsearch:7.14.26.4 GiB60 s after the HTTP API answered: the JVM plus the memory-mapped index files, as docker counts them

So the default install — app and mariadb, serving every project — idles at about 113 MiB. Add redis and memcached for under 8 MiB more. MySQL 8.0 costs roughly five times what MariaDB costs in memory (375 MiB against 78 MiB, same configuration), which is worth knowing before you pick the mysql profile over mariadb. Elasticsearch is the exception in every stack, and here it is the only container that costs gigabytes: 6.4 GiB in docker stats, with the JVM and the memory-mapped index files counted against it, and tens of seconds of start-up.

Time to start

what is measuredtimemethod
laser up -d until the database answers a query1.0 smariadb profile, data volume present
laser up -d until nginx and php-fpm are RUNNING2.9 sapp + mariadb, containers recreated by the command
the same, with mysql + redis + memcached2.8 sfour services up at once
the same, with postgres2.1 s
the same, with elasticsearch3.6 sthe app container is up long before elasticsearch is: its API needs tens of seconds
laser start of stopped containers3.7 sno recreation, containers already exist

These are warm numbers: the images are pulled and the data volumes exist. A first run also initializes the data directory of a database, which is where the extra seconds of a brand-new install go.

Serving projects

whatfigurehow
projects served by the same two containers17find "$PROJECTS_DIRECTORY" -maxdepth 2 -name artisan
laser scan over those projects2.3 sincluding hashing the vhosts to decide about an nginx restart
first response of a plain PHP project8.1 mscurl from the host through nginx and php-fpm, 1-file project, first request
second response, same project5.6 ms
Laravel 12 /up route, first request after start492 msframework boot and opcode cache warm-up
Laravel 12 /up route, steady state25–31 msfour consecutive requests: 31.4, 26.1, 26.0, 25.3 ms

What these numbers are not

  • Not a comparison. A fair one compares whole workflows on the same machine: how much of your disk a VM takes, how long a per-project container set needs to come up, what a file-sync daemon costs in CPU. Those are other people's numbers and they are on their sites;
  • Not cold starts. Pulling 600 MB of images takes as long as your connection does; a first database start also writes its data directory;
  • One machine, one run. An M3 laptop with fast storage. Memory of a database grows with its workload, and docker stats memory includes page cache;
  • Not a promise about your project. 26 ms is a Laravel boot plus a health route. Your application's response time is your application's business.

Reproduce it

With a running stack, from the repository:

# images
docker images --format '{{.Repository}}:{{.Tag}} {{.Size}}' | sort

# idle memory, after a minute of doing nothing
docker stats --no-stream --format '{{.Name}} {{.MemUsage}}'

# time from a stopped stack to a database that answers
laser stop
time ( laser up -d; until laser mysql -e 'select 1' >/dev/null 2>&1; do sleep 0.5; done )

# projects served, and how long the scan takes
find "$PROJECTS_DIRECTORY" -maxdepth 2 -name artisan | wc -l
time laser scan

# response time through nginx and php-fpm
curl -sS -o /dev/null -w '%{http_code} %{time_total}s\n' -H "Host: myapp.$SUFFIX" http://127.0.0.1/

If your numbers differ, they are the ones that matter: this page exists so that nobody has to take a marketing claim on faith.