Docker Compose for nginx, PHP, Redis, and MySQL
A friend of mine has a side project, currently deployed on AWS, using nginx for static assets, and PHP for for the backend. The backend connects to Redis and MySQL. This is all working fine, but he’s had to set it up a couple times, and each time he forgets some setting, and scratches his head for a while until he remebers what was missed.
As well, he has been talking about migrating away from AWS to a dedicated server.
I’ve been talking up Docker, and DevOps/Infrastructure As Code to him, so he asked me to show him. This is what I came up with.
In addition to what I mentioned above, I’ll be using Docker Compose and Traefik in the following. You can find my final files on GitHub
Ok, So I started with the following docker-compose.yml
file:
version: "3.7"
services:
proxy:
image: traefik:v2.2
command:
- --api.insecure=true
- --providers.docker
ports:
- 80:80
- 443:443
- 8080:8080
volumes:
- /var/run/docker.sock:/var/run/docker.sock
Start Traefik by running docker-compose up
, then visit
http://localhost:8080 and you should see the Traefik
dashboard. Press ctrl-c
in the shell where you ran docker to
shout things down, or go to a separate shell, in the same directory,
and type docker-compose down
.
Next, I added Nginx:
web:
image: nginx:latest
labels:
- "traefik.enable=true"
- "traefik.http.routers.web.rule=Host(`web.docker.localhost`)"
If you refresh the dashboad, you should see an entry for
web.docker.localhost
. If you visit http://web.docker.localhost,
you should see the nginx welcome page.
Then I created a local directory, called nginx
. Inside it, I created anther directory,
public
, and a file called site.conf
:
server {
index index.php index.html;
server_name web.docker.localhost;
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
root /public;
}
Inside public
, I created index.html
:
Hello Frustrated.Blog
I updated the web
section of docker-compose.yml
:
web:
image: nginx:latest
labels:
- "traefik.enable=true"
- "traefik.http.routers.web.rule=Host(`web.docker.localhost`)"
volumes:
- ./nginx/public:/public
- ./nginx/site.conf:/etc/nginx/conf.d/site.conf
Restart everything, and vist web.docker.localhost.
You should see Hello Frustrated.Blog
.
Then, I added PHP:
php:
image: php:7.4-fpm
volumes:
- ./nginx/public:/public
Added index.php
to nginx/public
:
<?php
echo phpinfo();
?>
and added this to nginx/site.cnf
:
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass php_php_1:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
Restart everything, and load web.docker.localhost. You should see the PNP info page.
Time for Redis! Now, this requires installing a plugin to the PHP container. My friend used [compose][compose] to install [predis][predis], so I needed to do the same. After a lot of searching, I came up with this solution.
I created a php
directory. Inside, I created composer.json
which will
be used to install predis:
{
"require": {
"predis/predis": "^1.1"
}
}
I also added a Dockerfile
to build on top of php:fpm-alpine
, install
composer, and then run composer to install predis.
FROM php:fpm-alpine
RUN docker-php-ext-install mysqli
RUN curl -sS https://getcomposer.org/installer | php
COPY composer.json ./
RUN php composer.phar install
CMD ["php-fpm"]
EXPOSE 9000
And updated the php
section of docker-compose.yml
:
php:
build:
context: php
dockerfile: Dockerfile
volumes:
- ./nginx/public:/public
Then I added Redis:
redis:
image: redis:latest
I renamed nginx/public/index.php
to nginx/public/info.php
, and added
nginx/public/redis.php
(this file was donated by my friend, as it has
been a long time since I wrote any PHP):
<?php
require_once '/var/www/html/vendor/predis/predis/autoload.php';
$redis = new Predis\Client(['host' => 'redis']);
$redisStatus = redisConnect($redis);
// check redis I/O
if ($redisStatus == "OK") {
$ok = $redis->set("testKey", "testValue");
if ($ok == "OK") {
$okBool = $redis->get("testKey");
if ($okBool) {
$redis->del("testKey");
$redisStatus = "Yessir. Readin' and ritin' to redis";
} else {
$redisStatus = "Failed reading redis testKey";
}
} else {
$redisStatus = "Failed writing redis testKey";
}
}
// associative array maps nicely to json
$payload = array(
"greeting" => " Hello Moz!",
"redis" => $redisStatus
);
// send a repsonse to caller
sendResponse(200, json_encode($payload));
// bye -- no close for redis
exit(1);
function sendResponse($status = 200, $body = '', $content_type = 'application/json')
{
// old-school http response
$status_header = 'HTTP/1.1 ' . $status . ' OK';
header($status_header);
header('Content-type: ' . $content_type);
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS, HEAD');
header('Access-Control-Allow-Headers: apikey, Authorization, Origin, X-Requested-With, Content-Type, Accept');
echo $body;
}
function redisConnect($mem) {
try {
$mem->connect();
$mem->select(0);
$status = "OK";
}
catch (Exception $exception) {
$status = "Redis failed to connect";
}
return $status;
}
?>
If you restarted the containers after the previous step, you’ll need to purge the old php container before you restart. The easieast way is to run
docker container prune
docker image prune -a
But remember, this will remove a lot of images! Maybe more than you want. The
other option is to use docker container ls
and docker image ls
to find the
image and container to remove.
Ok, restart everything, and visit http://web.docker.localhost/redis.php. If everything worked, you should see:
{"greeting":" Hello Moz!","redis":"Yessir. Readin' and ritin' to redis"}
Almost there! Only MySQL to add now.
I added mysql
to `docker-compose.yml’:
mysql:
image: mysql/mysql-server
command: --default-authentication-plugin=mysql_native_password
environment:
MYSQL_ROOT_PASSWORD: password
And copied redis.php
to all.php
, and added the following:
// check mysql I/O
if ($mysqlStatus == "OK") {
$data = [];
$sql = "SELECT variable FROM sys_config;";
$db = $mysql->query($sql);
if ($db) {
while($row = $db->fetch_assoc()) {
$data[] = $row;
}
if (sizeof($data) < 1) {
$mysqlStatus = "No data found";
} else {
$mysqlStatus = "All good. Found data," . sizeof($data) . " items";
}
$db->free();
} else {
$mysqlStatus = "Mysql query failed";
}
}
Restart everything, and load all.php. And, there is an error.
After a lot of searching, I finally found a solution. MySQL in Docker will run initialization scripts if it starts and there are no databases.
So, I created a mysql
directory, and inside created a
docker-entrypoint-initdb.d
directory. In there, I created script.sql
:
use mysql;
CREATE USER 'root'@'%' IDENTIFIED BY 'root';
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION;
and modified docker-compose.yml
:
Note: You will need to remove the mysql container before restarting, to force the config scripts to run.
Load all.php, and you should see:
{"greeting":" Hello Moz!","redis":"Yessir. Readin' and ritin' to redis","mysql":"All good. Found data,6 items"}
We did it! nginx, PHP, Redis, and MySQL! All started with one command!
Hopefully this saves some one else from the hours of frustration I expereinced getting to this point!