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 later
- container_name: it will be used in- build/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: mount- current_directory/srcto- /appdirectory inside container
- composer:latest: Official docker container of- composer
- composer require my_package --no-install: the command to run inside this container. In this case it installs- my_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 this- docker 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 start- swooleserver, if you don't, default to- roadrunner
- --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 in- public(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.