Adding a real database
BenchKit uses SQLite by default so that it runs with no setup at all. SQLite runs inside the PHP process, which means it has no connection cost and none of the indexing and contention behavior you get from a database server. If you want numbers that reflect how your application talks to a real database, use Laravel's native database drivers to point BenchKit at MySQL, MariaDB, or Postgres.
The two files
Put both files in the same directory. Compose reads .env on its own, so you write each value once and it reaches both containers:
.envholds the credentials.docker-compose.ymlhands them to BenchKit as Laravel'sDB_*variables, and to the database as the variables its own image expects.
Pick your engine and copy both files.
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=benchkit
DB_USERNAME=benchkit
DB_PASSWORD=benchkit_secret
# Used by the database container only
DB_ROOT_PASSWORD=root_secret
DB_CONNECTION=mariadb
DB_HOST=mariadb
DB_PORT=3306
DB_DATABASE=benchkit
DB_USERNAME=benchkit
DB_PASSWORD=benchkit_secret
DB_ROOT_PASSWORD=root_secret
DB_CONNECTION=pgsql
DB_HOST=postgres
DB_PORT=5432
DB_DATABASE=benchkit
DB_USERNAME=benchkit
DB_PASSWORD=benchkit_secret
Then bring both containers up:
docker compose up -d
DB_HOST is the service name from the compose file rather than localhost. Containers reach each other by service name on the Compose network, and localhost inside the BenchKit container means the BenchKit container itself.The healthcheck on depends_on is what keeps your first run honest. Database images accept connections shortly before they are ready to serve them well, and a benchmark that starts in that window measures the warmup rather than the database.
Why the connection limit is raised
Every database image ships a connection limit meant for ordinary traffic, and a load test is not ordinary traffic. Every request in flight holds one database connection, so the database needs at least as many connections as the app has workers — on a pool of a few hundred, the stock limit is reached partway through the sweep and the DB route starts failing while it is still gaining throughput.
The figure above covers the default pool with room to raise it. The rule is that it must stay above the worker count shown in the Environment panel, so if you size the pool up, size this up with it. If your results page reports that the DB read route broke at some number of connections, it says which of these limits it was.
On Postgres each connection is a separate process, so a very high limit costs memory. Size it to your worker count rather than reaching for the largest number that works.
Why the app container needs more ports
Under PHP-FPM every request on the DB route opens a connection to the database and closes it when the request ends. A closed TCP connection holds its source port for a minute afterwards, and a container has about 28,000 source ports to draw from. A host serving a few hundred DB requests a second uses them all partway up the sweep, and from there every new connection fails before it reaches the database. The route answers 503, the database's own log stays empty, and no connection limit on the database side changes it.
The two sysctls in the compose files above are the fix. tcp_tw_reuse lets the kernel hand a port that is still waiting out its minute to a new outbound connection after one second, and the wider ip_local_port_range doubles the pool. Both apply to the app container alone and need no privileges on the host.
What a connection costs on each serving model
How long a connection lives depends on how PHP is served, and it shows on the /bench/db-read route. Under PHP-FPM a connection is opened when a request starts and closed when it ends, so every request on that route pays a connection setup. In worker mode the application stays resident and its connection with it, so requests after the first pay nothing.
On MySQL and MariaDB that setup is cheap. On Postgres each new connection is a forked server process, so an FPM run against Postgres carries real connection cost on the DB route that a worker-mode run against the same database does not. That is honest — it is how an application on FPM without a connection pooler behaves — but when you compare the two variations on that route, part of the gap is connection cost rather than query speed. The other three routes never touch the database and are unaffected.
On a host without Docker
If you are running from source, there is no compose file involved. Set the same DB_* variables however your platform manages environment variables, and point them at a database it can reach.
There is no migration step
BenchKit creates the tables it benchmarks when it needs them, and drops them afterwards. You do not run artisan migrate and there is no seeder to run. Point BenchKit at a database it can reach, and the database route and the PHP stage use it on the next run.