PHP applications are usually composed by a webserver, a relational database system and the language interpreter itself. In this tutorial we will be leveraging a full PHP application stack using docker. This is an in-depth tutorial in which we are going to build and orchestrate containers for Nginx (the webserver), MySQL (the database system) and PHP.<\/p>\n\n\n\n
For the sake of this tutorial, we will write a simple application that reads a list of cities from a database and displays it on a web page, this way we will demonstrate a basic, but working, PHP application.<\/p>\n\n\n\n
This guide assumes that you have Docker-CE already installed and at least a minimal working knowledge of docker. For that matter you may review the following tutorials:<\/p>\n\n\n\n
A real life docker-based application will typically be composed of several containers. Managing these manually can easily become quite messy and cumbersome. That’s where docker-compose comes into play. It helps you to manage a number of containers through a simple Install docker-compose.<\/p>\n\n\n\n Create a folder to hold all the necessary files of this example and then Now create three more folders.<\/p>\n\n\n\n The In this example, we are going to use Note that we are using the Alpine version of the official PHP image. Alpine is a very tiny distribution targeted towards containers by providing much smaller footprints. Also, note the use of the command Now, let’s build this Docker image by issuing the following (inside our As already mentioned, Now put the following contents into this file.<\/p>\n\n\n\n We will explain this syntax. First, note the first line.<\/p>\n\n\n\n This specifies the version of the Note that every service has a specific key inside the This tells We can now orchestrate our containers.<\/p>\n\n\n\n You can run the following command to make sure that the PHP container was executed:<\/p>\n\n\n\n Still inside the The You will see the following output.<\/p>\n\n\n\n Just like the PHP container, we need to create a custom image for the webserver. But in this case, we just need to provide a configuration for our Now put the following contents into this We are using the default Nginx image based on Alpine. On this Docker file we simply copy a configuration file into our application setup. Before building this image, create a configuration file.<\/p>\n\n\n\n Now populate it with this content.<\/p>\n\n\n\n Note that at the Now update the We have only added a new service. The configuration is nearly the same, except for the following.<\/p>\n\n\n\n Once the Nginx container needs the PHP service to be fully initialized, we force this requirement in the Now create a file called Make sure the port Once again, double check that the service is up.<\/p>\n\n\n\n Open a browser and access\u00a0 You will see the PHP info page.<\/p>\n\n\n\n The official MySQL image allows you to configure the container through simple environment variables. This can be done with an Now we’ve defined a new service for the database. Notice the line Before orchestrating our new configuration, let’s download a sample MySQL database. The\u00a0official MySQL documentation<\/strong><\/a>\u00a0provides some sample databases. We will be using the well-known world database. This database provides a listing of countries and cities. To download this sample, execute the following inside our app folder.<\/p>\n\n\n\n Now lets orchestrate our containers.<\/p>\n\n\n\n As you may have already noticed, the One more time, check that the MySQL container was started.<\/p>\n\n\n\n Now populate the world database.<\/p>\n\n\n\n You can verify that the database was populated by selecting data directly from the database. First access the MySQL prompt inside the container.<\/p>\n\n\n\n In the MySQL prompt, run the following.<\/p>\n\n\n\n You will see a list of cities. Now quit the MySQL prompt.<\/p>\n\n\n\n Now that all of the necessary containers are up and running, we can focus on our sample application. Update the If you access\u00a0 In this tutorial, I have demonstrated step by step how to configuring a fully working PHP application. We built custom images for PHP and Nginx, and configured docker-compose to orchestrate our containers. Despite being very basic and simple, this setup reflects a real life scenario.<\/p>\n\n\n\n In this guide, we have built and tagged our images locally. For a more flexible setup, you can push these images to a\u00a0docker registry<\/strong><\/a>. You may push to the official docker registry or even setup your own docker registry. In any case, this will allow you to build your images on one host and use them on another.<\/p>\n\n\n\n For a more detailed usage of\u00a0 Depending on your application requirements and the PHP framework you use, you may want to add more extensions. This can easily be done by modifying the\u00a0yaml<\/code> configuration file.<\/p>\n\n\n\n
curl -L https:\/\/github.com\/docker\/compose\/releases\/download\/1.19.0\/docker-compose-`uname -s`-`uname -m` -o \/usr\/local\/bin\/docker-compose\nchmod +x \/usr\/local\/bin\/docker-compose<\/code><\/pre>\n\n\n\n
cd<\/code> into it. From now on, this is our working directory and every command will be executed inside this folder and every path will be referenced relative to it. This folder may be referenced later as
WORKING_DIR<\/code>.<\/p>\n\n\n\n
mkdir ~\/docker\ncd ~\/docker<\/code><\/pre>\n\n\n\n
mkdir php nginx app<\/code><\/pre>\n\n\n\n
php<\/code> folder is where we will build our custom PHP image, the
nginx<\/code> folder will hold the necessary files for our custom nginx image and the
app<\/code> folder is where we will be putting the source code and configuration of our sample application.<\/p>\n\n\n\n
Configuring the PHP container<\/h2>\n\n\n\n
php-fpm<\/code> to connect to the Nginx webserver. We will be using the official PHP base image. However, we also need to install and enable some extensions so that we may access the database. Inside the
php<\/code> folder create a file named
Dockerfile<\/code> and put the following contents into it.<\/p>\n\n\n\n
FROM php:7.1-fpm-alpine3.4\nRUN apk update --no-cache \\\n && apk add --no-cache $PHPIZE_DEPS \\\n && apk add --no-cache mysql-dev \\\n && docker-php-ext-install pdo pdo_mysql<\/code><\/pre>\n\n\n\n
docker-php-ext-install<\/code>, the official PHP image provides this command to ease the process of installing and configuring PHP extensions.<\/p>\n\n\n\n
WORKING_DIR<\/code>):<\/p>\n\n\n\n
docker build -t aklwebhost-php php\/<\/code><\/pre>\n\n\n\n
The
docker-compose.yml<\/code> file<\/h3>\n\n\n\n
docker-compose<\/code> allows you to manage a number of containers through a simple configuration file. This configuration file is typically named
docker-compose.yml<\/code>. Create this file inside the
app<\/code> folder.<\/p>\n\n\n\n
touch app\/docker-compose.yml<\/code><\/pre>\n\n\n\n
version: '2'\nservices:\n php:\n image: aklwebhost-php\n volumes:\n - .\/:\/app\n working_dir: \/app<\/code><\/pre>\n\n\n\n
version: '2'<\/code><\/pre>\n\n\n\n
docker-compose.yml<\/code> configuration file used. The next line specifies the services, or in other words, the containers to be provisioned.<\/p>\n\n\n\n
services:\n php:\n image: aklwebhost-php\n volumes:\n - .\/:\/app\n working_dir: \/app<\/code><\/pre>\n\n\n\n
services<\/code> block. The name specified here will be used to reference this specific container later. Also note that inside the
php<\/code> configuration, we define the image used to run the container (this is the image we built previously). We also define a volume mapping.<\/p>\n\n\n\n
volumes:\n - .\/:\/app<\/code><\/pre>\n\n\n\n
docker-compose<\/code> to map the current directory (
.\/<\/code>) to the
\/app<\/code> directory inside the container. The last line sets the
\/app<\/code> folder inside the container as the working directory, which means that this is the folder where all future commands inside a container are by default executed from.<\/p>\n\n\n\n
cd ~\/docker\/app\ndocker-compose up -d<\/code><\/pre>\n\n\n\n
docker ps<\/code><\/pre>\n\n\n\n
How to execute commands inside the containers<\/h3>\n\n\n\n
app<\/code> folder, we can run any command inside a defined service container with the help of the
docker-compose<\/code> command.<\/p>\n\n\n\n
docker-compose exec [service] [command]<\/code><\/pre>\n\n\n\n
[service]<\/code> placeholder refers to the service key. In our case, this was
php<\/code>. Let’s run a command inside the container to check our PHP version.<\/p>\n\n\n\n
docker-compose exec php php -v<\/code><\/pre>\n\n\n\n
PHP 7.1.14 (cli) (built: Feb 7 2018 00:40:45) ( NTS )\nCopyright (c) 1997-2018 The PHP Group\nZend Engine v3.1.0, Copyright (c) 1998-2018 Zend Technologies<\/code><\/pre>\n\n\n\n
Configuring the Nginx container<\/h2>\n\n\n\n
virtual host<\/code>. Make sure you are inside our
WORKING_DIR<\/code> and create a
Dockerfile<\/code> inside the
nginx<\/code> folder:<\/p>\n\n\n\n
cd ~\/docker\ntouch nginx\/Dockerfile<\/code><\/pre>\n\n\n\n
Dockerfile<\/code>:<\/p>\n\n\n\n
FROM nginx:1.13.8-alpine\nCOPY .\/default.conf \/etc\/nginx\/conf.d\/default.conf<\/code><\/pre>\n\n\n\n
touch nginx\/default.conf<\/code><\/pre>\n\n\n\n
server {\n listen 80 default_server;\n listen [::]:80 default_server ipv6only=on;\n\n root \/app;\n index index.php;\n\n #server_name server_domain_or_IP;\n\n location \/ {\n try_files $uri $uri\/ \/index.php?$query_string;\n }\n\n location ~ \\.php$ {\n try_files $uri \/index.php =404;\n fastcgi_split_path_info ^(.+\\.php)(\/.+)$;\n fastcgi_pass php:9000;\n fastcgi_index index.php;\n fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n include fastcgi_params;\n }\n}<\/code><\/pre>\n\n\n\n
fastcgi_pass php:9000<\/code> line we are referencing the PHP container by it’s name inside the
service<\/code> block of the
docker-compose.yml<\/code> configuration file. Internally
docker-compose<\/code> creates a network and assigns the service name as the host name to each of the services defined. We can now build the Nginx image.<\/p>\n\n\n\n
docker build -t aklwebhost-nginx nginx\/<\/code><\/pre>\n\n\n\n
Updating
docker-compose.yml<\/code><\/h3>\n\n\n\n
app\/docker-compose.yml<\/code> file.<\/p>\n\n\n\n
version: '2'\nservices:\n php:\n image: aklwebhost-php\n volumes:\n - .\/:\/app\n working_dir: \/app\n web:\n image: aklwebhost-nginx\n volumes:\n - .\/:\/app\n depends_on:\n - php\n ports:\n - 80:80<\/code><\/pre>\n\n\n\n
depends_on:\n - php\nports:\n - 80:80<\/code><\/pre>\n\n\n\n
depends_on<\/code> option. The
ports<\/code> configuration key maps a host port to a container port, here we map the port
80<\/code> in the host to the port
80<\/code> in the container.<\/p>\n\n\n\n
index.php<\/code> inside the
app<\/code> folder and put the following in it.<\/p>\n\n\n\n
<?php phpinfo();<\/code><\/pre>\n\n\n\n
80<\/code> is accessible through your firewall and execute the following.<\/p>\n\n\n\n
cd ~\/docker\/app\ndocker-compose up -d<\/code><\/pre>\n\n\n\n
docker ps<\/code><\/pre>\n\n\n\n
[aklwebhost-instance-ip]<\/code>. You may find out your AKLWEB HOST<\/a><\/strong> instance IP address by running the following.<\/p>\n\n\n\n
hostname -I<\/code><\/pre>\n\n\n\n
Configuring the MySQL container<\/h2>\n\n\n\n
environment<\/code> option inside the service block definition. Update the
~\/docker\/app\/docker-compose.yml<\/code> file to the following.<\/p>\n\n\n\n
version: '2'\nservices:\n php:\n image: aklwebhost-php\n volumes:\n - .\/:\/app\n working_dir: \/app\n web:\n image: aklwebhost-nginx\n volumes:\n - .\/:\/app\n depends_on:\n - php\n ports:\n - 80:80\n mysql:\n image: mysql:5.7.21\n volumes:\n - .\/:\/app\n - dbdata:\/var\/lib\/mysql\n environment:\n - MYSQL_DATABASE=world\n - MYSQL_ROOT_PASSWORD=root\n working_dir: \/app\nvolumes:\n dbdata:<\/code><\/pre>\n\n\n\n
dbdata:\/var\/lib\/mysql<\/code>. This mounts the path on the container
\/var\/lib\/mysql<\/code> to a persistent volume managed by Docker, this way the database data persists after the container is removed. This volume needs to be defined in a top-level block as you can see in the end of the file.<\/p>\n\n\n\n
curl -L http:\/\/downloads.mysql.com\/docs\/world.sql.gz -o world.sql.gz\ngunzip world.sql.gz<\/code><\/pre>\n\n\n\n
docker-compose up -d<\/code><\/pre>\n\n\n\n
docker-compose up<\/code> command starts only the containers that are not already started. It checks for the differences between your
docker-compose.yml<\/code> file and the current configuration of running containers.<\/p>\n\n\n\n
docker ps<\/code><\/pre>\n\n\n\n
docker-compose exec -T mysql mysql -uroot -proot world < world.sql<\/code><\/pre>\n\n\n\n
docker-compose exec mysql mysql -uroot -proot world<\/code><\/pre>\n\n\n\n
select * from city limit 10;<\/code><\/pre>\n\n\n\n
mysql> exit<\/code><\/pre>\n\n\n\n
Building our application<\/h2>\n\n\n\n
app\/index.php<\/code> file to the following.<\/p>\n\n\n\n
<?php\n\n$pdo = new PDO('mysql:host=mysql;dbname=world;charset=utf8', 'root', 'root');\n\n$stmt = $pdo->prepare(\"\n select city.Name, city.District, country.Name as Country, city.Population\n from city\n left join country on city.CountryCode = country.Code\n order by Population desc\n limit 10\n\");\n$stmt->execute();\n$cities = $stmt->fetchAll(PDO::FETCH_ASSOC);\n\n?>\n\n<!doctype html>\n<html>\n<head>\n <meta charset=\"UTF-8\">\n <title>AKLWEB HOST Rocks!<\/title>\n<\/head>\n<body>\n <h2>Most Populous Cities In The World<\/h2>\n <table>\n <thead>\n <tr>\n <th>Name<\/th>\n <th>Country<\/th>\n <th>District<\/th>\n <th>Population<\/th>\n <\/tr>\n <\/thead>\n <tbody>\n <?php foreach($cities as $city): ?>\n <tr>\n <td><?=$city['Name']?><\/td>\n <td><?=$city['Country']?><\/td>\n <td><?=$city['District']?><\/td>\n <td><?=number_format($city['Population'], 0)?><\/td>\n <\/tr>\n <?php endforeach ?>\n <\/tbody>\n <\/table>\n<\/body>\n<\/html><\/code><\/pre>\n\n\n\n
[aklwebhost-instance-ip]<\/code>\u00a0in a web browser, you will see a list of the most populous cities in the world. Congratulations, you have deployed a fully working PHP application using docker.<\/p>\n\n\n\n
Conclusion<\/h2>\n\n\n\n
docker-compose<\/code>, you should refer to the\u00a0official documentation<\/a><\/strong>.<\/p>\n\n\n\n
Dockerfile<\/code>\u00a0used to build our custom PHP image. However, some extensions need extra dependencies to be installed in the container. You should refer to the list of extensions in the\u00a0PHP official documentation<\/strong><\/a>\u00a0to review the basic requirements of each extension.<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"open","ping_status":"closed","template":"","format":"standard","manualknowledgebasecat":[109,110,242,587],"manual_kb_tag":[646,657,658,660,659,661],"_links":{"self":[{"href":"https:\/\/support.aklwebhost.com\/wp-json\/wp\/v2\/manual_kb\/3842"}],"collection":[{"href":"https:\/\/support.aklwebhost.com\/wp-json\/wp\/v2\/manual_kb"}],"about":[{"href":"https:\/\/support.aklwebhost.com\/wp-json\/wp\/v2\/types\/manual_kb"}],"author":[{"embeddable":true,"href":"https:\/\/support.aklwebhost.com\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/support.aklwebhost.com\/wp-json\/wp\/v2\/comments?post=3842"}],"version-history":[{"count":1,"href":"https:\/\/support.aklwebhost.com\/wp-json\/wp\/v2\/manual_kb\/3842\/revisions"}],"predecessor-version":[{"id":3843,"href":"https:\/\/support.aklwebhost.com\/wp-json\/wp\/v2\/manual_kb\/3842\/revisions\/3843"}],"wp:attachment":[{"href":"https:\/\/support.aklwebhost.com\/wp-json\/wp\/v2\/media?parent=3842"}],"wp:term":[{"taxonomy":"manualknowledgebasecat","embeddable":true,"href":"https:\/\/support.aklwebhost.com\/wp-json\/wp\/v2\/manualknowledgebasecat?post=3842"},{"taxonomy":"manual_kb_tag","embeddable":true,"href":"https:\/\/support.aklwebhost.com\/wp-json\/wp\/v2\/manual_kb_tag?post=3842"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}