Hello everyone!

We have been investing plenty of personal time and energy for many years to share our knowledge with you all. However, we now need your help to keep this blog running. All you have to do is just click one of the adverts on the site, otherwise it will sadly be taken down due to hosting etc. costs. Thank you.

This example makes use of Nginx, MySQL and PHP-FPM docker containers to run Symfony application. All you have to do is, create files listed below and copy your symfony application into the root folder or do the opposite. After all, just run the docker-compose command to build the system. That's all!


Structure


I would suggest you to checkout the alternative structure version right at the bottom of this post.


$ tree -a
.
├── docker
│   ├── mysql
│   │   ├── Dockerfile
│   │   ├── init.sh
│   │   └── mysqld.cnf
│   ├── nginx
│   │   ├── default.conf
│   │   ├── Dockerfile
│   │   └── nginx.conf
│   └── php
│   ├── app.sh
│   ├── Dockerfile
│   ├── init.sh
│   ├── install.sh
│   ├── php.ini
│   └── www.conf
├── docker-compose.yml
├── public # You can remove this
│   └── index.php
├── .env
└── .env.dist # Create a .env file from this

Files


.env


###> symfony ###
APP_ENV=dev
APP_SECRET=secret
APP_DB_USER=user
APP_DB_PASS=pass
###< symfony ###

###> docker ###
MYSQL_ROOT_PASSWORD=root
###< docker ###

docker-compose.yml


Defining the php service as shown below gives you chance to copy application specific config files into container. For instance, a file from app/config folder.


version: '3'

services:

mysql:
build:
context: ./docker/mysql
hostname: nginx
user: mysql
ports:
- 3306:3306
volumes:
- ./var/database:/var/lib/mysql:rw
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
APP_DB_USER: ${APP_DB_USER}
APP_DB_PASS: ${APP_DB_PASS}

php:
build:
context: ../symfony4-docker-platform
dockerfile: ./docker/php/Dockerfile
args:
APP_ENV: ${APP_ENV}
hostname: php
depends_on:
- mysql
ports:
- 9000:9000
volumes:
- .:/app:cached
working_dir: /app
environment:
APP_ENV: ${APP_ENV}
APP_SECRET: ${APP_SECRET}
APP_DB_USER: ${APP_DB_USER}
APP_DB_PASS: ${APP_DB_PASS}

nginx:
build:
context: ./docker/nginx
hostname: nginx
depends_on:
- mysql
- php
ports:
- 80:80
volumes:
- .:/app:cached

docker/mysql/Dockerfile


FROM mysql:5.7.22

COPY init.sh /docker-entrypoint-initdb.d
COPY mysqld.cnf /etc/mysql/mysql.conf.d

docker/mysql/init.sh


#!/bin/bash

printf "\n\033[0;44mPreparing the database\033[0m\n"

# Makes sure that the database is up before running database queries
echo "Checking if the database is up ..."
while ! mysqladmin ping -h"localhost" --silent; do
echo "Waiting for database to come up ..."
sleep 2
done
echo "Database is up ..."

# Create an application specific non-root user with all privileges
create="CREATE USER IF NOT EXISTS '${APP_DB_USER}'@'%' IDENTIFIED BY '${APP_DB_PASS}';"
grant="GRANT ALL PRIVILEGES ON *.* TO '${APP_DB_USER}'@'%' IDENTIFIED BY '${APP_DB_PASS}' WITH GRANT OPTION;"
mysql -u root -p${MYSQL_ROOT_PASSWORD} -e "$create$grant"

docker/mysql/mysqld.cnf


[mysqld]
pid-file = /var/run/mysqld/mysqld.pid
socket = /var/run/mysqld/mysqld.sock
datadir = /var/lib/mysql
symbolic-links = 0

character_set_server=utf8
collation_server=utf8_unicode_ci

explicit_defaults_for_timestamp = 1

docker/nginx/Dockerfile


FROM nginx:1.13.8

COPY default.conf /etc/nginx/conf.d
COPY nginx.conf /etc/nginx

docker/nginx/default.conf


