How to create a Laravel development environment using Docker
In this tutorial we are going to set up a modern Laravel development enviroment using Docker and Docker Compose.
We will run a Laravel 8 application in a custom container that will communicate with other containers (database, cache, etc) the build a complete development environment.
By the end of this tutorial you should have a total of 6 services (containers) running on your machine, one for each specific need of our application, here is the full list:
- PHP 8.0 (application code)
- MySQL 5.7 (database)
- NGINX (webserver)
- phpMyAdmin (database managment)
- Redis (caching)
- MailHog (local email testing)
I'm assuming you have docker installed on your machine, if you don't go ahead and do so. There are a ton of tutorials on the internet teaching you how to install Docker in the 3 major operating systems.
Also here is the link to the official Docker website with installations instructions.
https://www.docker.com/products/docker-desktop
If you have any problems just Google the error message and you should be able to solve it.
With that out of the way, let's get our hands dirty.
Step 1: Clone Laravel's git repository
Go to a folder where you want to have your project stored and clone the Laravel repository to a folder called src using git.
git clone https://github.com/laravel/laravel.git src
We will now create a couple of files and folders we need to setup and organize our docker environment. Create two files at the root of the project, one called docker-compose.yaml and another called Dockerfile. Then create the following list of folder/subfolder.
- nginx/conf
- mysql/data
- redis/data
We will use the nginx/conf folder to store the configuration files for NGINX and the other two to store Redis and MySQL containers data, if we dont's setup these folder we will loose their data everytime our containers are destroyed.
Step 2: Create a custom PHP 8 image
In order to run our Laravel app we will need a Docker image that has PHP in it. We sort of got lucky here, because there is an official PHP image available in Docker Hub and all we need to do is to pull it to our machine.
But, according to Laravel's documentation (https://laravel.com/docs/8.x/deployment#server-requirements), there a list of PHP extensions (listed below) the framework version we will use (8.x) needs installed to properly work:
- BCMath PHP Extension
- Ctype PHP Extension
- Fileinfo PHP Extension
- JSON PHP Extension
- Mbstring PHP Extension
- OpenSSL PHP Extension
- PDO PHP Extension
- Tokenizer PHP Extension
- XML PHP Extension
To know which extensions a given PHP installation has available we just need to run the following command, php -m, after I executed it inside the PHP image we will be using (I already pulled it to my machine) and I got the following list of installed extensions.
$ docker run php:8.0.3-fpm-buster php -m [PHP Modules] Core ctype curl date dom fileinfo filter ftp hash iconv json libxml mbstring mysqlnd openssl pcre PDO pdo_sqlite Phar posix readline Reflection session SimpleXML sodium SPL sqlite3 standard tokenizer xml xmlreader xmlwriter zlib [Zend Modules]
Comparing it with the list of extensions required by Laravel we can see we are missing only a single extension and since we will be using MySQL, we will also need to install the pdo_mysql.
So that means we can't use the default PHP image available at Docker Hub out of the box to run our Laravel code and we will have to create a custom one that has the missing extensions installed, but don't worry, because that's a piece of cake to do with Docker, trust me, I will show you how.
To create a custom image we will need to write some code to the Dockerfile we created earlier, the code we need is below.
FROM php:8.0.3-fpm-buster RUN docker-php-ext-install bcmath pdo_mysql RUN apt-get update RUN apt-get install -y git zip unzip COPY --from=composer:latest /usr/bin/composer /usr/bin/composer EXPOSE 9000
The first line specifies the base image that will be using to create our custom one, in the tutorial we will use the PHP version 8.0.3.
Next line tells docker to run a handy little command that will install the missing PHP extension Laravel needs, trust me, that single line saved us from a lot of pain, bacause installing PHP extensions is not fun at all.
Then we will install some tools composer will need to do its job.
Next line is sort of installing composer inside our image, but it's actually just copying its binaries from the standard Composer image available at Docker Hub.
And the final line tells docker to expose the container's port 9000.
And that's it, that's all we need to do to create a custom Docker image ready to run our Laravel code.
Step 3: Setup the basic services (PHP, NGINX and MySQL)
Now that we have a custom image we can use to run our Laravel app, all we have to do now is to configure it and the other services (containers) our application will need.
We will use a tool called docker-compose, which is part of Docker itself, that will allow us to define any number of containers along with specific configuration for each one in a single file, then we can start all of them at once running a single command.
Another benefit of using docker-compose is that our docker environment is now saved in a file we can store, send straight to GitHub for version control, share with others and so on.
That's what the docker-compose.yaml file we created earlier is for, so open it up because we will add some code to it.
For now we will define only the three most basic containers our app needs, which are PHP, NGINX and MySQL.
NGINX, as the webserver, will receive the HTTP requests, send them to the PHP container that will execute our code, then the PHP container will send back the result to NGINX which will send the response to the user, if needed, before sending the response to the NGINX container, the PHP container can talk to the MySQL container to store or retrieve data requested by the user.
Let's start defining docker setup and you will see how simple it actually is.
version: "3.7" networks: app-network: driver: bridge services: app: mysql: nginx:
First we define the version of our Dockerfile as 3.7.
Then we define a network that our containers will be connected to so that all of them can talk to each other.
We then start the section that define/configure our services.
For now we defined the app, mysql and nginx services, let's add configuration to each of them.
3.2 App (custom PHP) service
Remeber the custom PHP container we created earlier? It's now time to put it to work! Let's define it's configuration inside the docker-compose.yaml file.
app: build: context: ./ dockerfile: Dockerfile image: laravel8-php-fpm-80 container_name: app restart: unless-stopped tty: true working_dir: /var/www volumes: - ./src:/var/www networks: - app-network
This piece of code tells Docker how we want our app service to be configured.
The first command specifies that the image for this service has to built first, this is because the actual image does not exists yet, just its definition (Dockerfile).
Under the build command, the dockerfile option tells docker the name of the file that contains the definition and the context option tells Docker the path to the folder where the definition file is located.
Next we are telling Docker to name the image that will be built as "laravel8-php-fpm-80".
We then specify how we want to call that container when its running, then we define its restart policy and working directory.
We then define a volume the container will use, which is basically a mapping between a folder/file in our machine to a folder/file inside the container.
We are saying something like this, "hey Docker, map the current ./src folder on my machine to the folder /var/www inside this container", and them the two folders will share the files and any modification will be reflected on both sides .
And that's how we are going to "put" our code inside the container for it to be executed.
Next we tell docker to connect our PHP container to the network we previously created so that it will be able to comunicate with other containers connected to the same network.
3.3 NGINX service
Next we need to setup the container that will serve as the webserver itself, which will receive the HTTP requests from the end users and send them to the PHP container that will process our Laravel code.
This container will be a standard NGINX container and will have the following set of configurations.
nginx: image: nginx:1.19.8-alpine container_name: nginx restart: unless-stopped tty: true ports: - 8100:80 volumes: - ./src:/var/www - ./nginx/conf:/etc/nginx/conf.d networks: - app-network
In it we are calling this service nginx, it will be based on the nginx:1.19.8-alpine image, which is very lightweight, only a couple of MB, then we are specifying the container name to also be nginx, adding some restart-policy, mapping the 8100 port of our machine to port 80 of the container, mapping our local config file at ./nginx/conf/ to the folder /etc/nginx/conf.d/ inside the container and then we connect it to the app-network.
Besides the settings in the docker-compose.yaml file, the NGINX container will also need additional configuration in the form of a file to work properly and it will be placed in the nginx/conf folder we created earlier.
I called mine app.conf, but you can call it whatever you want, just remeber to keep the .conf extension otherwise it won't work, this file will be mapped to the /etc/nginx/conf.d/ folder inside the NGINX container, which is its default configuration folder.
It's content will be the following.
server { listen 80; index index.php index.html; error_log /var/log/nginx/error.log; access_log /var/log/nginx/access.log; root /var/www/public; location ~ \.php$ { try_files $uri =404; fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_pass app:9000; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_path_info; } location / { try_files $uri $uri/ /index.php?$query_string; gzip_static on; } }
We are basically saying that the server will listen port 80, the root folder is located at /var/www/public, any requests to php files it receives it will pass it down to a network service called app at port 9000, which is our PHP container.
Don't worry too much about this configuration file because NGINX can get very complicated very fast if you decide to really get into it beyond the basics.
Just save the file with .conf extension in the appropriate folder and you should be good to go.
3.4 MySQL service
For our database container running MySQL, its configuration will be a bit different, it won't be in the form of a file as we did with NGINX, but in the form os environment variables defined directly in the docker-compose.yaml file, in that case we will need to define just a few basic variables like root user password, regular user name and password and so on.
There is one special environment variable that will be very useful to us, which the MYSQL_DATABASE variable. If you specify a value for it, MySQL will create a database with that name for us during startup, meaning we won't have to CLI into it just to create the database to connect to Laravel, isn't that nice?
I will call that database laravel8, but you can call it whatever you want.
mysql: image: mysql:5.7.33 container_name: mysql restart: unless-stopped tty: true environment: MYSQL_DATABASE: laravel8 MYSQL_ROOT_PASSWORD: 123456 MYSQL_PASSWORD: 123456 MYSQL_USER: laravel8 SERVICE_TAGS: dev SERVICE_NAME: mysql volumes: - ./mysql/data:/var/lib/mysql networks: - app-network
Since we are here let's update our .env file in the Laravel project to be able to connect to our MySQL container, the database config will look like this.
DB_CONNECTION=mysql DB_HOST=mysql DB_PORT=3306 DB_DATABASE=laravel8 DB_USERNAME=laravel8 DB_PASSWORD=123456
With these setting Laravel should be able to connect to MySQL without any issues.
The first version of our docker-compose.yaml file will look like this.
version: "3.7" networks: app-network: driver: bridge services: app: build: context: ./ dockerfile: Dockerfile image: laravel8-php-fpm-80 container_name: app restart: unless-stopped tty: true working_dir: /var/www volumes: - ./src:/var/www networks: - app-network mysql: image: mysql:5.7.33 container_name: mysql restart: unless-stopped tty: true environment: MYSQL_DATABASE: laravel8 MYSQL_ROOT_PASSWORD: 123456 MYSQL_PASSWORD: 123456 MYSQL_USER: laravel8 SERVICE_TAGS: dev SERVICE_NAME: mysql volumes: - ./mysql/data:/var/lib/mysql networks: - app-network nginx: image: nginx:1.19.8-alpine container_name: nginx restart: unless-stopped tty: true ports: - 8100:80 volumes: - ./src:/var/www - ./nginx/conf:/etc/nginx/conf.d networks: - app-network
Step 4: Test our initial setup
We are finally ready to make our first tests. To do so we only need to run a single command to startup our docker environment.
docker-compose up
It will first build our custom image since it doesn't exist yet.
And when it is done it will start all the services (containers) we defined.
The first time you execute it may take quite some time because our custom image will be first built and then the containers started, but after that the startup should take just a few seconds.
After some time you should see the console output indicating that our containers are up and running as the image above.
There is no official indication that the startup is finished, at some point the logs just stops coming, but one pretty good indicator I always use is to look for the entry from mysql that says mysqld: ready for connections.
mysql | 2021-04-03T00:02:19.314435Z 0 [Note] mysqld: ready for connections.
If you see this line it's a pretty good sign the startup is finished.
4.1 Accessing the homepage
Before we perform the most basic of all tests, which to access Laravel's default homepage, we need to run a few commands and perform some setup.
Remember that we used git to clone Laravel's repository? That means no dependency was intalled, just the application files.
So now we need to run composer install to install the PHP dependencies Laravel needs, create a copy of .env.example file and generate a new encryption key, let's do it!
But before you type composer install in your terminal, this is not how you run commands inside a Docker container, if you were to run that command you would just run it in your system, not from inside the container and that's not what we want.
To be able to run commands inside a container you need to use docker-compose exec command and that's how it works.
docker-compose [OPTIONS] exec [SERVICE NAME] [COMMAND]
So to install our dependencies will execute.
docker-compose exec -T app composer install
The installation process should now start.
And finish some time later.
We then need to create a copy of the .env.example file and generate a new encryption key, to do this let's run two commands, again, from inside our app container using the docker-compose exec command.
But since our terminal is locked because we ran the docker-compose up command, we will have to open a new one and navigate to the project folder to be able to execute additional commands while our Docker setup is running and from now on, everytime we need to execute a command while docker-compose up is running we will do it in a new terminal windows.
docker-compose exec -T app cp .env.example .env docker-compose exec -T app php artisan key:generate
Let's now try to access the home page of our Laravel app, go to your browser and access localhost:8100, that's the local port we mapped to port 80 of our NGINX container, remember?
You should now see Laravel's default homapage and at the right bottom you should see the version of PHP being used, which is 8.0.3, matching the version we used when creating our custom PHP container, pretty cool right?
Let's keep testing our docker environment.
4.2 Running migrations
In order to test the connection between the app and MySQL containers, let's run the migrations.
But first, we need to update the connection to our database inside Laravel.
DB_CONNECTION=mysql DB_HOST=mysql DB_PORT=3306 DB_DATABASE=laravel8 DB_USERNAME=laravel8 DB_PASSWORD=123456
Since we made modification to our .env file, it's a good idea to clear out our configuration cache running the following artisan command.
docker-compose exec -T app php artisan config:clear
Now we can use docker-compose exec command the run our migrations from inside the container, let's tru it.
docker-compose exec -T app php artisan migrate
After running the right command we should see our usual migrations output as shown below.
Awesome, our migrations are working just as expected.
4.3 Installing additional composer packages
Using the same command structure we used to migrate our database, we can also run composer commands, let's install a package to check if it works as expected.
Let's install the Laravel Socialite package and to do so we will run the following command.
docker-compose exec -T app composer require laravel/socialite
And the following output will be generated, proving that composer does work from inside our container.
So far so good, we have tested the connectivity from our PC to the container, running our migrations and also installing additional packages using composer, all from inside the container.
We now have a fully functional Laravel development enviroment complete with a MySQL database that our application can access, we can run artisan commands and even install additional composer packages.
Step 5: Setup additional services (Redis, phpMyAdmin and MailHog)
But we can do better than this, and we will! Let's add some additional services (containers) to make our development environment even better and also because, let's face it, most real life projects need much more than just Laravel and MySQL.
We may need some sort of visual database managment tool, we most likely need a fast caching solution (Redis is one of the best calls here), queues (again Laravel Queues works beaultifully with Redis), maybe you need some sort of NoSQL database alongside your MySQL or maybe your project uses another DB like Postgres, we can add some local email testing funtionality, so on and so forth.
So let's make our environment a little better and add Redis for caching, Mailhog for local email testing and phpMyAdmin to manager our MySQL database.
Ready? Let's dive in.
5.1 Adding Redis (caching)
There are a few steps we will need to perform to get Redis to work, the first one is to add another service to our docker-compose.yaml file with some basic configuration outlined below.
redis: image: redis:6.2.1-buster container_name: redis restart: unless-stopped tty: true volumes: - ./redis/data:/data networks: - app-network
Our service will be called redis-cache, it will be based on version 6.2.1 and the container name will also be named redis-cache. We also added some restart policy, volumes for data storag and, attached the container to the network.
Now we need to add a depency called predis so that Laravel can talk to Redis, go ahead and open up a new terminal in the same folder as the project and execute the command below.
docker-compose exec -T app composer require predis/predis
Now we need to add/modify some configuration to our Laravel project so that it uses Redis for caching, in our .env file let's modify the cache driver to use redis intead of file and add the REDIS_CLIENT key with vaue predis so that Laravel will use the composer package we installed just earlier.
CACHE_DRIVER=redis REDIS_HOST=redis REDIS_CLIENT=predis
Now we need to clear out our cached config file to make sure the news settings are picked up by the framework.
docker-compose exec -T app php artisan config:clear
Let's now add some routes to test our caching system. I'm gonna add the code directly to the web.php routes file.
//web.php use Illuminate\Support\Facades\Redis; Route::get('/store', function() { Redis::set('foo', 'bar'); }); Route::get('/retrieve', function() { return Redis::get('foo'); });
As you can see we are just storing the key pair foo, bar and then retriving it from the cache, let's hit these two routes and see what we get.
But before we hit these routes we need to stop and restart our services so that Redis goes online.
docker-compose down docker-compose up
After hitting the /store route we should see nothing on the screen since we are just storin our key value pair and returning nothing, but after hitting the /retrieve route we should see on the screen the value we stored for the foo key, which is bar.
And as expected that's exactly what we get as result, which means our Redis container is up and running and our Laravel app is being able to talk to it to store and retrieve values.
We now have an app, with database and caching up and running, let's keep adding more services to our development environment.
5.2 Adding MailHog (local mail testing)
Let's start by creating a mailhog service called mailhog in our docker-compose.yaml file and add the following configurations to it.
mailhog: image: mailhog/mailhog:v1.0.1 container_name: mailhog restart: unless-stopped ports: - 8025:8025 networks: - app-network
Since Laravel already comes configured to be used with MailHog, all we need to modify is the from email address in our .env file as shown below.
[email protected]
To make sure the changes we made to the .env file will take effect let's go ahead and clear Laravel's configuration cache running the command below once more.
docker-compose exec -T app php artisan config:clear
Now we will create a new Laravel Mail, which will define the email that will be sent, let's run the following artisan command.
docker-compose exec -T app php artisan make:mail TestMail
It will create a file called TestMail.php inside the app/Mail folder and in it we will the following line of code to its build method.
/** * Build the message. * * @return $this */ public function build() { return $this->view('mail.test'); }
All we are saying here is that we will return a regular blade view called test.blade.php that is located inside the folder resources/views/email, go ahead create that view and add some text to it.
Mine looks like this.
// resources/views/email/test.blade.php testing mailhog
Now let's add a new route that will send the email we have just created and again I will add the code directly to our web.php routes file, since this is just for testing purposes its not a big deal.
I called the route send-email, then I used the Mail facade to send the email.
//web.php <?php use App\Mail\TestMail; use Illuminate\Support\Facades\Mail; Route::get('/send-email', function() { Mail::to('[email protected]')->send(new TestMail); });
Before we can hit these routes let's not forget to restart out docker-compose setup so that MailHog comes online.
docker-compose down docker-compose up
Hoepfully you can see a pattern by now, if you modified the .env file you gotta clear the configuration cache and if you added another service to the docker-compose.yaml file you gotta shutdown the services and restart them again.
Finally let's test our email setup and MailHog by hitting the /send-email route we just created.
If everything was configured correctly you should receive a notification on your desktop from MailHog that na email was received, mine looks like the image below.
Open up MailsHog’s web interface to check your inbox accessing localhost:8025 on your browser. The actual email received look like this.
So our MailHog container is running just as expected, our app can now send as many emails as needed without any issues, that’s fantastic!
Just one more and we will be done, I promise.
5.3 Adding phpMyAdmin (MySQL managment)
To finish up this tutorial let’s add a phpMyAdmin container to allow us to manage our database visually instead of using the CLI, this one is pretty simple actually.
We will start by creating a new service in our docker-compose.yaml file called, you guessed it, phpmyadmin.
phpmyadmin: image: phpmyadmin:5.1.0-apache container_name: phpmyadmin restart: unless-stopped ports: - 8200:80 environment: PMA_HOST: mysql PMA_PORT: 3306 PMA_USER: laravel8 PMA_PASSWORD: 123456 networks: - app-network
By now you should recognize most if not all the configuration option of a typical Docker Compose service, so I won’t go into detail this time.
For phpMyAdmin this is pretty much all we have to do, the most import part is to set the correct environment variables to allow it to connect to the database inside the MySQL container.
As always, before we can access a new service we need to restart our docker-compose setup.
docker-compose down docker-compose up
Now if you go to your browser and hit localhost:8200 you should see phpMyAdmin’s default homepage as below.
And there you have it, we can now manage our database from a nice user interface if you are not really into CLIs, pretty easy, right?
Conclusion
It took us a while but now we have a fully functioning Docker based Laravel 8 development environment. One that can be easily extended by adding new services to the docker-compose.yaml file and the appropriate configuration to the Laravel project files.
https://isaacsouza.dev/laravel-development-environment-docker/
Student at Indian Institute of Technology, Kanpur
1 年I'm stuck at 4.1 Accessing the homepage: getting following error: └─$ sudo docker-compose exec -T app composer install No composer.lock file present. Updating dependencies to latest instead of installing from lock file. See https://getcomposer.org/install for more information. Loading composer repositories with package information Updating dependencies Your requirements could not be resolved to an installable set of packages. ?Problem 1 ??- Root composer.json requires php ^8.1 but your php version (8.0.3) does not satisfy that requirement. ?Problem 2 ??- Root composer.json requires laravel/framework 10.10.1 -> satisfiable by laravel/framework[v10.10.1]. ??- laravel/framework v10.10.1 requires php ^8.1 -> your php version (8.0.3) does not satisfy that requirement. ?Problem 3 ??- nunomaduro/collision[v7.0.0, ..., v7.5.2] require php ^8.1.0 -> your php version (8.0.3) does not satisfy that requirement. ??- Root composer.json requires nunomaduro/collision ^7.0 -> satisfiable by nunomaduro/collision[v7.0.0, ..., v7.5.2]. Please help
Front end developer | Web developer | Full Stack Developer | PHP| JS | MERN | LAMP | IA ENGINEER
2 年.
Senior Software Engineer at ExpertApps | PHP - Laravel
2 年very good Thanks.
Lead Backend Developer – WN Media group
3 年Oh man) I have been looking for this so much time) Thank u for this manual)