— Artikel — № 110

110 —PHP

Xdebug 3 via een SSH-tunnel: opzet voor SFTP-only hosts

Een werkende Xdebug 3-opzet tegen een externe SFTP-only host, met de tunnelrichting, padmapping en idekey-valkuilen die anders je middag kosten.

Bovenaanzicht op linnen: getekend netwerkschema, manilamap, indexkaart, php.ini-vel, messing plaatje, rode lakzegel.
Hero · gestileerd stilleven№ 110

Je erft een PHP 8.1-site op een host die je SFTP geeft en verder niks. Geen shell. Geen php.ini-knop in het controlepaneel. Een staging-URL die in 4,2 seconden laadt om redenen die niemand uit het vorige team heeft opgeschreven. De klant wil de trage pagina vrijdag getraced hebben. Jij wilt Xdebug 3 laten praten met je laptop zonder een gat in de firewall te slaan, zonder iets te installeren dat je niet kunt terugdraaien, en zonder de hele middag te raden waarom breakpoints nooit afgaan.

Dit is de walkthrough. Xdebug 3 tegen een externe SFTP-only host, getunneld over SSH, met de path map en idekey correct ingesteld. De valkuilen staan achteraan, omdat je ze daar pas echt nodig hebt.

Wat 'SFTP-only' hier eigenlijk betekent

De meeste managed PHP-hosts die 'SFTP-toegang' adverteren, draaien er nog steeds een SSH-daemon onder. De shell is uitgeschakeld of het account zit in een jail, maar het transport is er. Dat is belangrijk, want Xdebug 3 heeft geen shell op de remote machine nodig om te debuggen. Het heeft twee dingen nodig: de Xdebug-extensie geladen in het remote PHP-proces, en een TCP-pad terug naar je IDE op poort 9003. Het SSH-transport regelt dat tweede gratis.

Voordat je iets aanraakt, bevestig drie dingen met de host of in het controlepaneel:

  • Xdebug is geïnstalleerd (de meeste shared hosts op cPanel, Plesk, RunCloud en SpinupWP kunnen het per PHP-versie aanzetten).
  • Je kunt een .user.ini-bestand in de docroot plaatsen, of er is een PHP-config UI per site.
  • SSH op poort 22 accepteert je sleutel, ook als de shell op /usr/sbin/nologin staat.

Als Xdebug niet geïnstalleerd is en de host het niet wil inschakelen, stop dan. Je kunt op een managed machine geen PHP-extensie sideloaden via SFTP. Alles hieronder gaat ervan uit dat de extensie aanwezig is en dat het jouw taak is om hem te configureren.

De tunnel: remote forward, geen local forward

Hier verliezen mensen het eerste uur. Xdebug 3 is de client in dit gesprek. Het zet vanuit het PHP-proces op de host een verbinding op naar jouw IDE. Dat betekent dat de tunnel moet forwarden vanaf de remote machine terug naar je laptop, niet andersom. Dat is -R, geen -L.

ssh -N -R 9003:localhost:9003 you@host.example.com

Lees dat als: 'open poort 9003 op de remote machine; alles wat daar verbinding mee maakt, komt uit op mijn laptop op 9003.' De -N zegt tegen SSH dat hij geen remote shell hoeft op te vragen, wat belangrijk is op accounts waar de shell uitgeschakeld is. Weigert je host -N met This service allows sftp connections only, dan heb je een strakker afgesloten opzet en heb je de SFTP-subsystem-fallback nodig die verderop in de callout staat.

Op de remote machine bindt de standaard sshd_config geforwarde poorten aan 127.0.0.1. Mooi. Dat betekent dat PHP op de host localhost:9003 kan bereiken en niemand anders op het netwerk. Zie je in een hostconfig ooit GatewayPorts yes staan, dan is dat een andere en slechtere setup; voor dit verhaal heb je het niet nodig.

Xdebug 3 configureren via .user.ini

Xdebug 3 heeft de oude xdebug.remote_*-namen laten vallen. Alles heet nu xdebug.client_* en xdebug.mode. De upgrade-handleiding op xdebug.org is de autoritatieve referentie; de korte versie staat hieronder.

Zet dit als .user.ini in de docroot (of plak het in de PHP-instellingen per site bij de host als daar een UI voor is):

xdebug.mode = debug
xdebug.start_with_request = trigger
xdebug.client_host = 127.0.0.1
xdebug.client_port = 9003
xdebug.idekey = pier
xdebug.discover_client_host = false
xdebug.log = /tmp/xdebug-pier.log
xdebug.log_level = 7

Een paar daarvan zijn cruciaal. client_host moet 127.0.0.1 zijn, niet het publieke IP van je laptop, want de SSH-tunnel komt op de remote loopback naar boven. discover_client_host = false is degene die mensen missen; staat hij op true, dan probeert Xdebug HTTP_X_FORWARDED_FOR uit te lezen en terug te bellen naar het IP dat hij daar vindt, wat zelden is wat je wilt en zeker niet wat je wilt door een tunnel.

start_with_request = trigger betekent dat Xdebug alleen aanhaakt als de request een XDEBUG_TRIGGER-cookie, GET-parameter of POST-veld bevat. Dat is de verstandige standaard. Je wilt niet dat elke request op een gedeelde staging-server vastloopt op een breakpoint omdat iemand een IDE-listener heeft laten openstaan.

Let op de .user.ini-cache. PHP-FPM leest .user.ini om de user_ini.cache_ttl seconden uit (standaard 300). Na het uploaden moet je dus vijf minuten wachten of de host vragen om FPM te herladen. Vanuit alleen SFTP is hier geen omweg voor.

