In this example we are going to create three containers (Apache, MySQL, PHP-FPM) for a web application.


Flow


  1. Requests goes to Apache container.

  2. Apache container calls PHP-FPM container.

  3. PHP-FPM container calls MySQL container.

  4. Response is returned to Apache container.

  5. Page is served by Apache container.

Setup



The most important point here is that, when we remove containers we won't lose any data such as application files, logs and database because they are not stored in containers.


Initial structure


The "logs" and "database" folders get created when we run ./build.sh file first time so no need to create them manually. You need to run chmod +x docker/build.sh only once.


ubuntu@linux:~/helloworld$ tree -a
.
├── bad.php
├── docker
│   ├── apache
│   │   ├── Dockerfile
│   │   ├── httpd.conf
│   │   └── httpd-vhosts.conf
│   ├── build.sh
│   ├── destroy.sh
│   ├── docker-compose.yml
│   ├── .env
│   ├── mysql
│   │   └── Dockerfile
│   └── php
│   ├── Dockerfile
│   └── www.conf
└── index.php

4 directories, 12 files

Files


bad.php


ubuntu@linux:~/helloworld$ cat bad.php 
<?php

echo 'This is bad file which will create entry in log file'

index.php


ubuntu@linux:~/helloworld$ cat index.php 
<?php

echo 'Hello World!'.PHP_EOL;

$servername = getenv('MYSQL_IP');
$username = getenv('MYSQL_ROOT_USER');
$password = getenv('MYSQL_ROOT_PASSWORD');

$conn = mysqli_connect($servername, $username, $password);
if (!$conn) {
exit('Connection failed: '.mysqli_connect_error().PHP_EOL);
}

echo 'Successful database connection!'.PHP_EOL;

docker/build.sh


ubuntu@linux:~/helloworld$ cat docker/build.sh 
#!/bin/bash
set -e

if ! [[ -d ../logs/apache ]]; then
mkdir -p ../logs/apache
fi

if ! [[ -d ../logs/mysql ]]; then
mkdir -p ../logs/mysql
fi

if ! [[ -d ../logs/php ]]; then
mkdir -p ../logs/php
fi

if ! [[ -d ../database ]]; then
mkdir ../database
fi

docker-compose up -d --build

docker exec helloworld_apache_con chown -R root:www-data /usr/local/apache2/logs
docker exec helloworld_php_con chown -R root:www-data /usr/local/etc/logs

docker/destroy.sh


ubuntu@linux:~/helloworld$ cat docker/destroy.sh 
#!/bin/bash
set -e

docker-compose down --volumes
docker rmi helloworld_apache_img helloworld_php_img

docker/.env


ubuntu@linux:~/helloworld$ cat docker/.env 
COMPOSE_PROJECT_NAME=helloworld

WEB_USER=www-data
WEB_GROUP=www-data

APACHE_IP=192.168.0.11
APACHE_EXPOSED_PORT=9000
APACHE_ROOT_DIR=/usr/local/apache2

MYSQL_IP=192.168.0.22
MYSQL_CONTAINER_USER=mysql
MYSQL_CONTAINER_GROUP=mysql
MYSQL_ROOT_USER=root
MYSQL_ROOT_PASSWORD=root
MYSQL_DATA_DIR=/var/lib/mysql
MYSQL_LOG_DIR=/var/log/mysql

PHP_IP=192.168.0.33
PHP_APP_DIR=/srv/app
PHP_ROOT_DIR=/usr/local/etc

NETWORK_SUBNET=192.168.0.0/24

docker/docker-compose.yml


ubuntu@linux:~/helloworld$ cat docker/docker-compose.yml 
version: '3'

