PhpStorm and Docker: Running PHPUnit Cleanly in the Container
AI generated
@test
assert
PHPUnit · Docker · PhpStorm · Xdebug
PhpStorm and Docker:
Running PHPUnit Cleanly in the Container

Anyone developing PHP inside Docker containers but still wanting to run tests directly from the IDE faces a configuration challenge: PhpStorm needs to know which PHP interpreter lives in the container, how Xdebug can be reached, and how paths are mapped between host and container. This article shows the full path.

12 min read Remote interpreter · Run configurations · Coverage · Xdebug PHPUnit 11 · PHP 8.4 · Docker Compose

1. Why not run PHPUnit locally?

The obvious solution for running PHPUnit tests sounds simple: install PHP locally, call vendor/bin/phpunit, done. In practice, though, this path creates a classic environment drift. The local PHP version diverges from the one in the container, database connections only work inside the container network, and environment variables such as APP_ENV, DB_HOST or API keys are set differently in the container than on the developer laptop. Tests that pass locally fail in the CI pipeline, or the other way around.

The clean solution is the remote interpreter in PhpStorm: PhpStorm communicates with the PHP process in the container directly over Docker or SSH. All tests run in exactly the same environment as in production, with the same PHP extensions, the same php.ini and the same environment variables. The developer still sees the result directly in the IDE, including error navigation, a test tree and coverage highlighting.

For projects with several developers this matters even more: a shared docker-compose.yml and a committed .idea/ configuration ensure that everyone uses the same setup. No more "works on my machine" scenarios because a colleague has PHP 8.2 locally while the container runs PHP 8.4.

2. Prerequisites: Docker Compose and PHP container

The first step is a cleanly set up PHP container with Xdebug and the PHP extensions the project needs. The Dockerfile should install xdebug as a PECL extension and configure it via php.ini or a separate xdebug.ini. Important: Xdebug 3 uses different INI keys than Xdebug 2, xdebug.mode replaces the earlier xdebug.remote_enable. For PHPUnit coverage you set xdebug.mode=coverage, for debugging xdebug.mode=debug,develop.

The Docker Compose file must configure the PHP container so that PhpStorm can reach it. On Docker Desktop for macOS the host is reachable via host.docker.internal; on Linux you either need to use the host network or set the IP explicitly. The variable XDEBUG_CONFIG="client_host=host.docker.internal" in the Compose file ensures that Xdebug can establish the connection to the IDE.


# docker-compose.yml: PHP service with Xdebug for PHPUnit
services:
  php:
    build:
      context: .
      dockerfile: .docker/php/Dockerfile
    volumes:
      - .:/var/www/html:cached
      - .docker/php/xdebug.ini:/usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
    environment:
      XDEBUG_MODE: "coverage,debug"
      XDEBUG_CONFIG: "client_host=host.docker.internal client_port=9003 start_with_request=trigger"
      PHP_IDE_CONFIG: "serverName=mironsoft-docker"
    networks:
      - app-network

# .docker/php/xdebug.ini
# xdebug.mode=coverage,debug
# xdebug.client_host=host.docker.internal
# xdebug.client_port=9003
# xdebug.start_with_request=trigger
# xdebug.log_level=0

3. Setting up the remote interpreter in PhpStorm

The remote interpreter is set up in PhpStorm under Settings → PHP → CLI Interpreter. There you choose "From Docker, Vagrant, VM, WSL, Remote" and select "Docker Compose". PhpStorm asks for the docker-compose.yml file and the service name, in our case php. After clicking "OK", PhpStorm automatically detects the PHP version and the installed extensions from the container.

Important: the PHP executable path must be correct inside the container. For official PHP images the binary lives under /usr/local/bin/php. After configuration, PhpStorm displays the detected PHP version and the loaded extensions, this is where you check whether xdebug and every extension the project needs actually appear. If an extension is missing, it is missing in the container and must be installed in the Dockerfile.

For Mark Shust Docker setups (the setup we use at Mironsoft), the service name is typically phpfpm and the interpreter lives under /usr/local/bin/php. PhpStorm stores the interpreter in the project file under .idea/php.xml, this file should be committed to the repository so every team member ends up with the same configuration.

4. PHPUnit configuration in the project

The phpunit.xml (or phpunit.xml.dist) in the project root controls how PHPUnit finds tests, which bootstrap file is loaded and which coverage reports are generated. A clean configuration explicitly defines the bootstrap file, the test suites and the source paths for coverage. In Magento projects the bootstrap is typically an autoloader from dev/tests/unit/framework/bootstrap.php.

