Building a Complete PHP Stack Using Docker and Docker-Compose

Typical PHP setups consist of three main components: a web server, a relational database system, and the PHP interpreter. In this walkthrough, we will assemble a full PHP environment using Docker. You will learn how to create and manage containers for Nginx (web server), MySQL (database), and PHP.

Creating a Simple PHP App to Read from a Database

We will build a basic web application that retrieves a list of cities from a MySQL database and shows them on a web page. This practical example will demonstrate how to implement a functional PHP-based setup.

Prerequisites for This Tutorial

This guide assumes that Docker-CE is already installed on your system and that you have a basic understanding of Docker.

Setting Up the Project Environment

In real-world scenarios, Docker-based applications often consist of several containers. Managing these manually can quickly become chaotic. Docker Compose simplifies this by allowing control over multiple containers through a YAML configuration file.

Installing Docker Compose

curl -L https://github.com/docker/compose/releases/download/1.19.0/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose

Creating the Project Directory

Start by setting up a main directory for your project files and move into it. This will be your working directory (referred to here as WORKING_DIR):

mkdir ~/docker
cd ~/docker

Now create three subdirectories inside it:

The php directory will store the custom PHP image, the nginx folder will contain the Nginx configuration, and app will house the application code and config files.

Setting Up the PHP Container

We will configure PHP-FPM to communicate with Nginx. The PHP image will be based on the official lightweight Alpine image. It also requires some additional extensions to interact with the MySQL database. Inside the php folder, create a file named Dockerfile and include the following:

FROM php:7.1-fpm-alpine3.4
RUN apk update --no-cache \
    && apk add --no-cache $PHPIZE_DEPS \
    && apk add --no-cache mysql-dev \
    && docker-php-ext-install pdo pdo_mysql

This setup uses Alpine Linux for its minimal footprint, ideal for containerized environments. The command docker-php-ext-install provided by the PHP image simplifies enabling required extensions.

Building the PHP Docker Image

Build your Docker image from the WORKING_DIR:

docker build -t centron-php php/

Creating the docker-compose.yml File

Docker Compose uses a configuration file—commonly named docker-compose.yml—to define and control multi-container applications. Create this file inside the app directory:

touch app/docker-compose.yml

Insert the following content into it:

version: '2'
services:
  php:
    image: centron-php
    volumes:
      - ./:/app
    working_dir: /app

Understanding the docker-compose Configuration

The first line, version: '2', defines the Compose file format. Under services, we declare all the containers to be created—in this case, only one for PHP.

The service php refers to the image we just built. The volume mapping:

…binds the current host directory to /app inside the container. The working_dir specifies where future container commands will run by default.

Running the Containers

Now navigate to the app directory and bring up the containers:

cd ~/docker/app
docker-compose up -d

To confirm that the PHP container is running properly, use the following:

Running Commands Inside Containers with Docker Compose

From the app directory, you can execute commands inside any containerized service using the following syntax:

docker-compose exec [service] [command]

For instance, to verify the PHP version inside the container, run:

docker-compose exec php php -v

This will output something like:

PHP 7.1.14 (cli) (built: Feb  7 2018 00:40:45) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.1.0, Copyright (c) 1998-2018 Zend Technologies

Customizing the Nginx Container

Similar to the PHP setup, we need to configure a custom container for Nginx. This time, we just need to supply a virtual host configuration file. While in WORKING_DIR, create a Dockerfile for the Nginx container:

cd ~/docker
touch nginx/Dockerfile

Insert this content into the Dockerfile:

FROM nginx:1.13.8-alpine
COPY ./default.conf /etc/nginx/conf.d/default.conf

This setup uses the lightweight Alpine-based Nginx image and copies a configuration file into place. Create the configuration file:

Paste the following into default.conf:

server {
    listen 80 default_server;
    listen [::]:80 default_server ipv6only=on;

    root /app;
    index index.php;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        try_files $uri /index.php =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass php:9000;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }
}

The line fastcgi_pass php:9000; links to the PHP container by service name. Docker Compose creates an internal network where each service is accessible by its name.

Now build the Nginx container image:

docker build -t centron-nginx nginx/

Expanding the docker-compose.yml File

Update the app/docker-compose.yml file to include the web server:

version: '2'
services:
  php:
    image: centron-php
    volumes:
      - ./:/app
    working_dir: /app
  web:
    image: centron-nginx
    volumes:
      - ./:/app
    depends_on:
      - php
    ports:
      - 80:80

