DEV Community

V
V

Posted on • Edited on

docker-compose up your entire Laravel + Apache + MySQL development environment.

Contents

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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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 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:



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


Enter fullscreen mode Exit fullscreen mode

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.



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


Enter fullscreen mode Exit fullscreen mode

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 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.



COPY --from=composer:latest /usr/bin/composer /usr/bin/composer


Enter fullscreen mode Exit fullscreen mode

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.



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


Enter fullscreen mode Exit fullscreen mode

docker-compose.yml

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

docker-compose.yml



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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode
  • 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


Enter fullscreen mode Exit fullscreen mode


HOST_PORT=8080


Enter fullscreen mode Exit fullscreen mode
  • 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



services:
   another-app:
    networks:
      backend:
        aliases:
          - another-app

networks:
  backend:
    external:
      name: backend-network


Enter fullscreen mode Exit fullscreen mode

Now moving onto mysql:

  • mysql:5.7 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`
Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode
  • 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


Enter fullscreen mode Exit fullscreen mode

.dockerignore:



run/var


Enter fullscreen mode Exit fullscreen mode

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
...


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

Composer and artisan:



$ docker exec -it laravel-app bash -c "sudo -u devuser /bin/bash"


Enter fullscreen mode Exit fullscreen mode


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.


Enter fullscreen mode Exit fullscreen mode


devuser@c1ae3002d260:/var/www/html$ php artisan key:generate
Application key set successfully.


Enter fullscreen mode Exit fullscreen mode


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


Enter fullscreen mode Exit fullscreen mode


devuser@c1ae3002d260:/var/www/html$ php artisan make:auth
Authentication scaffolding generated successfully.


Enter fullscreen mode Exit fullscreen mode

With hostfile:



127.0.0.1       laravel-app.local


Enter fullscreen mode Exit fullscreen mode

We're all set!
App Screenshot
App Screenshot

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



#!/bin/bash

docker exec -it laravel-app bash -c "sudo -u devuser /bin/bash"


Enter fullscreen mode Exit fullscreen mode

Running ./container takes you inside the laravel-app container under user uid(1000) (same with host user)



$ ./container
devuser@8cf37a093502:/var/www/html$


Enter fullscreen mode Exit fullscreen mode

db



#!/bin/bash

docker exec -it mysql-db bash -c "mysql -u dbuser -psecret db"


Enter fullscreen mode Exit fullscreen mode

Running ./db will connect to your database container's daemon using mysql client.



$ ./db
mysql>


Enter fullscreen mode Exit fullscreen mode

composer



#!/bin/bash

args="$@"
command="composer $args"
echo "$command"
docker exec -it laravel-app bash -c "sudo -u devuser /bin/bash -c \"$command\""


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

php-artisan



#!/bin/bash

args="$@"
command="php artisan $args"
echo "$command"
docker exec -it laravel-app bash -c "sudo -u devuser /bin/bash -c \"$command\""


Enter fullscreen mode Exit fullscreen mode

Run php artisan commands, example:



$ ./php-artisan make:controller BlogPostController --resource
php artisan make:controller BlogPostController --resource
Controller created successfully.


Enter fullscreen mode Exit fullscreen mode

phpunit



#!/bin/bash

args="$@"
command="vendor/bin/phpunit $args"
echo "$command"
docker exec -it laravel-app bash -c "sudo -u devuser /bin/bash -c \"$command\""


Enter fullscreen mode Exit fullscreen mode

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!


Enter fullscreen mode Exit fullscreen mode

TL;DR

Links:

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


Enter fullscreen mode Exit fullscreen mode

Latest comments (37)

Collapse
 
mdr0414 profile image
Mark Rafn

Hello - thank you for the great article. I am very very close to getting this working with an EXISTING Laravel 7 / PHP 7.4 project. But I am having a little difficulty ...

Everything works fine, I can run php artisan commands within the container, login to MySQL container and see tables, etc. But when I try to run in the browser with localhost:8000/ I get the following error: Exception -- The /var/www/html/bootstrap/cache directory must be present and writable.

Looking at the folder within the Docker container's Exec ...

ls bootstrap/cache -al

total 12
drwxrwxr-x 2 root nogroup 4096 Sep 27 16:23 .
drwxrwxr-x 3 root nogroup 4096 Mar 30 2018 ..
-rwxrwxr-- 1 root nogroup 14 Mar 30 2018 .gitignore

The same folder on my local machine is:
total 12
drwxrwxr-x 2 mark www-data 4096 Sep 27 11:23 .
drwxrwxr-x 3 mark www-data 4096 Mar 30 2018 ..
-rwxrwxr-- 1 mark www-data 14 Mar 30 2018 .gitignore

the folder is there and writeable ?? I am sure this is a permissions thing but unsure how to correct it.

Any idea how to possibly fix this?

tia
_mark

Collapse
 
tracydavis profile image
Tracy • Edited

My laravel application is working on the localhost. But while running through localhost:port , I get error of 403.
This is the error:

Forbidden
You don't have permission to access this resource.

Collapse
 
mrigesh901 profile image
Mrigesh patni

RUN composer install:

0 0.554 Composer plugins have been disabled for safety in this non-interactive session. Set COMPOSER_ALLOW_SUPERUSER=1 if you want to allow plugins to run as root/super user.

0 0.554 Do not run Composer as root/super user! See getcomposer.org/root for details

0 0.559 Composer could not find a composer.json file in /var/www/html

0 0.559 To initialize a project, please create a composer.json file. See getcomposer.org/basic-usage


failed to solve: executor failed running [/bin/sh -c composer install]: exit code: 1
PS C:\xampp\htdocs\portal>

Collapse
 
firecentaur profile image
Paul Preibisch • Edited

By the way, if doing this on your local machine, make sure you
change docker-composer.yml to use bind - otherwise when you run the command .container, no files will be listed inside!

volumes:
#- ./:/var/www/html

- type: bind
source: ./
target: /var/www/html

Collapse
 
firecentaur profile image
Paul Preibisch

Hi there, the containers are running, but it seems that the volume mapping never worked. When I look in the container, no files are listed except the run folder.

Collapse
 
samuraipetrus profile image
Petrus Nogueira

In order to help other users, i’ll share here some troubles i was facing while running docker-compose build.

  1. No package oniguruma found

How to solve: Add libonig-dev package to apt-get's installation

  1. No package libzip found

How to solve: Add libzip-dev package to apt-get's installation

Hope it was helpful to someone! 😄

Collapse
 
kevintimothyift profile image
kevintimothyift

can you solve this error on composer install?

devuser@b37b652e508b:/var/www/html$ composer install
Installing dependencies from lock file (including require-dev)
Verifying lock file contents can be installed on current platform.
Package operations: 101 installs, 0 updates, 0 removals

[RuntimeException]

/var/www/html/vendor does not exist and could not be created.

Collapse
 
davidramos010 profile image
David Ramos

I have this error when I login: GET localhost:8000/js/app.js net::ERR_ABORTED 404 (Not Found).

I need help, I could not solve it.

Collapse
 
akonibrahim profile image
Akonibrahim

Thank you for this amazing post.
I'm trying to dockerize an existing laravel application using your post. But my static files are returning 404.
stackoverflow.com/questions/626231...

Collapse
 
rant989 profile image
rant989

Awesome, thanks for it!
It helped me a lot

If I want to add SSL what is the way to do this?