Mezzio php framework – using swoole/openswoole with nginx reverse proxy (with docker containers and docker compose)

There are many reasons why you may want to use reverse proxy in front of your application – (such as mezzio php framework with swoole/openswoole).

Reasons to use nginx with your php+swoole/openswoole applications:

  • efficient serving of static assets – while swoole will serve it very efficiently, but it will never beat raw compiled performance of nginx at serving of static assets. Nginx will do it fast and with minimal memory/cpu overhead -especially if you add brotli/gzip/ssl requirements into the mix ( https://stackoverflow.com/questions/9967887/node-js-itself-or-nginx-frontend-for-serving-static-files)
  • ssl termination – while it’s possible to do so in php+swoole/openswoole application – again this is a job nginx will do a lot more effectively with smaller cpu footprint. For production use – its very very common to terminate ssl at nginx (or higher reverse proxy like aws ALB) and relieve your application from such mundane task (https://docs.nginx.com/nginx/admin-guide/security-controls/terminating-ssl-http/)
  • caching – personally I’m a huge fan of varnish cache reverse proxy for caching. However nginx also comes with such capability. Think about it – why always hit the slow filesystem for your static files that never change when you can just keep them in RAM at nginx/varnish level.
  • others – load balancing, routing, extra security etc.

Let’s get started – clone the git repository

I’ve prepared ready to use mezzio app skeleton + openswoole + nginx application preconfigured and ready to be launched – see this git repository: https://github.com/linuxd3v/mezzio-nginx-swoole-demo

Go ahead and clone this repository on your laptop/computer – it contains all of the required dockerfiles and docker-compose file along with mezzio skeleton application (which includes mezzio swoole package).

Note: do not run docker-compose up -d yet – there are still some configs to be done.

git clone https://github.com/linuxd3v/mezzio-nginx-swoole-demo

Cloned repository directory (mezzio-nginx-swoole-demo) will contain only these assets:

  • /stack directory – all of the docker related files, dockerfiles, docker-compose, nginx and php configs
  • /mezzio directory – mezzio php application files only
  • /readme.md file – short readme file

Alternatively: prepare mezzio skeleton application yourself

Some people like to do things themselves – so they have better understanding how things were setup. If you prefer to setup mezzio skeleton application yourself – here’s how to do this:

a) Remove shipped mezzio directory and install the mezzio skeleton application project (all on the same level as ./stack directory):

rm -rf ./mezzio
composer create-project --ignore-platform-reqs mezzio/mezzio-skeleton mezzio

Note that we use --ignore-platform-reqs option – as you may not necessarily have all of the php extensions on your computer (they are present in docker container however).

You will be presented with some choices you need to pick, I would recommend these:

  • What type of installation would you like? modular
  • Which container do you want to use for dependency injection? pimple
  • Which router do you want to use? FastRoute
  • Which template engine do you want to use? Plates
  • Which error handler do you want to use during development? Whoops

See this terminal output (yours may vary slightly):

linuxdev@hs-dev1 : /mnt/480g_drive/projects/mezzio-nginx-swoole-demo$ composer create-project mezzio/mezzio-skeleton mezzio
Creating a "mezzio/mezzio-skeleton" project at "./mezzio"
Installing mezzio/mezzio-skeleton (3.11.0)
  - Installing mezzio/mezzio-skeleton (3.11.0): Extracting archive
Created project in /mnt/480g_drive/projects/mezzio-nginx-swoole-demo/mezzio
> MezzioInstaller\OptionalPackages::install
Setting up optional packages
Setup data and cache dir
Removing installer development dependencies

  What type of installation would you like?
  [1] Minimal (no default middleware, templates, or assets; configuration only)
  [2] Flat (flat source code structure; default selection)
  [3] Modular (modular source code structure; recommended)
  Make your selection (2): 3
  - Copying src/App/src/ConfigProvider.php

  Which container do you want to use for dependency injection?
  [1] Pimple (supported by laminas)
  [2] laminas-servicemanager (supported by laminas)
  [3] Symfony DI Container
  [4] PHP-DI
  [5] chubbyphp-container
  Make your selection or type a composer package name and version (laminas-servicemanager (supported by laminas)): 1
  - Adding package laminas/laminas-pimple-config (^1.1.1)
  - Copying config/container.php

  Which router do you want to use?
  [1] FastRoute (supported by laminas)
  [2] laminas-router (supported by laminas)
  Make your selection or type a composer package name and version (FastRoute (supported by laminas)): 1
  - Adding package mezzio/mezzio-fastroute (^3.0.3)
  - Whitelist package mezzio/mezzio-fastroute
  - Copying config/routes.php

  Which template engine do you want to use?
  [1] Plates (supported by laminas)
  [2] Twig (supported by laminas)
  [3] laminas-view installs laminas-servicemanager (supported by laminas)
  [n] None of the above
  Make your selection or type a composer package name and version (n): 1
  - Adding package mezzio/mezzio-platesrenderer (^2.2)
  - Whitelist package mezzio/mezzio-platesrenderer
  - Copying src/App/templates/error/404.phtml
  - Copying src/App/templates/error/error.phtml
  - Copying src/App/templates/layout/default.phtml
  - Copying src/App/templates/app/home-page.phtml

  Which error handler do you want to use during development?
  [1] Whoops (supported by laminas)
  [n] None of the above
  Make your selection or type a composer package name and version (Whoops (supported by laminas)): 1
  - Adding package filp/whoops (^2.7.1)
  - Copying config/autoload/development.local.php.dist
Remove installer
Removing composer.lock from .gitignore
Removing Mezzio installer classes, configuration, tests and docs
Installing dependencies from lock file (including require-dev)
Verifying lock file contents can be installed on current platform.
Your lock file does not contain a compatible set of packages. Please run composer update.

  Problem 1
    - elie29/zend-phpdi-config is locked to version v6.0.0 and an update of this package was not requested.
    - elie29/zend-phpdi-config v6.0.0 requires php ^7.1 -> your php version (8.1.2) does not satisfy that requirement.

If you got this error message:

Your lock file does not contain a compatible set of packages. Please run composer update.

Simply do what it suggests – run: composer update --ignore-platform-reqs (from inside of mezzio directory as that’s where the composer.json file is).

b) Add swoole/openswoole integration into mezzio framework.

