Laravel Docker Compose stack
Tags: laravel, php, docker, alpine
Published: 2021-08-27 02:44:42

In my last publishing, I talked about how I prefer to build my Laravel Docker image for production environments.

Now, I would like to share how I use Docker Compose to extend the production environment into a local environment and have some development conveniences such as xdebug, linters, and static analysis tools available to use through Compose.

As for services compounding the stack, we will have app, mysql, and redis. Each service will use a specific Docker image; the app service will use the previously built [Dockerfile](https://dev.to/jackmiras/laravel-with-php7-4-in-an-alpine-container-3jk6#dockerfile), while mysql and redis will look for their images on Docker Hub.

### Content
- [Docker Compose](#docker-compose)
- [Version](#version)
- [Services](#services)
    - [App service](#app-service)
        - [Directive build](#app-directive-build)
        - [Directive container_name](#app-container-name-directive)
        - [Directive command](#app-command-directive)
        - [Directive ports](#app-ports-directive)
        - [Directive env_file](#app-env-file-directive)
        - [Directive volumes](#app-volumes-directive)
        - [Directive networks](#app-networks-directive)
    - [MySQL service](#mysql-service)
        - [Directive image](#mysql-image-directive)
        - [Directive container_name](#mysql-container-name-directive)
        - [Directive ports](#mysql-ports-directive)
        - [Directive volumes](#mysql-volumes-directive)
        - [Directive environment](#mysql-environment-directive)
        - [Directive networks](#mysql-networks-directive)
        - [Connecting Laravel to MySQL](#connecting-laravel-to-mysql)
    - [Redis service](#redis-service)
        - [Directive image](#redis-image-directive)
        - [Directive container_name](#redis-container-name-directive)
        - [Directive ports](#redis-ports-directive)
        - [Directive volumes](#redis-volumes-directive)
        - [Directive networks](#redis-networks-directive)
        - [Connecting Laravel to Redis](#connecting-laravel-to-redis)
- [Volumes](#volumes)
- [Networks](#networks)

### <a id="docker-compose"></a>Docker Compose ###

Down below, there is an entire docker-compose.yml; read it carefully. YAML files strongly rely on indentation, and their directives depend entirely on the interpreter.

A good part of the directives will be explained further, but if you want to know every bit of this file, check the [compose file reference](https://docs.docker.com/compose/compose-file/compose-file-v3/) doc.

```yml
---
version: "3.8"
services:
  app:
    build: .
    container_name: laravel_scaffold
    command: >-
      sh -c "
        apk add php82-pecl-xdebug && composer install ;\
        cp .docker/php.ini-development /etc/php82/php.ini ;\
        cp .docker/xdebug.ini /etc/php82/conf.d/50_xdebug.ini ;\
        php artisan serve --host 0.0.0.0 --port 80"
    ports:
      - "8000:80"
    env_file:
      - .env
    volumes:
      - .:/var/www/html:rw
    networks:
      - app_network

  mysql:
    image: mysql:8
    container_name: laravel_scaffold_db
    ports:
      - "3306:3306"
    restart: "always"
    volumes:
      - db_data:/var/lib/mysql
    environment:
      - MYSQL_DATABASE=homestead
      - MYSQL_USER=homestead
      - MYSQL_PASSWORD=secret
      - MYSQL_ROOT_PASSWORD=secret
    networks:
      - app_network

  redis:
    image: redis:6-alpine
    container_name: laravel_scaffold_cache
    ports:
      - "6379:6379"
    volumes:
      - cache_data:/data
    networks:
      - app_network

volumes:
  db_data:
    driver: "local"
  cache_data:
    driver: "local"

networks:
  app_network:
    driver: "bridge"
```

### <a id="version"></a>Version ###

The first step towards creating docker-compose.yml is to define the version of the file format.

```yml
version: "3.8"
```

The file format version is directly related to the Docker releases, which means that this version may change according to the version of the Docker engine running on your computer.

### <a id="services"></a>Services ###

Services are a set of configurations that are applied to each container at the start of that service, much like passing command-line parameters to docker run. To let Compose know that the definition of services is happening, you need to place the directive `services:`, right after the line defining the version, as the docker-compose up above shows.

#### <a id="app-services"></a>App service ####

As mentioned before, the services are app, mysql, and redis. Let's dive into the app service, since this service holds some interactions with the container previously built using the Dockerfile from the last publishing.

```yml
app:
  build: .
  container_name: laravel_scaffold
  command: >-
    sh -c "
      apk add php82-pecl-xdebug && composer install ;\
      cp .docker/php.ini-development /etc/php82/php.ini ;\
      cp .docker/xdebug.ini /etc/php82/conf.d/50_xdebug.ini ;\
      php artisan serve --host 0.0.0.0 --port 80"
  ports:
    - "8000:80"
  env_file:
    - .env
  volumes:
    - .:/var/www/html:rw
  networks:
    - app_network
```

As the code shows, `app:` is both a directive and the name definition for the service. Under this service definition, we have seven configuration directives: build, container_name, command, ports, env_file, volumes, and networks.

##### <a id="app-directive-build"></a>Directive build #####

```yml
build: .
```

It tells Compose that we want to build a container when this service specification gets processed, while the `.` after the directive tells Compose that the container must be built from the Dockerfile that is relative to docker-compose.yml.

> Remember, they have to be in the same directory, otherwise, you have to specify the path to the Dockerfile. A suggestion is to keep both the Dockerfile and docker-compose.yml at the root of the Laravel projects.

##### <a id="app-container-name-directive"></a>Directive container_name #####

```yml
container_name: laravel_scaffold
```

It specifies a custom container name rather than a generated default one, works for built or pulled containers, and makes it easier to identify containers from specific docker-compose stacks. In this case, it's saying that the container behind the app service should be named _laravel_scaffold_.

> A good naming convention around container_name will help you find and manage containers easily, especially if you have a few docker-compose.yml files. This can easily grow to dozens of containers and having standardized names will help you know if a Redis container is from a Laravel or an Express.js stack.

##### <a id="app-command-directive"></a>Directive command #####

```yml
command: >-
  sh -c "
    apk add php82-pecl-xdebug && composer install ;\
    cp .docker/php.ini-development /etc/php82/php.ini ;\
    cp .docker/xdebug.ini /etc/php82/conf.d/50_xdebug.ini ;\
    php artisan serve --host 0.0.0.0 --port 80"
```

It's used to override the default command from the Dockerfile in use; looking at the first line, we can see that xdebug and composer with all dependencies got installed.

On the second line, the _php.ini-development_ file gets copied from a local .docker folder into **/etc/php82/php.ini** at the container. This file contains all the configurations that PHP will run on. The content of this file was copied from [PHP's official repository on GitHub](https://github.com/php/php-src/blob/master/php.ini-development).

On the third line, the _xdebug.ini_ file gets copied from a local .docker folder into **/etc/php82/conf.d/50_xdebug.ini** at the container. This file contains the configurations that allow the host, your computer, to connect to the container and use xdebug. The xdebug configurations are the following.

```ini
zend_extension=xdebug.so
xdebug.mode=develop,coverage,debug,profile
xdebug.idekey=docker
xdebug.remote_handler=dbgp
xdebug.start_with_request=yes
xdebug.log=/dev/stdout
xdebug.log_level=0
xdebug.client_port=9003
xdebug.client_host=<YOUR_COMPUTER_IP>
```

Lastly, Laravel's standard server got started using the Artisan command-line interface. Since we overrode the Dockerfile command, a server must be started to serve the application.

> This approach may seem odd at first, but this is what makes it possible to have a Dockerfile with production definitions only. Installing xdebug will allow any person working in the project to use a full-featured debugger, while the installation of all composer dependencies allows the person to use hooks, linters, static analysis tools, or any other development tool in a containerized environment.

> It is helpful because it avoids people having to set up a development environment on their computers; with this approach, they just need to run the Compose stack, while the DevOps team can just build the Dockerfile and deploy it.

##### <a id="app-ports-directive"></a>Directive ports #####

```yml
ports:
  - "8000:80"
```

It exposes ports in the format HOST:CONTAINER; hence, the directive is mapping port 8000 from the host to port 80 of the container.

The host port of choice is arbitrary; any available port can be used, while the container port has to match the port used to serve the application; as the third line of the command directive shows, that port is port 80.

Then, to access the application from Postman, Insomnia, or a browser, you just need to use _http://localhost:8000_.

##### <a id="app-env-file-directive"></a>Directive env_file #####

```yml
env_file:
  - .env
```

Add environment variables from a file; it can be a single value or a list. Hence, the directive is loading the .env file that is relative to docker-compose.yml.

##### <a id="app-volumes-directive"></a>Directive volumes #####

```yml
volumes:
  - .docker/xdebug.ini
  - .:/var/www/html:rw
```

It maps files or directories in the format SOURCE:TARGET[:MODE]. This syntax can be used to ignore or sync volumes between the host and the container and specify modes or permissions for the volumes mapped.

In the first line, we are specifying a SOURCE volume without TARGET or MODE; this syntax will ignore the file **.docker/xdebug.ini**. The file got ignored because we already copied him to **/etc/php82/conf.d/50_xdebug.ini** at the command directive, and there is no need to copy him again.

In the second line, we are syncing the directory where docker-compose.yml is into **/var/www/html** of the container with reading and write permission. As mentioned, docker-compose.yml is usually at the root of my Laravel projects; this means that the entire project got synced.

> The second line of the volume mapping is the approach that allows live reloading in the project; in this way, you don't need to stop the container and spin it up again after changing something in the text editor or IDE. Since the volumes (directories and files) are synced, any changes made on the host will reflect in the container, and vice versa.

##### <a id="app-networks-directive"></a>Directive networks #####

```yml
networks:
  - app_network
```

It specifies networks to join, referencing entries under the top-level networks key. The app_network will be defined and explained further in this article.

#### <a id="mysql-service"></a>MySQL service ####

```yml
mysql:
  image: mysql:8
  container_name: laravel_scaffold_db
  ports:
    - "3306:3306"
  volumes:
    - db_data:/var/lib/mysql
  environment:
    - MYSQL_DATABASE=homestead
    - MYSQL_USER=homestead
    - MYSQL_PASSWORD=secret
    - MYSQL_ROOT_PASSWORD=secret
  networks:
    - app_network
```

As mentioned before, `mysql:` is both a directive and the name definition for the service. Under this service definition, we have six configuration directives: image, container_name, ports, environment, and networks.

##### <a id="mysql-image-directive"></a>Directive image #####

```yml
image: mysql:8
```

Specify the image to start the container from, usually a repository with a tag. Bear in mind that if the tag is not present, the Docker engine will look for the repository plus the _latest_ tag. In case the image does not exist, Compose attempts to pull it, usually from Docker Hub, unless you have also specified build, in which case it builds it using the specified options and tags it with the specified tag.

> In case you have a custom registry configured, such as AWS ECR, the Docker engine will look into ECR and try to find an image _mysql_ with tag _8_ and only if an image cannot be found in ECR will it fall back to Docker Hub, so be careful with your image's naming convention.

##### <a id="mysql-container-name-directive"></a>Directive container_name #####

```yml
container_name: laravel_scaffold_db
```

It specifies a custom container name rather than a generated default one; it works for built or pulled containers. In this case, it's defining the container name behind the mysql service as _laravel_scaffold_db_.

##### <a id="mysql-ports-directive"></a>Directive ports #####

```yml
ports:
  - "3306:3306"
```

The directive maps port 3306 from the host to port 3306 of the container. To access the application from MySQL Workbench, DBeaver, or DataGrip you just need to use _localhost_ as the host at port _3306_.

##### <a id="mysql-volumes-directive"></a>Directive volumes #####

```yml
volumes:
  - db_data:/var/lib/mysql
```

Here, we are using `db_data:` as a named volume in the top-level volumes key. The definition of the volume will be explained further, but basically, Compose gets instructed to create a db_data directory at **/var/lib/docker/volumes**.

> Since **/var/lib/docker/volumes/db_data** is being created the result of the volume mounting is that everything crated by MySQL at **/var/lib/mysql** will be synced to the db_data directory, this includes the databases and the data stored in them.

##### <a id="mysql-environment-directive"></a>Directive environment #####

```yml
environment:
  - MYSQL_DATABASE=homestead
  - MYSQL_USER=homestead
  - MYSQL_PASSWORD=secret
  - MYSQL_ROOT_PASSWORD=secret
```

Defines environment variables for the container; here we are defining MySQL variables that define the default database, user, password, and root password. Note that the environment variables follow the same patter that the .env file present in the Laravel project that we loaded at the app service using the env_file directive.

##### <a id="mysql-networks-directive"></a>Directive networks #####

```yml
networks:
  - app_network
```

It specifies networks to join, referencing entries under the top-level networks key. The app_network will be defined and explained further in this article.

##### <a id="connecting-laravel-to-mysql"></a>Connecting Laravel to MySQL #####

With the app service defined and the Laravel application ready to run, we need to update Laravel's .env file to connect to this MySQL service. For that, it's necessary to change the environment variable _DB_HOST=127.0.0.1_ to _DB_HOST=mysql_.

As you may have noticed, we are using the service name _mysql_ as the host because Docker resolves the service name into a network connection and further into an IP address that can be used between _app_ and _mysql_ services containers.

#### <a id="redis-service"></a>Redis service ####

```yml
redis:
  image: redis:alpine
  container_name: laravel_scaffold_cache
  ports:
    - "6379:6379"
  volumes:
    - cache_data:/data
  networks:
    - app_network
```

As mentioned before, `redis:` is both a directive and the name definition for the service. Under this service definition, we have five configuration directives: image, container_name, ports, environment, and networks.

##### <a id="redis-image-directive"></a>Directive image #####

```yml
image: redis:alpine
```

As mentioned, this directive specifies the image to start the container from a repository, and in case the image does not exist, Compose will attempt to pull it from Docker Hub.

##### <a id="redis-container-name-directive"></a>Directive container_name #####

```yml
container_name: laravel_scaffold_cache
```

It specifies a custom container name rather than a generated default one; it works for built or pulled containers. In this case, it's defining the container name behind the redis service as _laravel_scaffold_cache_.

##### <a id="redis-ports-directive"></a>Directive ports #####

```yml
ports:
  - "6379:6379"
```

The directive maps port 6379 from the host to port 3306 of the container. To access the application from RedisInsight, or RDM (Redis Desktop Manager), you just need to use _localhost_ as the host at port _6379_.

##### <a id="redis-volumes-directive"></a>Directive volumes #####

```yml
volumes:
  - cache_data:/data
```

Here, we are using `cache_data:` as a named volume in the top-level volumes key. The definition of the volume will be explained further, but basically, Compose gets instructed to create a cache_data directory at **/var/lib/docker/volumes**.

> Since **/var/lib/docker/volumes/cache_data** is being created the result of the volume mounting is that everything crated by Redis at **/data** will be synced to the cache_data directory, this includes the databases and the hashes stored in them.

##### <a id="redis-networks-directive"></a>Directive networks #####

```yml
networks:
  - app_network
```

It specifies networks to join, referencing entries under the top-level networks key. The app_network will be defined and explained further in this article.

##### <a id="connecting-laravel-to-redis"></a>Connecting Laravel to Redis #####

To connect to the Redis service, we need to update Laravel's .env file, changing the environment variable from _REDIS_HOST=127.0.0.1_ to _REDIS_HOST=redis_. As mentioned, we have to use the service name as the host, so Docker can resolve the service name into a network connection that can be used between _app_ and _redis_ services containers.

### <a id="volumes"></a>Volumes ###

```yml
volumes:
  db_data:
    driver: "local"
  cache_data:
    driver: "local"
```

Here we have what Docker Compose defines as top-level volumes; these volumes get defined outside the services. This approach is used when a high level of reuse is needed or if volumes have a lifecycle that differs from the service.

For both volumes, we are defining the named volumes `db_data:`, and `cache_data:`. Similar to the service, these are directives and name definitions; the volume definitions also have the directive `driver:` set to `local`, which means that this volume will be attached to the host disk.

Since the driver is local, the result will be **/var/lib/docker/volumes/db_data** and **/var/lib/docker/volumes/cache_data** directories getting created. After attaching these volumes to a service, everything created by the service will be synced to the previously created directories.

Furthermore, the data on volumes will not be deleted when the service using the volume gets restarted, recreated, or deleted, and this is a behavior that we want when using volumes with persistent data services, such as MySQL and Redis.

### <a id="networks"></a>Networks ###

```yml
networks:
  app_network:
    driver: "bridge"
```

Here we are defining top-level networks; this network got defined outside the service. The network definition also has the directive `driver:` set to `bridge`, which means that services attached to this network get bridged to each other. This approach is what allows app services to connect to MySQL and redis services without any special configuration.

___

Now your docker-compose.yml is finally done, and you can build and run all the containers from it by executing `docker-compose up` in your terminal. To run the containers in the background, hiding logs and outputs, you can do it by executing `docker-compose up -d` in your terminal.

Happy coding!