The depends_on ensures the PHP container starts first. ports exposes container port 80 to the host.

Create a simple PHP file at app/index.php:

Ensure your firewall allows port 80. Start the containers:

cd ~/docker/app
docker-compose up -d
docker ps

Open your browser and navigate to your server’s IP address. Find it by running:

Setting Up the MySQL Container

The MySQL image can be configured using environment variables. Add a new mysql service in docker-compose.yml:

version: '2'
services:
  php:
    image: centron-php
    volumes:
      - ./:/app
    working_dir: /app
  web:
    image: centron-nginx
    volumes:
      - ./:/app
    depends_on:
      - php
    ports:
      - 80:80
  mysql:
    image: mysql:5.7.21
    volumes:
      - ./:/app
      - dbdata:/var/lib/mysql
    environment:
      - MYSQL_DATABASE=world
      - MYSQL_ROOT_PASSWORD=root
    working_dir: /app
volumes:
  dbdata:

This configuration uses dbdata to persist MySQL data. Docker manages this named volume to retain your database even if the container is destroyed.

Importing the Sample Database

Download and extract the sample world database from the official MySQL resources:

curl -L http://downloads.mysql.com/docs/world.sql.gz -o world.sql.gz
gunzip world.sql.gz

Now start the containers:

docker-compose up -d
docker ps

Import the database into MySQL:

docker-compose exec -T mysql mysql -uroot -proot world < world.sql

Access the MySQL CLI and confirm the data import:

docker-compose exec mysql mysql -uroot -proot world

Inside MySQL, run:

select * from city limit 10;

You’ll see a sample list of cities from the database. To exit MySQL:

Developing the Sample PHP Application

With all required containers now operational, we can shift focus to developing our demo application. Update the app/index.php file with the following content:

<?php

$pdo = new PDO('mysql:host=mysql;dbname=world;charset=utf8', 'root', 'root');

$stmt = $pdo->prepare("
    select city.Name, city.District, country.Name as Country, city.Population
    from city
    left join country on city.CountryCode = country.Code
    order by Population desc
    limit 10
");
$stmt->execute();
$cities = $stmt->fetchAll(PDO::FETCH_ASSOC);

?>

<!doctype html>
<html>
<head>
    <meta charset="UTF-8">
    <title>centron Rocks!</title>
</head>
<body>
    <h2>Most Populous Cities In The World</h2>
    <table>
    <thead>
        <tr>
            <th>Name</th>
            <th>Country</th>
            <th>District</th>
            <th>Population</th>
        </tr>
    </thead>
    <tbody>
        <?php foreach($cities as $city): ?>
            <tr>
                <td><?=$city['Name']?></td>
                <td><?=$city['Country']?></td>
                <td><?=$city['District']?></td>
                <td><?=number_format($city['Population'], 0)?></td>
            </tr>
        <?php endforeach ?>
    </tbody>
    </table>
</body>
</html>

Open a browser and go to your server’s IP address ([centron-instance-ip]). You’ll see a table displaying the ten largest cities in the world by population. Well done — you’ve successfully deployed a complete PHP application using Docker!

Final Thoughts

In this guide, you’ve learned how to build and configure a working PHP application environment using Docker. You created custom container images for PHP and Nginx and managed them using Docker Compose. Although simple, this layout closely resembles production-ready Docker environments.

All container images were built and tagged locally. To make your setup more scalable, consider pushing your images to a Docker registry. You can use Docker Hub or create a private registry, which allows for image sharing across multiple hosts.

If you want to expand this setup to support other frameworks or features, you may need to install additional PHP extensions. You can modify the Dockerfile accordingly. Some extensions require extra system libraries to function correctly, so be sure to check the official PHP documentation to understand their dependencies.

For a deeper dive into Docker Compose and advanced features, consult the official documentation.

Source: vultr.com

Create a Free Account

Register now and get access to our Cloud Services.

Posts you might be interested in:

Moderne Hosting Services mit Cloud Server, Managed Server und skalierbarem Cloud Hosting für professionelle IT-Infrastrukturen

CI/CD with Cypress & GitHub Actions for React Apps

React, Tutorial
Automating CI/CD Pipelines with Cypress and GitHub Actions CI/CD pipelines simplify software delivery by automating code integration, validation, and deployment. Incorporating end-to-end (E2E) tests ensures that changes function as expected…