Go ahead and run this command (again from inside of the mezzio directory- as that’s where composer.json file is):

composer require mezzio/mezzio-swoole

c) update mezzio configs.

We are almost done. However – by default mezzio skeleton application only listens on 127.0.0.1 which is localhost (this is a default for security reasons) – and this is not going to work for when we try to connect to it from nginx container – as we need nginx container to be able to access swoole application container from nginx container ip.

To solve this – we need to edit some swoole/openswoole server settings – go ahead and create this config file: mezzio/config/autoload/server-config.global.php with following content:

<?php

declare(strict_types=1);

return [
    //Disable debug mode
    'debug' => false,

    //Lets disable cache - we dont need it with swoole as its all in memory
    \Laminas\ConfigAggregator\ConfigAggregator::ENABLE_CACHE => false,

    'mezzio-swoole' => [
        'enable_coroutine'   => true,
        'swoole-http-server' => [
            'process-name'   => 'somalia',
            'host'           => '0.0.0.0',
            //'port'           => 9601,
            //'mode'           => SWOOLE_PROCESS,
            'options'        => [
                //Enable swoole awesomeness
                'task_enable_coroutine' => true,
                
                //Make sure we set some max on conection number
                //The default value of max_conn is ulimit -n - which is probably good,
                //check your system ulimit -n before changing this
                //'max_conn' => 8096000,

                //Set the CPU affinity for reactor and worker threads/processes. 
                //This option is disabled by default and is for hardware which runs multi-core CPUs.
                'open_cpu_affinity' => true,

                // Enable task workers.
                //'task_worker_num' => 3,
                
                // Change number of workers - depends on your usecase
                // https://openswoole.com/docs/modules/swoole-server/configuration#worker_num
                'worker_num' => swoole_cpu_num(),

                //Safety feature to avoid memory leaks.
                //A worker process is restarted to avoid memory leak when receiving max_request + rand(0, max_request_grace) requests.
                //he default value of max_request is 0 which means there is no limit of the max request. 
                //'max_request' => 1000000,

                //Increase to avoid this error:
                //WARNING	Worker_reactor_try_to_exit() (ERRNO 9012): worker exit timeout, forced termination
                'max_wait_time' => 10,

                // PID file
                'pid_file' => '/appdata/example-com.pid',
            ],

            //Static files shoud be served from nginx instead - for performance
            'static-files' => [
                'enable' => false,
            ],
        ],
    ],
];

