PHP inside Docker Environment
September 26, 2023 • 28 minutes read

PHP inside Docker Environment

Introduction

Nowadays Docker and the process of Dockerization is a modern way to run most of the backend applications. PHP is not an exception. What is the right way to run PHP in Docker Environment?

If you are a beginner in Docker I would recommend you to start with the official Docker Guide. Also you may find useful my Docker Cheat Sheet

PHP Dockerization in Development Environment

Let’s start from the usage in the Development Environment. The most convenient option is by using Docker Compose It allows you to run multiple interconnected Docker containers that are required for your application. It is a comfortable way, because you may have only one configuration, in one yaml file, which includes a description for all required containers with all required dependencies (volumes, network, startup order, etc.). Even in the simplest scenario for dockerized PHP you need at least 2 containers: web server (NGINX/Apache), PHP. In more complex applications you may need more than 2 containers (database, cache, message broker, etc.)

The simple example of docker-compose.yml file may look like this:

version: "3.9"
services:
    nginx:
      image: nginx:stable
      environment:
          NGINX_ENVSUBST_TEMPLATE_DIR: /var/www/app/docker/nginx
          NGINX_ENVSUBST_TEMPLATE_SUFFIX: .template
          NGINX_ENVSUBST_OUTPUT_DIR: /etc/nginx/conf.d
          NGINX_PORT: 8080
          NGINX_ROOT: /var/www/app/public
          PHP_HOST: php
          PHP_PORT: 9000
      working_dir: /var/www/app/
      volumes:
          - ".:/var/www/app/"
      networks:
          - local
      depends_on:
          - php
      ports:
          - "8080:8080"
    php:
      image: php:8.2.6-fpm-alpine
      working_dir: /var/www/app/
      volumes:
          - ".:/var/www/app/"
      networks:
          - local

networks:
    local:
      name: local

volumes:
    app_data:
        driver: local

Reference to docker-compose.yml on GitHub

There is a configuration for 2 containers: NGINX, php-fpm. Some environment variables to configure NGINX for communicating with PHP. Pretty simple configuration.

Docker provides an isolated environment. That’s also valid for the file system as well. Files placed on your host machine are not accessible by the docker until you do some extra steps. The way to share PHP source code from your host machine into a docker container is by sharing files via docker volume
With the Docker Compose it is easy to do via volumes section with the syntax below:

volumes:
   - ".:/var/www/app/"

. sign here means current directory. So, if you place docker-compose.yml in the root folder of your project, then all source code from this root folder will be accessible in /var/www/app/ folder inside docker.

That’s super comfortable to use during development. All changes in the PHP code on your local machine would be immediately applied in the docker environment.

Due to the specific nature of PHP and the way it works in combination NGINX + PHP-fpm, we have to share the same code to both NGINX and PHP containers. So, that’s not an issue with Docker Compose. We can share PHP source code from the host machine to both docker containers the same way. The schematic representation will look like this:

PHP code in Docker Compose

That’s it. Pretty simple way to run PHP in docker during development.

Why separate containers instead of one for everything?

Some people use one container for everything. Like the Official PHP Docker image which provides an option to use php:apache image. This image includes both PHP and Apache. That’s possible. You may also put there a source code of your application as well. But is it really a good way to go?

What Docker's philosophy says about multiple services in a container ? Right, it’s better to separate responsibility areas by using one service per container.

SOLID design principles says about "Single-responsibility principle", which stays for one instance per one responsibility.

The Twelve-Factor App methodology has some recommendations about Codebase, Dependencies, Config, etc. All these practical recommendations also work for containers.

So, it’s better to separate PHP, NGINX containers and codebase as well. Such recommendation is valid for multiple reasons:

  1. You can update only what your need to update, but not everything at once
  2. You have multiple lightweight images instead of one heavy image for everything
  3. You have flexibility for scalability. You can scale only what you need to scale
  4. You can automate scalability with more flexible rules
  5. You can analyze logs for only one specific service (container)

Why NGINX instead of Apache?

NGINX is more resource-efficient and shows better performance than Apache. Apache is the precessor of NGINX. From my personal experience you don’t need Apache anymore, because you can use NGINX instead. It can cover all possible scenarios that you may need

