docker-compose up your entire Laravel + Apache + MySQL development environment.
Tags: beginners, laravel, docker, dockercompose
Published: 2019-04-04 14:28:30

---
title: docker-compose up your entire Laravel + Apache + MySQL development environment.
published: true
description: Onboarding new dev into your Laravel stack using Docker, docker-compose and some 1-liner bash scripts.
tags: #beginner, #laravel, #docker, #dockercompose
---

## Contents

- [Contents](#contents)
- [Intro](#intro)
- [The set-up](#the-setup)
- [Webserver image for Laravel](#webserver-image-for-laravel)
- [`docker-compose.yml`](#-raw-dockercomposeyml-endraw-)
- [Up and Running](#up-and-running)
- [Helper scripts (optional)](#helper-scripts-optional)
  - [`container`](#-raw-container-endraw-)
  - [`db`](#-raw-db-endraw-)
  - [`composer`](#-raw-composer-endraw-)
  - [`php-artisan`](#-raw-phpartisan-endraw-)
  - [`phpunit`](#-raw-phpunit-endraw-)
- [TL;DR](#tldr)

## Intro
When on-boarding new devs to contribute to your project, you probably don't want them to spend hours hopping between documentations and StackOverflow, figuring out how to get anything working. There is just so much stuffs they would potentially have to go through to have a version of the app running locally: php, ini config, php extensions, apache configs, apache site-enabled configs, set up mysql, ... the list goes on. That is except you have a `docker` environment set up, so they can simply:
```
$ git clone git@git.repo.url/laravel-project
$ cd laravel-project
$ docker-compose up
```
and be able to start with `composer`, `php artisan`, and write some code.

## The set-up
To demo an existing laravel app, I will be using a blank laravel app cloned from https://github.com/laravel/laravel.git
```
$ git clone https://github.com/laravel/laravel.git
$ cd laravel
$ git checkout -b dev-env
$ cp .env.example .env
```
Here's how I will structure my docker environment files:
```
  app
  |__bootstrap
  |__config
  |__database
  |__public
  |__resources
  |__routes
  |__run               (+)
     |__.gitkeep       (+)
  |__storage
  |__tests
  .dockerignore        (+)
  .editorconfig
  .env
  .env.example
  .gitattributes
  .gitignore
  artisan
  CHANGELOG.md
  composer.json
  docker-compose.yml   (+)
  Dockerfile           (+)
  package.json
  phpunit.xml
  readme.md
  server.php
  webpack.mix.js
```
The idea is we'll be builiding the image as well as running `docker-compose` commands from the main application folder, while `run` folder contains necessary config and local database for development. With `docker` volumes, we'll be able to keep the source, the `vendor` dependencies and local development database in our host, while all the runtime (`apache`, `php`) are kept and manged by the container.

In this article I'll explain to the best of my knowledge what each part of the set-up does. TL;DR as well as github link to list all the changes at the bottom if you just need a working version.

## Webserver image for Laravel
[`php-apache:7.2`](https://github.com/docker-library/php/blob/873725e57ec2fc5f2642dc0023676597bcc4bea9/7.2/stretch/apache/Dockerfile) image from the php dockerhub has out-of-the-box configurable and functional Apache webserver running `mod_php`, which is a great place to start with. We'll need a couple of extensions and some access control configuration to make development easier (optional). Here is the `Dockerfile`:
```Dockerfile
FROM php:7.2-apache

RUN apt-get update

# 1. development packages
RUN apt-get install -y \
    git \
    zip \
    curl \
    sudo \
    unzip \
    libicu-dev \
    libbz2-dev \
    libpng-dev \
    libjpeg-dev \
    libmcrypt-dev \
    libreadline-dev \
    libfreetype6-dev \
    g++

# 2. apache configs + document root
ENV APACHE_DOCUMENT_ROOT=/var/www/html/public
RUN sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf
RUN sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf

# 3. mod_rewrite for URL rewrite and mod_headers for .htaccess extra headers like Access-Control-Allow-Origin-
RUN a2enmod rewrite headers

# 4. start with base php config, then add extensions
RUN mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini"

RUN docker-php-ext-install \
    bz2 \
    intl \
    iconv \
    bcmath \
    opcache \
    calendar \
    mbstring \
    pdo_mysql \
    zip

# 5. composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

# 6. we need a user with the same UID/GID with host user
# so when we execute CLI commands, all the host file's ownership remains intact
# otherwise command from inside container will create root-owned files and directories
ARG uid
RUN useradd -G www-data,root -u $uid -d /home/devuser devuser
RUN mkdir -p /home/devuser/.composer && \
    chown -R devuser:devuser /home/devuser
```
Starting with the webserver itself, `php-apache` image by default set document root to `/var/www/html`. However since laravel `index.php` is inside `/var/www/html/public`, we need to edit the apache config as well as sites-available. We'll also enable `mod_rewrite` for url matching and `mod_headers` for configuring webserver headers.
```Dockerfile
ENV APACHE_DOCUMENT_ROOT=/var/www/html/public
RUN sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf
RUN sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf
```
Moving onto `php` configuration, we start by using the provded `php.ini`, then add a couple of extensions via `docker-php-ext-install`. The order of doing these tasks are not important (`php.ini` won't be overwritten) since the configs that loads each extensions are kept in separate files.

For `composer`, what we're doing here is fetching the `composer` binary located at `/usr/bin/composer` from the [`composer:latest`](https://github.com/composer/docker/blob/ebbb9efd87b78cf9b837f97105bcb6c5f0ee44ef/1.8/Dockerfile) docker image. Obviously you can specify any other version you want in the tag, instead of `latest`. This is part of docker's multi-stage build feature.
```Dockerfile
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
```

Final steps are optional. Since we're going to mount the application source code from host into the container for development, any command run from within the container CLI shouldn't affect host files/folder ownership. This is helpful for configs and such generated by `php artisan`. Here I'm using `ARG` to let other team members set their own `uid` that matches their host user `uid`.
```Dockerfile
ARG uid
RUN useradd -G www-data,root -u $uid -d /home/devuser devuser
RUN mkdir -p /home/devuser/.composer && \
    chown -R devuser:devuser /home/devuser
```

## `docker-compose.yml`
Webserver is set. Now we just need to bring a database container in using a `docker-compose` config

`docker-compose.yml`
```yaml
version: '3.5'

services:
  laravel-app:
    build:
      context: '.'
      args:
        uid: ${UID}
    container_name: laravel-app
    environment:
      - APACHE_RUN_USER=#${UID}
      - APACHE_RUN_GROUP=#${UID}
    volumes:
      - .:/var/www/html
    ports:
      - 8000:80
    networks:
      backend:
        aliases:
          - laravel-app

  mysql-db:
    image: mysql:5.7
    container_name: mysql-db
    volumes:
      - ./run/var:/var/lib/mysql
    environment:
      - MYSQL_ROOT_PASSWORD=securerootpassword
      - MYSQL_DATABASE=db
      - MYSQL_USER=dbuser
      - MYSQL_PASSWORD=secret
    networks:
      backend:
        aliases:
          - db

networks:
  backend:
    name: backend-network
```
A few things to go through here. First of all for the laravel container:
- `build:context` refers to the `Dockerfile` that we just written, kept in the same directory as `docker-compose.yml`.
- `args` is for the `uid` I mentioned above. We'll write `UID` value in the app `.env` file to let `docker-compose` pick it up.

`.env`
```
...
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

UID=1000
```
- `APACHE_RUN_USER` and `APACHE_RUN_GROUP` ENV variables comes with `php-apache`. By doing this, files generated by the webserver will also have consistent ownership.
- `volumes` directive tells `docker` to mount the host's app source code into `/var/www/html` - which is consistent with `apache` configuration. This enables any change from host files be reflected in the container. Commands such as `composer require` will add `vendor` to host, so we won't need to install dependencies everytime container is brought down and up again.
- If you are building container for CI / remote VM envrionment however, you'll need to add the source files into the container pre-build. For example:
```Dockerfile
COPY . /var/www/html
RUN cd /var/www/html && composer install && php artisan key:generate
```
- `ports` is optional, leave out if you're fine with running it under port 80. Alternatively, it can be configurable using `.env` similar to build args:
```yaml
ports:
  - ${HOST_PORT}:80
```
```
HOST_PORT=8080
```
- `networks` with `aliases` is also optional. By default, `docker-compose` create a `default` network prefixed with the parent folder name to connect all the services specified in `docker-compose.yml`. However if you have a development of more than 1 `docker-compose`, specifying `networks` name like this allow you to join it from the other `docker-compose.yml` files. `another-app` here will be able to reach `laravel-app` and vice versa, using the specified `aliases`.

`docker-compose.yml`
```yaml
services:
   another-app:
    networks:
      backend:
        aliases:
          - another-app

networks:
  backend:
    external:
      name: backend-network
```

Now moving onto `mysql`:
- [`mysql:5.7`](https://github.com/docker-library/mysql/blob/bb7ea52db4e12d3fb526450d22382d5cd8cd41ca/5.7/Dockerfile) is very configurable and just works well out-of-the-box. So we won't need to extend it.
- Simply pick up the `.env` set in laravel app to set username and password for the db user:
```yaml
environment:
   - MYSQL_ROOT_PASSWORD=securerootpassword
   - MYSQL_DATABASE=${DB_DATABASE}
   - MYSQL_USER=${DB_USERNAME}
   - MYSQL_PASSWORD=${DB_PASSWORD}
```
- Also make sure `.env DB_HOST` set to what mysql-db service name, or its aliases:
`.env`
```
DB_HOST=mysql-db
```
- Ideally you want to keep database changes in the repository, using a series of migrations and seeders. However if you want to start the mysql container with an existing SQL dump, simply mount the SQL file:
```yaml
volumes:
   - ./run/var:/var/lib/mysql
   - ./run/dump/init.sql:/docker-entrypoint-initdb.d/init.sql
```
- Using `volumes`, we're keeping the database locally under `run/var`, since any data written by `mysqld` is inside the container's `/var/lib/mysql`. We just need to ignore the local database in both `.gitignore` and `.dockerignore` (for build context):

`.gitignore`:
```
/node_modules
/public/hot
/public/storage
/storage/*.key
/vendor
.env
.phpunit.result.cache
Homestead.json
Homestead.yaml
npm-debug.log
yarn-error.log

run/var
```
`.dockerignore`:
```
run/var
```

## Up and Running
Now let's build the environment, and get it up running. We'll also be installing composer dependencies as well as some artisan command.
```
$ docker-compose build && docker-compose up -d && docker-compose logs -f
Creating network "backend-network" with the default driver
Creating mysql-db    ... done
Creating laravel-app ... done
Attaching to laravel-app, mysql-db
...
```
Once all the containers are up and running, we can check them by `docker ps`:
```
CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS              PORTS                  NAMES
c1ae3002d260        laravel_laravel-app   "docker-php-entrypoi…"   4 minutes ago       Up 4 minutes        0.0.0.0:8000->80/tcp   laravel-app
6f6546224051        mysql:5.7             "docker-entrypoint.s…"   4 minutes ago       Up 4 minutes        3306/tcp               mysql-db
```
Composer and artisan:
```
$ docker exec -it laravel-app bash -c "sudo -u devuser /bin/bash"
```
```
devuser@c1ae3002d260:/var/www/html$ composer install
...
Generating optimized autoload files
> Illuminate\Foundation\ComposerScripts::postAutoloadDump
> @php artisan package:discover --ansi
Discovered Package: beyondcode/laravel-dump-server
Discovered Package: fideloper/proxy
Discovered Package: laravel/tinker
Discovered Package: nesbot/carbon
Discovered Package: nunomaduro/collision
Package manifest generated successfully.
```
```
devuser@c1ae3002d260:/var/www/html$ php artisan key:generate
Application key set successfully.
```
```
devuser@c1ae3002d260:/var/www/html$ php artisan migrate
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table
```
```
devuser@c1ae3002d260:/var/www/html$ php artisan make:auth
Authentication scaffolding generated successfully.
```

With hostfile:
```
127.0.0.1       laravel-app.local
```
We're all set!
![App Screenshot](https://thepracticaldev.s3.amazonaws.com/i/fn4w33g06gm61rwkkqoe.png)
![App Screenshot](https://thepracticaldev.s3.amazonaws.com/i/n7a4zocaqlw127rcmsoj.png)

## Helper scripts (optional)
From time to time, I want to be able to quickly run CLI commands (`composer`, `artisan`, etc.) without having to type `docker exec` everytime. So here are some bash scripts I made wrapping around `docker exec`:
### `container`
```bash
#!/bin/bash

docker exec -it laravel-app bash -c "sudo -u devuser /bin/bash"
```
Running `./container` takes you inside the `laravel-app` container under user `uid(1000)` (same with host user)
```
$ ./container
devuser@8cf37a093502:/var/www/html$
```

### `db`
```bash
#!/bin/bash

docker exec -it mysql-db bash -c "mysql -u dbuser -psecret db"
```
Running `./db` will connect to your database container's daemon using mysql client.
```
$ ./db
mysql>
```

### `composer`
```bash
#!/bin/bash

args="$@"
command="composer $args"
echo "$command"
docker exec -it laravel-app bash -c "sudo -u devuser /bin/bash -c \"$command\""
```
Run any `composer` command, example:
```
$ ./composer dump-autoload
Generating optimized autoload files> Illuminate\Foundation\ComposerScripts::postAutoloadDump
> @php artisan package:discover --ansi
Discovered Package: beyondcode/laravel-dump-server
Discovered Package: fideloper/proxy
Discovered Package: laravel/tinker
Discovered Package: nesbot/carbon
Discovered Package: nunomaduro/collision
Package manifest generated successfully.
Generated optimized autoload files containing 3527 classes
```

### `php-artisan`
```bash
#!/bin/bash

args="$@"
command="php artisan $args"
echo "$command"
docker exec -it laravel-app bash -c "sudo -u devuser /bin/bash -c \"$command\""
```
Run `php artisan` commands, example:
```
$ ./php-artisan make:controller BlogPostController --resource
php artisan make:controller BlogPostController --resource
Controller created successfully.
```

### `phpunit`
```bash
#!/bin/bash

args="$@"
command="vendor/bin/phpunit $args"
echo "$command"
docker exec -it laravel-app bash -c "sudo -u devuser /bin/bash -c \"$command\""
```
Run `./vendor/bin/phpunit` to execute tests, example:
```
$ ./phpunit --group=failing
vendor/bin/phpunit --group=failing
PHPUnit 7.5.8 by Sebastian Bergmann and contributors.



Time: 34 ms, Memory: 6.00 MB

No tests executed!
```

## TL;DR
Links:
- [Repository](https://github.com/veevidify/laravel-apache-docker)
- [commit](https://github.com/veevidify/laravel-apache-docker/commit/48933bd10c6da6c1d3addb1b1dfc6d9b7ec4d49b)

`Dockerfile` consists of basic apache document root config, mod_rewrite and mod_header, composer and sync container's uid with host uid.

`docker-compose.yml` boots up `php-apache` (mount app files) and `mysql` (mount db files), using `networks` to interconnect.

Use the environment:
```
$ docker-compose build && docker-compose up -d && docker-compose logs -f
$ ./composer install
$ ./php-artisan key:generate
```