# Local configuration
For the local development environment we will be using docker-compose
to orchestrate all the containers.
For the purposes of this demo, I will be using a fresh Laravel application that I've just installed
at ~/Sites/demo
using:
cd ~/Sites
git clone https://github.com/laravel/laravel.git demo
# Docker configuration
All the files used in this guide can be found on GitHub at https://github.com/daursu/laradocker
We will be configuring 4 containers (nginx, php-fpm, redis and mysql). The reason for splitting nginx
and php-fpm
in separate containers is that each one can scale independently of each other. In a
real application you might have 2 instances of php-fpm
and only once instance of nginx
running.
This separation allows us to upgrade nginx
and php
versions by simply changing the base images.
Let's create a new folder called docker
at the root of our project ~/Sites/demo/docker
.
# Nginx configuration
Create a new file called nginx.conf
in ~/Sites/demo/docker/nginx.conf
with the following contents:
pid /var/run/nginx.pid;
worker_processes auto;
events {
worker_connections 1024;
}
http {
include mime.types;
include fastcgi.conf;
default_type application/octet-stream;
sendfile on;
tcp_nopush on;
server_tokens off;
client_max_body_size 10M;
gzip on;
gzip_disable "msie6";
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;
server {
listen [::]:80;
listen 80 default_server;
server_name _;
root /app/public;
index index.php index.html index.htm;
access_log /dev/stdout;
error_log /dev/stdout info;
disable_symlinks off;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options "nosniff";
index index.html index.htm index.php;
charset utf-8;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
error_page 404 /index.php;
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
if (!-f $document_root$fastcgi_script_name) {
return 404;
}
fastcgi_pass php:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
}
INFO
Feel free to customise this file to suit your application needs. The important part is fastcgi_pass php:9000;
which routes the requests to the FPM process running in the php
container.
# Nginx Dockerfile
We need to define a nginx Dockerfile that will extend from a base nginx
image. The purpose of this
is to add our application files to the final docker image that can be deployed on production.
Let's create a file called nginx.Dockerfile
inside ~/Sites/demo/docker/nginx.Dockerfile
folder
with the following content:
FROM nginx:1.19-alpine
WORKDIR /app/public
COPY ./docker/nginx.conf /etc/nginx/nginx.conf
COPY ./public/* /app/public/
NOTICE
While developing locally, we will mount our local public folder inside the container, however in production the public folder will be built into the Docker image.
# PHP-fpm configuration
Now let's create a configuration file for the php-fpm pool. I will call it www.conf
and I will place it in
~/Sites/demo/docker/www.conf
with the following contents:
; Start a new pool named 'www'.
; the variable $pool can be used in any directive and will be replaced by the
; pool name ('www' here)
[www]
; Unix user/group of processes
; Note: The user is mandatory. If the group is not set, the default user's group
; will be used.
user = www-data
group = www-data
; The address on which to accept FastCGI requests.
; Valid syntaxes are:
; 'ip.add.re.ss:port' - to listen on a TCP socket to a specific IPv4 address on
; a specific port;
; '[ip:6:addr:ess]:port' - to listen on a TCP socket to a specific IPv6 address on
; a specific port;
; 'port' - to listen on a TCP socket to all addresses
; (IPv6 and IPv4-mapped) on a specific port;
; '/path/to/unix/socket' - to listen on a unix socket.
; Note: This value is mandatory.
listen = 9000
; Choose how the process manager will control the number of child processes.
; Possible Values:
; static - a fixed number (pm.max_children) of child processes;
; dynamic - the number of child processes are set dynamically based on the
; following directives. With this process management, there will be
; always at least 1 children.
; pm.max_children - the maximum number of children that can
; be alive at the same time.
; pm.start_servers - the number of children created on startup.
; pm.min_spare_servers - the minimum number of children in 'idle'
; state (waiting to process). If the number
; of 'idle' processes is less than this
; number then some children will be created.
; pm.max_spare_servers - the maximum number of children in 'idle'
; state (waiting to process). If the number
; of 'idle' processes is greater than this
; number then some children will be killed.
; ondemand - no children are created at startup. Children will be forked when
; new requests will connect. The following parameter are used:
; pm.max_children - the maximum number of children that
; can be alive at the same time.
; pm.process_idle_timeout - The number of seconds after which
; an idle process will be killed.
; Note: This value is mandatory.
pm = dynamic
; The number of child processes to be created when pm is set to 'static' and the
; maximum number of child processes when pm is set to 'dynamic' or 'ondemand'.
; This value sets the limit on the number of simultaneous requests that will be
; served. Equivalent to the ApacheMaxClients directive with mpm_prefork.
; Equivalent to the PHP_FCGI_CHILDREN environment variable in the original PHP
; CGI. The below defaults are based on a server without much resources. Don't
; forget to tweak pm.* to fit your needs.
; Note: Used when pm is set to 'static', 'dynamic' or 'ondemand'
; Note: This value is mandatory.
pm.max_children = 5
; The number of child processes created on startup.
; Note: Used only when pm is set to 'dynamic'
; Default Value: min_spare_servers + (max_spare_servers - min_spare_servers) / 2
pm.start_servers = 2
; The desired minimum number of idle server processes.
; Note: Used only when pm is set to 'dynamic'
; Note: Mandatory when pm is set to 'dynamic'
pm.min_spare_servers = 1
; The desired maximum number of idle server processes.
; Note: Used only when pm is set to 'dynamic'
; Note: Mandatory when pm is set to 'dynamic'
pm.max_spare_servers = 3
; The number of seconds after which an idle process will be killed.
; Note: Used only when pm is set to 'ondemand'
; Default Value: 10s
;pm.process_idle_timeout = 10s;
; The number of requests each child process should execute before respawning.
; This can be useful to work around memory leaks in 3rd party libraries. For
; endless request processing specify '0'. Equivalent to PHP_FCGI_MAX_REQUESTS.
; Default Value: 0
;pm.max_requests = 500
# PHP Dockerfile
In order to enable custom PHP extension we will have to extend from the base php
docker image.
Also in this step we'll use a staggered build to install the required composer packages.
Let's create a file called php.Dockerfile
inside ~/Sites/demo/docker/php.Dockerfile
folder
with the following content:
# ----------------------
# The FPM base container
# ----------------------
FROM php:7.4-fpm as dev
RUN docker-php-ext-install -j$(nproc) pdo_mysql
WORKDIR /app
# ----------------------
# Composer install step
# ----------------------
FROM composer:1.10 as build
WORKDIR /app
COPY composer.* ./
COPY database/ database/
RUN composer install \
--ignore-platform-reqs \
--no-interaction \
--no-plugins \
--no-scripts \
--prefer-dist
# ----------------------
# npm install step
# ----------------------
FROM node:12-alpine as node
WORKDIR /app
COPY *.json *.mix.js /app/
COPY resources /app/resources
RUN mkdir -p /app/public \
&& npm install && npm run production
# ----------------------
# The FPM production container
# ----------------------
FROM dev
COPY ./docker/www.conf /usr/local/etc/php-fpm.d/www.conf
COPY . /app
COPY --from=build /app/vendor/ /app/vendor/
COPY --from=node /app/public/js/ /app/public/js/
COPY --from=node /app/public/css/ /app/public/css/
COPY --from=node /app/mix-manifest.json /app/public/mix-manifest.json
RUN chmod -R 777 /app/storage
Add any extensions your application needs to docker-php-ext-install
. In this example I've installed the PDO mysql driver.
More details about how to install custom extensions can be found here: https://hub.docker.com/_/php/
# docker-compose.yml
The docker-compose.yml
contains the structure of our docker environment. It defines the services we are running,
their environment variables and how they interact with each other.
In order to support multiple environments for our project, we will split our docker-compose.yml
file
into two parts, one called docker-compose.yml
, which mimics the production environment, and another one called
docker-compose.override.yml
that provides local development override.
TIP
By default, the configuration in docker-compose.override.yml
takes precedence.
# Base docker-compose.yml
Inside my ~/Sites/demo
folder I will create a new file called docker-compose.yml
with the following
content:
version: "3.8"
services:
nginx:
build:
context: .
dockerfile: ./docker/nginx.Dockerfile
restart: always
depends_on:
- php
ports:
- "8080:80"
networks:
- default
php:
build:
context: .
dockerfile: ./docker/php.Dockerfile
working_dir: /app
env_file: .env
restart: always
expose:
- "9000"
# Local dev overrides
Next, let's create a new file called docker-compose.override.yml
. Paste the following contents in:
version: "3.8"
services:
redis:
image: redis:6.0-alpine
expose:
- "6379"
db:
image: mysql:8
ports:
- "3307:3306"
environment:
MYSQL_ROOT_PASSWORD: secret
MYSQL_DATABASE: laravel
volumes:
- db-data:/var/lib/mysql
nginx:
image: nginx:1.19-alpine
environment:
VIRTUAL_HOST: testing.local
restart: "no"
volumes:
- ./docker/nginx.conf:/etc/nginx/nginx.conf:ro
- ./public:/app/public:ro
php:
build:
target: dev
restart: "no"
depends_on:
- composer
- redis
- db
volumes:
- ./:/app
- ./docker/www.conf:/usr/local/etc/php-fpm.d/www.conf:ro
node:
image: node:12-alpine
working_dir: /app
volumes:
- ./:/app
command: sh -c "npm install && npm run watch"
composer:
image: composer:1.10
working_dir: /app
environment:
SSH_AUTH_SOCK: /ssh-auth.sock
volumes:
- ./:/app
- "$SSH_AUTH_SOCK:/ssh-auth.sock"
- /etc/passwd:/etc/passwd:ro
- /etc/group:/etc/group:ro
command: composer install --ignore-platform-reqs --no-scripts
volumes:
db-data:
The local environment will be mounting our local code as a volume so that changes are reflected immediately inside the container without the need to rebuild the image.
Locally we are starting up MySQL
and redis
containers. You will notice that these two containers are not part
of the production docker-compose.yml
file, as stateful containers bring in a lot more complexity in terms of scale
and deployment. More information on this can be found in the Best Practices section.
We also added two new services: composer
& node
. The composer container will install all the
dependencies listed in composer.json at start-up. The node container will install all the npm packages
and will run npm run watch
in the background, which is part of Laravel Mix.
In the next chapter, we will look at how to manually invoke the composer
and node
containers.
# Application .env
Next step is to update the .env
file with following:
DB_HOST=db
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=secret
CACHE_DRIVER=redis
SESSION_DRIVER=redis
WARNING
In order to use redis you will need to install predis\predis
composer package.
# Logging
When running applications in Docker containers, it's a good practice to write the application logs to stdout/stderr instead of writing them locally to disk. This is because docker has native plugins/drivers to handle the logs and store or forward them to a third party service like CloudWatch. You can read more about the logging features in Docker on the official documentation page.
To get the Laravel application to write to stdout, we need to update the config/logging.php
file. Add the following
block:
'stdout' => [
'driver' => 'monolog',
'handler' => StreamHandler::class,
'formatter' => env('LOG_STDOUT_FORMATTER'),
'with' => [
'stream' => 'php://stdout',
],
],
Change the default logging driver to stdout. In .env
add the following line:
LOG_CHANNEL=stdout
If you are using a stack driver, you can replace your file log driver with stdout. For example if you use the single log driver in the stack, replace it with stdout:
'stack' => [
'driver' => 'stack',
'channels' => ['stdout'],
'ignore_exceptions' => false,
],
# Customizing versions
By default, I have required the latest versions of each service in docker-compose.override.yml
- PHP
7.4
- Nginx
1.19
- MySQL
8
- Redis
6
You can easily swap the version of a service with another one. For example,
if you would like to use MySQL 5.7 for example simply change image: mysql:8
to image: mysql:5.7
.
← Introduction Running →