Note: I noticed that some of the steps are redundant. I will update this post from time to time to remove the redundant parts.
Requirements
The steps should be similar (if not identical) on different versions of PHP and Laravel.
- Laravel 11
- PHP 8.3
- Xdebug PHP extension version 3 (please refer to the documentation if you are still on version 2)
- Vscode PHP Xdebug plugin (1.35 at the time the post was written)
Name: PHP Debug Id: xdebug.php-debug Description: Debug support for PHP with Xdebug Version: 1.35.0 Publisher: Xdebug VS Marketplace Link: https://marketplace.visualstudio.com/items?itemName=xdebug.php-debug
Step 1. Create a new Laravel application (or use an existing one)
dev$ curl -s "https://laravel.build/xdebug-demo" | bash dev$ cd xdebug-demo dev/xdebug-demo$ ./vendor/bin/sail up -d
Step 2: Publish Laravel Sail configurations (optional)
This is necessary if you need to modify the Dockerfile that comes with Laravel Sail or in case you need to add any dependency into the Docker image.
dev/xdebug-demo$ ./vendor/bin/sail artisan vendor:publish --provider="Laravel\Sail\SailServiceProvider"
Step 3: Add Environment Variables to .env
Add the following lines to .env to override the Xdebug configuration
SAIL_XDEBUG_MODE=develop,debug,coverage
SAIL_XDEBUG_CONFIG="client_host=host.docker.internal"
Step 4: Modify docker-compose.yml
Create a file called “20-xdebug.ini” inside ./docker/php with the following content:
zend_extension=xdebug
[xdebug]
xdebug.mode=develop,debug
xdebug.client_host=host.docker.internal
xdebug.start_with_request=yes
xdebug.log=/tmp/xdebug.log
Please note the PHP version (8.3) used in these two lines. Substitute with whatever PHP version that the application requires.
services: laravel.test: build: # point this to the published sail config directory context: './docker/8.3' ---------- truncated ---------- volumes: - '.:/var/www/html' # add this volume - './docker/php/20-xdebug.ini:/etc/php/8.3/mods-available/xdebug.ini'
Instead of exposing the Xdebug .ini file as a volume, you can also add it into the Docker image by modifying the Dockerfile
Step 5: Configure PHP Debug Extension
Open the Run and Debug panel in the sidebar and click “create a launch.json file”
Add the pathMappings property into the “Listen for Xdebug” task inside the file that pops up:
The object key should be the directory inside the container, and the object value should be the root directory of your project (NOT the public folder). In this case, we can get it automatically using the ${workspaceFolder}
variable.
Step 6: Verify
First, run the Listen for Xdebug task inside the Run and Debug panel which we have configured previously.
After that, start docker compose:
xdebug-demo$ ./vendor/bin/sail up xdebug-demo$ ./vendor/bin/sail up -d --build # in case you need to rebuild the container
Then, add a breakpoint anywhere in the code and open the application URL in your browser. The application should pause at the breakpoint that you set.
Troubleshooting
If Xdebug does not trigger the breakpoint, you can check the log inside the container at /tmp/xdebug.log
If you get this error, it means you have not set up the path mappings properly (please refer to step 5)
If it still does not work, the xdebug.start_with_request config may not be set. Try adding “?XDEBUG_TRIGGER=1” to the application URL
Summary
Full content of docker-compose.yml
:
services: laravel.test: build: context: './docker/8.3' dockerfile: Dockerfile args: WWWGROUP: '${WWWGROUP}' image: 'sail-8.3/app' extra_hosts: - 'host.docker.internal:host-gateway' ports: - '${APP_PORT:-80}:80' - '${VITE_PORT:-5173}:${VITE_PORT:-5173}' environment: WWWUSER: '${WWWUSER}' LARAVEL_SAIL: 1 XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}' XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}' IGNITION_LOCAL_SITES_PATH: '${PWD}' volumes: - '.:/var/www/html' - './docker/php/20-xdebug.ini:/etc/php/8.3/mods-available/xdebug.ini' networks: - sail depends_on: - mysql - redis - meilisearch - mailpit - selenium mysql: image: 'mysql/mysql-server:8.0' ports: - '${FORWARD_DB_PORT:-3306}:3306' environment: MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}' MYSQL_ROOT_HOST: '%' MYSQL_DATABASE: '${DB_DATABASE}' MYSQL_USER: '${DB_USERNAME}' MYSQL_PASSWORD: '${DB_PASSWORD}' MYSQL_ALLOW_EMPTY_PASSWORD: 1 volumes: - 'sail-mysql:/var/lib/mysql' - './vendor/laravel/sail/database/mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh' networks: - sail healthcheck: test: - CMD - mysqladmin - ping - '-p${DB_PASSWORD}' retries: 3 timeout: 5s redis: image: 'redis:alpine' ports: - '${FORWARD_REDIS_PORT:-6379}:6379' volumes: - 'sail-redis:/data' networks: - sail healthcheck: test: - CMD - redis-cli - ping retries: 3 timeout: 5s meilisearch: image: 'getmeili/meilisearch:latest' ports: - '${FORWARD_MEILISEARCH_PORT:-7700}:7700' environment: MEILI_NO_ANALYTICS: '${MEILISEARCH_NO_ANALYTICS:-false}' volumes: - 'sail-meilisearch:/meili_data' networks: - sail healthcheck: test: - CMD - wget - '--no-verbose' - '--spider' - 'http://127.0.0.1:7700/health' retries: 3 timeout: 5s mailpit: image: 'axllent/mailpit:latest' ports: - '${FORWARD_MAILPIT_PORT:-1025}:1025' - '${FORWARD_MAILPIT_DASHBOARD_PORT:-8025}:8025' networks: - sail selenium: image: selenium/standalone-chromium extra_hosts: - 'host.docker.internal:host-gateway' volumes: - '/dev/shm:/dev/shm' networks: - sail networks: sail: driver: bridge volumes: sail-mysql: driver: local sail-redis: driver: local sail-meilisearch: driver: local
Full content of launch.json
:
{ // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "name": "Listen for Xdebug", "type": "php", "request": "launch", "port": 9003, "pathMappings": { "/var/www/html": "${workspaceFolder}" } }, { "name": "Launch currently open script", "type": "php", "request": "launch", "program": "${file}", "cwd": "${fileDirname}", "port": 0, "runtimeArgs": [ "-dxdebug.start_with_request=yes" ], "env": { "XDEBUG_MODE": "debug,develop", "XDEBUG_CONFIG": "client_port=${port}" } }, { "name": "Launch Built-in web server", "type": "php", "request": "launch", "runtimeArgs": [ "-dxdebug.mode=debug", "-dxdebug.start_with_request=yes", "-S", "localhost:0" ], "program": "", "cwd": "${workspaceRoot}", "port": 9003, "serverReadyAction": { "pattern": "Development Server \\(http://localhost:([0-9]+)\\) started", "uriFormat": "http://localhost:%s", "action": "openExternally" } } ] }
Full content of docker/8.3/Dockerfile
(no modification):
FROM ubuntu:22.04 LABEL maintainer="Taylor Otwell" ARG WWWGROUP ARG NODE_VERSION=20 ARG MYSQL_CLIENT="mysql-client" ARG POSTGRES_VERSION=15 WORKDIR /var/www/html ENV DEBIAN_FRONTEND noninteractive ENV TZ=UTC ENV SUPERVISOR_PHP_COMMAND="/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80" ENV SUPERVISOR_PHP_USER="sail" RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone RUN echo "Acquire::http::Pipeline-Depth 0;" > /etc/apt/apt.conf.d/99custom && \ echo "Acquire::http::No-Cache true;" >> /etc/apt/apt.conf.d/99custom && \ echo "Acquire::BrokenProxy true;" >> /etc/apt/apt.conf.d/99custom RUN apt-get update && apt-get upgrade -y \ && mkdir -p /etc/apt/keyrings \ && apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python2 dnsutils librsvg2-bin fswatch ffmpeg nano \ && curl -sS 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x14aa40ec0831756756d7f66c4f4ea0aae5267a6c' | gpg --dearmor | tee /etc/apt/keyrings/ppa_ondrej_php.gpg > /dev/null \ && echo "deb [signed-by=/etc/apt/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu jammy main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \ && apt-get update \ && apt-get install -y php8.3-cli php8.3-dev \ php8.3-pgsql php8.3-sqlite3 php8.3-gd \ php8.3-curl \ php8.3-imap php8.3-mysql php8.3-mbstring \ php8.3-xml php8.3-zip php8.3-bcmath php8.3-soap \ php8.3-intl php8.3-readline \ php8.3-ldap \ php8.3-msgpack php8.3-igbinary php8.3-redis \ php8.3-memcached php8.3-pcov php8.3-imagick php8.3-xdebug php8.3-swoole \ && curl -sLS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer \ && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \ && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_VERSION.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \ && apt-get update \ && apt-get install -y nodejs \ && npm install -g npm \ && npm install -g pnpm \ && npm install -g bun \ && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /etc/apt/keyrings/yarn.gpg >/dev/null \ && echo "deb [signed-by=/etc/apt/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \ && curl -sS https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /etc/apt/keyrings/pgdg.gpg >/dev/null \ && echo "deb [signed-by=/etc/apt/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt jammy-pgdg main" > /etc/apt/sources.list.d/pgdg.list \ && apt-get update \ && apt-get install -y yarn \ && apt-get install -y $MYSQL_CLIENT \ && apt-get install -y postgresql-client-$POSTGRES_VERSION \ && apt-get -y autoremove \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* RUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.3 RUN groupadd --force -g $WWWGROUP sail RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail COPY start-container /usr/local/bin/start-container COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf COPY php.ini /etc/php/8.3/cli/conf.d/99-sail.ini RUN chmod +x /usr/local/bin/start-container EXPOSE 80/tcp ENTRYPOINT ["start-container"]
Container environment variables:
sail@15850a79eaa6:/var/www/html$ env XDEBUG_MODE=develop,debug,coverage HOSTNAME=15850a79eaa6 LARAVEL_SAIL=1 PWD=/var/www/html SUPERVISOR_PHP_COMMAND=/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80 TZ=UTC XDEBUG_CONFIG=client_host=host.docker.internal HOME=/home/sail LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.zst=01;31:*.tzst=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.wim=01;31:*.swm=01;31:*.dwm=01;31:*.esd=01;31:*.jpg=01;35:*.jpeg=01;35:*.mjpg=01;35:*.mjpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.webp=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.m4a=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.oga=00;36:*.opus=00;36:*.spx=00;36:*.xspf=00;36: WWWUSER=501 LESSCLOSE=/usr/bin/lesspipe %s %s IGNITION_LOCAL_SITES_PATH=/Users/ad/Development/xdebug-demo TERM=xterm LESSOPEN=| /usr/bin/lesspipe %s SHLVL=1 SUPERVISOR_PHP_USER=sail PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin DEBIAN_FRONTEND=noninteractive _=/usr/bin/env