Docker + php-fpm + PhpStorm + Xdebug

Not so long ago, the team lead said: guys I want everyone to have the same development environment for our combat projects + we need to be able to debug everything - both web applications, and api requests, and console scripts in order to save our nerves and time. And help us with this docker.



No sooner said than done. Details under the cut.



There are many containerization manuals on the network, but how to apply them to real combat development? For each project, write your own docker-compose.yml? But all our projects communicate with each other through api, they all use the standard technology stack: nginx + php-fpm + mysql.



Therefore, let's clarify the conditions of the problem:



  1. We work in a company, in a team, accompany several combat projects. We all work under Ubuntu + PhpStorm
  2. For local development, we want to use the docker in order to have the same development environment for each team member, and also so that when a new developer arrives, he can quickly deploy a working environment
  3. We want to develop with comfort, we want to debut everything: both web applications, and console scripts, and api requests.


Once again: we want to bring several work projects into the docker.



On battle servers, the standard nginx + php-fpm + mysql bundle is used. And what's the problem?



We deploy the exact same environment + Xdebug on the local machine, configure our projects in PhpStorm, and work. For debug, turn on the “tube” in PhpStorm, everything works out of the box, everything is fine.







All this is true - everything works out of the box. But, let's try to look under the hood of our working environment.



Nginx + php-fpm communicate through the socket, xdebug listens on port 9000, PhpStorm also, by default, listens on port 9000 for debugging and everything seems to be fine. And if we have several applications open in PhpStorm, and wiretapping ("tube)" is enabled for several applications? What will PhpStorm do? He will begin to swear that a new connection for Xdebug has been detected, do you want to ignore it, or not?



That is, with the default settings in PhpStorm, at a particular point in time, I can debut only one application. For all other open applications, debug must be turned off. Damn, but it's inconvenient. I want to listen to all applications for debugging, and if there is a breakpoint in one of them, then I want PhpStorm to stop in this application, on the line where I need it.



And what is needed for this? But you need each application to start with its own settings for Xdebug. So that each application listens to its port, looks for its server, and not like we have everything in common, all in one heap.



And for this there is a wonderful docker! We can run each of our combat applications in a separate container, based on one common image, for example, php: 7.1-fpm. Thanks to docker technology, we can isolate our applications with minimal overhead.



Ok, let's start our combat projects under the docker, run each project in a separate container, configure each project in PhpStorm to debug individually, everything should be fine.



And, oops, the first problem: the containers in the docker are run as root, and locally we work, usually as a user with uid 1000, gid 1000. Applications are operational, and giving each application 777 rights to everything is not an option. Our applications are under git, and if we give rights 777 locally, then git will record all this and transfer it to the battle server.



Crutches, here is an example php image: 7.1-fpm that will be compiled.



Update



As the community rightly pointed out, there is absolutely no need to crutches very hard.

For example 1ntrovert habrozer in the comment



Initial php image example: 7.1-fpm (uid and gid are hardcoded)
FROM php:7.1-fpm RUN apt-get update && apt-get install -y \ git \ curl \ wget \ libfreetype6-dev \ libjpeg62-turbo-dev \ libmcrypt-dev \ libpng-dev zlib1g-dev libicu-dev g++ libmagickwand-dev libxml2-dev \ && docker-php-ext-configure intl \ && docker-php-ext-install intl \ && docker-php-ext-install mbstring zip xml gd mcrypt pdo_mysql \ && docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ \ && docker-php-ext-install -j$(nproc) gd \ && pecl install imagick \ && docker-php-ext-enable imagick \ && pecl install xdebug \ && docker-php-ext-enable xdebug ADD ./php.ini /usr/local/etc/php/php.ini RUN wget https://getcomposer.org/installer -O - -q \ | php -- --install-dir=/bin --filename=composer --quiet RUN usermod -u 1000 www-data && groupmod -g 1000 www-data WORKDIR /var/www USER 1000:1000 CMD ["php-fpm"]
      
      









Corrected Dockerfile Example



 FROM php:7.1-fpm ARG USER_ID ARG GROUP_ID RUN apt-get update && apt-get install -y \ git \ curl \ wget \ libfreetype6-dev \ libjpeg62-turbo-dev \ libmcrypt-dev \ libpng-dev zlib1g-dev libicu-dev g++ libmagickwand-dev --no-install-recommends libxml2-dev \ && docker-php-ext-configure intl \ && docker-php-ext-install intl \ && docker-php-ext-install mbstring zip xml gd mcrypt pdo_mysql \ && pecl install imagick \ && docker-php-ext-enable imagick \ && pecl install xdebug-2.5.0 \ && docker-php-ext-enable xdebug ADD ./php.ini /usr/local/etc/php/php.ini RUN wget https://getcomposer.org/installer -O - -q \ | php -- --install-dir=/bin --filename=composer --quiet RUN usermod -u ${USER_ID} www-data && groupmod -g ${GROUP_ID} www-data WORKDIR /var/www USER "${USER_ID}:${GROUP_ID}" CMD ["php-fpm"]
      
      







When starting a container from this image, www-data user gets uid = 1000, gid = 1000. Typically, the first created user in Linux has these rights. And, with such rights our php-fpm containers will work. I would be very grateful if someone would tell me how to work without crutches with access rights to the docker.



When starting a container from this image, the www-data user receives uid and gid, which will be transferred from outside.



Also in the comments, the topic was raised : why change the rights of the www-data user at all, why the standard rights do not suit 33. Only one thing: when we go into the container and create, for example, a migration file, we will not be the owner of the file on the host machine. And each time it will be necessary to run something like
  sudo chown -R user:user ./
      
      







And the second small problem: for Xdebug to work correctly, you need to register the correct ip address for the host machine. Each member of the team is different. 127.0.0.1 does not roll. And here the docker himself comes to our aid. For example, we can explicitly configure the network - 192.168.220.0/28. And then our machine will always have the address 192.168.220.1. We will use this address to configure PhpStorm, as well as to configure other applications. For example, when working with MySql.



Docker-compose.yml itself, after considering the comments, looks like this:



 version: '3' services: php71-first: build: context: ./images/php71 args: - USER_ID - GROUP_ID volumes: - ./www:/var/www - ./aliases/php71/bash.bashrc:/etc/bash.bashrc environment: XDEBUG_CONFIG: "remote_host=192.168.220.1 remote_enable=1 remote_autostart=off remote_port=9008" PHP_IDE_CONFIG: "serverName=first" networks: - test-network php71-two: build: context: ./images/php71 args: - USER_ID - GROUP_ID volumes: - ./www:/var/www - ./aliases/php71/bash.bashrc:/etc/bash.bashrc environment: XDEBUG_CONFIG: "remote_host=192.168.220.1 remote_enable=1 remote_autostart=off remote_port=9009" PHP_IDE_CONFIG: "serverName=two" networks: - test-network nginx-test: image: nginx volumes: - ./hosts:/etc/nginx/conf.d - ./www:/var/www - ./logs:/var/log/nginx ports: - "8080:80" depends_on: - php71-first - php71-two networks: test-network: aliases: #         . ,    api - first.loc - two.loc # mysql: # image: mysql:5.7 # ports: # - "3306:3306" # volumes: # - ./mysql/data:/var/lib/mysql # environment: # MYSQL_ROOT_PASSWORD: secret # networks: # - test-network networks: test-network: driver: bridge ipam: driver: default config: - subnet: 192.168.220.0/28
      
      







We see that in this config two containers php71-first and php71-two are created, based on one php image: 7.1-fpm. Each container has its own settings for Xdebug. Each individual container will listen, for debugging, its port and its server.



Also, I draw your attention to the directives

  args: - USER_ID - GROUP_ID
      
      







Without these variables, the php-fpm image will not start. Question: how to pass them to docker-compose.yml? Answer: as it is more convenient for you. You can at startup:

 USER_ID=$(id -u) GROUP_ID=$(id -g) docker-compose up -d
      
      





You can register these variables in the .env file, which lies on the same level as the docker-compose.yml file

USER_ID=1000

GROUP_ID=1000







I prefer the option with the .env file. Of course, you can use the Makefile. As you like more.



The full code for the demo version is posted on the github .



Listing demo project:







Briefly go over the listing of the project.



Aliases -> php71 -> bash.bashrc directory. Controversial moment. I prefer to communicate with php-fpm containers through aliases.



This file is forwarded to docker-compose.yml: - ./aliases/php71/bash.bashrc:/etc/bash.bashrc

Linux standard tool.



The hosts directory - configuration files for Nginx. Each config has its own php-fpm container. Example:



 server { listen 80; index index.php; server_name first.loc; error_log /var/log/nginx/first_error.log; root /var/www/first.loc; location / { try_files $uri /index.php?$args; } location ~ \.php$ { fastcgi_split_path_info ^(.+\.php)(/.+)$; #  php-fpm fastcgi_pass php71-first:9000; fastcgi_index index.php; fastcgi_read_timeout 1000; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_path_info; } }
      
      





The images directory — instructions for assembling php-fpm images, the mysql directory — store databases, the www directory — all of our web projects, in our example first.loc and two.loc.



Let's summarize the intermediate results : using the capabilities of the docker, we launched all of our working projects in one environment. All our projects see each other, unique settings for Xdebug are registered for each of the projects.



It remains to correctly configure PhpStorm for each of the projects. When setting up, we must register the port for debug and the server name in several places.



Create a project in PhpStorm















We will configure the menu sections

- PHP (you must correctly register the CLI Interpreter),

- Debug (change the port to 9008, as in the docker-compose.yml file),

- DBGp proxy (IDE key, Host, Port),

update Thanks to the CrazyLazy hub browser for the important point. The DBGp proxy menu item does not need to be configured.

- Servers (you must correctly specify the server name, as in the docker-compose.yml file, and use path mappings)







I will hide all further screenshots under the spoiler.



Configuring the CLI Interpreter from the docker-compose.yml file
There is nothing tricky - it is important, when setting up, select the desired image, and correctly register the server name. By default, the name of the Docker server, we have it.







































Set up the Debug menu section
Again, we prescribe everything from the docker-compose.yml settings for a specific container. At the same step, we validate how our debug works.













Set up the Servers menu section
It is important to correctly register use path mappings, again we take the server name from the settings











We leave the menu section File -> Settings, go to the menu section Run -> Edit Configuration, create a Php Web Page
We select our server, created in the previous step.















Well, that's all. It is written a lot of letters, it seems everything is not easy



In fact, the main thing is to understand a very simple thing. Thanks to docker technology, we can run all our working applications in a single space, but with different settings for Xdebug. Each application works in its own container, and we have to carefully prescribe the settings for each application in PhpStorm.



And at the exit we get a wonderful picture.



1. We clone a repository on a github . Create a .env file with variables

USER_ID= uid

GROUP_ID= gid









2. We register the nodes first.loc and two.loc in the file / etc / hosts



 127.0.0.1 first.loc 127.0.0.1 two.loc
      
      





3. In the git folder, run the docker-compose up -d







4. Set up both projects first.loc and two.loc in PhpStorm, as described above, and run both projects in PhpStorm. Those. we have two PhpStorm windows open, with two projects, each of them listens for incoming connections (the handset is on).



5. In the project two.loc we put a breakpoint on the second, for example, line. In the first project first.loc, we start the http request from the file http.http



And lo and behold! We are thrown into the second project, at our breakpoint.



To debug console scripts, we do exactly the same thing. We turn on the wiretap for wiretapping, set a breakpoint, go to the right container, run the right script.



Something like:



 alex@alex-Aspire-ES1-572 ~ $ php71first www-data@a0e771cfac72:~$ cdf www-data@a0e771cfac72:~/first.loc$ php index.php I'am first host www-data@a0e771cfac72:~/first.loc$
      
      





Where php71first is the alias on the host machine:



 alias php71first="cd ~/docker_git && docker-compose exec php71-first bash"
      
      





cdf



- an alias that works in a container. I wrote above that I prefer to use aliases to communicate with containers.



That's all, constructive criticism, comments are welcome.



PS I would like to express my deep gratitude to Denis Bondar for his article PhpStorm + Docker + Xdebug , which was the starting point for writing this tutorial.



All Articles