loading...

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

veevidify profile image V Ng Updated on ・8 min read

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

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

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

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

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

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

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

networks:
  backend:
    external:
      name: backend-network

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

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

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

db

#!/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

#!/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

#!/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

#!/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:

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

Discussion

pic
Editor guide
Collapse
memel06 profile image
Memel06

Awesome article, thanks for it!

I thought that integrating also phpmyadmin would be very useful, so I added to my docker-compose.yaml this little snippet

phpmyadmin:
    image: phpmyadmin/phpmyadmin
    links:
      - mysql-db
    environment:
      PMA_HOST: mysql-db
      PMA_PORT: 3306
    ports:
      - '8080:80'
    networks:
      backend:
        aliases:
          - phpmyadmin

That's it, now the useful phpmyadmin buddy joins the squad :)

This can be improved adding PMA_HOST and PMA_PORT to the .env file for complete customization

Collapse
manishsharmait52 profile image
manishsharmait52

can you please provide me docker file and docker compose related full source code download link ? because i tried last 2 days but not getting success with docker file and docker compose to setup any php project.

Collapse
veevidify profile image
V Ng Author

Are you setting up a new Laravel project or trying to create docker environment for existing one?

If you are setting up new Laravel project, you can refer to my repo:
github.com/veevidify/laravel-apach...

and then do:

docker-compose build
docker-compose up -d
docker-compose logs -f
Collapse
tenerecodes profile image
tenerecodes

HI there, i am setting an new project with the latest laravel repo + your dockerfile and docker-compose.yml i get this error:

Step 13/14 : RUN useradd -G www-data,root -u $uid -d /home/devuser devuser
---> Running in e24b11b14717
useradd: invalid user ID '-d'
ERROR: Service 'laravel-app' failed to build: The command '/bin/sh -c useradd -G www-data,root -u $uid -d /home/devuser devuser' returned a non-zero code: 3

any solution ?

Thread Thread
veevidify profile image
V Ng Author

If you refer back to docker-compose.yml, this part:

...
      args:
        uid: ${UID}

docker-compose requires UID environment variable to be set. Check your .env file, make sure

UID=1000 # or whatever your host's user id is

If you're unsure about host user id, type id in your terminal to double check.

Thread Thread
tenerecodes profile image
tenerecodes

That worked fine thank you. but i have another question, if you permit, regarding your Dockerfile. how to copy npm from nodejs docker image like you did with composer.

the reason i ask this is because when i run "php artisan ui vue --auth" i get this message:

Vue scaffolding installed successfully.
Please run "npm install && npm run dev" to compile your fresh scaffolding.
Authentication scaffolding generated successfully.

Thread Thread
veevidify profile image
V Ng Author

There is a neat little trick to run a docker container as if it's a binary in your host. This is very useful for things like composer or npm which only modifies files statically. Example:

$ cd /your/project/folder
$ docker run -it -u 1000:1000 -v "$PWD":/usr/src/app -w /usr/src/app --rm node:10 npm install

Make sure all the parameters are what you need, e.g. uid, node version, etc.

Even though I copy composer binary into the my own app image for "encapsulation", if we intend to only ever use such binary for static files modification on the host, that wouldn't be necessary. Instead this trick makes it more clean once you start having 5 6 container runtime for these sorts of purposes.

I conveniently "missed out" this part due to how varied people's use cases with node are.

Collapse
manishsharmait52 profile image
manishsharmait52

Thank you so much to reply me , i will check it and let you know if i am facing another issue .

Thank you again ..... !

Thread Thread
manishsharmait52 profile image
manishsharmait52

Hello Sir, I facing 500 server error issue , can you please give me some tips. what happens over there ?

Collapse
marcuswii profile image
marcuswii

Trying to learn laravel and docker.
I cloned your repo and ran
docker-compose build
docker-compose up -d
docker-compose logs -f

docker ps gives me two containers. If I type localhost:8000 I see that the webserver is running but I get a Forbidden
You don't have permission to access this resource.
Apache/2.4.38 (Debian) Server at localhost Port 8000
error.
The log file gives : [authz_core:error] [pid 16] [client 172.19.0.1:43254] AH01630: client denied by server configuration: /var/www/html/public
and also
AH00112: Warning: DocumentRoot [/var/www/html/public] does not exist

I.m running on Windows10, under WSL.

Can you help this one?

Marcus

Collapse
taviroquai profile image
Marco Afonso

Thanks for this writting :)

I'm setting up a complete automated deployment of a laravel app using docker and I found that Laravel uses .env files instead of docker-compose.yml enviroment section.

Is there a way to make laravel to use docker-compose.yml environment section?

Thanks!

Collapse
veevidify profile image
V Ng Author

