Dockerizing 3-Tier E-Commerce Web Application

Madhesh Waran - Aug 20 - - Dev Community

We are going to deploy a simple 3 Tier Architecture containing a frontend, a backend and a database on Docker.
Docker can be used to create simple containers. But you need Docker Compose to deploy applications that contain multiple containers.

Frontend Container:

Create a frontend folder that has the required html, CSS and JavaScript files.
Next, create a file called docker-compose.yml with the below information.

version: "3"
services:
  frontend:
    image: httpd:latest
    volumes:
      - "./frontend:/usr/local/apache2/htdocs"
    ports:
      - 3000:80

Enter fullscreen mode Exit fullscreen mode

In this file, we are telling docker to create a container using a httpd image with the latest tag and mount the local files in ./frontend to the /usr/local/apache2/htdocs directory inside the container. We are also mapping port 3000 of your local machine to port 80 of the container.

Use docker compose up to deploy the application and access the frontend application on 'http://localhost:3000' in your browser. Use Ctrl + C to stop the container.

Backend Container:

Create a backend folder that contains the required PHP file. Make sure that the PHP file contains,

header('Access-Control-Allow-Origin: http://localhost:3000');
Enter fullscreen mode Exit fullscreen mode

so that we can call the backend api from the frontend.
Instead of using a pre-existing image, we are going to create an image that contains all our required dependencies.
Create a new file called Dockerfile that contains the following code.

FROM ubuntu:20.04

LABEL maintainer="testexample@gmail.com"
LABEL description="Apache / PHP development environment"

ARG DEBIAN_FRONTEND=newt
RUN apt-get update && apt-get install -y lsb-release && apt-get clean all
RUN apt install ca-certificates apt-transport-https software-properties-common -y
RUN add-apt-repository ppa:ondrej/php

RUN apt-get -y update && apt-get install -y \
apache2 \
php8.0 \
libapache2-mod-php8.0 \
php8.0-bcmath \
php8.0-gd \
php8.0-sqlite \
php8.0-mysql \
php8.0-curl \
php8.0-xml \
php8.0-mbstring \
php8.0-zip \
mcrypt \
nano

RUN apt-get install locales
RUN locale-gen fr_FR.UTF-8
RUN locale-gen en_US.UTF-8
RUN locale-gen de_DE.UTF-8

# config PHP
# we want a dev server which shows PHP errors
RUN sed -i -e 's/^error_reporting\s*=.*/error_reporting = E_ALL/' /etc/php/8.0/apache2/php.ini
RUN sed -i -e 's/^display_errors\s*=.*/display_errors = On/' /etc/php/8.0/apache2/php.ini
RUN sed -i -e 's/^zlib.output_compression\s*=.*/zlib.output_compression = Off/' /etc/php/8.0/apache2/php.ini

# to be able to use "nano" with shell on "docker exec -it [CONTAINER ID] bash"
ENV TERM xterm

# Apache conf
# allow .htaccess with RewriteEngine
RUN a2enmod rewrite
# to see live logs we do : docker logs -f [CONTAINER ID]
# without the following line we get "AH00558: apache2: Could not reliably determine the server's fully qualified domain name"
RUN echo "ServerName localhost" >> /etc/apache2/apache2.conf
# autorise .htaccess files
RUN sed -i '/<Directory \/var\/www\/>/,/<\/Directory>/ s/AllowOverride None/AllowOverride All/' /etc/apache2/apache2.conf

RUN chgrp -R www-data /var/www
RUN find /var/www -type d -exec chmod 775 {} +
RUN find /var/www -type f -exec chmod 664 {} +

EXPOSE 80

# start Apache2 on image start
CMD ["/usr/sbin/apache2ctl","-DFOREGROUND"]
Enter fullscreen mode Exit fullscreen mode

The above code creates an Apache server and downloads all the necessities to run PHP on it.

Now let us update the docker-compose.yml file to use the above image to create the backend.

  backend:
    container_name: simple-backend
    build:
      context: ./
      dockerfile: Dockerfile
    volumes:
      - "./backend:/var/www/html/"
    ports:
      - 5000:80
Enter fullscreen mode Exit fullscreen mode

This tells docker to create a container for the backend using the Dockerfile image and mount the local files in ./backend to the /var/www/html directory inside the container. We are also mapping port 5000 of your local machine to port 80 of the container.

Rerun docker compose up to create the frontend and backend containers and access the backend application on 'http://localhost:5000' in your browser.

Make sure to update the JavaScript and PHP files so that we can access the backend API from the frontend

Database

Create a folder named db and write a dump.sql file that creates a table and dumps values into that table.

To make docker create our database let us append the following code into our docker-compose.yml file.

    database:
    image: mysql:latest
    environment:
      MYSQL_DATABASE: web_commerce
      MYSQL_USER: testuser
      MYSQL_PASSWORD: password
      MYSQL_ALLOW_EMPTY_PASSWORD: 1
    volumes:
      - "./db:/docker-entrypoint-initdb.d"
  phpmyadmin:
    image: phpmyadmin/phpmyadmin
    ports:
      - 8080:80
    environment:
      - PMA_HOST=database
      - PMA_PORT=3306
    depends_on:
      - database