PHP Dockerization in Production/Live Environment

What options do we have to run docker in a Production/Live Environment? Nowadays there are a variety of options. I will cover just some of them that are really popular. Multiple cloud vendor lock-in solutions from different cloud providers: Amazon Elastic Container Service, Google Cloud Run, Azure Container Instances, etc. Non vendor lock-in solutions like Docker Swarm, Kubernetes. All solutions have different Pros and Cons. I would like to cover Docker Swarm, Kubernetes as solutions that may run almost anywhere. But before covering these 2 topics let’s talk about Docker Compose in a Production Environment

Docker Compose in a Production Environment

Can you run Docker Compose in a Production Environment? Yes, you can. Docker has an article about Compose in Production So, basically you can run almost the same docker compose in Production Environment like for Development Environment. Meanwhile, there is a recommendation about volume bindings, which recommends removing any volume bindings for application code, so that code stays inside the container and can't be changed from outside. So, the approach, by sharing code with the next volume syntax ".:/var/www/app/" is not recommended.

Also there are limitations to Docker Compose which may be crucial for the most modern applications. One of the major limitations is you can run Docker Compose only on one server. That brings a limitation to scalability, fault tolerance, reliability which rely on modern backend design.

So, Docker Compose may be a good and quick option for Demo or Prototype purposes, but not an option for modern applications under real Production workload.

Idea of delivering PHP code into the Production/Live Docker Environment

As we can’t use the same way of sharing source code from local machine to container like in Docker Compose, we can use a different approach. We can use docker volume with the PHP source code which will be used by both NGINX and PHP-fpm containers

docker volume for php and nginx

This approach is pretty similar to Docker Compose. We can use a docker volume instead of binding the code directly from the local machine into the container. We have to copy source code into the docker volume before the start of both NGINX and PHP containers.

How can we copy source code into the volume? I came up with the idea of using a code container. The idea of a code container is pretty simple. You can prepare a docker image with the source code. The image should be very lightweight. For this purpose you can use something lightweight like busybox docker image. We need this image only for next things:

  1. Pack the source code into docker image
  2. Copy source code into volume before PHP/NGINX container start

So the Dockerfile for this image may look like this:

FROM busybox:latest
ARG SOURCE_FOLDER=/source
ENV DEST_FOLDER=/tmp/code
WORKDIR $SOURCE_FOLDER
RUN mkdir -p $SOURCE_FOLDER
ADD . $SOURCE_FOLDER
CMD ["sh", "-c", "rm -rf $DEST_FOLDER/*; \
    cp -rp * $DEST_FOLDER; cp -rp . $DEST_FOLDER; \
    chown www-data:www-data $DEST_FOLDER -Rf"]

Reference to Dockerfile on GitHub

Put this Dockerfile into the root folder with your PHP source code. Then run command to build Docker image:

docker build --force-rm=true \
   --tag="php-docker-env-demo-code" \
   --build-arg="SOURCE_FOLDER=/source" \
   --file="Dockerfile" --no-cache .

Note: pay attention to any sensitive data in the folder with the source code. Do not store any sensitive data in a git repository or Docker image. Use .dockerignore file

This image should be stored on some private docker registry and not accessible publicly. You don't want to share private or commercial source code, right?

After preparing the Docker image we have to push it to the private docker registry (Docker Hub, AWS ECR, Google Container Registry, etc.). After that we would be able to use this image with a code container which will pull the image and run the container. Container will copy source code into the volume and complete. Pay attention to the DEST_FOLDER environment variable. By default, it points to /tmp/code folder, but you can use any path you need by changing the value of this variable

The image below demonstrates the process of copying PHP source code into docker volume

PHP code delivery into the Production Docker environment

Dockerize PHP application in Docker Swarm

In my opinion, Docker Swarm is a great option to run small - medium range projects. Pretty simple configuration. Very close to Docker Compose syntax. Easy to understand for any developer familiar with Docker Compose. A great option for PHP projects as well. If you are looking for an option to start with, Docker Swarm would be a nice choice.

Simple configuration for Docker Swarm may look like this:

