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
<?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).
I hope this helps.
Goodluck 🫡