The only change we really need here is 'host' => '0.0.0.0', but other options not going to hurt – they are thoroughly commented. Note that we use 0.0.0.0 – which makes our mezzio swoole/openswoole application listen on all ips.

Another thing I would recommend is – head over to mezzio/config/pipeline.php and comment out or remove these middlewares:

$app->pipe(ServerUrlMiddleware::class);
$app->pipe(UrlHelperMiddleware::class);

These are just used to inject some view helpers – issue is they are written in stateful manner (which is fine for php-fpm but not swoole -where all of our classes should be stateless).

That’s all the changes you needed to setup mezzio skeleton application with swoole/openswoole – let’s move on to the docker containers and nginx reverse proxy.

Quick review of the nginx + mezzio + openswoole docker-compose stack

./dockerfiles directory

nginx-sandbox.Dockerfile – nginx container (based on official docker nginx image) with brotli extension included. Here we are using multistage build (see FROM nginx:stable as builder) purely to demonstrate how to add additional modules to nginx container (such as brotli module in our case).

Note how we inject nginx site configs into container from ./stack/env-sandbox/nginx directory:

# Nginx: Clearout any domain configs
RUN rm -f /etc/nginx/conf.d/*

# Nginx: copying our configuration files
RUN mv /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak
ADD stack/env-${ENV_NAME}/nginx/site.conf /etc/nginx/conf.d/site.conf
ADD stack/env-${ENV_NAME}/nginx/nginx.conf /etc/nginx/nginx.conf

Also note how we use sed to fill in specific environmental variables from ./stack/env-sandbox/.env file (we will get to that file in one of the next segments):

# Nginx: filling in specific configurations
RUN sed -i "s/ENV_UP_HOST/${UP_HOST}/" /etc/nginx/conf.d/site.conf
RUN sed -i "s/ENV_SERVER_NAME/${SERVER_NAME}/" /etc/nginx/conf.d/site.conf
RUN sed -i "s/ENV_SERVER_ALIAS/${SERVER_ALIAS}/" /etc/nginx/conf.d/site.conf
RUN sed -i "s/ENV_PROJECT_NAME/${PROJECT_NAME}/" /etc/nginx/conf.d/site.conf
RUN sed -i "s/ENV_NAME/${ENV_NAME}/" /etc/nginx/conf.d/site.conf
RUN sed -i "s#ENV_SRV_DOC_ROOT#${SRV_DOC_ROOT}#" /etc/nginx/conf.d/site.conf
RUN sed -i "s#ENV_PHP_VER#${PHP_VER}#" /etc/nginx/conf.d/site.conf

Of specific interest is this env variable: ENV_UP_HOST – that is a variable that should be the container name/ID of the swoole/openswoole php container. That’s because in docker – a container’s hostname defaults to be the container’s ID (see docker docs here). You see what’s happening here? – nginx container will talk to swoole container using swoole container hostname (which is it’s container id) – how cool is that!

php-8.1-sandbox.Dockerfile – php 8.1 container with openswoole extension included as well as a lot of php extensions included. Container is based on Ubuntu 22.04 LTS. We do very similar things as in nginx container – inject config files from ./stack/env-sandbox/php8.1 directory, then sed is used to environmentalize one thing or another.

Note that we expose 2 volumes in that dockerfile:

  • /app volume – for actual mezzio application (you should not write any data into it. In production you should use docker’s COPY or ADD commands to embed code inside of the container instead of mounting)
  • /appdata volume – for any FS writes we may want to do from our php mezzio application. As you know – we cannot just write anywhere inside of the container – we have to use these specially mounted directories.

Also note how we setup the entrypoint in mezzio + swoole/openswoole:

ENTRYPOINT ["./mezzio/vendor/bin/laminas", "mezzio:swoole:start"]

While mezzio skeleton application has public/index.php file- it’s not actually being used with swoole/openswoole! This is not typical for php applications – here we use beforementioned laminas command instead to launch the stack (similar to how that is done in many other frameworks in other languages perhaps).

docker-compose.yml

Docker-compose as you know is what allows us to run several services as one cohesive application. I will not be going in depth here as that’s outside of scope of this tutorial. Note that nginx container is set to depend on swoole container – so if swoole container doesn’t start – neither will nginx.

Also note that we require a bridge network called infranet (we will set it up later):

networks:
  default:
    external: true
    name: infranet

Why use a bridge network? That’s because in real life we should only expose ports on our reverse proxy and not allow any web users to access our php+swoole/openswoole application directly without nginx. That’s why docker’s bridge network is there for – php+swoole/openswoole container runs only inside that network and nginx is the only exposed container from that network.

Launch nginx and swoole containers using docker-compose

Whether you configured mezzio skeleton application yourself or used mezzio directory that came with this repo – now we need to perform several more steps before we launch our containerized swoole/openswoole + nginx stack .

Step 1: Rename the ./stack/env-sandbox/.env.template file to ./stack/env-sandbox/.env file and make sure that FS_DIR variable points to some empty directory on your computer. This is because we need to mount some directory into the mezzio/swoole container as mezzio/swoole container needs to write onto the filesystem (the .pid file at the very minimum but you can write anything else you want – images, pdf, whatever) – and as we know when we use containers we cannot just write into container – we have to mount a writable directory into the container, ex:

# here - change to use your directory path
mkdir /mnt/480g_drive/dockervolumes/mezzio-somalia

# Make sure directory is writable.
# I'm doing chmod 777 here merely for illustration purposes for this article,
# but in production you should use a specific user or less open
#permissions typically
chmod 777 mkdir /mnt/480g_drive/dockervolumes/mezzio-somalia

Step 2: Edit the stack/env-sandbox/.env file and make sure that CODE_DIR variable is pointing to the repository directory you checked out for this article, ex:

CODE_DIR=/mnt/480g_drive/projects/mezzio-nginx-swoole-demo

Step 3: create a separate docker bridge network (friends don’t let friend use docker’s default bridge network). This is per dockers’ best practices recommendation:

 docker network create  \
--opt com.docker.network.bridge.name=br_infranet \
--subnet=172.21.0.0/16 \
--ip-range=172.21.11.0/24 \
--gateway=172.21.11.255 \
--attachable -d bridge infranet

Step 4: Now you are ready to launch your containerized nginx and php+swoole/openswoole stack. Go ahead and execute docker-compose command to launch your stack. Note that this has to be done from directory where docker-compose.yml file is:

cd stack/env-sandbox && docker-compose up -d  
Creating sandbox-mezzio-somalia-swoole8.1 ... done
Creating sandbox-mezzio-somalia-nginx     ... done

This command uses the provided docker-compose.yml file (that itself uses .env) file and launches 2 docker containers:

  • sandbox-mezzio-somalia-swoole8.1
  • sandbox-mezzio-somalia-nginx

Now – if everything went right – you should see php+swoole/openswoole and nginx powered mezzio framework site if you open the browser on this address: http://YOUR_IP:3111, for example this is what I see:

mezzio swoole and nginx powered site

And to prove even further – that we are indeed using nginx reverse proxy in front of the mezzio application – you could simply look at the headers:

That is all there is to it. If you have questions on this article – Ill try to answer them and update the article (if I have time).

Thank you for reading.

1 thought on “Mezzio php framework – using swoole/openswoole with nginx reverse proxy (with docker containers and docker compose)”

Leave a Comment