services:
apache_img:
container_name: ${COMPOSE_PROJECT_NAME}_apache_con
build:
context: ./apache
args:
- WEB_USER=${WEB_USER}
- WEB_GROUP=${WEB_GROUP}
- APACHE_ROOT_DIR=${APACHE_ROOT_DIR}
volumes:
- ../logs/apache:${APACHE_ROOT_DIR}/logs
ports:
- ${APACHE_EXPOSED_PORT}:80
networks:
public_net:
ipv4_address: ${APACHE_IP}
environment:
- APACHE_EXPOSED_PORT=${APACHE_EXPOSED_PORT}
- APACHE_ROOT_DIR=${APACHE_ROOT_DIR}
- PHP_IP=${PHP_IP}
- PHP_APP_DIR=${PHP_APP_DIR}
- WEB_USER=${WEB_USER}
- WEB_GROUP=${WEB_GROUP}
mysql_img:
container_name: ${COMPOSE_PROJECT_NAME}_mysql_con
build:
context: ./mysql
args:
- MYSQL_CONTAINER_USER=${MYSQL_CONTAINER_USER}
- MYSQL_CONTAINER_GROUP=${MYSQL_CONTAINER_GROUP}
volumes:
- ../logs/mysql:${MYSQL_LOG_DIR}
- ../database:${MYSQL_DATA_DIR}
networks:
public_net:
ipv4_address: ${MYSQL_IP}
environment:
- MYSQL_CONTAINER_USER=${MYSQL_CONTAINER_USER}
- MYSQL_CONTAINER_GROUP=${MYSQL_CONTAINER_GROUP}
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
php_img:
container_name: ${COMPOSE_PROJECT_NAME}_php_con
build:
context: ./php
args:
- WEB_USER=${WEB_USER}
- WEB_GROUP=${WEB_GROUP}
- PHP_ROOT_DIR=${PHP_ROOT_DIR}
working_dir: ${PHP_APP_DIR}
volumes:
- ..:${PHP_APP_DIR}
- ../logs/php:${PHP_ROOT_DIR}/logs
depends_on:
- apache_img
- mysql_img
networks:
public_net:
ipv4_address: ${PHP_IP}
environment:
- PHP_ROOT_DIR=${PHP_ROOT_DIR}
- APACHE_IP=${APACHE_IP}
- APACHE_EXPOSED_PORT=${APACHE_EXPOSED_PORT}
- WEB_USER=${WEB_USER}
- WEB_GROUP=${WEB_GROUP}
- MYSQL_IP=${MYSQL_IP}
- MYSQL_ROOT_USER=${MYSQL_ROOT_USER}
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}

networks:
public_net:
driver: bridge
ipam:
driver: default
config:
- subnet: ${NETWORK_SUBNET}

docker/apache/Dockerfile


ubuntu@linux:~/helloworld$ cat docker/apache/Dockerfile 
FROM httpd:2.4

ARG WEB_USER
ARG WEB_GROUP
ARG APACHE_ROOT_DIR

COPY httpd-vhosts.conf ${APACHE_ROOT_DIR}/conf/extra/httpd-vhosts.conf
COPY httpd.conf ${APACHE_ROOT_DIR}/conf/httpd.conf

RUN chgrp -R ${WEB_GROUP} ${APACHE_ROOT_DIR}/conf/httpd.conf \
&& chgrp -R ${WEB_GROUP} ${APACHE_ROOT_DIR}/conf/extra/httpd-vhosts.conf

RUN usermod -u 1000 ${WEB_USER} \
&& groupmod -g 1000 ${WEB_GROUP} \
&& chgrp -R ${WEB_GROUP} ${APACHE_ROOT_DIR}

docker/apache/httpd.conf


ubuntu@linux:~/helloworld$ cat docker/apache/httpd.conf 
ServerRoot ${APACHE_ROOT_DIR}
Listen 80

LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so
LoadModule authn_file_module modules/mod_authn_file.so
LoadModule authn_core_module modules/mod_authn_core.so
LoadModule authz_host_module modules/mod_authz_host.so
LoadModule authz_groupfile_module modules/mod_authz_groupfile.so
LoadModule authz_user_module modules/mod_authz_user.so
LoadModule authz_core_module modules/mod_authz_core.so
LoadModule access_compat_module modules/mod_access_compat.so
LoadModule auth_basic_module modules/mod_auth_basic.so
LoadModule reqtimeout_module modules/mod_reqtimeout.so
LoadModule filter_module modules/mod_filter.so
LoadModule mime_module modules/mod_mime.so
LoadModule log_config_module modules/mod_log_config.so
LoadModule env_module modules/mod_env.so
LoadModule headers_module modules/mod_headers.so
LoadModule setenvif_module modules/mod_setenvif.so
LoadModule version_module modules/mod_version.so
LoadModule unixd_module modules/mod_unixd.so
LoadModule status_module modules/mod_status.so
LoadModule autoindex_module modules/mod_autoindex.so
<IfModule !mpm_prefork_module>
</IfModule>
<IfModule mpm_prefork_module>
</IfModule>
LoadModule dir_module modules/mod_dir.so
LoadModule alias_module modules/mod_alias.so

<IfModule unixd_module>
User daemon
Group daemon
</IfModule>

ServerAdmin you@example.com

<Directory />
AllowOverride none
Require all denied
</Directory>

DocumentRoot ${APACHE_ROOT_DIR}/htdocs
<Directory ${APACHE_ROOT_DIR}/htdocs>
Options Indexes FollowSymLinks
AllowOverride None
Require all granted
</Directory>

<IfModule dir_module>
DirectoryIndex index.php index.html
</IfModule>

<Files ".ht*">
Require all denied
</Files>