Path mapping

Jouw IDE heeft de code op iets als /Users/you/work/legacy-site staan. De host draait hem vanaf /home/sites/legacy/public_html. Xdebug stuurt absolute remote paden in het DBGp-protocol; je IDE moet die vertalen.

In PhpStorm: Settings → PHP → Servers, voeg een server toe met de hostnaam, poort 443, debugger Xdebug, en vink Use path mappings aan. Map je lokale project root op de remote docroot. In VS Code met de PHP Debug-extensie zet je het in launch.json:

{
  "name": "Listen for Xdebug (Pier tunnel)",
  "type": "php",
  "request": "launch",
  "port": 9003,
  "pathMappings": {
    "/home/sites/legacy/public_html": "${workspaceFolder}"
  }
}

Zit je hier één symlink-niveau naast, dan blijven je breakpoints voor eeuwig als open cirkeltjes staan. De snelste manier om te verifiëren: roep een pagina aan met ?XDEBUG_TRIGGER=pier, kijk of de IDE de verbinding accepteert, en controleer dat het bestand dat hij opent hetzelfde is als wat je lokaal hebt. Opent hij niks of een remote stub, dan klopt de map niet.

De sessie triggeren

Met start_with_request = trigger zet je de cookie één keer en vergeet je hem verder. De schoonste manier is de Xdebug Helper-browserextensie met de IDE key op pier. Voor CLI-scripts op de remote host (als de host scheduled tasks draait die je wel kunt lezen maar niet zelf kunt aanroepen), prefix met:

XDEBUG_TRIGGER=pier XDEBUG_CONFIG="idekey=pier" php script.php

Voor een ad-hoc curl tegen een endpoint:

curl -b 'XDEBUG_TRIGGER=pier' https://staging.example.com/slow-page

De valkuilen waar je echt tegenaan loopt

Deze staan op volgorde van hoe vaak ze toeslaan.

Poort 9003 is al bezet op de remote machine. Als een andere developer ook aan het tunnelen is, faalt jouw -R 9003:localhost:9003 stilletjes en houdt SSH de sessie open zonder forward. Kies een unieke remote port per developer en zet xdebug.client_port erop af. Wij hanteren 9${USER_DIGIT}03 als afspraak.

OPcache cachet nog het oude .user.ini-gedrag. OPcache cachet .user.ini niet direct, maar wel de gecompileerde scripts, en op sommige hosts staat opcache.validate_timestamps uit in een productieachtige staging. Wijzig je de config en gebeurt er niks, vraag de host dan om OPcache te flushen of opcache.revalidate_freq op te hogen.

De Xdebug-log blijft leeg. Of Xdebug laadt niet (controleer phpinfo()) of het log-pad is niet schrijfbaar voor de FPM-gebruiker. /tmp is dat bijna altijd wel. Zie je bij het triggeren geen [Step Debug] INFO: Connecting to configured address/port: 127.0.0.1:9003 in de log, dan heeft de extensie je request nooit gezien en wordt je config niet ingelezen.

De connectielogs zeggen Could not connect to debugging client. Dan staat de tunnel niet aan, of je IDE luistert niet. Bevestig met ss -tlnp op de host (als dat kan) of zet gewoon de tunnel opnieuw op en zet de IDE-listener uit en weer aan.

Een werkende sanity check

Zodra de tunnel staat en de IDE luistert, zet dit oneliner-bestand neer als /debug-check.php in de docroot:

<?php xdebug_info();

Roep het aan met de trigger-cookie. xdebug_info() print de geladen mode, de resolved client_host en client_port, en het pad naar de log. Staat er bij client_host iets anders dan 127.0.0.1, dan is je .user.ini nog niet opgepikt. Verwijder het bestand daarna; het lekt meer dan je publiek wilt hebben.

Waar Pier in past

Toen we Pier bouwden voor het dagelijks werk van een legacy site bewerken via SFTP, hebben we de debugger-workflow bewust met rust gelaten: Xdebug plus PhpStorm is voor de trace zelf al het juiste gereedschap. Pier neemt het omliggende werk op zich, met version history op elk bestand dat je aanraakt terwijl je de bug achterna zit, en een MySQL editor voor de queries waar de trace je naartoe wijst.

Het kleinste wat je vandaag kunt doen: open de docroot van één site die je beheert, zet het .user.ini-blok hierboven erin, en draai de ssh -N -R 9003:localhost:9003-tunnel één keer. Toont de Xdebug-log het verbindingsverzoek, dan zit je op 90% en is de rest path mapping.

— Vragen —

Waarom is de SSH-tunnel een reverse forward (-R) in plaats van een local forward (-L)?

Xdebug 3 is de client in het DBGp-protocol. Het remote PHP-proces belt jouw IDE, dus moet de tunnel poort 9003 van je laptop blootleggen op de remote loopback. Daar is -R voor.

Heb ik xdebug.remote_host nog nodig bij Xdebug 3?

Nee. Xdebug 3 heeft die instellingen hernoemd. Gebruik xdebug.client_host en xdebug.client_port. De oude xdebug.remote_*-namen worden stilletjes genegeerd, wat mede verklaart waarom een upgrade voelt alsof er niks werkt.

Waarom hebben mijn wijzigingen in .user.ini geen effect?

PHP cachet .user.ini voor user_ini.cache_ttl seconden, standaard 300. Wacht vijf minuten, of vraag de host om PHP-FPM te herladen. Vanaf de SFTP-kant is er geen omweg.