TL;DR
The goal is to create and run a Laravel 12 project without installing any dependencies on the host system. To achieve this, I created a Dockerfile containing PHP, Composer and Bun. You can jump to the Dockerfile section to see the final result if you are in a hurry.
A side quest is to install the MongoDB PHP extension. You can jump to the Extending the Dockerfile and dependencies section to see how I did it.
Introduction
As a Web Development teacher, I have to deal with many different environments, languages and frameworks. Often I have to install a lot of binaries, libraries and dependencies system-wide to be able to run a simple "Hello World" program. This is not only time-consuming but also can lead to conflicts between different versions of the same library or binary.
Since I’m quite disorganized, I tend to forget to uninstall these dependencies and binaries after finishing my work. This leads to a lot of garbage in my system, which can slow down my computer and make it harder to find the right version of a library when I need it. Also, again, lots of conflicts.
Yesterday I faced a new challenge on a student's project: they wanted to create a Laravel 12 project on the college lab environment, but they didn't have the permissions to install anything on the system. Moreover, the PHP version installed on the lab was outdated (for Laravel 12 purposes) and they couldn't update it. And they wanted to connect Laravel with a MongoDB database - which wasn't installed.
So, I proposed a challenge: create a Laravel 12 project with zero installed dependencies. And now I'm sharing this challenge with you - with my solution.
In this article, I'll present you my workflow for achieving this goal. I'll share my thoughts and decisions, the problems I faced and how I solved them.
In addition to being a very passionate developer, I'm also a teacher, so I'll try to explain everything I did in a way that everyone can understand. I hope you enjoy it.
The process
After some consideration, the solution I found was to use Docker to create a container with all the dependencies needed to run a Laravel 12 project. This way, we can create a Laravel project inside the container and run it without installing anything on the host system. Or just use the container as a development environment and (kinda) run the project on the host system. This way, we can also avoid conflicts between different versions of the same library or binary.
Of course we could achieve the same result using a virtual machine, but Docker is faster and more lightweight. We could (probably) also use virtual environments, but they would be less straightforward and more complex. And they either wouldn’t work in the college lab environment or students would leave the virtual environment running indefinitely, leading to other (less experienced) students using and messing with it.
Back to the Docker approach, this can be a bit challenging at first, because Laravel 12's requirements are: PHP, Composer, the Laravel Installer (the kind of binary I usually avoid to install system-wide) and Node + NPM (or Bun (?)). There are some more requirements they don't mention, but we'll talk about them later.
Usually when I need to run something on a Docker container, I look at the official Docker Hub to find an image that already has the dependencies I need. I also have a personal preference for using official images, because I (1) trust them more and (2) they are usually more up-to-date.
For this challenge, considering my personal preferences, I found the following images:
Each of these has an official image on Docker Hub, but they are not meant to be used together. So, I had to create a custom Dockerfile to create a container with all the dependencies I needed. And now I have to decide which one will be the base image that will make work easier.
As for now, the latest
image for Composer is based onAlpine Linux, that is very minimal and lightweight, but would require me to install PHP and Node manually. Node and PHP are both based on Debian (what makes me feel more comfortable). So, I decided to use the PHP image as the base image for my custom Dockerfile.
The proof of concept
I envy those who can jump straight into writing a Dockerfile without real-world testing — people who truly know what they’re doing and which commands and packages they need. I’m not one of them. So, before heading to the Dockerfile, I decided to create a container with the PHP image and try to install Composer and Bun on it.
docker run -it --rm php bash
This command will create a container with the PHP image and run the
bash
command inside it. The--rm
flag will remove the container when it stops. This way, I can test things without leaving garbage on my system.
Composer
Okay, now I'm inside the container. Let's try to install Composer.
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php composer-setup.php
php -r "unlink('composer-setup.php');"
It worked! Just one more command to move the composer.phar
file to the /usr/local/bin
directory and make it system-wide available (not on my system, but on the container).
mv composer.phar /usr/local/bin/composer
Bun
Nice. Now, for a bit of a challenge, let's try to install Bun - a Node.js version manager. I'm not sure if it will work, but let's try.
curl -fsSL https://bun.sh/install | bash
Oh crap! Bun needs unzip
that isn't installed on the container. Let's install it.
apt-get update && apt-get install -y unzip
Back to the Bun installation.
curl -fsSL https://bun.sh/install | bash
It worked! One more command Bun asked me to run and it's installed.
source /root/.bashrc
In fact it was already "installed", but in order to get it working on my current shell session, I had to run this to reload the
.bashrc
file.
Now, let’s test if everything is working so far.
composer --version
bun --version
Beautiful! Let's create a Laravel project now as it is recommended by the official documentation.
composer global require laravel/installer
This command will install the Laravel Installer globally, so we can run it from anywhere.
Okay, next.
laravel new example-app
# bash: laravel: command not found
Wait, wat? This wasn't part of the plan. Why is this happening? Let's try to find out.
According to Composer documentation, the composer global
command I have just run installs packages in the COMPOSER_HOME
directory. Let's check it out.
echo $COMPOSER_HOME
Oh, great - there is no COMPOSER_HOME
environment variable. Let's go back to the Composer documentation and see what it says. Oh, okay, by default it is ~/.composer
. Let's check it out.
ls -la ~/.composer
Well, there is a vendor
directory there, and checking it out (ls -la ~/.composer/vendor
) I can see a Laravel
directory and a bin
directory inside it. Let me check the bin
directory.
ls -la ~/.composer/vendor/bin
Hmm, there is a laravel
executable file there. Let's try to run it.
~/.composer/vendor/bin/laravel --version
It worked! Now I have to add the ~/.composer/vendor/bin
directory to the PATH
environment variable.
export PATH=$PATH:/root/.composer/vendor/bin
Another way to access the
laravel
executable is through the Composer exec command.
composer global exec laravel -- --version
Now I can (hopefully) create a Laravel project.
laravel new my-app
Laravel now asks me some more questions than before (more than a year since my last Laravel project), but it (kinda) worked. By the end of the process, it asked if I wanted to run npm install
and npm run dev
. I said yes, but it failed because I opted for Bun instead and it has no npm
installed.
The Bun project looks promising (this is my first time using it), but Laravel still seems to be optimized for Node.js and NPM. Let me try installing dependencies using Bun instead.
cd my-app
bun i # or bun install
Well, it worked. I’m unsure if this approach will work for all Laravel projects. Still, I’m happy with the results so far.
composer run dev
Aaaanndd... no. It looks like Laravel 12 is transitioning from "old school laravel's" php artisan ...
to Composer's scripts and while it suggests Bun as an option to Node.js+NPM, it still uses NPM scripts. The last command failed because it tried to run npx ...
and I got a sh: 1: npx: not found
error. So sad.
Now I have some options here:
- install Node.js and
npx
on this container; - change the Laravel project to use Bun instead of NPM;
- find what is the bun replacement for
npx
and use it.
I'll dig a bit more on the last option. The Bun CLI documentation presents bunx
or bun x
as a replacement for npx
.Checking the bunx
origin inside the container (which bunx
) and listing it (ls -la $(which bunx)
), I see that it's a symlink to the bun
executable. So, let's try to create another symlink to it called - you guessed it - npx
.
ln -s $(which bunx) /usr/local/bin/npx
Now let's try running the composer run dev
command again.
composer run dev
And it worked! Well, at least the npx
command did. Now I have a PHP RuntimeException asking me for the pcntl
extension. Luckily I choose the PHP image as the base image - directly from its overview (https://hub.docker.com/_/php) we have "How to install more PHP extensions" (how I wish all images had this kind of documentation). Let's try to install the missing extension.
docker-php-ext-install pcntl
And now I can try again.
composer run dev
And it worked! I have a Laravel 12 project running on a container with zero installed dependencies. I'm happy with the results so far. I'm also happy with all steps I took to get here. Now, let's move on to the next step and create a Dockerfile to automate all these steps.
The Dockerfile
Now I feel confident enough to create a Dockerfile to automate all these steps. Let's create a file named Dockerfile
(touch Dockerfile
if you are on a Unix-like system (Linux or MacOS), or type nul > Dockerfile
if you are on Windows) and paste the following content.
FROM php:latest
# Install unzip
RUN apt update && apt install -y unzip
# Install Composer using another strategy but with the same result
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
# Set the environment variables
ENV HOME="/root"
ENV PATH="/root/.composer/vendor/bin:${PATH}:/root/.bun/bin"
# Install Bun
RUN curl -fsSL https://bun.sh/install | bash
# Create some symlinks to the bun executable
RUN ln -s $(which bun) /usr/local/bin/npm
RUN ln -s $(which bunx) /usr/local/bin/npx
# Install Laravel Installer
RUN composer global require laravel/installer
# Install pcntl extension
RUN docker-php-ext-install pcntl
Now let's build the image.
docker build -t laravel12 .
This command will create an image named laravel12
based on the Dockerfile
we just created. At this point, the file is no longer needed, so feel free to delete it.
In this step we just created an image on our local Docker daemon. If you want to share this image with others, you can push it to a Docker registry. But this is a topic for another time. Anyway, I have pushed this image to my Docker Hub account and it is available for you to use.
docker pull ranierivalenca/laravel12
Testing
With the image created and registered on the local Docker daemon, we can now create a container with it.
docker run -it --rm laravel12 bash
If everything went well, you should be inside the container. Now we can create a Laravel project.
Creating a Laravel project
Okay, let's take a moment to recap what we have done so far. We have created a Dockerfile that creates a container with all the dependencies needed to run a Laravel 12 project. We have built the image and were able to create a container with it. If now we create a Laravel project inside this container, all the project's files will be inside the container and will be lost when the container stops. To avoid this, we can create a volume to store the project's files on the host system.
docker run -it --rm -v $(pwd):/app laravel12 laravel new my-app
This way, we are creating a volume that maps the current directory ($(pwd)
) to the /app
directory inside the container. So, the my-app directory will be created inside the current directory on the host system. Now we can run the project.
cd my-app
docker run -it --rm -v $(pwd):/app -w /app laravel12 composer run dev
This command will create a container with the my-app
(now the current directory thanks to cd
command) directory mounted on the /app
directory inside the container. It will also set the working directory to /app
(so we don't have to type /app
on every command or do a cd
command).
Tadah! It's running. Unfortunately, we can't access the project on the browser because we didn't expose any ports. And, moreover, by default (at this present time at least), the composer run dev
command start both PHP and Vite (JS) servers bound to 127.0.0.1
(localhost
). This is fine when we are running the project directly on the host system, but not when we are running it inside a container. Even if we expose the ports, the app inside the container won’t respond to requests from the host system because it’s bound to 127.0.0.1
.
All this can look a bit confusing, but it's not that hard. In summary, all we have to do is to change the artisan serve
command to bind to 0.0.0.0
and add a --host
option to the npm run dev
command. We can do this by editing the composer.json
file.
sed -i 's/artisan serve/artisan serve --host 0.0.0.0/g' composer.json
sed -i 's/run dev/run dev --host/g' composer.json
If you are using Windows, you can use the
sed
command on Git Bash or WSL. If you are using PowerShell, you can use theGet-Content
andSet-Content
cmdlets. If you are using MacOS, thesed -i
command expects an argument after the-i
option, so you can usesed -i ''
.
Now we can run the project again., exposing the ports.
docker run -it --rm -v $(pwd):/app -w /app -p 8000:8000 -p 5173:5173 laravel12 composer run dev
Now we can access the project on your browser at http://localhost:8000
.
Extending the Dockerfile and dependencies
As I mentioned before, this specific Laravel 12 project requires a MongoDB database. So, we have to install the MongoDB PHP extension. We also need to have a MongoDB server running. Let's break this down into steps.
Adding the MongoDB PHP extension
Luckily, the PHP image has some scripts that may help us to install PHP extensions. Let's try using the most basic one, the docker-php-ext-install
script. Like before, I'll make some real-world testing before adding it to the Dockerfile.
docker run -it --rm laravel12 bash
# Inside the container
docker-php-ext-install mongodb
Oops. It didn't work. The mongodb extension isn't available on the /usr/src/php/ext
directory that came with our PHP image. We'll have to install it manually. Let's try to install it using PECL.
pecl install mongodb
The installation process will ask you some questions. Just press Enter to accept the default values. You can check this options on the PECL site, on the MongoDB site or on the PHP site.
Now (after a while - this is a kinda big extension), the extension is built and installed, but we need to enable it. Let's do this using another (very pleasant) PHP script.
docker-php-ext-enable mongodb
To ensure everything is working, let's check if the extension is installed and enabled.
php -m | grep mongodb
And we're done! Now we can exit the container and add these commands to a new Dockerfile
that will use the laravel12
image as the base image.
FROM laravel12
# Install the MongoDB PHP extension
RUN pecl install mongodb && docker-php-ext-enable mongodb
You'll find that this process is very similar to the one we used to install the pcntl
extension. The main difference is that we are using the pecl
command instead of the docker-php-ext-install
command.
Using this new Dockerfile
, we can build a new image and create a container with it. We can also test if the MongoDB PHP extension is installed and enabled.
docker build -t laravel12:with-mongodb .
docker run -it --rm laravel12:with-mongodb php -m | grep mongodb
Using this approach, we can now install any PHP extension we need. We just have to find the right package name and use either the docker-php-ext-install
or pecl install
plus docker-php-ext-enable
commands depending on the extension.
If this matches your needs, you can get this image from my Docker Hub account.
docker pull ranierivalenca/laravel12:with-mongodb
Adding the MongoDB server
To run a MongoDB server, we can use the official MongoDB image. We can create a container with this image and run it in the background. We can also expose the default MongoDB port (27017) to the host system.
docker run -d --name mongodb -p 27017:27017 --network-alias mongo mongo
This command creates a container named mongodb
with the mongo
image, exposing the default MongoDB port (27017) to the host system (in case you want to access it externally) and assigning it the alias mongo
in Docker’s internal network. Now we can connect to this server from our Laravel project through the mongo
hostname - just remember your project will be running inside a container connected to the same (docker) network as the MongoDB server.
Conclusion
I hope you enjoyed this journey as much as I did! I learned a lot, and I hope you did too. I’m pleased with the results and the journey that led me here — I hope you are too! If you have any questions, suggestions or corrections, please let me know. I’m always open to learning new things and improving my knowledge.
Top comments (0)