version: "3.9"
services:
    nginx:
        image: nginx:stable
        environment:
            NGINX_ENVSUBST_TEMPLATE_DIR: /var/www/app/docker/nginx
            NGINX_ENVSUBST_TEMPLATE_SUFFIX: .template
            NGINX_ENVSUBST_OUTPUT_DIR: /etc/nginx/conf.d
            NGINX_PORT: 80
            NGINX_ROOT: /var/www/app/public
            PHP_HOST: php
            PHP_PORT: 9000
        working_dir: /var/www/app/
        volumes:
            - "app_data:/var/www/app"
        networks:
            - swarm
        depends_on:
            - php
            - code
        ports:
            - "80:80"
        deploy:
            mode: global
            update_config:
                parallelism: 0
                delay: 3s
            restart_policy:
                condition: any
    php:
        image: php:8.2.6-fpm-alpine
        working_dir: /var/www/app
        volumes:
            - "app_data:/var/www/app"
        networks:
            - swarm
        depends_on:
            - code
        deploy:
            mode: global
            update_config:
                parallelism: 0
                delay: 1s
            restart_policy:
                condition: any
    code:
        image: luxurydab/php-docker-env-demo-code:latest
        environment:
            DEST_FOLDER: /var/www/app
        volumes:
            - "app_data:/var/www/app"
        networks:
            - swarm
        deploy:
            mode: global
            update_config:
                parallelism: 0
                delay: 0s
            restart_policy:
                condition: none

networks:
    swarm:

volumes:
    app_data:

Reference to docker-stack.yml on GitHub

So, what we have here. We have syntax and configuration very close to Docker Compose, but we also have a code container which will run and copy PHP source code into app_data volume. This volume is used by both PHP and NGINX containers.

Known issues and limitations of Docker Swarm

At the moment of writing this article depends_on section does not support "Long syntax" for Docker Swarm. So, it is not possible to use service_completed_successfully condition which ideally we need for tracking a successful code copy process. By default, depends_on waits just for service start, but not for successful completion. So, you may be in a situation when PHP or NGINX starts before PHP code has been copied into the volume.

You may add additional steps during deployment. You may track the state of the code service with the next command:

docker service inspect docker-swarm-demo_code --format "{{ .UpdateStatus.State }}"

But, this option is not available during the first Docker Swarm deployment. As an alternative solution you may track the state of the code container during first deployment with the next command:

docker inspect `docker ps -a -f 'name=docker-swarm-demo_code' -q --no-trunc | head -n1` --format "{{ .State.Status }}"

This option is less preferable, because it tracks only one container on the server, but not the state of the whole service on multiple machines within the Docker Swarm cluster.

You can track the state of the code container with some deployment tool like Ansible. After successful source code copy you can restart dependent containers (NGINX, PHP).

Dockerize PHP application in Kubernetes

Kuberentes is a powerful, flexible and very popular Docker orchestration tool. This is a top choice for Production/Live Environment. You can get scalability with auto-scaling features, fault tolerance, and reliability. Everything that needs a modern PHP backend. The same time, Kubernetes is not as simple as Docker Swarm. So, the price is complexity. You have to spend time learning Kubernetes, or you need a DevOps engineer for help.

How can we run PHP inside Kubernetes? You have to describe all required resources for the cluster. Kubernetes has resources described in yaml format. In case of a PHP project you need to describe at least 2 Services (PHP, NGINX), Deployments, volumes. Volume will store the source code of PHP.

We can copy PHP source code from Docker image by using Init Containers. Init containers always run to completion and must complete successfully before the next one starts. That’s exactly what we need to run PHP and Nginx pods. So, init container will run the container with the image containing the source code and copy it into the volume storing this code.

I’ve prepared an example configuration for Minikube (kind simple local Kubernetes). The configuration is pretty long. So, please reffer minikube.yaml on GitHub

If you want to play with all or any of the described options, please refer to the GitHub repository which was created while writing this article https://github.com/luxurydab/php-docker-env-demo/ There is a README file as well which explains what to install and how to run any of these options

Last update September 26, 2023
Development docker php
Do you have any questions?

Contact Me