Efficient Containerization: A Comprehensive Guide to Building and Deploying Secure PHP-MySQL Web Applications with Docker

Photo by sydney Rae on Unsplash

Efficient Containerization: A Comprehensive Guide to Building and Deploying Secure PHP-MySQL Web Applications with Docker

To dockerize a PHP form application and link it to a MySQL database, there are several steps you would need to take. This guide will also include the use of Docker Compose for easier orchestration of multi-container applications, data persistence, and Apache as the web server.

I am using a Mac Operating System (OS), feel free to use any other OS.

Prerequisite

Before reading this article, ensure you have knowledge of how Docker and docker-compose work.

Step 1: Create the PHP Application with a MySQL backend

Guide

<?php   
$host = "db";
$db_name = "test_db";
$username = "root";
$password = getenv('MYSQL_ROOT_PASSWORD');

try{
$connection = new PDO("mysql:host=" . $host . ";dbname=" . $db_name, $username, $password);
$connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); #enabled error reporting via PDOexceptions
$connection->exec("set names utf8");
}catch(Exception $exception){
echo "Connection error: " . $exception->getMessage();
}

function saveData($name, $email, $message){
global $connection;
$query = "INSERT INTO users(name, email, message) VALUES( :name, :email, :message)";

$callToDb = $connection->prepare( $query );
$name=htmlspecialchars(strip_tags($name));
$email=htmlspecialchars(strip_tags($email));
$message=htmlspecialchars(strip_tags($message));
$callToDb->bindParam(":name",$name);
$callToDb->bindParam(":email",$email);
$callToDb->bindParam(":message",$message);

if($callToDb->execute()){
return '<h3 style="text-align:center;">Saved to DB!</h3>';
}
}

if( isset($_POST['submit'])){
$name = htmlentities($_POST['name']);
$email = htmlentities($_POST['email']);
$message = htmlentities($_POST['message']);

// then you can use them in a PHP function. 
$result = saveData($name, $email, $message);
echo $result;
}
else{
echo '<h3 style="text-align:center;">A very detailed error message ( ͡° ͜ʖ ͡°)</h3>';
}
?>

index.php

<head>
    <title>
    Form
    </title>
    </head>
    <body>
    <form action="index.php" class="alt" method="POST">
    <div class="row uniform">
    <div class="name">
    <input name="name" id="" placeholder="Name" type="text">
    </div>
    <div class="email">
    <input name="email" placeholder="Email" type="email">
    </div>
    <div class="message">
    <textarea name="message" placeholder="Message" rows="4"></textarea>
    </div>
    </div>
    <br/>
    <input class="alt" value="Submit" name="submit" type="submit">
    </form>
    </body>

index.html

MYSQL_ROOT_PASSWORD=<some-value>
MYSQL_PASSWORD=<some-value>
DB_HOST=db
DB_PORT=3306
MYSQL_DATABASE=<some-value>
MYSQL_PORT=3306

.env

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    email VARCHAR(255) NOT NULL,
    message TEXT NOT NULL
);

main.sql

Step 2: Create Docker and Docker-compose files

FROM php:7.4-apache

# Install PDO MySQL extension
RUN docker-php-ext-install pdo_mysql

Dockerfile - by default, Apache exposes port 80; hence no need to add the code block EXPOSE 80

version: '3.8'
services:
  web:
      build: 
        context: .
        dockerfile: Dockerfile
      ports:
        - "8080:80"
      env_file:
        - .env
      volumes:
        - .:/var/www/html #to persist and kep track of changes to the code
      depends_on:
        - db

  db:
    image: mysql:latest
    command: --default-authentication-plugin=mysql_native_password
    restart: always
    env_file:
      - .env
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    ports:
      - 3306:3306
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "db"]
      interval: 20s
      timeout: 10s
      retries: 10
    volumes:
      - db_data:/var/lib/mysql
      - ./main.sql:/docker-entrypoint-initdb.d/main.sql #run the main.sql script upon startup

  phpmyadmin:
    image: phpmyadmin:latest
    ports:
        - '80:80'
    restart: always
    environment:
        PMA_HOST: db
    depends_on:
        - db

volumes:
  db_data:

networks:
  app-network:

docker-compose.yaml

Notes:

  • The volumes code block, persists data in the container, such that when the container is stopped and restarted, you will still have your database entries intact.

  • The healthcheck, ensures the MySQL service is fully ready before the web service tries to connect.

  • Since I did not specify a network driver, my containers, by default are connected to docker's bridge network. Bridge networks are usually used when your applications run in standalone containers that need to communicate.

  • To get the main.sql executed in the MySQL container, you have to delete any existing volumes in the container.

Step 3: Build and run the Docker application

$ docker-compose up --build

Testing the App

  • Head over to localhost:80, to access phpMyAdmin - to get a visual of your MySQL database*.* Sign in using the values of these variables: MYSQL_ROOT_PASSWORD and MYSQL_USER (.env file).

  • To access the application, head to localhost:8080/index.html, upon filling out the form, you will be redirected.

  • Log into phpMySQL, to get a visual of the data you just filled into the form.

Best Practices Used

  • Implement error handling in the PHP app.

  • Add a health check to the Docker compose file.

  • Mount a migration file that creates the database table on the MySQL container.

  • Create a docker ignore file .dockerignore .

      .git
      .gitignore
      .DS_Store
    
  • Create a git ignore file .gitignore.

      mysql_password.txt
      mysql_root_password.txt
      *.log
      .DS_Store
      .env
    
  • Store sensitive information as environment variables or use docker secrets (used with Docker swarm).

Repository

I hope this helps.

Goodluck 🫡