For projects outside Magento, the Composer autoloader is usually enough as the bootstrap: <bootstrap>vendor/autoload.php</bootstrap>. The <source> directive in PHPUnit 11 replaces the earlier <whitelist>: it specifies which directories are considered during coverage analysis. Only code listed here shows up in coverage reports, undefined code from vendor packages stays out of it.


<?xml version="1.0" encoding="UTF-8"?>
<!-- phpunit.xml.dist: Project root, committed to repository -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         cacheDirectory=".phpunit.cache"
         executionOrder="depends,defects"
         requireCoverageMetadata="false"
         beStrictAboutCoverageMetadata="false">

  <testsuites>
    <testsuite name="Unit">
      <directory>tests/Unit</directory>
    </testsuite>
    <testsuite name="Integration">
      <directory>tests/Integration</directory>
    </testsuite>
  </testsuites>

  <source>
    <include>
      <directory suffix=".php">src</directory>
    </include>
    <exclude>
      <directory>src/Migrations</directory>
    </exclude>
  </source>

  <php>
    <env name="APP_ENV" value="testing"/>
    <env name="DB_CONNECTION" value="sqlite"/>
    <env name="DB_DATABASE" value=":memory:"/>
  </php>
</phpunit>

5. Creating a run configuration in PhpStorm

With the remote interpreter in place, you now create a new run configuration under Run → Edit Configurations → + → PHPUnit. As the interpreter you pick the Docker interpreter you just set up. For "Test scope" you can select the entire test suite (phpunit.xml), a single directory, or a single class. PhpStorm reads phpunit.xml automatically from the project and offers the configured test suites for selection.

The run configuration can be started directly via the green arrow button in the toolbar or with Ctrl+R (macOS: Cmd+R). PhpStorm then starts a container (or connects to the running one) and executes PHPUnit inside it. The result appears in the "Run" window with a test tree, green tests, red failures with stack traces, and the ability to jump directly to the failing line.

For frequently used setups it is worth sharing the run configuration: Run → Edit Configurations → check "Share through VCS". PhpStorm then stores the configuration under .idea/runConfigurations/PHPUnit.xml and every team member can use it immediately, without rebuilding the configuration by hand.

6. Enabling Xdebug in the container for PHPUnit

Xdebug and PHPUnit coverage in the container require careful coordination. Xdebug 3 knows several modes: debug for setting breakpoints and stepping through code, coverage for coverage analysis, and develop for extended error output. For PHPUnit coverage, xdebug.mode=coverage is enough. If you want to debug at the same time, set xdebug.mode=debug,coverage.

A common problem: Xdebug slows PHPUnit down considerably. A test suite that runs in 2 seconds without Xdebug can take 15 to 30 seconds with Xdebug active. The solution: enable Xdebug only for coverage runs. This can be controlled via a separate Docker Compose override file or through the XDEBUG_MODE environment variable. For normal test runs without coverage, set XDEBUG_MODE=off, this way PHPUnit runs at full speed.

7. Displaying code coverage directly in PhpStorm

Code coverage in PhpStorm is one of the most compelling features of the remote interpreter setup. When you start the run configuration with the coverage button (the shield icon next to the play button), PhpStorm runs PHPUnit with --coverage-xml and reads in the result. Covered lines are then highlighted green and uncovered lines red directly in the editor, without having to open a browser or a separate tool.

In the "Coverage" window, PhpStorm shows a tree of all classes with their line and branch coverage percentages. Classes below 80% line coverage stand out immediately. You can jump straight into a class and see exactly which lines were never reached by a test. This integration only works if the remote interpreter is correctly configured and Xdebug has the coverage mode enabled in the container.

8. Path mapping: configuring host vs. container correctly

The most common problem with remote interpreter setups is path mapping. PhpStorm runs on the host, where the code lives under /home/user/project. Inside the container, the same code lives under /var/www/html. Without correct mapping, PhpStorm cannot correctly translate stack traces and coverage data back to host paths, error navigation and coverage highlighting then simply do not work.

The mapping is configured under Settings → PHP → Servers. There you create a server with the name set in PHP_IDE_CONFIG="serverName=mironsoft-docker". Then you define the path mapping: host path on the left (/home/mir/development/mironsoft/src), container path on the right (/var/www/html). With correct mapping, PhpStorm jumps straight to the right line in the host filesystem when a test fails, even though the failure technically occurred inside the container.

Aspect Local PHP interpreter Docker remote interpreter Recommendation
Environment parity Local != CI/production Identical to CI and production Docker remote
Startup speed Instant, no container start Container must be running Local (quick tests only)
Database access Only with a local DB server Container network, no setup Docker remote
Xdebug breakpoints Direct, no mapping Possible with path mapping Docker with correct mapping
Team consistency Everyone has a different PHP version Everyone uses the same container Docker remote

