Creating a simple laravel docker environment

By
Anthony Protano

Since I started at liquidfish a little over a year ago I was introduced to the wonderful world of Laravel Homestead. Laravel Homestead is great, it supports multiple php versions, xDebug, MySQL and allows the user to install any other required software straight into the virtual machine.

This is a tried and tested method of local development that has worked incredibly well for our development team for many years, but as we continue to grow we are always looking for ways to improve our development and deployment process, enter docker.

- Docker is a platform for developers and sysadmins to develop, deploy, and run applications with containers. - docs.docker.com

Today we are going to create a dead simple docker environment to run Laravel 5.8 using PHP 7 with xDebug and Mysql on Windows 10 WSL.

My current development environment:

  • Windows Subsystem for Linux (WSL)
  • My main hard drive is mounted under /c/ in the WSL
  • ConEmu using ZSH hooked up to WSL
  • Docker for Windows
  • Docker & Docker Compose installed on WSL
  • PHPSTORM

As long as you have Docker & Docker Compose running on your machine you will be able to follow along with this tutorial. Let’s get started!

First create a new project folder for our docker environment, mine will be under /c/docker/laravel.

Now create the following directories inside the project folder

  • docker
  • nginx
  • php
  • config
  • www

The docker folder contains three folders, nginx, php, and mysql. These folders will hold Docker related files, Dockerfiles, .inis, configuration files, etc.

The www folder will be our environments web root where laravel will be installed.

First we are going to create a .env file in the root of the project to hold our Docker configuration variables that our compose file and PHP Docker file will use. The .env file will allow for easier flexibility in the event you need to change your environment.


# Docker Compose Environment Variables

# APP Environment
APP_ENV=local

# PHP Version
PHP_VERSION=7.3

# Local working directory webroot
LOCAL_WORKING_DIR=./www

# Remote working directory webroot
REMOTE_WORKING_DIR=/var/www/html

Now let's create our docker-compose. YML file inside the root of our project and add the following services to it.




Docker-compose.yml explanation

  • Version

    • The version of the docker-compose file

  • Services

    • Defines the services that will be ran on docker-compose up

  • App - PHP-FPM service

    • Build - We are going to be building a docker file

      • Context - The location of the docker file

      • Args - Variables we want to use in the Docker file from the .env

        • APP_ENV - Docker environment

        • PHP_VERSION - Version of php

        • REMOTE_WORKING_DIR - Remote working directory laravel will live

    • Container_name - Name of the container

    • Restart - Service will always restart unless it is stopped

    • Volumes - Local directories we want to mount and files we want to mount to the service

      • ./www (LOCAL_WORKING_DIR) to /var/www/html (REMOTE_WORKING_DIR)

      • Local xdebug ini file

    • Env_file - location of our env file

    • Ports - Ports we want to expose to the outside

      • 9001:9001

    • Networks - Internal app-network for inter-container communication

      • App-network

  • Nginx - Nginx service

    • Image - Nginx image we are using

      • Nginx:alpine

    • Container_name - name of container

      • Nginx

    • Restart - Service will always restart unless it is stopped

    • Volumes - Local directories we want to mount and files we want to mount to the service

      • ./www (LOCAL_WORKING_DIR) to /var/www/html (REMOTE_WORKING_DIR)

      • ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf

        • Default nginx configuration file

      • ./docker/nginx/conf.d/:/etc/nginx/conf.d/

        • Nginx site configuration files

      • ./docker/nginx/ssl/:/etc/nginx/ssl/

        • SSL directory

    • Ports - Ports we want to expose to the outside

      • 80:80

      • 443:443

    • Depends_on - Nginx depends on the app service to start

    • Networks - Internal app-network for inter-container communication

      • App-network

Next up is creating our PHP dockerfile


# PHP Version environment variable
ARG PHP_VERSION

# PHP Version alpine image to install based on the PHP_VERSION environment variable
FROM php:$PHP_VERSION-fpm-alpine

# Application environment variable
ARG APP_ENV

# Remote working directory environment variable
ARG REMOTE_WORKING_DIR

