Laravel Development Environment Setup using Docker

Laravel Development Environment Setup using Docker

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.

  1. Some kind of server to serve the app (we will use NGINX)
  2. PHP
  3. MySQL
  4. Composer
  5. Artisan
  6. 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.

  1. dockerfiles, All the dockerfiles will be in this directory.
  2. env, All the environment files will live here.
  3. src, The source code of our Laravel application will be mapped here.
  4. 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! 👨🏽‍💻