Working example of Laravel (PHP Swoole) + Docker + Nginx, all PHP code in PHP container, Nginx as Reverse proxy
- Nginx as a reverse proxy
- Resided in a stand-alone docker container
- No static file, not serving any file in this docker container
- PHP as a backend laravel server, as another stand-alone docker container
- Serving both
PHP
and static files (e.g. jpg, txt, json) - All PHP code files are resided in PHP container
- Serving both
- I use
docker-compose.yml
to join the above together - You can optionally add any MySQL database (e.g. MariaDB) in this .yml file.
- Most of the online tutorials are teaching how to use Nginx + PHP-FPM, which is fine
- However, if we are using docker, and you want to separate Nginx and PHP into different containers, there is a problem
- In order for this Nginx + PHP-FPM to work, both of these containers must be able to access the same code directory (e.g.
/var/www/html/
) - This is because, Nginx needs to first see the file first, then use
fastcgi_pass
to pass the php file to PHP container- One thing to point out:
php-fpm
is a CGI-compatible program, not a server. You cannot viewphp-fpm
as a server - which means, you cannot use
proxy_pass
to pass HTTP request to it, but it is OK when you usefastcgi_pass
.
- One thing to point out:
- In other words, if you want to completely separate these two containers, and connect it via
fastcgi_pass
, you must copy your PHP code to both Nginx and PHP containers.- which is not acceptable (to me)
- So my objectives are simply:
- Nginx as reverse proxy, as slim as possible
- PHP(-FPM) as another separate container, all codes resided here, including static files
- It is much more acceptable (to me).
<root_dir>
`-- build/
`---- nginx/
`------ default.conf
`---- php/
`------ .env
`------ Dockerfile
`------ Dockerfile_base
`------ php.ini
`-- src/
`---- <laravel source files>
`---- app/
`---- storage/
`---- composer.json
`---- composer.lock
`---- ...etc...
`docker-compose.yml
`README.md
- I use Swoole as a replacement of PHP-fpm, which is better than PHP-fpm.
- I tried not to describe what is Swoole here, you can read it on your own.
- But all in all, I use Swoole to replace PHP-FPM
- From Laravel v10.x, it supports Swoole and RoadRunner through Laravel Octane
- So I install it as well.
- It defines how Nginx container and PHP container works each other
- Nginx container
- The structure is simple, I use Nginx-Alpine as base to keep the docker image size small
- Expose port
80
- Sync the
default.conf
to container directory:/etc/nginx/conf.d/default.conf
- In production environment, it is recommended to use
COPY
inDockerfile
to copy the config to container
- In production environment, it is recommended to use
- PHP container
- Expose port
9000
context
: According to docker doc on context, you can view it as the base directory of executing everything.- So, from now on, all paths are being referenced from current directory
.
- It takes me quite a lot of time to understand such a simple thing, which is not told/describe/written in docker documentation.
- So, from now on, all paths are being referenced from current directory
dockerfile
: The docker file used to setup this container, more on this latercontainer_name
: it will be used inbuild/nginx/default.conf
.
- Expose port
- Most of it copied from laravel octane
- For
upstream
directive (upstream_backend_php
), make sure you are referring to container namebackend_php_laravel
defined indocker-compose.yml
- First, read this code:
location ~* \.(jpg|jpeg|png|gif|svg|webp|html|txt|json|ico|css|js)$ {
expires 1d;
add_header Cache-Control public;
access_log off;
try_files $uri $uri/ @octane;
}
- These code means:
- For files end with
jpg|jpeg|png|gif|svg|webp|html|txt|json|ico|css|js
(this symbol: | (pipes) means OR) - Set
expires
to1d
, i.e. One day from today - Add header
Cache-Control public;
access_log off
: Do not show these files in access log- IMPORTANT
try_files $uri $uri/ @octane;
: try to see if the files exists in local path (i.e. Nginx container), if not, refer to@octane
block in this same config file - In
@octane
block (insideserver
block):location @octane { set $suffix ""; if ($uri = /index.php) { set $suffix ?$query_string; } proxy_http_version 1.1; proxy_set_header Http_Host $http_host; proxy_set_header Host $host; proxy_set_header Scheme $scheme; proxy_set_header SERVER_PORT $server_port; proxy_set_header REMOTE_ADDR $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header X-Real-IP $remote_addr; proxy_pass http://upstream_backend_php$suffix; }
- It sets a lot of proxy headers
- IMPORTANT
proxy_pass
: it will route/redirect/send the traffic from nginx toupstream_backend_php
block, with$suffix
- Different from
proxy_pass
,fastcgi_pass
can only be used on FastCGI container (i.e. PHP-fpm). - When you use
proxy_pass
, you can safely assume that you are using reverse proxy.
- Different from
- And
upstream
block (outsideserver
block)
upstream upstream_backend_php { server backend_php_laravel:9000; }
- It goes to PHP container named
backend_php_laravel
port9000
- For files end with
- I want to have your attention to these two lines in
default.conf
:location = /favicon.ico { log_not_found off; access_log off; try_files $uri $uri/ @octane;} location = /robots.txt { allow all; log_not_found off; access_log off; try_files $uri $uri/ @octane;}
- These two lines means:
- For
favicon.ico
:- Do not log if not found
- Do not add access log
- IMPORTANT:
try_files $uri $uri/ @octane;
: Try to find this file in nginx container, if it can't be found, go to@octane
block
- For
robots.txt
:- Do not log if not found
- Do not add access log
- Allow access
- IMPORTANT:
try_files $uri $uri/ @octane;
: Try to find this file in nginx container, if it can't be found, go to@octane
block
- For
- This line
try_files $uri $uri/ @octane;
are the key to access files in Laravelpublic
directory
- These two lines means:
- TL;DR: Using
--no-install
flag when runningcomposer require <package>
- Let's assume I am trying to update
src/composer.json
in this situation. - In
<root_dir>
where it containssrc/
, I execute this command:docker run --rm --mount type=bind,src=$pwd/src,target=/app composer:latest composer require my_package --no-install
--rm
: Remove the container after the container finished the task--mount
: mountcurrent_directory/src
to/app
directory inside containercomposer:latest
: Official docker container ofcomposer
composer require my_package --no-install
: the command to run inside this container. In this case it installsmy_package
without really installing the files.- which means, it just updates the content in
composer.json
andcomposer.lock
without really downloading the files, which is what we want.
- Similarly, to update/remove without really downloading the files:
- Remove:
docker run --rm --mount type=bind,src=$pwd/src,target=/app composer:latest composer remove my_package --no-install
- Update:
docker run --rm --mount type=bind,src=$pwd/src,target=/app composer:latest composer update --no-install
- Remove:
- Just in case you saw error that you did not have certain version of the package, but you want to force update your composer.json, run this:
docker run --rm --mount type=bind,src=$pwd/src,target=/app composer:latest composer require my_package --no-install --ignore-platform-reqs
- To learn the available options of
composer
command, you can actually print the--help
composer menu by thisdocker run --rm --mount type=bind,src=$pwd/src,target=/app composer:latest composer require --help
- This is
base
because I need to install a lot of PHP extentions, including Swoole in a single PHP docker image - As you may also see, I also install
gRPC
,protobuf
,xlswriter
,apcu
,pcntl
, some extentions are required by Swoole (e.g.pcntl
) - I also install
composer
in this docker image - Then I copy
build/php/php.ini
to PHP container. - To install Swoole, you can refer to Swoole documentation. The steps outline here are only my "history" of installing Swoole.
About composer install
in PHP base image: how not to run composer install
everytime when updating PHP code in CI/CD?
- I want to talk more about this part, as it is important for me.
- Since we are running CI/CD, which means when we did some updates on PHP, usually we need to re-run everything
- For example in this case, sometimes we need to reinstall everything, especially
composer
, even if we are just doing minor changes (or even print debug information) - And since the whole process of CI/CD takes very long time, I decided to shortern this time by pre-install composer files, i.e. all files inside
vendor
dir - To accomplish this, I reference how we just run
npm install
once and we don't need subsequent execution everytime we run CI/CD.- Reference: https://github.com/jstandish/cached-node-module-build-example/blob/master/DOCKER_BUILD.md
- It is simply:
- Copy the
composer.json
to an empty dir - Run
composer install
inside that directory - After that, use
move
orcp
to move thevendor
directory,composer.json
andcomposer.lock
to target directory
- Copy the
- Since most of the time, the code in
vendor
directory will not change, we can make use of docker cache mechanism to speed up the process of updating our PHP code.
- While I use it in
composer.json
, I learn it from above link, which means you can do the same thing usingnpm install
- This is the main
Dockerfile
used indocker-compose.yml
- But it refers the image
php_base
built byDockerfile_base
- After running an update, we copy
src
directory to/var/www/html
- It will not overwrite our existing files, e.g.
vendor
,composer.*
- We also do
chown
andchmod
to/var/www/html/storage
directory - To enable Swoole, we run
php artisan octane:install --server=swoole
- To start Swoole + Laravel, we run
php artisan octane:start --server=swoole --host=0.0.0.0 --port=9000 --log-level=debug
- Most of the arguments are self-explained, but I want to point out several things
--server=swoole
: Add this to startswoole
server, if you don't, default toroadrunner
--host=0.0.0.0
: Bind Swoole to all addresses. i.e. Allow Swoole to listen to all addresses. If you don't define it, default to 127.0.0.1- If you don't define it, your Nginx container would not be able to access this PHP container
- Because Swoole only listen to
127.0.0.1
, and127.0.0.1
is not the same as0.0.0.0
. 127.0.0.1
did not listen to all addresses,0.0.0.0
does.- If you don't, you will see HTTP 502 errors, even if you confirm that you open every port.
- This problem blocks me for 3 days.
All you need is running below command:
docker compose down && docker compose build && docker compose up
.php
file cannot be executed inpublic
(including subfolder)- which means, if you have a PHP file called
images\myfile.php
, and you go tohttp://nginxserver/images/myfile.php
, it will return404 not found
.
- which means, if you have a PHP file called
- It takes very long time (around 10 mins) to just build
php_base
docker image, because there are a lot to download. - I haven't tried
php:fpm
orphp:cli
docker images as base image. You can try if you want to, and let me know if it works. - The size of
php_base
docker images, as of this writing, is1.41GB
.
Hope it helps someone.