server {
listen 80 default_server;

server_name localhost; # OR app.com www.app.com

root /app/public;

location / {
try_files $uri /index.php$is_args$args;
}

location ~ ^/index\.php(/|$) {
fastcgi_pass php:9000;
fastcgi_split_path_info ^(.+\.php)(/.*)$;
fastcgi_hide_header X-Powered-By;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
internal;
}

location ~ \.php$ {
return 404;
}

error_log /var/log/nginx/app_error.log;
access_log /var/log/nginx/app_access.log;
}

docker/nginx/nginx.conf


user nginx;

# 1 worker process per CPU core.
# Check max: $ grep processor /proc/cpuinfo | wc -l
worker_processes 2;

error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
# Tells worker processes how many people can be served simultaneously.
# worker_process (2) * worker_connections (2048) = 4096
# Check max: $ ulimit -n
worker_connections 2048;

# Connection processing method. The epoll is efficient method used on Linux 2.6+
use epoll;
}

http {
include /etc/nginx/mime.types;
default_type application/octet-stream;

log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

access_log /var/log/nginx/access.log main;

# Used to reduce 502 and 504 HTTP errors.
fastcgi_buffers 8 16k;
fastcgi_buffer_size 32k;
fastcgi_connect_timeout 300;
fastcgi_send_timeout 300;
fastcgi_read_timeout 300;

# The sendfile allows transfer data from a file descriptor to another directly in kernel.
# Combination of sendfile and tcp_nopush ensures that the packets are full before being sent to the client.
# This reduces network overhead and speeds the way files are sent.
# The tcp_nodelay forces the socket to send the data.
sendfile on;
tcp_nopush on;
tcp_nodelay on;

# The client connection can stay open on the server up to given seconds.
keepalive_timeout 65;

# Hides Nginx server version in headers.
server_tokens off;

# Disable content-type sniffing on some browsers.
add_header X-Content-Type-Options nosniff;

# Enables the Cross-site scripting (XSS) filter built into most recent web browsers.
# If user disables it on the browser level, this role re-enables it automatically on serve level.
add_header X-XSS-Protection "1; mode=block";

# Prevent the browser from rendering the page inside a frame/iframe to avoid clickjacking.
add_header X-Frame-Options DENY;

# Enable HSTS to prevent SSL stripping.
add_header Strict-Transport-Security "max-age=31536000; includeSubdomains; preload";

# Prevent browser sending the referrer header when navigating from HTTPS to HTTP.
add_header 'Referrer-Policy' 'no-referrer-when-downgrade';

# Sets the maximum size of the types hash tables.
types_hash_max_size 2048;

# Compress files on the fly before transmitting.
# Compressed files are then decompressed by the browsers that support it.
gzip on;

include /etc/nginx/conf.d/*.conf;
}

docker/php/Dockerfile


FROM php:7.2-fpm

ARG APP_ENV

COPY ./docker/php/init.sh /tmp
RUN chmod +x /tmp/init.sh
RUN /tmp/init.sh

COPY ./docker/php/install.sh /tmp
RUN chmod +x /tmp/install.sh
RUN /tmp/install.sh

COPY ./docker/php/www.conf /usr/local/etc/php-fpm.d/www.conf.default
COPY ./docker/php/php.ini /usr/local/etc/php/conf.d

COPY ./docker/php/app.sh /
RUN chmod +x /app.sh
RUN /app.sh

ENV LANG en_GB.UTF-8
ENV LANGUAGE en_GB:en
ENV LC_ALL en_GB.UTF-8

RUN apt-get autoremove --purge \
&& apt-get -y clean \
&& rm -rf /var/lib/apt/lists/*

docker/php/php.ini


[PHP]
date.timezone = Europe/London
log_errors = On
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
display_errors = Off
max_execution_time = 60
memory_limit = 256M

[opcache]
; http://symfony.com/doc/current/performance.html
opcache.enable_cli = 1
opcache.memory_consumption = 256
opcache.max_accelerated_files = 20000
realpath_cache_size = 4096K
realpath_cache_ttl = 600

docker/php/www.conf


[www]

user = www-data
group = www-data

listen = nginx:9000

; Dynamicaly chooses how the process manager will control the number of child processes.
pm = dynamic
; The maximum number of child processes to be created.
; This option sets the limit on the number of simultaneous requests that will be served.
; Availalbe RAM in MB / Average RAM used by php-fpm processes in MB = max_children
; 1500MB / 30MB = 50 (minus a bit)
pm.max_children = 40
; The number of child processes created on startup.
; min_spare_servers + (max_spare_servers - min_spare_servers) / 2
pm.start_servers = 2
; The desired minimum number of idle server processes.
pm.min_spare_servers = 2
; The desired maximum number of idle server processes.
; 2 or 4 times of the CPU core
pm.max_spare_servers = 4
; The number of requests each child process should execute before respawning.
; This can be useful to work around memory leaks in 3rd party libraries.
pm.max_requests = 500

docker/php/init.sh


#!/bin/bash

printf "\n\033[0;44mChecking the existence of 'APP_ENV' variable\033[0m\n"

if [[ -z "${APP_ENV}" ]]
then
printf "\033[0;31mVariable does not exist.\033[0m\n\n"
exit 1;
fi

printf "\033[0;32mVariable exists.\033[0m\n\n"

docker/php/install.sh


#!/bin/bash

printf "\n\033[0;44mInstalling system packages for the \"${APP_ENV}\" environment\033[0m\n"

apt-get update
apt-get install -y --no-install-recommends zip unzip nano tree locales

sed -i 's/# en_GB.UTF-8 UTF-8/en_GB.UTF-8 UTF-8/g' /etc/locale.gen
ln -s /etc/locale.alias /usr/share/locale/locale.alias
locale-gen en_GB.UTF-8

ln -snf /usr/share/zoneinfo/Europe/London /etc/localtime
echo Europe/London > /etc/timezone

curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

docker-php-ext-install opcache pdo_mysql
docker-php-ext-enable opcache

if [ "${APP_ENV}" == "dev" ] || [ "${APP_ENV}" == "test" ]
then
pecl install xdebug
docker-php-ext-enable xdebug
fi

docker/php/app.sh


#!/bin/bash

printf "\n\033[0;44mPreparing the application for the \"${APP_ENV}\" environment\033[0m\n"

if [ "${APP_ENV}" == "dev" ] || [ "${APP_ENV}" == "test" ]
then
echo "Run symfony commands for \"dev\" or \"test\" environments"
# composer install --no-interaction
else
echo "Run symfony commands for \"prod\" or \"stag\" environments"
# composer install --no-interaction --no-dev --optimize-autoloader
fi

echo "Run symfony commands for all environments"

# bin/console doctrine:migrations:migrate --no-interaction
# ...

printf "\n\033[0;44mBringing the \"${APP_ENV}\" environment up\033[0m\n"

Build


$ docker-compose up -d --build

Test


Run command below to obtain IP address first.


$ echo $(docker network inspect {your-network-name-goes-here} | grep Gateway | grep -o -E '[0-9\.]+')
172.18.0.1

$ curl 172.18.0.1 # Or localhost
# You should get a nice response

Alternative Version


Structure


.
├── app # Symfony application goes in here
│   └── public
│   └── index.php
├── docker
│   ├── mysql
│   │   ├── Dockerfile
│   │   ├── init.sh
│   │   └── mysqld.cnf
│   ├── nginx
│   │   ├── default.conf
│   │   ├── Dockerfile
│   │   └── nginx.conf
│   └── php
│   ├── Dockerfile
│   ├── init.sh
│   ├── install.sh
│   ├── php.ini
│   └── www.conf
├── .env
├── .env.dist # Create a .env file from this
└── docker-compose.yml

docker-compose.yml


version: '3'

services:

mysql:
build:
context: ./docker/mysql
hostname: nginx
user: mysql
ports:
- 3306:3306
volumes:
- ./data/database:/var/lib/mysql:rw
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
APP_DB_USER: ${APP_DB_USER}
APP_DB_PASS: ${APP_DB_PASS}

php:
build:
context: ../api-doc
dockerfile: ./docker/php/Dockerfile
args:
APP_ENV: ${APP_ENV}
hostname: php
ports:
- 9000:9000
depends_on:
- mysql
volumes:
- ./app:/app:cached
working_dir: /app
environment:
APP_ENV: ${APP_ENV}
APP_DB_USER: ${APP_DB_USER}
APP_DB_PASS: ${APP_DB_PASS}

nginx:
build:
context: ./docker/nginx
hostname: nginx
ports:
- 80:80
depends_on:
- mysql
- php
volumes:
- ./app:/app:cached