In the previous article, we learned how to set up a LAMP stack application. In this article, we will be looking at containerizing the LAMP stack application.
Containerization of an application involves packaging it along with its dependencies, configurations, and runtime environment into a Docker container. This encapsulation ensures that the application can run consistently across different environments and eliminates potential compatibility issues.
For this process, our primary focus will be on the PHP script. Initially, we'll create a Dockerfile for the PHP application, allowing us to build the Docker image and subsequently run the container. To establish a database connection, we'll attach a MySQL container. Additionally, we'll introduce an extra container, phpMyAdmin, which will serve as a user-friendly GUI for accessing and managing the database.
Container Architecture diagram
Please check the below embed to view the image in higher resolution:
The diagram above illustrates the container architecture utilizing Docker Compose. The applications are executed with Docker Compose and are encapsulated within a Docker bridge network, facilitating smooth communication among them. The host OS provides a Docker Engine, making Docker functionality available for seamless container management and orchestration.
Container Communication Diagram
Please check the below embed to view the image in higher resolution:
Approach to Running the Containers
We will be employing 2 different methods to run the containers, without Docker Compose and with Docker Compose.
In the "without Docker Compose" approach, we will rely on Makefiles to automate the container building and running processes. The Makefiles will handle tasks like pulling the required Docker images, creating and configuring containers, setting up networking, and other necessary actions. This method allows us to manage the containerization workflow efficiently, automating key steps with concise and easily maintainable scripts.
On the other hand, the "with Docker Compose" approach involves utilizing Docker Compose, a powerful tool for defining and managing multi-container applications. With Docker Compose, we can specify the services, networks, volumes, and configurations required for our application within a single YAML file. This streamlines the entire process, making it easier to deploy and manage multiple containers in a cohesive manner.
Project Structure
The project structure should be identical to the below:
.
├── .env
├── docker-compose.yml
├── install.sh
├── mysql
│ ├── db.sql
│ └── makefile
├── php
│ ├── .env
│ ├── Dockerfile
│ ├── form.html
│ ├── form_submit.php
│ └── makefile
├── setup.sh
└── vagrantfile
Without Docker Compose (Makefile Approach)
A Makefile is a file used with the Unix utility "make" to automate the build process of software projects. It contains instructions, typically written in shell commands, and is named "makefile" or "Makefile" depending on the system. Each rule in the Makefile has a target, dependencies, and commands. When you run the "make" command, it reads the Makefile and executes the specified commands to build the software according to the defined rules.
In the context of containerizing our application, a Makefile serves as a valuable automation tool. It streamlines the entire containerization process by automating the necessary tasks. The Makefile is designed to install essential tools, lint both the Dockerfile and PHP application code, build the Docker image, and run the container image.
The primary objective behind using a Makefile is to achieve seamless automation, making the development workflow more efficient and consistent.
Create PHP directory
In the root of the project folder, create a php
directory and move the form_submit.php
and form.html
files into it:
mkdir php
mv form_submit.php /php
mv form.html /php
Environment Variables
We will create environment variables in the php folder. These variables will be used for the form_submit.php.
- Create a
.env file
:
touch .env
- Paste the below into the file:
MYSQL_PASSWORD=Strongpassword@123
DB_HOST=mysql
MYSQL_DATABASE=dev_to
DB_USER=root
Remember to change the values and use values of your choice but be consistent with it across the environment. These variables are what php will use to communicate with the MySQL server for data storage.
Write the Dockerfile
To containerize the PHP application, we need to create a Dockerfile. In the php directory, create a new file called Dockerfile without any file extension. Dockerfiles don't require extensions.
Now, paste the following content into the Dockerfile:
FROM php:7.4-apache
WORKDIR /var/www/html
RUN docker-php-ext-install mysqli pdo pdo_mysql && docker-php-ext-enable mysqli
COPY form.html /var/www/html/index.html
COPY form_submit.php /var/www/html
RUN chown -R www-data:www-data /var/www/html \
&& a2enmod rewrite
EXPOSE 80
Breakdown of what each line means:
FROM php:7.4-apache
: This line sets the base image for our container. It uses the official PHP image with Apache server, version 7.4. This base image already includes PHP and Apache, making it convenient for hosting PHP applications.WORKDIR /var/www/html
: TheWORKDIR /var/www/html
instruction sets the working directory inside the container to/var/www/html
. It provides a context for file operations, and subsequent commands likeCOPY
orRUN
will be executed relative to this directory. In this case, it is set to the common location for web application files in Apache servers, and the following COPY commands copy the PHP application files into this directory inside the container for use by the Apache web server.RUN docker-php-ext-install mysqli pdo pdo_mysql && docker-php-ext-enable mysqli
: This line uses theRUN
instruction to execute commands during the Docker image build process. Here, we are installing PHP extensions mysqli, pdo, and pdo_mysql required for database connections. Thedocker-php-ext-enable
command is used to enable the mysqli extension.COPY form.html /var/www/html/index.html
: The COPY instruction copies theform.html
file from the host (your local machine) to the container's/var/www/html
directory. In this case, it is renamed to index.html, serving as the default HTML page when accessing the root URL.COPY form_submit.php /var/www/html
: Similarly, this line copies theform_submit.php
file from the host to the container's/var/www/html
directory. This PHP file handles the form submissions from theform.html
page.RUN chown -R www-data:www-data /var/www/html && a2enmod rewrite
: Here, we use theRUN
instruction to set the ownership of the/var/www/html
directory to the www-data user and group. Apache typically runs under the www-data user, so this ensures proper permissions for serving the website content.
The second part of this line uses the a2enmod
command to enable the Apache module rewrite. The rewrite module is needed to allow URL rewriting, enabling cleaner URLs and better routing for the PHP application.
-
EXPOSE 80
: TheEXPOSE
instruction is a metadata declaration that indicates which network ports the container will listen on during runtime. In this case, we specify that the container will listen on port 80. However, this does not actually publish the port to the host machine; it only serves as documentation for the user.
Create the Environment Variables File
In the php folder directory, create a file called .env
, it will be used to store environment variables for the containers:
touch .env
- Copy the below contents into it, ensure to fill it up with your desired values but leave the DB_Host as mysql and the DB_USER as root:
MYSQL_PASSWORD=
DB_HOST=mysql
MYSQL_DATABASE=
DB_USER=root
Write the Makefile
- To create the
Makefile
file, run the following command:
touch Makefile
- Paste the below configuration into the makefile:
# Makefile for PHP Dockerfile and PHP Code
.SILENT:
install:
# Check if Homebrew is installed otherwise install it
@if ! command -v brew &> /dev/null; then \
echo "Installing Homebrew..."; \
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"; \
echo "Homebrew installed!"; \
else \
echo "Homebrew is already installed."; \
fi
# Check if Hadolint is installed otherwise install it
@if ! command -v hadolint &> /dev/null; then \
echo "Installing Hadolint..."; \
brew install hadolint; \
echo "Hadolint installed!"; \
else \
echo "Hadolint is already installed."; \
fi
# Check if wget is installed otherwise install it
@if ! command -v wget &> /dev/null; then \
echo "Installing wget..."; \
brew install wget; \
echo "wget installed!"; \
else \
echo "wget is already installed."; \
fi
# Check if php is installed otherwise install it
@if ! command -v php &> /dev/null; then \
echo "Installing PHP..."; \
brew install php; \
echo "PHP installed!"; \
else \
echo "PHP is already installed."; \
fi
# Install Xcode Developer Tools but check if they are already installed first
@if ! xcode-select -p > /dev/null 2>&1; then \
echo "Installing Xcode Developer Tools..."; \
xcode-select --install; \
else \
echo "Xcode Developer Tools are already installed."; \
fi
.SILENT:
lint_dockerfile:
# Lint the Dockerfile using hadolint
# See local hadolint install instructions: https://github.com/hadolint/hadolint
hadolint Dockerfile
# Print a successful message
@echo "Dockerfile linting completed successfully. No errors found, Dockerfile follows best practices."
.SILENT:
lint_php:
set -x # Enable verbose output
# Check if PHPCS and PHPCBF linter files are downloaded, otherwise download them
@if [ ! -f "phpcs.phar" ]; then \
echo "Downloading phpcs.phar..."; \
wget -q https://squizlabs.github.io/PHP_CodeSniffer/phpcs.phar; \
echo "phpcs.phar downloaded!"; \
else \
echo "phpcs.phar is already downloaded."; \
fi
# For PHPCBF
@if [ ! -f "phpcbf.phar" ]; then \
echo "Downloading phpcbf.phar..."; \
wget -q https://squizlabs.github.io/PHP_CodeSniffer/phpcbf.phar; \
echo "phpcbf.phar downloaded!"; \
else \
echo "phpcbf.phar is already downloaded."; \
fi
# Make the downloaded PHAR files executable
chmod +x phpcs.phar
chmod +x phpcbf.phar
# Lint the php code and check for any errors
php phpcs.phar --standard=PSR12 form_submit.php || true
# Continue with other targets by recursively invoking `make`
$(MAKE) build run
build:
# Build the docker image using the Dockerfile in this directory and create a network
docker build -t php-test .
# Create docker network but check if the network exists first
if ! docker network inspect test_network > /dev/null 2>&1; then \
docker network create test_network; \
fi
run:
# for php
docker run -d --name php --network test -p 80:80 --env-file .env php-test:latest
# for phpmyadmin
docker run -d --name phpmyadmin --network test -p 8000:80 -e PMA_ARBITRARY=1 -e PMA_HOST=mysql phpmyadmin
# Target to run the entire setup process
all: install lint_dockerfile lint_php build run
Let's breakdown the configurations:
-
.SILENT
: In a Makefile, the .SILENT special target is used to suppress the normal echoing of commands that are executed during the build process. When you include .SILENT in your Makefile, it tells Make to operate in silent mode for the rules that follow it.
By default, when you run make, it displays each command it executes in the terminal. This can be helpful for understanding what's happening during the build process, but it can also lead to a lot of noise, especially for simple and repetitive commands.
-
install
: This target automates the installation of essential tools and dependencies, including Homebrew, Hadolint, wget, php, and Xcode Developer Tools. The@
symbol before each command suppresses the output of the command, providing a cleaner output during the installation process.
For each tool, the Makefile checks if it is already installed using command -v followed by the tool's name. If the tool is not found, it proceeds with the installation by fetching the necessary installation script or using Homebrew to install the tool. This automation ensures a smooth and efficient setup of the development environment, allowing developers to focus on the project without the hassle of manual tool installations.
For example, when you run the command make install
, it should return the below output if the tools are already installed, if otherwise, it will proceed to install them:
User-demo:php daniel$ make install
Homebrew is already installed.
Hadolint is already installed.
wget is already installed.
PHP is already installed.
Xcode Developer Tools are already installed.
-
lint_dockerfile
: This target employs Hadolint, a Dockerfile linter, to enforce best practices for writing Dockerfiles. When the make lint_dockerfile command is executed, it automatically lints the Dockerfile in the current directory, ensuring it adheres to industry-standard guidelines.
The purpose of this target is to provide developers with a quick and automated way to validate their Dockerfiles. If the Dockerfile is free from issues or errors, Hadolint does not produce any output, potentially leading to confusion. To address this, the target includes a specified echo message to indicate that the Dockerfile has passed the linting process successfully.
In case the Dockerfile contains issues or errors, the target will display the relevant error output in the terminal as specified by Hadolint, providing developers with valuable feedback to rectify any non-compliant code.
-
lint_php
: This target is responsible for linting the PHP code using PHP_CodeSniffer (PHPCS). This target serves as a linter for PHP code, ensuring it adheres to PHP coding standards, particularly the PSR-12 standard. To provide more insight into the execution process, theset -x
command enables verbose output, displaying the actual commands executed in the terminal during the target's execution.
The target begins by checking if the phpcs.phar
and phpcbf.phar
files exist in the current directory. If not, it proceeds to download them using the wget command from their official URLs. The chmod +x
command is then used to make the downloaded PHAR files (phpcs.phar and phpcbf.phar) executable, allowing them to be run as commands.
Next, the linter executes the phpcs.phar command with the --standard=PSR12
option, analyzing the form_submit.php
file for any violations of the PSR-12 standard. If there are errors or warnings, they will be displayed in the terminal. The || true
at the end ensures that the Makefile execution continues even if this command encounters failures. This is done to prevent the linter's failure from halting the entire Makefile execution, allowing the focus to remain on the Docker-related tasks. If we have interest in fixing the code errors, we will use the phpcbf.phar
to fix them.
build
: This target automates the process of building a Docker image based on the Dockerfile located in the current working directory. The image is tagged with the name php-test. Additionally, it checks for the existence of a Docker network named test_network, and if it doesn't exist, the target creates it.run
: This target is responsible for starting two Docker containers: one for PHP and another for phpMyAdmin
For the php container, the command uses docker run to start a new container, -d
flag runs the container in detached mode, meaning it runs in the background. The --name php
option assigns the name "php" to the container. The --network test option
connects the container to the Docker network named test
. The -p 80:80
option maps port 80 from the host to port 80 in the container, allowing access to the web server running inside the container. The --env-file .env
option specifies the location of an environment file (.env), which is in the current directory, that contains environment variables for the container. php-test
specifies the Docker image to use for the container.
For phpmyadmin container, the command uses docker run to start another container, this time for phpMyAdmin. The -d
flag runs the container in detached mode. The --name phpmyadmin-test
option assigns the name phpmyadmin-test
to the container. The --network test
option connects the container to the same Docker network test
as the PHP container. The -p 8000:80
option maps port 8000 from the host to port 80 in the container, allowing access to phpMyAdmin's web interface.
The -e PMA_ARBITRARY=1
and -e PMA_HOST=mysql
options are environment variables for phpMyAdmin container to configure its behaviour. phpmyadmin
specifies the Docker image to use for the phpMyAdmin container. The image is pulled from the Docker registry.
all
: This target acts as a meta-target, referencing all the targets defined in the Makefile. When you execute the commandmake all
, it will sequentially execute each target in the order they are defined in the Makefile.To execute the Makefile and automate the build process, run the following command in the terminal:
make all
The Makefile will automatically execute all the defined targets in the specified order, as described previously. This one-liner command saves you from manually performing individual tasks and ensures that the complete workflow, including installation, linting, Docker image building, and running containers, is handled seamlessly.
After executing all the targets in the Makefile using make all, you can check your browser, http://localhost:8000
, to access the phpMyAdmin container. However, please note that the PHP and phpMyAdmin containers are not fully functional at this point since they still need to be connected to a MySQL database for full functionality.
Create the MySQL database
In the root of the project folder, create a folder called mysql
, cd into the directory and create two files: db.sql
and makefile
mkdir mysql
cd mysql/
touch db.sql
touch makefile
- Paste the below contents into the
db.sql
file:
CREATE TABLE IF NOT EXISTS test (
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(255),
email VARCHAR(255),
message TEXT,
PRIMARY KEY (id)
);
The above is an SQL script that creates a table named test
with specific column definitions.
-
CREATE TABLE IF NOT EXISTS
: This statement creates the test table only if it doesn't already exist in the database. This prevents any potential errors from re-creating an existing table.
The test table has four columns:
-
id:
An integer column with the NOT NULL constraint, meaning it cannot contain null values. It is also defined asAUTO_INCREMENT
, which automatically generates a unique value for each new row. -
name:
A variable-length character column with a maximum length of 255 characters. -
email:
Another variable-length character column with a maximum length of 255 characters. message:
A column of the TEXT data type, which can store large amounts of text.Paste the below configuration into the
makefile
file:
# Makefile for mysql
pull:
docker pull mysql:latest
run:
docker run -d --name mysql --network test -v ./data:/var/lib/mysql -v ./db.sql:/docker-entrypoint-initdb.d/db.sql -e MYSQL_USER=favour -e MYSQL_PASSWORD=mypassword -e MYSQL_ROOT_PASSWORD=Strongpassword@123 -e MYSQL_DATABASE=dev_to -p 3306:3306 mysql:latest
# Target to run the entire setup process
all: pull run
This Makefile provides a convenient way to set up a MySQL container using Docker and automates the process with the following targets:
pull:
The pull target is used to pull the latest MySQL Docker image (mysql:latest) from the Docker registry. This ensures that the latest version of the MySQL image is available locally for running the container.run:
The run target is responsible for starting the MySQL container. It uses docker run to create and run a new container namedmysql-test
. The container is connected to a network calledtest
(--network test) and mounts a local SQL file (db.sql) to the container's/docker-entrypoint-initdb.d/db.sql
path. This allows the SQL file to be executed during container initialization, populating the database with any initial data.
Additionally, environment variables are provided for configuration:
-
-e MYSQL_ROOT_PASSWORD=Strongpassword@123:
Sets the root user's password toStrongpassword@123.
-
-e MYSQL_DATABASE=dev_to:
Creates a database nameddev_to.
-
all:
Theall
target acts as a meta-target, referencing both pull and run.
NB: Keep it in mind that important credentials shouldn't be passed directly on the command line, we are passing them directly here just to show the process.
- To create the database, run the makefile with the following command from inside the mysql directory:
make all
When you execute make all, it will execute both targets in the specified order. This allows you to pull the latest MySQL image and then run the MySQL container with the necessary configurations in a single command.
Now on your browser, visit localhost
to see the form.html file being served by apache. When you fill the form, if the input you gave is successful saved in the database, you will be redirected to localhost:80
where php is running. You will get a php message saying it was successful, if it wasn't, php will still print out an error message.
With Docker-Compose
After using Makefiles to run the containers as well as other processes, we will use Docker Compose which is an effective container orchestration tool to run the containers.
Environment Variables
We need to first create environment variables for the MySQL container to use since it is not good practice to pass sensitive information directly.
In the root of the folder, create a .env
file:
touch .env
- Paste the below into it
MYSQL_PASSWORD=mypassword
MYSQL_ROOT_PASSWORD=Strongpassword@123
DB_HOST=mysql
MYSQL_DATABASE=dev_to
DB_USER=favour
Replace with the appropriate attributes.
Docker Compose
Now that the environment variables have been set, we can proceed to writing the Docker Compose file.
In the root of the project folder, create a file called docker-compose.yml
:
touch docker-compose.yml
- Paste the below contents into the file:
version: '3'
services:
php:
env_file:
- .env
build:
context: ./php
args:
- --no-cache
dockerfile: Dockerfile
ports:
- "80:80"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/"]
interval: 10s
timeout: 5s
retries: 3
container_name: php
networks:
- backend
mysql:
env_file:
- .env
image: mysql:latest
restart: always
ports:
- "3306:3306"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
timeout: "5s"
interval: "10s"
start_period: "3s"
retries: 5
environment:
MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD}"
MYSQL_DATABASE: "${MYSQL_DATABASE}"
MYSQL_USER: "${DB_USER}"
MYSQL_PASSWORD: "${MYSQL_PASSWORD}"
container_name: mysql
volumes:
- ./mysql/db.sql:/docker-entrypoint-initdb.d/db.sql
- ./mysql/mysql_data:/var/lib/mysql
networks:
- backend
phpmyadmin:
image: phpmyadmin:latest
container_name: phpmyadmin
ports:
- 8000:80 # Expose phpMyAdmin on port 8000
restart: always
environment:
PMA_ARBITRARY: 1 # Use the value '1' for arbitrary hostname resolution
PMA_HOST: "${DB_HOST}" # Use the container name of the mysql service as the host
depends_on:
- mysql
networks:
- backend
networks:
backend:
driver: bridge
The above docker-compose file defines a multi-container environment using Docker Compose, allowing you to run and manage multiple services (containers) together as part of a single application.
Breaking down the files to bits:
Version:
The file specifies the version of Docker Compose syntax being used. In this case, it's using version 3.Services:
This section defines three services (containers): php, mysql, and phpmyadmin.php service:
The php service is built using the Dockerfile located in the ./php directory. It uses environment variables defined in the .env file. The service is accessible on port 80 and has a health check to test the health of the container by making an HTTP request tohttp://localhost/
.mysql service:
The mysql service uses the official MySQL image (mysql:latest) from Docker Hub. It reads environment variables from the .env file to set up MySQL's root password, database, user, and password. The container is accessible on port 3306, and it has a health check to verify the health of the MySQL server by pinging it.phpmyadmin service:
The phpmyadmin service uses the official phpMyAdmin image (phpmyadmin:latest) from Docker Hub. It exposes phpMyAdmin on port 8000 and depends on the mysql service. Environment variables are set to configure phpMyAdmin to connect to the MySQL container.Volumes:
The mysql service mounts two volumes:./mysql/db.sql
to initialize the database with an SQL file and./mysql/mysql_data
to persist MySQL data.Networks:
The backend network is created to allow communication between the services (php, mysql, phpmyadmin).To run the containers, in the root directory where the docker compose file exists, run the following command:
docker compose up -d
This command automatically builds the Docker image and runs the containers.
== we didn't persist data for makefile command
- To bring down the containers:
docker compose down
Container Health Checks
Going by standard and best practices, containers should have health checks to ensure they are running properly and responding to requests as expected. Health checks have already been defined in the Docker Compose file, ensuring that the containers are periodically monitored for their health status. When you run the command docker ps -a
, you should see the containers listed with their health status displayed under the STATUS
column.
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
eb6e3ddc3e53 phpmyadmin:latest "/docker-entrypoint.…" 17 seconds ago Up 15 seconds (healthy) 0.0.0.0:8000->80/tcp phpmyadmin
1f1abdcbd82d mysql:latest "docker-entrypoint.s…" 17 seconds ago Up 15 seconds (healthy) 0.0.0.0:3306->3306/tcp, 33060/tcp mysql
2b91dfbbb74d module-2-php "docker-php-entrypoi…" 17 seconds ago Up 15 seconds (healthy) 0.0.0.0:80->80/tcp php
At initial start of the containers, the STATUS column will show (health: starting) since the container is in the process of starting, and the health check is currently being evaluated. The status of a container can also be unhealthy
. This status indicates that the container's health check has failed, signaling that there might be an issue with the container or its underlying application. If the status shows exited, it means that the container has stopped running, either because it has completed its task or due to an error.
Health checks can also be done manually
- To run health checks manually on your container, for the
mysql
container, run:
docker inspect --format='{{json .State.Health.Status}}' <container name>
and it should return an output of the container's health status
- Do the same for the php container and then the phpmyadmin container
Top comments (1)
nice article.
Question pls, how you create the Container Communication Diagram?