9. Local PHP vs. Docker interpreter compared

The local PHP interpreter has one decisive advantage: it starts instantly. If the container is not running yet, PhpStorm first has to wait for it to boot, several seconds depending on the image. For teams that keep the container running permanently anyway, this makes no difference. For developers who only start the container when needed, it can disrupt the workflow.

The Docker remote interpreter, on the other hand, wins on every aspect relevant to test quality: environment parity, database access and team consistency. A test that runs locally against SQLite but against MySQL in the container can hide bugs that only surface in CI. Path mapping is a one-time setup and not a recurring problem afterward, PhpStorm stores it in the project configuration.

Mironsoft

PHPUnit setup, Docker integration and CI/CD for PHP projects

PHPUnit in the container, cleanly configured and CI-ready?

We set up PHPUnit for your Docker project: remote interpreter, Xdebug coverage, run configurations and CI integration, so tests run identically locally and in the pipeline.

Docker setup

PHP container with Xdebug 3, correct INI configuration and Compose integration

PhpStorm configuration

Remote interpreter, path mapping, run configurations and coverage display

CI integration

Running the same tests in GitLab CI and GitHub Actions, with no adjustments

10. Summary

Running PHPUnit in a Docker container with PhpStorm requires careful one-time configuration, but it pays off immediately. The remote interpreter ensures tests run in exactly the same environment as CI and production. Xdebug 3 with xdebug.mode=coverage delivers coverage data that PhpStorm visualizes directly in the editor. Path mapping between host and container makes error navigation and stack trace links work correctly.

The key steps: set up a PHP container with Xdebug in the Dockerfile, configure the remote interpreter in PhpStorm under Settings → PHP, create a server with path mapping, give phpunit.xml correct source paths, and create a run configuration. After this one-time setup, you start PHPUnit with a single click directly from PhpStorm, and get the result with a test tree, coverage highlighting and direct error navigation into the container code.

PHPUnit in the Docker container, the essentials at a glance

Remote interpreter

Settings → PHP → CLI Interpreter → Docker Compose. Give the correct service name and PHP binary. Stored in .idea/php.xml.

Xdebug mode

xdebug.mode=coverage for coverage reports. XDEBUG_MODE=off for fast test runs without coverage overhead.

Path mapping

Settings → PHP → Servers: hostname mapping with serverName from PHP_IDE_CONFIG. Enter host path and container path correctly.

phpunit.xml

Define <source> for coverage paths, the bootstrap file, test suites and environment variables. Commit to the repository.

11. FAQ: PHPUnit in the Docker Container with PhpStorm

1How do I set up a Docker remote interpreter in PhpStorm?
Settings → PHP → CLI Interpreter → + → Docker Compose. Provide docker-compose.yml and the service name. PhpStorm detects the PHP version and extensions automatically.
2Why does PHPUnit fail in the container but pass locally?
Missing extensions in the container, a different PHP version, environment variables not set, or wrong paths in phpunit.xml. Check the Xdebug mode and the INI.
3Which Xdebug mode is correct for PHPUnit coverage?
xdebug.mode=coverage. For normal runs without coverage: XDEBUG_MODE=off, this prevents the performance hit from Xdebug.
4What is PHP_IDE_CONFIG and why do I need it?
Tells Xdebug which server configuration from PhpStorm is used. PhpStorm looks for the entry under Settings → PHP → Servers for the path mapping.
5How do I configure the path mapping?
Settings → PHP → Servers → +. The server name must match PHP_IDE_CONFIG. Enter the host path on the left, the container path on the right.
6Can I share the PhpStorm configuration with the team?
Commit .idea/php.xml and .idea/runConfigurations/ to the repository. Enable "Share through VCS" on run configurations.
7How do I prevent Xdebug from slowing down all tests?
Set XDEBUG_MODE=off as the default. Switch to coverage only for coverage runs. In PhpStorm choose between "Run" and "Run with Coverage".
8How do I run a single test directly from the editor?
Click the play button next to the @test annotation, or place the cursor in the test method and press Ctrl+Shift+R (Cmd+Shift+R on macOS).
9Does the Docker container need to be running before I start PHPUnit?
Yes. Recommendation: keep docker compose up -d running permanently in the background. PhpStorm can start the container if needed, but that extends the test start.
10Why does PhpStorm show no coverage markers?
Xdebug is not in coverage mode, the path mapping is wrong, or <source> in phpunit.xml does not cover the file. Check all three points in order.