# Install Additional dependencies
RUN apk update && apk add --no-cache $PHPIZE_DEPS \
   build-base shadow nano curl gcc git bash \
   php7 \
   php7-fpm \
   php7-common \
   php7-pdo \
   php7-pdo_mysql \
   php7-mysqli \
   php7-mcrypt \
   php7-mbstring \
   php7-xml \
   php7-openssl \
   php7-json \
   php7-phar \
   php7-zip \
   php7-gd \
   php7-dom \
   php7-session \
   php7-zlib

# Install extensions
RUN docker-php-ext-install pdo pdo_mysql
RUN docker-php-ext-enable pdo_mysql

# install xdebug and enable it if the development environment is local
RUN if [ $APP_ENV = "local" ]; then \
   pecl install xdebug; \
   docker-php-ext-enable xdebug; \
fi;

# Install PHP Composer
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

# Remove Cache
RUN rm -rf /var/cache/apk/*

# Add UID '1000' to www-data
RUN apk add shadow && usermod -u 1000 www-data && groupmod -g 1000 www-data

# Copy existing application directory permissions
COPY --chown=www-data:www-data . $REMOTE_WORKING_DIR

# Change current user to www
USER www-data

# Expose port 9000 and start php-fpm server
EXPOSE 9000

# Run php-fpm
CMD ["php-fpm"]

Dockerfile explanation

  • ARG “VARIABLE”

    • sets the arguments supplied in the docker-comopose.yml for use within the Dockerfile

  • FROM php:$PHP_VERSION-fpm-alpine

    • is installing the php-fpm alpine image supplied through the variable

      • Alpine Linux is an independent, non-commercial, general purpose Linux distribution designed for power users who appreciate security, simplicity and resource efficiency. - https://alpinelinux.org/about/

      • Using Alpine Linux will help keep our image sizes down

  • RUN apk update

    • Is updating Alpine Linux and then adding our dependencies, if you need additional php extensions or linux packages this is where you would add them.

  • RUN docker-php-ext install and enable

    • install the php extension into docker and then enable it so we can edit the configuration of these extensions. For the sake of simplicity I am only install pdo. If you need a more robust configuration you would install and enable the extension here.

  • RUN if [ $APP_ENV = “local” ]

    • we are only installing xdebug if we are on a local development environment, there is no need to install it on staging or production.

  • RUN curl -sS composer

    • We are installing composer for laravel

  • RUN rm -rf /var/cache/apk/*

    • we are cleaning up the apk cache

  • RUN apk add shadow && usermod

    • we are adding the UID 1000 to www-data for php-fpm

  • COPY --chown=www-data

    • we are copying the application directory permissions

  • USER www-data

    • changing the current user to www

  • EXPOSE 9000

    • exposing port 9000 for internal use within the containers

  • CMD [“php-fpm”]

    • start php-fpm

Now lets create our default configuration files

Create a nginx.conf file in the root of our docker/nginx folder with the following contents.


pid /run/nginx.pid;
worker_processes auto;
worker_rlimit_nofile 65535;

events {
multi_accept on;
worker_connections 65535;
}

http {
charset utf-8;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
server_tokens off;
log_not_found off;
types_hash_max_size 2048;
client_max_body_size 16M;

# MIME
include mime.types;
default_type application/octet-stream;

# logging
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log warn;

# SSL
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;

# Diffie-Hellman parameter for DHE ciphersuites
ssl_dhparam /etc/nginx/dhparam.pem;

# OWASP B (Broad Compatibility) configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256;
ssl_prefer_server_ciphers on;

# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 1.0.0.1 8.8.8.8 8.8.4.4 208.67.222.222 208.67.220.220 valid=60s;
resolver_timeout 2s;

# load configs
include /etc/nginx/conf.d/*.conf;
}

Create a site.conf file in the root of our docker/nginx/conf.d folder with the following contents. This is a standard laravel nginx config.


server {

   listen 80;
   listen [::]:80;

   # For https
   # listen 443 ssl;
   # listen [::]:443 ssl ipv6only=on;
   # ssl_certificate /etc/nginx/ssl/default.crt;
   # ssl_certificate_key /etc/nginx/ssl/default.key;

   server_name test.local;
   root /var/www/html/public;
   index index.php index.html index.htm;

   location / {
        try_files $uri $uri/ /index.php$is_args$args;
   }

   location ~ \.php$ {
       try_files $uri /index.php =404;
       # We are using our app service container name instead of 127.0.0.1 as our connection
       fastcgi_pass app:9000;
       fastcgi_index index.php;
       fastcgi_buffers 16 16k;
       fastcgi_buffer_size 32k;
       fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
       #fixes timeouts
       fastcgi_read_timeout 600;
       include fastcgi_params;
   }

   location ~ /\.ht {
       deny all;
   }

   location /.well-known/acme-challenge/ {
       root /var/www/letsencrypt/;
       log_not_found off;
   }

   error_log /var/log/nginx/laravel_error.log;
   access_log /var/log/nginx/laravel_access.log;
}

I am using test.local as my local domain, be sure to add 127.0.0.1 test.local (or your preferred local domain) to your hosts file.

Create a xdebug.ini file in the root of our docker/php/config folder with the following contents.


xdebug.remote_enable=1
xdebug.remote_handler=dbgp
xdebug.remote_port=9000
xdebug.remote_autostart=1
xdebug.remote_connect_back=0
xdebug.idekey=PHPSTORM
xdebug.remote_host=host.docker.internal

The big takeaway with this xdebug configuration is setting connect_back to 0, setting your idekey to PHPSTORM, and setting your hoist to host.docker.internal. I ran into issues with enabling setting remote_connect_back instead of using the remote_host. This has been tested on Windows 10 WSL and has been working really well. If you can’t get xdebug to work with that configuration you may need to search around to find one that will work for you.

Let's build it

To build the environment make sure you are in your projects root folder and run docker-compose build and let it run. When it is done you should  see the following output.

Now that PHP is built lets run docker-compose up -d this command starts all of the containers in detached mode, if you want to be able to run the containers while looking at the output logs just run docker-compose up. If you are in detached mode and want to view the logs for a specific container you can run the following command, docker-compose logs app (container name.)

Installing Laravel

Now that are containers are running lets install laravel, run the following command to jump into the container docker-compose exec app bash.

Now that we are in the container run the following command to install laravel composer create-project --prefer-dist laravel/laravel . Setting the directory to the (.) just means we are going to install laravel inside the current directory.

Great Laravel is installed, now open up test.local (or the domain you set in the nginx config / hosts file) and you will see the Laravel welcome screen!

Setting the laravel mysql environment variables

Just like in the Nginx config where we set the fastcgi_pass to app:9000 we are going to do a similar thing to the mysql environment variables.


DB_CONNECTION=mysql
DB_HOST=database
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=user
DB_PASSWORD=secret

Now that we have the environment variables set lets exec back into our container (docker-compose exec app bash) and run the default migrations (php artisan migrate)

Creating a PHP remote debug configuration in PHPstorm

To enable use xdebug in PHPSTORM follow these steps

  • Top right of PHPSTORM there is a drop down, click add or edit configuration
  • Set the name (Can be anything)
  • Select Filter debug connection by IDE key
  • IDE Key is PHPSTORM
  • Click the servers button to the right of the server dropdown
  • Set the name of the server (Can be anything)
  • Set the host to your local site url (mine is test.local) Port 80, Debugger Xdebug
  • Open the File/Directory selection until you see the www folder
  • On the right where it says absolute path on the server click to edit it
  • Enter /var/www/html which is the remote path and hit enter on the keyboard to save it
  • Apply the changes then hit ok on the servers menu and then the debug menu

To test this go into the web.php routes file and add the following code


Route::get('/', function () {
   $word = 'Debugger';

   $word .= ' In Action';
  
   echo $word;
});

Break on the first $word variable and you will now be able to step through the code!

Sweet! Docker is setup, Laravel is installed and configured, our migrations have been ran, and our xDebug configuration for PHPSTORM has been setup!

Now all that's left to do is get building!