It's 2021, the vaccine seems to be coming soon. In the meantime, I have heard the yells and troubles of my LAMP-stack colleagues. I get it, Folks! How painful can it be to continuously update your machines to be able to work with LAMP Stack, especially when working with Laravel? That Composer dump autoloads, clearing your MAMP or XAMPP servers (too many databases). It's a whole wild-west out there. Well, Docker and I together will solve your problem once and for all.
The Full Development Environment Setup
Before moving on to the Dockerized setup, let me quickly remind you of the long and boring process of Laravel development environment setup. Firstly, we have to install zsh
which is a Unix shell (for Linux and macOS only).
Secondly, we need to install PHP
, PHP-dependencies
, and composer
. Now if you are done setting up, you would surely like to set up Valet
and its external libraries so that your app can be served successfully.
Then, to create a Laravel application you need to install the Laravel installer.
With all these in place, you would need a SQL database locally. For that, you would need to install XAMPP
, MAMP
, MySQL
, or any SQL server of your choice.
But wait! You are not done yet. In addition to these, you will also need nodeJS
because Laravel uses nodeJS
and npm
behind the scenes.
This is a lot of work and you will have to update all the required dependencies frequently. So, let's now move on to the Dockerized setup but let me give you a spoiler first. You will not have to install anything except Docker on your machine, not even PHP
.
The Dockerized Setup
From the above discussion, it is clear that we need the following to have a Laravel development environment.
- Some kind of server to serve the app (we will use NGINX)
- PHP
- MySQL
- Composer
- Artisan
- npm
In the world of Docker, everything is a container! So, we need 6 containers for this setup. Make sure you have Docker installed on your machine.
The Folder Structure
Create an empty folder anywhere on your machine, I will name it Laravel-Dockerized. Open this empty folder on any Editor/IDE of your choice and create a docker-compose.yaml
file in the root of your project directory. Also, create the following folders in the root of your project directory.
- dockerfiles, All the dockerfiles will be in this directory.
- env, All the environment files will live here.
- src, The source code of our Laravel application will be mapped here.
- nginx, The configuration file for NGINX will be specified here.
With the folder structure done, let's move on to the server configuration.
NGINX Container Setup
We are using NGINX to serve our Laravel app locally. So in the nginx
folder create an nginx.conf
, the configuration file, so that we can configure our server.
You can search for NGINX configuration file and you will get a plethora of configuration options. The nginx.conf
file that I am using is as below,
server {
listen 80;
index index.php index.html;
server_name localhost;
root /var/www/html/public;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass php:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
}
You can use this configuration file or bring your own configuration. According to my configuration file, my server listens on port 80 and the directory specified is /var/www/html/public
.
Now, in your docker-compose.yaml
let's specify the NGINX container using this configuration file.
version: "3.8"
services:
server:
image: 'nginx:stable-alpine'
ports:
- '8000:80'
volumes:
- ./src:/var/www/html
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
Here, I have specified the container name as server
. The image I am using is nginx:stable-alpine
which is a stable build of NGINX and a very slimmed down version. The port I have mapped is the exposed port by our configuration file, i.e, container port:80
to external port:8000
.
Also, I am using two Volumes
as Bind Mounts. The ./src:/var/www/html
will be used to serve/funnel our Laravel application. The src
folder on our Local Machine is binded to the /var/www/html
folder inside the Container.
The second bind mount
./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
is used to write our custom configuration to the NGINX container, i.e, server. Here, our nginx/nginx.conf
file is mapped as Read Only ro
to the container file /etc/nginx/conf.d/default.conf
. Read the Dockerhub NGINX documentation for more details.
With the server
container in place let's move on to the PHP
container setup.
PHP Container Setup
For PHP we will need a separate dockerfile because we need to run some commands during the image creation. So, create a php.dockerfile
in the dockerfiles
directory. The php.dockerfile
will have a base PHP
image and mine looks like this,
FROM php:7.4-fpm-alpine
WORKDIR /var/www/html
RUN docker-php-ext-install pdo pdo_mysql
I am using php:7.4-fpm-alpine
as my base image which is also an alpine (the slimmed-down version of PHP). You can already see that the Working Directory of the container is set the same as our server (NGINX) container.
Now, we need to specify our PHP container in our docker-compose.yaml
file below the server configuration.
php:
build:
context: ./dockerfiles
dockerfile: php.dockerfile
volumes:
- ./src:/var/www/html:delegated
Here, I have specified the build context and the dockerfile which will be used to create the PHP container. Also, I have used a bind mount to map the src
folder on the local machine to the container folder /var/www/html
so that our PHP container can write files to the src
folder. The delegated
key here specifies that the writing will be done in batch rather instantaneously.
This is all we need for the PHP container. So now let's set up our database.
MySQL Container Setup
MySQL container setup is quite simple. All you need is some environment variables and a base image. More details can be found on the Dockerhub MySQL official image repository.
For the environment variables required by the MySQL container, we will create a mysql.env
file in the env
directory.
MYSQL_DATABASE=homestead
MYSQL_USER=homestead
MYSQL_PASSWORD=yoursuperdupersecretpassword
MYSQL_ROOT_PASSWORD=yoursuperdupersecretpassword
The database name and database username is kept homestead
as per the Laravel official documentation and the password and root password as per your own choice.
Now, we need to specify this container in the docker-compose.yaml
file below the php
container.
mysql:
image: 'mysql:5.7'
env_file:
- ./env/mysql.env
The image I have used is mysql:5.7
and the path to the mysql.env
file is also specified.
Composer Utility Container
Next, we will build a composer container, which will be a utility container. It will just be used to create a Laravel project for us. This container will not run all the time but only when we require to run composer commands.
For composer, we will also need a separate dockerfile. So, let's build one named composer.dockerfile
FROM composer:latest
WORKDIR /var/www/html
ENTRYPOINT [ "composer", "--ignore-platform-reqs" ]
Here, I am using the composer image with the latest
tag to pull the latest image and the working directory is also set /var/www/html
the same as php
and server
containers.
Now, we need to specify this container in our docker-compose.yaml
file below our mysql
container.
composer:
build:
context: ./dockerfiles
dockerfile: composer.dockerfile
volumes:
- ./src:/var/www/html
Same as our php
container we specify the build context and the dockerfile. The bind mount is also the same just without delegated
key.
Artisan Utility Container
Just like the composer, Artisan will also be a utility container which will be required only when we need to run artisan
related commands.
This container will also use the php:7.4-fpm-alpine
image and the working directory will also be the same as php
container. Just we need a different entrypoint
.
Rather than creating a new dockerfile we can use the php.dockerfile
instead. So, in our docker-compose.yaml
file we specify our artisan container below the composer.
artisan:
build:
context: ./dockerfiles
dockerfile: php.dockerfile
volumes:
- ./src:/var/www/html
entrypoint: ["php", "/var/www/html/artisan"]
We use the same bind mount to map our ./src
folder to the /var/www/html
on the container. Also we can override the php.dockerfile
's ENTRYPOINT
by specifying our own entrypoint: ["php", "/var/www/html/artisan"]
in our docker-compose.yaml
.
npm Utility Container
Just like our previous two containers, composer
and artisan
, our npm
container will also be a utility container that will only be required to run npm
commands.
We don't need a separate dockerfile here as we can directly configure this in our docker-compose.yaml
file.
npm:
image: node:14
working_dir: /var/www/html
entrypoint: ["npm"]
volumes:
- ./src:/var/www/html
This is all the setup that we need! Now, let's test if our setup works.
Creating Laravel App Using Composer Utility Container
We will use our composer
container to create a Laravel application. The container will spit out code into our src
folder. In the terminal/ powershell/ command prompt run,
docker-compose run --rm composer create-project laravel/laravel .
Note: In your terminal/powershell you must navigate to your project directory or if using integrated terminal like in Visual Studio Code you will be automatically navigated to your project directory.
This command will only run our composer
utility container.
More details can be found on creating a Laravel Project using composer in the Laravel Documentation.
Here, I have specified the --rm
flag so that as soon as the Laravel project creation is complete the composer container is destroyed automatically.
Now explore your src
folder in your project directory and you will see your newly created Laravel project! But, wait we are not done yet.
Visiting Our App on Localhost
To see our Laravel application we need to bring up our server
, php
and mysql
containers.
In our docker-compose.yaml
file, under the server container service, we need to add php
and mysql
container as our dependencies so that our server
container is only started only when php
and mysql
containers are up and running.
server:
image: 'nginx:stable-alpine'
ports:
- '8000:80'
volumes:
- ./src:/var/www/html
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- php
- mysql
So with that our completed docker-compose.yaml
file looks like this,
version: "3.8"
services:
server:
image: 'nginx:stable-alpine'
ports:
- '8000:80'
volumes:
- ./src:/var/www/html
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- php
- mysql
php:
build:
context: ./dockerfiles
dockerfile: php.dockerfile
volumes:
- ./src:/var/www/html:delegated
mysql:
image: 'mysql:5.7'
env_file:
- ./env/mysql.env
composer:
build:
context: ./dockerfiles
dockerfile: composer.dockerfile
volumes:
- ./src:/var/www/html
artisan:
build:
context: ./dockerfiles
dockerfile: php.dockerfile
volumes:
- ./src:/var/www/html
entrypoint: ["php", "/var/www/html/artisan"]
npm:
image: node:14
working_dir: /var/www/html
entrypoint: ["npm"]
volumes:
- ./src:/var/www/html
Now, it's time to bring up our three main containers. So in your terminal/powershell run,
docker-compose up -d --build server
This docker command will bring up the server
container along with its dependency containers , i.e, php
and mysql
. This will spin up our containers in detached
mode because of -d
flag and will also force a fresh build of images due to --build
flag.
Now, visit localhost:8000
and you will see your Laravel application running, which is served through the NGINX server.
Using Artisan Utility Container for Table Migration
Now, lets connect our mysql
container to our Laravel Application. In the ./src/.env
we need to add our MySQL credentials to authorize our app.
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=homestead
DB_USERNAME=homestead
DB_PASSWORD=yoursuperdupersecretpassword
Next, we need to bring all the containers down using
docker-compose down
for a fresh build and fresh containers.
To ensure image rebuilds we will start our containers with --build
flag.
docker-compose up -d --build server
Now using the artisan
container to migrate tables to dabatase.
docker-compose run --rm artisan migrate
This will successfully run all your migrations to mysql
container. You can run all artisan commands using this syntax. Example, docker-compose run --rm artisan make:model Post -mc
, etc.
Useful Tips and Bringing Containers Up/Down
With all the above steps you have successfully configured your development environment. Now anytime you want to have your containers up and running (during development) or if you want to shut them down these simple docker commands will do it for you,
docker-compose up -d server
and docker-compose down
.
For fresh rebuilds (if you made changes in your dockerfiles or docker-compose.yaml) then,
docker-compose up -d --build server
will ensure a fresh image builds.
I have uploaded this project to my Github. You can clone it from there and just start working with it without any issues.
Conclusion
That's all for this post. Kudos for having a portable development environment setup that you can deploy on any machine in seconds! I hope this helps you a lot in your development workflow.
The best thing here is that we didn't need to install anything except Docker. Also if you want any other version changes in PHP/Composer/Artisan/Laravel/MySQL/npm, just go ahead and change the versions in dockerfiles or docker-compose.yaml and then rebuild the images.
This is the beauty of Docker. Thank you so much for reading this post till the end. I hope you liked it. If you liked this a star on this repository would be great.
If you would like a quick walkaround video of this setup, you can watch it on my Facebook . Happy Coding! 👨🏽💻