Enter fullscreen mode Exit fullscreen mode

This creates a MySQL server and creates our table with the required values. To access our databases, we use a phpmyadmin image to create it and access our databases with the given username and password

Typing 'docker compose up' in your command line will create all the three frontend, backend and database containers. Use 'http://localhost:5000' to access the database.

Configuring backend container to access the database

Create a folder called app inside the backend folder.
To let the backend access the data from the database, we first create a config.php file that gives the necessary data to access the database.

config.php

<?php 
define("DB_HOST", "database");
define("DB_USERNAME", "testuser");
define("DB_PASSWORD", "password");
define("DB_NAME", "web_commerce");
Enter fullscreen mode Exit fullscreen mode

We then create a database.php file that contains the script that maintains our connection to the database.

database.php

<?php
class Database
{
    protected $connection = null;

    public function __construct()
    {
        try {
            $this->connection = new mysqli(DB_HOST, DB_USERNAME, DB_PASSWORD, DB_NAME);
            if (mysqli_connect_errno()) {
                throw new Exception("Database connection failed!");
            }
        } catch (Exception $e) {
            throw new Exception($e->getMessage());
        }
    }

    private function executeStatement($query = "", $params = [])
    {
        try {
            $stmt = $this->connection->prepare($query);
            if ($stmt === false) {
                throw new Exception("Statement preparation failure: " . $query);
            }
            if ($params) {
                $stmt->bind_param($params[0], $params[1]);
            }
            $stmt->execute();
            return $stmt;
        } catch (Exception $e) {
            throw new Exception($e->getMessage());
        }
    }

    public function select($query = "", $params = [])
    {
        try {
            $stmt = $this->executeStatement($query, $params);
            $result = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
            $stmt->close();
            return $result;
        } catch (Exception $e) {
            throw new Exception($e->getMessage());
        }
        return false;
    }
}
Enter fullscreen mode Exit fullscreen mode

We create a new web-commerce.php file that contains the data that needs to be updated in the index.php file.

<?php 
require_once "./app/Database.php";

class Products extends Database
{
    public function getProducts($limit)
    {
        return $this->select("SELECT * FROM products");
    }
}

Enter fullscreen mode Exit fullscreen mode

This was my index.php file after final updates. Use this as a reference to make your own.

<?php
header("Content-Type: application/json");
header('Access-Control-Allow-Origin: http://localhost:3000');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');

session_start();
file_put_contents('php://stderr', print_r($_GET, TRUE)); // Log to the error log

require "./app/config.php";
require_once "./app/web_commerce.php";

$productModel = new Products();
$products = $productModel->getProducts(10);

$cart = [];

// Determine the API action based on the 'action' parameter in the query string
$action = isset($_GET['action']) ? $_GET['action'] : null;

switch ($action) {
    case 'getProducts':
        getProducts();
        break;
    case 'getProductDetails':
        getProductDetails();
        break;
    case 'addToCart':
        addToCart();
        break;
    case 'getCart':
        getCart();
        break;
    default:
        echo json_encode(["error" => "Invalid action"]);
        break;
}

// Function to get all products
function getProducts() {
    global $products;
    echo json_encode($products);
}

// Function to get details of a single product by ID
function getProductDetails() {
    global $products;
    $id = isset($_GET['id']) ? intval($_GET['id']) : null;

    if ($id === null) {
        echo json_encode(["error" => "Product ID is required"]);
        return;
    }

    foreach ($products as $product) {
        if ($product['id'] === $id) {
            echo json_encode($product);
            return;
        }
    }

    echo json_encode(["error" => "Product not found"]);
}

// Function to add a product to the cart
function addToCart() {
    global $cart, $products;
    $id = isset($_GET['id']) ? intval($_GET['id']) : null;

    if ($id === null) {
        echo json_encode(["error" => "Product ID is required"]);
        return;
    }

    foreach ($products as $product) {
        if ($product['id'] === $id) {
            $cart[] = $product;
            echo json_encode(["message" => "Product added to cart", "cart" => $cart]);
            return;
        }
    }

    echo json_encode(["error" => "Product not found"]);
}

// Function to get the current cart contents
function getCart() {
    global $cart;
    echo json_encode($cart);
}

?>
Enter fullscreen mode Exit fullscreen mode

This will ensure that your backend can effortlessly fetch the values from your database.

Final folder structure:
C:.
│   docker-compose.yml
│   Dockerfile
│
├───backend
│   │   index.php
│   │
│   └───app
│           config.php
│           database.php
│           web_commerce.php
│
├───db
│       dump.sql
│
└───frontend
        index.html
        script.js
        styles.css
Enter fullscreen mode Exit fullscreen mode
. . . .
Terabox Video Player