DEV Community

Ranieri Valença
Ranieri Valença

Posted on

Creating and running a Laravel 12 Project with (kinda) Zero Dependency using Docker

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
Enter fullscreen mode Exit fullscreen mode

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');"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Oh crap! Bun needs unzip that isn't installed on the container. Let's install it.

apt-get update && apt-get install -y unzip
Enter fullscreen mode Exit fullscreen mode

Back to the Bun installation.

curl -fsSL https://bun.sh/install | bash
Enter fullscreen mode Exit fullscreen mode

It worked! One more command Bun asked me to run and it's installed.

source /root/.bashrc
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Beautiful! Let's create a Laravel project now as it is recommended by the official documentation.

composer global require laravel/installer
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Hmm, there is a laravel executable file there. Let's try to run it.

~/.composer/vendor/bin/laravel  --version
Enter fullscreen mode Exit fullscreen mode

It worked! Now I have to add the ~/.composer/vendor/bin directory to the PATH environment variable.

export PATH=$PATH:/root/.composer/vendor/bin
Enter fullscreen mode Exit fullscreen mode

Another way to access the laravel executable is through the Composer exec command.

composer global exec laravel -- --version
Enter fullscreen mode Exit fullscreen mode

Now I can (hopefully) create a Laravel project.

laravel new my-app
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Now let's try running the composer run dev command again.

composer run dev
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

And now I can try again.

composer run dev
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Now let's build the image.

docker build -t laravel12 .
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

If you are using Windows, you can use the sed command on Git Bash or WSL. If you are using PowerShell, you can use the Get-Content and Set-Content cmdlets. If you are using MacOS, the sed -i command expects an argument after the -i option, so you can use sed -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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

To ensure everything is working, let's check if the extension is installed and enabled.

php -m | grep mongodb
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)