You have a Laravel API and a React frontend. Add one file and your team never fights dependency versions again.
Note: Ignore the quality of the PHP and React code, this post is focused on Nix and creating a simple environment for your development team.
How This Started
A friend of mine works with Symfony for a local government office. His team is growing fast, they went from 10 to 73 developers in a few months, and onboarding has become a nightmare. Every new hire spends their first day fighting PHP versions, MySQL configs, and missing extensions.
He uses Docker for development and it works fine. But he was curious if there was something lighter for local dev environments, especially for smaller projects or quick onboarding, without having to maintain Dockerfiles and docker-compose configs.
He called me one evening, "do you have 5 minutes?" We caught up, had some good laughs, but at the end of the call I suggested he look at Nix.
Nix is a package manager that guarantees reproducibility. You declare what your project needs in a single file, commit it, and anyone who clones your repo gets the exact same environment, same PHP, same Node, same MySQL, same everything. No containers, no VMs. Just your terminal with the right tools available. And it works with any language or framework, Python, Ruby, Go, you name it.
# Outside nix develop
$ php --version # whatever your system has (or nothing)
# Inside nix develop
$ php --version # PHP 8.3.x, exactly what the file says
$ node --version # v22.x.x
$ mysql --version # mysql Ver 8.4.x
$ exit # back to normal, nothing changed
I told him I'd write a post showing exactly how to do it. So here we are. I'm using Laravel for this tutorial, but everything you'll see here applies to any framework.
Installing Nix
Nix runs on macOS, Linux, and WSL2. The recommended installer is from Determinate Systems, it's simpler than the official one and enables Flakes by default:
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install
Restart your terminal after installing, then verify:
nix --version
# nix (Determinate Nix) 2.33.x
nix flake --help
# Should show flake commands
If
nix flake --helpfails, addexperimental-features = nix-command flakesto~/.config/nix/nix.conf.
WSL2 users: Keep your projects inside the Linux filesystem (~/projects/), not /mnt/c/. Performance is much better.
That's it. One install, and you're ready for any Nix-managed project, forever.
The Sample Project
For this post I created a simple backend with a product CRUD in Laravel and a React frontend that lists those products. The code was generated with AI, ignore it, our focus is Nix.
You can find the full source code here: github.com/rodrigonbarreto/nix_blog_post
The structure looks like this:
workspace/
├── product-api/ ← Laravel API (PHP + MySQL)
├── product-frontend/ ← React list (Node)
Both projects work fine with my local PHP 8.3, Node, and MySQL. The question is: how do I make sure the next person who clones this doesn't spend half a day setting things up?
The Problem
Your project README probably looks something like this:
## Setup
1. Install PHP 8.3
2. Install Composer
3. Install Node 22
4. Install MySQL
5. Copy .env.example to .env
6. Run composer install
7. Run npm install
8. Create the database
9. Run migrations
10. Pray it works
Every new developer spends half a day on this. Someone has PHP 8.1, someone else has MySQL 5.7, and the intern can't compile the pdo_mysql extension.
Nix replaces all of that with one command: nix develop.
The Solution: One File
We add a single file, flake.nix, next to our projects:
workspace/
├── product-api/
├── product-frontend/
└── flake.nix ← this is all we add
Here's the file:
{
description = "Dev environment, PHP, Node, MySQL";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
php = pkgs.php83.buildEnv {
# All extensions Laravel needs out of the box
extensions = { all, ... }: with all; [
mbstring
iconv
pdo_mysql
mysqlnd
openssl
tokenizer
xmlwriter
xmlreader
ctype
fileinfo
bcmath
curl
dom
filter
pdo
session
simplexml
zip
];
};
in
{
devShells.default = pkgs.mkShell {
buildInputs = [
php
php.packages.composer
pkgs.nodejs_22
pkgs.mariadb
pkgs.git
pkgs.curl
pkgs.unzip
];
shellHook = ''
# Prevent tools like Laravel Herd from hijacking PHP config
unset PHP_INI_SCAN_DIR
unset PHPRC
export MYSQL_HOME="$PWD/.nix-mysql"
export MYSQL_DATADIR="$MYSQL_HOME/data"
export MYSQL_UNIX_PORT="$MYSQL_HOME/mysql.sock"
export MYSQL_TCP_PORT="3307"
if [ ! -d "$MYSQL_DATADIR" ]; then
echo "Initializing MySQL..."
mkdir -p "$MYSQL_HOME"
mysql_install_db --datadir="$MYSQL_DATADIR" --auth-root-authentication-method=normal > /dev/null 2>&1
fi
echo ""
echo "=========================================="
echo " Dev Environment Ready"
echo "=========================================="
echo " PHP: $(php -r 'echo PHP_VERSION;')"
echo " Node: $(node --version)"
echo " MySQL: port $MYSQL_TCP_PORT"
echo "=========================================="
echo ""
echo " Start MySQL:"
echo " mysqld --datadir=\$MYSQL_DATADIR --socket=\$MYSQL_UNIX_PORT --port=\$MYSQL_TCP_PORT &"
echo ""
'';
};
}
);
}
That's it. One file.
What's Happening Here?
flake.nix
│
├── inputs → where packages come from (pinned to nixos-24.11)
│
└── outputs
└── devShell
├── buildInputs → packages: PHP, Node, MySQL, etc.
└── shellHook → bash that runs on entry (sets up local MySQL)
A few things worth noting:
-
nixos-24.11pins the package versions. Everyone with this file gets the same PHP, same Node, same MySQL. Always. -
php83.buildEnvbuilds PHP with the exact extensions your project needs. The base PHP in Nix is minimal, you declare what you need explicitly. -
unset PHP_INI_SCAN_DIRprevents tools like Laravel Herd from injecting their own PHP config and overriding the Nix extensions. -
mariadbis used instead of MySQL because it's easier to manage locally with Nix and is fully MySQL-compatible. Your Laravel app won't notice the difference. -
Port
3307avoids conflicts if you already have MySQL running on 3306. -
shellHookinitializes a MySQL data directory inside the project. Nothing touches your system.
Let's Use It
Enter the shell
cd workspace
nix develop
First time takes a few minutes (downloads packages). After that, it's instant.
You'll see:
==========================================
Dev Environment Ready
==========================================
PHP: 8.3.11
Node: v22.9.0
MySQL: port 3307
==========================================
Everything from this point runs inside nix develop.
Start MySQL
mysqld --datadir=$MYSQL_DATADIR --socket=$MYSQL_UNIX_PORT --port=$MYSQL_TCP_PORT &
Wait a couple of seconds, then create the database:
mysql -u root --socket=$MYSQL_UNIX_PORT -e "CREATE DATABASE IF NOT EXISTS product_api;"
Run the backend
Update product-api/.env to use port 3307:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3307
DB_DATABASE=product_api
DB_USERNAME=root
DB_PASSWORD=
Then:
cd product-api
composer install
php artisan migrate
php artisan db:seed
php artisan serve
If you previously ran
composer installwith a different PHP, deletevendor/first:rm -rf vendor && composer install.
Run the frontend
Open a new terminal, same directory:
cd workspace
nix develop
cd product-frontend
npm install
npm run dev
Open http://localhost:5173, your product list is running, powered entirely by Nix.
The Day-to-Day
Every time you work on the project:
cd workspace
nix develop
# Start MySQL
mysqld --datadir=$MYSQL_DATADIR --socket=$MYSQL_UNIX_PORT --port=$MYSQL_TCP_PORT &
# Terminal 1: backend
cd product-api && php artisan serve
# Terminal 2 (new terminal, nix develop first): frontend
cd product-frontend && npm run dev
Everything you're used to works normally:
php artisan tinker
php artisan migrate
php artisan make:model
composer require <package>
npm install <package>
mysql -u root --socket=$MYSQL_UNIX_PORT product_api
What to Commit
git add flake.nix flake.lock
git commit -m "Add Nix dev environment"
Add to .gitignore:
.nix-mysql/
.direnv/
result
The flake.lock pins exact versions, treat it like composer.lock.
When a New Developer Joins
Before Nix:
1. Read the README
2. Install PHP 8.3 (they install 8.1)
3. Install Node (wrong version)
4. Install MySQL (can't connect)
5. Spend 4 hours debugging
6. Finally get it running
After Nix:
# One-time: install Nix
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install
# Every time:
git clone https://github.com/rodrigonbarreto/nix_blog_post.git
cd nix_blog_post
nix develop
# Same PHP, same Node, same MySQL. Done.
Customizing
Need Redis? Add it:
buildInputs = with pkgs; [
# ... existing packages
redis
];
Need more PHP extensions? Add them to the buildEnv block in flake.nix:
php = pkgs.php83.buildEnv {
extensions = { all, ... }: with all; [
# ... existing extensions
redis
imagick
gd
];
};
Run nix develop again and the new packages are there.
Troubleshooting
"experimental feature 'flakes' is disabled": Add experimental-features = nix-command flakes to ~/.config/nix/nix.conf.
"The iconv OR mbstring extension is required": Add missing extensions to the buildEnv block in flake.nix, then exit and re-enter nix develop. Also make sure the shellHook includes unset PHP_INI_SCAN_DIR, tools like Laravel Herd can hijack the PHP config and hide Nix extensions.
MySQL won't start: Check if port 3307 is in use. Look at the error output from mysqld.
"command not found" outside the shell: Expected. Everything only exists inside nix develop.
Want auto-activation? Install direnv, create .envrc with use flake, run direnv allow. The environment activates automatically when you cd into the project.
Next Steps
If you found this useful, I'm planning a Part 2 where we set up the entire stack, MySQL, Laravel, and React, to start with a single command. No more opening 3 terminals. Just:
nix develop
start
And everything is running. Drop a comment or a like if you'd want to see that.
In the meantime, some useful links to go deeper:
-
direnv, auto-activate the environment on
cd - devenv.sh, higher-level Nix tool with built-in services
- Nix language tutorial, understand what's inside the flake
One file. One command. Every machine. That's Nix.
Source code: github.com/rodrigonbarreto/nix_blog_post

Top comments (0)