Right, so what .env essentially does is that it emulates Laravel reading the actual runtime environment variables. In other words Laravel treats .env variables as if they're your actual host or build context's environment variables. Conveniently for us, docker-compose is also able to read from .env.

So to put it clearly, both Laravel and docker read these variables in a build context, which are mocked by .env file for development purposes.

In your deployment / build context, you would want to export these variables too. To name a few, Gitlab CI allows configuring them under Settings > CI/CD. Circle CI does also, under Build Settings > Environment Variables. If you only deploy from local (instead of remote CI), simply run deployment from within a container built with .env variables exported.

Collapse
dhodyrahmad profile image
Dhody Rahmad Hidayat

Hello sir, there's any different configuration between setting up a new Laravel project or trying to create docker environment for existing one?

Collapse
veevidify profile image
V Ng Author

The difference lies in post containers startup.

So for existing project, we will create all the necessary docker files, such as Dockerfile, docker-compose, place all the configs in place.

Since we're volume mounting the app, depends on what your current setup is missing. If you havent run composer, and there is no vendor folder, you can go ahead and use the container to run composer install. If there is no app key, or passport keys in your setup, you will run respective artisan command to setup.

Similarly, since this setup aims at a new database container, you will start with an empty database (we mount it under var/ inside the project folder). If you have any migrations and seeders, you will need to run those to get a database setup. Alternatively if you have developed a dev database without those migrations and seeders, you can initiate and fill the database with a sql file mounted under run/dump/init.sql as I explained in the post.

Collapse
dhodyrahmad profile image
Dhody Rahmad Hidayat

Thank you for your response, I'm newbie in this scope. I have a Laravel application also have migrations and seeders for this application. I've done create necessary docker files and place the configs in place. But there's some problem at migration:

Access denied for user 'root'@'172.19.0.2' (using password: YES)

did I missed something?

Thread Thread
veevidify profile image
V Ng Author

that probably mean your database username and password are not set to what the docker container is built with.

Make sure your environment variables for the mysql service (specified in docker-compose.yml) are consistent with the process you are using to access the database (whatever you are trying that yields that error).

Collapse
edwinblack profile image
edwinblack

when execute the code php artisan migrate print a error

Database name seems incorrect: You're using the default database name laravel. This database does not exist.

Collapse
manishsharmait52 profile image
manishsharmait52

Hello Sir, I facing 500 server error issue , can you please give me some tips. what happens over there ?

Collapse
veevidify profile image
V Ng Author

The fact that you get a response from the webserver means the docker environment works.

If you get require_once ... vendor/autoload error, then you'd probably missing composer dependencies.

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

gets you inside the container.

devuser@0e382dff6a23:/var/www/html$

then

composer install
php artisan key:generate
php artisan migrate
php artisan config:cache
Collapse
manishsharmait52 profile image
manishsharmait52

okay i will check again .

Thank you .

Collapse
nickstock profile image
nickstock

Great post, I found this very easy to set up and work's like a charm, much quicker than setting up on a vm.

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
yaby88 profile image
yaby

git clone git@git.repo.url/laravel-project

fatal: repository 'git@git.repo.url/laravel-project' does not exist

why repository is not exist ????

Collapse
vicf profile image
Victor

This is just an example of how you set up YOUR OWN project, so "laravel-project" is just a fake example name. You can read it as :

git clone git@git.repo.url/MY-OWN-PROJECT-CREATED-AFTER-READING-THIS-ARTICLE

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?

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
ahmedsliman profile image
Ahmed sliman

Hello,

Amazing Tut. thanks,

I didn't get the last point regarding Helpers scripts, Where can I write the bash script like this

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

Collapse
veevidify profile image
V Ng Author

Those are quite literally bash scripts (if your development is linux based) which wraps around your host's docker binary, to run commands inside a running container.

You can have these scripts anywhere. I included them within the project folder itself to share with others working on the repo. Checkout the "TL;DR" section, and my repo on github: github.com/veevidify/laravel-apach...

Collapse
atnaize profile image
Atnaize

Hello,

Is it possible to run the composer update and migrations directly from the Dockerfile?

I tried to

WORKDIR /var/www/html
RUN composer install

But this is not working

Collapse
veevidify profile image
V Ng Author

Hi,
The setup I had on the repo use volume mount for the source, so that any changes, including composer install (vendor folder) persist on the host, for ease of development.

If you simply want a running app with vendor packages inside the container, you need to add composer.json into the container during build stage. Refer to this part of the article:

...you'll need to add the source files into the container pre-build...

COPY . /var/www/html
RUN cd /var/www/html && composer install && php artisan key:generate

Note that by doing this, your host pc won't have the vendor packages persisted.