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
PHPand static files (e.g. jpg, txt, json) - All PHP code files are resided in PHP container
- Serving both
- I use
docker-compose.ymlto 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_passto pass the php file to PHP container- One thing to point out:
php-fpmis a CGI-compatible program, not a server. You cannot viewphp-fpmas a server - which means, you cannot use
proxy_passto 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.confto container directory:/etc/nginx/conf.d/default.conf- In production environment, it is recommended to use
COPYinDockerfileto 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
upstreamdirective (upstream_backend_php), make sure you are referring to container namebackend_php_laraveldefined 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
expiresto1d, 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@octaneblock in this same config file - In
@octaneblock (insideserverblock):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_phpblock, with$suffix- Different from
proxy_pass,fastcgi_passcan 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
upstreamblock (outsideserverblock)
upstream upstream_backend_php { server backend_php_laravel:9000; }- It goes to PHP container named
backend_php_laravelport9000
- 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@octaneblock
- 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@octaneblock
- For
- This line
try_files $uri $uri/ @octane;are the key to access files in Laravelpublicdirectory
- These two lines means:
- TL;DR: Using
--no-installflag when runningcomposer require <package> - Let's assume I am trying to update
src/composer.jsonin 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/srcto/appdirectory inside containercomposer:latest: Official docker container ofcomposercomposer require my_package --no-install: the command to run inside this container. In this case it installsmy_packagewithout really installing the files.- which means, it just updates the content in
composer.jsonandcomposer.lockwithout 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
composercommand, you can actually print the--helpcomposer menu by thisdocker run --rm --mount type=bind,src=$pwd/src,target=/app composer:latest composer require --help
- This is
basebecause 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
composerin this docker image - Then I copy
build/php/php.inito 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
vendordir - To accomplish this, I reference how we just run
npm installonce 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.jsonto an empty dir - Run
composer installinside that directory - After that, use
moveorcpto move thevendordirectory,composer.jsonandcomposer.lockto target directory
- Copy the
- Since most of the time, the code in
vendordirectory 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
Dockerfileused indocker-compose.yml - But it refers the image
php_basebuilt byDockerfile_base - After running an update, we copy
srcdirectory to/var/www/html - It will not overwrite our existing files, e.g.
vendor,composer.* - We also do
chownandchmodto/var/www/html/storagedirectory - 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 startswooleserver, 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.1is not the same as0.0.0.0. 127.0.0.1did not listen to all addresses,0.0.0.0does.- 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
.phpfile 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_basedocker image, because there are a lot to download. - I haven't tried
php:fpmorphp:clidocker images as base image. You can try if you want to, and let me know if it works. - The size of
php_basedocker images, as of this writing, is1.41GB.
Hope it helps someone.