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, while mysql and redis will look for their images on Docker Hub.
Content
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 doc.
---
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"
Version
The first step towards creating docker-compose.yml is to define the version of the file format.
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.
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.
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.
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.
Directive build
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.
Directive container_name
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.
Directive command
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.
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.
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.
Directive ports
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.
Directive env_file
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.
Directive volumes
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.
Directive networks
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.
MySQL service
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.
Directive image
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.
Directive container_name
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.
Directive ports
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.
Directive volumes
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.
Directive environment
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.
Directive networks
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.
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.
Redis service
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.
Directive image
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.
Directive container_name
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.
Directive ports
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.
Directive volumes
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.
Directive networks
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.
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.
Volumes
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.
Networks
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!
Top comments (0)