ErrorLog /proc/self/fd/2

LogLevel info

<IfModule log_config_module>
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
LogFormat "%h %l %u %t \"%r\" %>s %b" common

<IfModule logio_module>
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio
</IfModule>

CustomLog /proc/self/fd/1 common
</IfModule>

<IfModule alias_module>
ScriptAlias /cgi-bin/ ${APACHE_ROOT_DIR}/cgi-bin/
</IfModule>

<IfModule cgid_module>
</IfModule>

<Directory ${APACHE_ROOT_DIR}/cgi-bin>
AllowOverride None
Options None
Require all granted
</Directory>

<IfModule headers_module>
RequestHeader unset Proxy early
</IfModule>

<IfModule mime_module>
TypesConfig conf/mime.types

AddType application/x-compress .Z
AddType application/x-gzip .gz .tgz
</IfModule>

Include conf/extra/httpd-vhosts.conf

<IfModule proxy_html_module>
Include conf/extra/proxy-html.conf
</IfModule>

<IfModule ssl_module>
SSLRandomSeed startup builtin
SSLRandomSeed connect builtin
</IfModule>

ServerName localhost

docker/apache/httpd-vhosts.conf


ubuntu@linux:~/helloworld$ cat docker/apache/httpd-vhosts.conf 
<VirtualHost *:80>
ProxyPassMatch ^/(.*\.php(/.*)?)$ fcgi://${PHP_IP}:${APACHE_EXPOSED_PORT}${PHP_APP_DIR}/$1

DocumentRoot ${APACHE_ROOT_DIR}/htdocs

<Directory ${APACHE_ROOT_DIR}/htdocs>
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
</Directory>

ErrorLog ${APACHE_ROOT_DIR}/logs/error.log
CustomLog ${APACHE_ROOT_DIR}/logs/access.log common
</VirtualHost>

docker/mysql/Dockerfile


ubuntu@linux:~/helloworld$ cat docker/mysql/Dockerfile 
FROM mysql:5.7

ARG MYSQL_CONTAINER_USER
ARG MYSQL_CONTAINER_GROUP

RUN sed -i "s/#log-error/log-error/g" /etc/mysql/mysql.conf.d/mysqld.cnf

RUN usermod -u 1000 ${MYSQL_CONTAINER_USER} \
&& groupmod -g 1000 ${MYSQL_CONTAINER_GROUP}

docker/php/Dockerfile


ubuntu@linux:~/helloworld$ cat docker/php/Dockerfile 
FROM php:7.1-fpm

ARG WEB_USER
ARG WEB_GROUP
ARG PHP_ROOT_DIR

COPY www.conf ${PHP_ROOT_DIR}/php-fpm.d/www.conf

RUN docker-php-ext-install mysqli

RUN usermod -u 1000 ${WEB_USER} \
&& groupmod -g 1000 ${WEB_GROUP} \
&& chgrp -R staff ${PHP_ROOT_DIR}/php-fpm.d/www.conf

docker/php/www.conf


ubuntu@linux:~/helloworld$ cat docker/php/www.conf 
[www]
user = ${WEB_USER}
group = ${WEB_GROUP}
listen = 80
listen.allowed_clients = ${APACHE_IP}

pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3

catch_workers_output = yes
php_flag[display_errors] = off
php_admin_flag[log_errors] = on
php_admin_value[error_log] = ${PHP_ROOT_DIR}/logs/error.log

Validation


Validate "docker-compose.yml" file and see the mapping.


ubuntu@linux:~/helloworld/docker$ docker-compose config
networks:
public_net:
driver: bridge
ipam:
config:
- subnet: 192.168.0.0/24
driver: default
services:
apache_img:
build:
args:
APACHE_ROOT_DIR: /usr/local/apache2
WEB_GROUP: www-data
WEB_USER: www-data
context: /home/ubuntu/helloworld/docker/apache
container_name: helloworld_apache_con
environment:
APACHE_EXPOSED_PORT: '9000'
APACHE_ROOT_DIR: /usr/local/apache2
PHP_APP_DIR: /srv/app
PHP_IP: 192.168.0.33
WEB_GROUP: www-data
WEB_USER: www-data
networks:
public_net:
ipv4_address: 192.168.0.11
ports:
- 9000:80/tcp
volumes:
- /home/ubuntu/helloworld/logs/apache:/usr/local/apache2/logs:rw
mysql_img:
build:
args:
MYSQL_CONTAINER_GROUP: mysql
MYSQL_CONTAINER_USER: mysql
context: /home/ubuntu/helloworld/docker/mysql
container_name: helloworld_mysql_con
environment:
MYSQL_CONTAINER_GROUP: mysql
MYSQL_CONTAINER_USER: mysql
MYSQL_ROOT_PASSWORD: root
networks:
public_net:
ipv4_address: 192.168.0.22
volumes:
- /home/ubuntu/helloworld/logs/mysql:/var/log/mysql:rw
- /home/ubuntu/helloworld/database:/var/lib/mysql:rw
php_img:
build:
args:
PHP_ROOT_DIR: /usr/local/etc
WEB_GROUP: www-data
WEB_USER: www-data
context: /home/ubuntu/helloworld/docker/php
container_name: helloworld_php_con
depends_on:
- apache_img
- mysql_img
environment:
APACHE_EXPOSED_PORT: '9000'
APACHE_IP: 192.168.0.11
MYSQL_IP: 192.168.0.22
MYSQL_ROOT_PASSWORD: root
MYSQL_ROOT_USER: root
PHP_ROOT_DIR: /usr/local/etc
WEB_GROUP: www-data
WEB_USER: www-data
networks:
public_net:
ipv4_address: 192.168.0.33
volumes:
- /home/ubuntu/helloworld:/srv/app:rw
- /home/ubuntu/helloworld/logs/php:/usr/local/etc/logs:rw
working_dir: /srv/app
version: '3.0'

