DEV Community

Daniel Favour
Daniel Favour

Posted on • Edited on

Containerizing a LAMP Stack Application

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

container architecture

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

Container Communication

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

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

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
Enter fullscreen mode Exit fullscreen mode
  • Paste the below into the file:
MYSQL_PASSWORD=Strongpassword@123
DB_HOST=mysql
MYSQL_DATABASE=dev_to
DB_USER=root
Enter fullscreen mode Exit fullscreen mode

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

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: The WORKDIR /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 like COPY or RUN 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 the RUN 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. The docker-php-ext-enable command is used to enable the mysqli extension.

  • COPY form.html /var/www/html/index.html: The COPY instruction copies the form.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 the form_submit.php file from the host to the container's /var/www/html directory. This PHP file handles the form submissions from the form.html page.

  • RUN chown -R www-data:www-data /var/www/html && a2enmod rewrite: Here, we use the RUN 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: The EXPOSE 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
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

Write the Makefile

  • To create the Makefile file, run the following command:
touch Makefile
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode
  • 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, the set -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 command make 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
Enter fullscreen mode Exit fullscreen mode

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

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

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 named mysql-test. The container is connected to a network called test (--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 to Strongpassword@123.
  • -e MYSQL_DATABASE=dev_to: Creates a database named dev_to.
  • all: The all 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • Paste the below into it
MYSQL_PASSWORD=mypassword
MYSQL_ROOT_PASSWORD=Strongpassword@123
DB_HOST=mysql
MYSQL_DATABASE=dev_to
DB_USER=favour
Enter fullscreen mode Exit fullscreen mode

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

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 to http://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
Enter fullscreen mode Exit fullscreen mode

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

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

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

and it should return an output of the container's health status
Screenshot 2023-07-26 at 11 01 58

  • Do the same for the php container and then the phpmyadmin container

Screenshot 2023-07-26 at 11 07 50

Top comments (1)

Collapse
 
jaded_developer profile image
jade

nice article.
Question pls, how you create the Container Communication Diagram?