POST
PhpStorm, Docker e XDebug su Linux
Come far funzionare il debugger di PhpStorm usando Docker su Linux.
Se state sviluppando in PHP su Linux, e state usando un ambiente Docker per lo sviluppo, potreste trovarvi di fronte a una serie di difficoltà inattese (e soprattutto sconosciute ai colleghi che usano Mac e Windows 😦). Questo post nasce nel tentativo di smarcarle tutte.
Ambiente di sviluppo
Per un’applicazione Symfony 5 che sto sviluppando, uso un ambiente di sviluppo basato su docker-compose che comprende tre container:
- php-fpm
- un webserver (Nginx nel mio caso, ma il post si applica quasi senza modifiche ad Apache)
- un database (postgresql), non rilevante ai fini di questo post.
php-fpm
Per questo progetto parto dall’immagine Docker php:7.4-fpm-buster (Debian 10), e nel Dockerfile faccio installare le estensioni PHP che servono a Symfony:
- intl
- pdo
- pdo_pgsql (siccome uso un db Postgres)
- pgsql (siccome uso un db Postgres)
- zip
Queste estensioni vengono installate col comando docker-php-ext-install; per il debug ho bisogno dell’estensione xdebug, che è invece un’estensione PECL e va installata usando il comando pecl install. Per poter compilare le estensioni in fase di build, e per avere sempre a portata di mano (nel container) alcuni comandi utili, installo alcuni pacchetti con apt-get. Il Dockerfile è riportato di seguito:
FROM php:7.4-fpm-buster
ENV PHP_EXTENSIONS \
intl \
opcache \
pdo \
pdo_pgsql \
pgsql \
zip
ENV PECL_EXTENSIONS xdebug
ENV PACKAGES \
g++ iproute2 iputils-ping \
libicu-dev libpq-dev libzip-dev unzip wget zlib1g-dev
RUN apt-get update && apt-get install -y ${PACKAGES} \
&& docker-php-ext-configure intl \
&& docker-php-ext-configure pgsql -with-pgsql=/usr/local/pgsql \
&& docker-php-ext-configure zip \
&& docker-php-ext-install ${PHP_EXTENSIONS}
Nel file docker-compose.yml
uso due file locali (php.ini e xdebug.ini) per la configurazione di php e dell’estensione xdebug, come segue.
version: "3.2"
services:
php:
build:
context: ./php
container_name: php_fpm
volumes:
- ./php/php.ini:/usr/local/etc/php/php.ini
- ./php/xdebug.ini:/usr/local/etc/php/conf.d/xdebug.ini
environment:
- PHP_IDE_CONFIG=serverName=symfony-docker
Uso due file locali perché, se ho bisogno di apportare modifiche alla configurazione, è sufficiente modificare i file e riavviare docker compose (5 secondi), anziché dover rifare il build della macchina (3 minuti).
Il file xdebug.ini
contiene le impostazioni seguenti:
zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20190902/xdebug.so
xdebug.idekey=PHPSTORM
xdebug.remote_enable=1
xdebug.remote_autostart=1
xdebug.remote_connect_back=0
xdebug.remote_port=9000
; xdebug.remote_host=host.docker.internal ; Windows/Mac
xdebug.remote_log=/var/www/html/symfony/var/log/xdebug.log
; xdebug.remote_timeout=200 ; default
xdebug.profiler_output_dir=/var/www/html/symfony/var
xdebug.profiler_output_name=cachegrind.out.%p
nginx (o il web server che usiamo)
In docker-compose.yml
facciamo il forward della porta 80 del container nginx sulla porta 8080, quindi il sito sarà disponibile all’URL http://localhost:8080. Ci tornerà utile più avanti.
nginx:
build:
context: ./nginx
container_name: nginx
ports:
- "8080:80"
volumes:
...
Il problema dell’IP dell’host
In Docker per Windows e Mac, i container possono usare il nome host.docker.internal
per accedere all’host senza doverne conoscere l’IP; quest’ultimo infatti può cambiare in seguito a un riavvio di docker-compose.
Docker per Linux non permette di usare questo nome (e francamente non mi è chiaro il motivo).
Questo problema verrà risolto dalla release 20 di Docker; se non avete tempo e voglia di aspettare che esca la release 20, potete provare la soluzione seguente.
Una soluzione
È possibile ricavare l’indirizzo IP dell’host con il comando ip route show
, che risponde qualcosa di simile:
default via 172.18.0.1 dev eth0
172.18.0.0/16 dev eth0 proto kernel scope link src 172.18.0.3
In questo esempio, l’IP dell’host è 172.18.0.1 e si ottiene prendendo la terza parola della riga che contiene default
, ad es. così:
ip route show | awk '/default/{ print $3 }'
oppure:
ip route show | grep default | cut -d ' ' -f 3
Cosa non fare 👎
Ora che sappiamo come ottenere l’IP dell’host, potremmo essere tentati di aggiungere una riga nel file /etc/hosts mediante Dockerfile…
🛑 Non fatelo: il file /etc/hosts viene sovrascritto ad ogni avvio del container!
Cosa fare 🆗
Quel che possiamo fare è invece creare un nuovo entrypoint, ossia uno script shell che viene eseguito a container avviato.
In questo script possiamo aggiungere la riga di cui sopra ad /etc/hosts, oppure -meglio, secondo me- impostare la variabile d’ambiente XDEBUG_CONFIG (che verrà letta da PHP), definendo a runtime il valore di xdebug.remote_host
(una delle righe commentate in xdebug.ini):
#!/usr/bin/env bash
# entrypoint.sh
set -euo pipefail # esci in caso di errori
export XDEBUG_CONFIG="remote_host=$(ip route show | awk '/default/ { print $3 }')"
exec "$@" # esegui il resto della linea comando
Salviamo questo script nella stessa directory del Dockerfile del container php.
Per far sì che il container usi questo nuovo entrypoint, aggiungiamo le seguenti istruzioni alla fine del Dockerfile:
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod 755 /usr/local/bin/entrypoint.sh
ENTRYPOINT [ "/usr/local/bin/entrypoint.sh", "php-fpm" ]
Configurare il debugging in PhpStorm
Ora sul versante Docker siamo operativi: ci resta da far parlare il nostro container con PhpStorm.
Build, Execution, Deployment
Apriamo la finestra Settings (Ctrl-Alt-S) e selezioniamo la voce Build, Execution, Deployment > Docker. Premiamo il pulsante + per configurare l’integrazione con Docker.
PHP > Servers
Nella finestra Settings, selezioniamo la voce Languages & Frameworks > PHP > Servers e clicchiamo sul pulsante + per definire il server PHP da usare: specifichiamo localhost come host e 8080 come porta (quella su cui facciamo il forward mediante docker-compose.yml
); spuntiamo inoltre Use path mappings, e nel pannello sottostante, troviamo (nella colonna a sinistra) la directory che contiene i sorgenti del progetto PHP. I corrispondelza di quella riga, a destra inseriamo il corrispondente percorso del container php, ossia il percorso a cui abbiamo montato i sorgenti in docker-compose.yml
.
🔥 Importante
Il nome che diamo al server deve corrispondere a quello impostato come serverName nella variabile d’ambiente PHP_IDE_CONFIG, cioè in docker-compose.yml
nella riga evidenziata:
php:
# ...
environment:
- PHP_IDE_CONFIG=serverName=symfony-docker
PHP > Cli interpreter
Nella finestra Settings, selezioniamo ora la voce Languages & Frameworks > PHP e clicchiamo sul pulsante con i tre puntini a fianco di CLI Interpreter per definire l’interprete PHP da usare.
Nella finestra che si aprirà premiamo il pulsante + per configurare l’interprete CLI del container e selezioniamo From Docker, Vagrant, VM, WSL, Remote…. Inseriamo questi valori:
Cliccando OK in questa finestra, la precedente dovrebbe mostrare quanto segue.
Diamo nuovamente OK e selezioniamo l’interpete appena creato alla voce CLI Interpeter.
PHP > Debug
Nella finestra Settings, selezioniamo Languages & Frameworks > PHP > Debug e clicchiamo sul link Validate. Nella finestra che si aprirà, controlliamo se:
-
Path to create validation script punta alla directory in cui viene mappata la document root: in un progetto Symfony >=3, questa directory non è la directory del progetto, ma la subdirectory public/
-
Url to validation script è http://localhost:8080/ (cioè localhost e la porta su cui il nostro web server è pubblicato).
Clicchiamo sul pulsante Validate in basso, e se tutto va bene dovrebbe comparire qualcosa di simile.
La validazione fallisce se…
PhpStorm, per validare la configurazione, crea un file PHP al percorso Path to create validation script e poi lo “visita” usando Url to validation script come prefisso. Questo processo fallisce miseramente se nel file di configurazione di Nginx la parte relativa a Symfony è racchiusa in una direttiva location che comprende solo index.php invece di comprendere tutti gli script con estensione .php
.
Quindi se la direttiva ha questa forma:
location ~ ^/index\.php$ {
include fastcgi_params;
# ...
}
Andrà cambiata in:
location ~ \.php$ {
include fastcgi_params;
# ...
}
Il momento della verità
A questo punto proviamo il debugger. In PhpStorm apriamo un file PHP, impostiamo un breakpoint e poi facciamo in modo di “visitarlo” da browser.
Non funziona ancora! 😱 Cosa faccio?
Per prima cosa ripercorro tutti i passi precedenti alla ricerca di qualche errore. Nel mio caso, il mio primo errore è stato pensare che host.docker.internal fosse valido anche in Linux, ma anche usando il nuovo entrypoint non succedeva niente.
Loggare cosa succede in xdebug
Per capire cosa sta succedendo possiamo far sì che l’estensione XDebug scriva in un file di log tutto quello che sta facendo. La direttiva xdebug.remote_log
in xdebug.ini serve proprio a questo.
xdebug.remote_log=/percorso/di/xdebug.log
Per capire se la connessione fra container e host avviene correttamente, possiamo tenere d’occhio il file di log con questo comando (nel container):
tail -f /percorso/di/xdebug.log # nel container!
Poi, con il debugger di PhpStorm in ascolto, visitiamo una pagina con un breakpoint impostato (o in alternativa, selezioniamo l’opzione Break at first line in PHP scripts in PHP > Debug).
Qualcosa sta bloccando la connessione se nel log vediamo comparire messaggi di questo tipo:
[10] Log opened at 2020-09-08 08:04:09
[10] I: Connecting to configured address/port: 172.18.0.1:9000.
[10] E: Time-out connecting to client (Waited: 200 ms). :-(
[10] Log closed at 2020-09-08 08:04:09
Nel mio caso sono riuscito a capire cos’era controllando syslog:
tail -f /var/log/syslog # nell'host
Provando a fare la stessa richiesta http, comparivano messaggi di questo tipo…
Sep 8 10:28:58 <machine> kernel: [ 5038.993851] [UFW BLOCK] IN=<interface_in> OUT= PHYSIN=<physical_int_in> MAC=<mac> SRC=172.18.0.3 DST=172.18.0.1 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=39676 DF PROTO=TCP SPT=... DPT=9000 WINDOW=64240 RES=0x00 SYN URGP=0
Quell’UFW BLOCK significa che UFW , il firewall di Ubuntu, sta bloccando il pacchetto! Su Mac e Windows questo tipo di evento (almeno la prima volta) avrebbe generato un avviso interattivo.
Se non è questo il vostro caso ma la connessione comunque fallisce, un altro sospettato può essere iptables , che però non logga niente in syslog. Per fargli loggare i pacchetti TCP droppati è possibile usare il comando seguente:
iptables -A INPUT -s ${CONTAINER_IP} -m limit --limit 5/min \
-j LOG --log-prefix "PACCHETTO DROPPATO DA IPTABLES " --log-level 7
ℹ️ Nota: ${CONTAINER_IP}
va sostituito con l’indirizzo IP del container, che si ottiene col comando ip addr show
nel container stesso.
🔗 V. questa risposta in Stackoverflow per una spiegazione approfondita.
Se il firewall si mette di traverso
In Linux Mint, apriamo le Impostazioni di sistema e selezioniamo Firewall. Alla scheda Regole premiamo il pulsante + in basso a sinistra e inseriamo una regola che permetta il transito di pacchetti in ingresso l’host sulla porta su cui PhpStorm e XDebug stanno comunicando (9000 nel nostro caso):
Grazie per l’attenzione. 👍