Build


ubuntu@linux:~/helloworld/docker$ ./build.sh

Creating network "helloworld_public_net" with driver "bridge"
Building apache_img
Successfully built 756224de2345
Successfully tagged helloworld_apache_img:latest

Building mysql_img
Successfully built cbb571547e11
Successfully tagged helloworld_mysql_img:latest

Building php_img
Successfully built 6214b34aec76

Creating helloworld_apache_con ... done
Creating helloworld_php_con ... done
Creating helloworld_mysql_con ...
Creating helloworld_php_con ...

Confirmation


If you want to see the details of each element, you can run docker inspect command.


Images


ubuntu@linux:~/helloworld/docker$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
helloworld_php_img latest 6214b34aec76 2 minutes ago 383MB
helloworld_mysql_img latest cbb571547e11 3 minutes ago 409MB
helloworld_apache_img latest 756224de2345 3 minutes ago 185MB
...

Network


ubuntu@linux:~/helloworld/docker$ docker network ls
NETWORK ID NAME DRIVER SCOPE
a7bbc7d71e55 helloworld_public_net bridge local
...

Containers


ubuntu@linux:~/helloworld/docker$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e8adf3e917fb helloworld_php_img "docker-php-entryp..." 4 minutes ago Up 4 minutes 9000/tcp helloworld_php_con
30d7405b37dc helloworld_mysql_img "docker-entrypoint..." 4 minutes ago Up 4 minutes 3306/tcp helloworld_mysql_con
40d387973713 helloworld_apache_img "httpd-foreground" 4 minutes ago Up 4 minutes 0.0.0.0:9000->80/tcp helloworld_apache_con

Structure


As you can see below, "logs" and "database" folders have been created automatically and contain some initial files in them. When we start using our application, there will be more files in each and logs will be updated as well. There isn't any log file in "php" folder yet because we have't started using our application yet.


ubuntu@linux:~/helloworld$ tree -a
.
├── database
│   ├── ...
│   ├── mysql
│   │   ├── ...
│   │   └── ...
│   ├── performance_schema
│   │   ├── ...
│   │   └── ...
│   └── sys
│   ├── ...
│   └── ...
└── logs
├── apache
│   ├── access.log
│   ├── error.log
│   └── httpd.pid
├── mysql
│   └── error.log
└── php

12 directories, 299 files

Test


Success


ubuntu@linux:~/helloworld$ curl 192.168.0.11
Hello World!
Successful database connection!

ubuntu@linux:~/helloworld$ curl localhost:9000
Hello World!
Successful database connection!

Access logs updated.


ubuntu@linux:~/helloworld$ cat logs/apache/access.log 
192.168.0.1 - - [03/Feb/2018:16:13:16 +0000] "GET / HTTP/1.1" 200 45
192.168.0.1 - - [03/Feb/2018:16:13:37 +0000] "GET / HTTP/1.1" 200 45

Failure


When you run this example, it will silently break but create a log file because "bad.php" file is broken.


ubuntu@linux:~/helloworld$ curl 192.168.0.11/bad.php

ubuntu@linux:~/helloworld$ cat logs/php/error.log 
[03-Feb-2018 16:24:06 UTC] PHP Parse error: syntax error, unexpected end of file, expecting ',' or ';' in /srv/app/bad.php on line 4

Information


It's up to you how to manage the permissions of files and folder in/out of containers. I added two lines at the bottom of "build.sh" file but if you want, you can remove them to see what changes it makes in container and host OS.