TechEarl

How to Run a Specific PHP Version in Docker (Without Installing PHP)

Test code against PHP 7.4, 8.1, 8.2, 8.3, or 8.4 without installing anything. Run a one-off script, an interactive REPL, Composer, or a local PHP server, then walk away with a clean machine.

Ishan Karunaratne⏱️ 8 min readUpdated
Share thisCopied
Run PHP 7.4, 8.1, 8.2, 8.3, or 8.4 in Docker without installing PHP. One-off scripts, REPL, Composer, and a local PHP server. With persistent vendor/ caching.

Need PHP 7.4 to test legacy code, or PHP 8.4 to try the latest features, but you don't want to install PHP on your machine — or you have one PHP already and don't want a second? Docker is the cleanest path. The official php image ships every supported version, you pick the one you need at docker run time, and when you're done the container is gone.

How do I run PHP without installing it?

For a one-off script:

bash
docker run --rm -v "$(pwd):/app" -w /app php:8.4-cli php script.php

For an interactive REPL:

bash
docker run --rm -it php:8.4-cli php -a

For a local PHP web server on port 8080 serving the current directory:

bash
docker run --rm -p 8080:8080 -v "$(pwd):/app" -w /app php:8.4-cli php -S 0.0.0.0:8080

Swap 8.4-cli for 7.4-cli, 8.1-cli, 8.2-cli, 8.3-cli to test other versions. Nothing is installed on your host; the container disappears the moment the command exits.

Try it with your own values

Set the PHP version and host port. The commands update as you type.

Jump to:

Pick the right image tag

The official php image comes in two main variants:

  • php:VERSION-cli — PHP plus the CLI. No web server. Best for running scripts, Composer, and PHP's built-in php -S server.
  • php:VERSION-apache — PHP + Apache + mod_php. For serving a web app.
  • php:VERSION-fpm — PHP-FPM. Pair with a separate Nginx container.

Variants for each: -cli, -apache, -fpm, -alpine, -cli-alpine, etc. The Alpine variants are smaller (~30 MB vs ~150 MB) but use musl libc, which occasionally trips up extensions; default to the regular Debian-based images unless image size matters.

Common tags in 2026:

TagWhat you get
php:8.4-cliPHP 8.4 (current GA), CLI only
php:8.3-cliPHP 8.3, CLI only
php:8.2-cliPHP 8.2, still supported (security fixes only)
php:8.1-cliPHP 8.1, EOL late 2025 — use only for legacy testing
php:7.4-cliPHP 7.4, long EOL — explicitly opt-in for legacy work
php:8.4-apachePHP 8.4 + Apache for serving an app
php:8.4-cli-alpineSmaller Alpine variant

Run a script

bash
docker run --rm -v "$(pwd):/app" -w /app php::php_version php script.php

-v "$(pwd):/app" bind-mounts your current directory into /app inside the container; -w /app makes it the working directory. The container has access to your files, runs the script, and exits.

bash
With argumentsdocker run --rm -v "$(pwd):/app" -w /app php::php_version php script.php arg1 arg2Run as your user so created files aren't owned by rootdocker run --rm -u "$(id -u):$(id -g)" -v "$(pwd):/app" -w /app php::php_version php script.php

Interactive REPL

bash
docker run --rm -it php::php_version php -a

That opens PHP's interactive mode. Useful for testing snippets without writing a file. Ctrl-D to exit.

For a slightly nicer REPL with autocomplete, psysh:

bash
docker run --rm -it -v "$(pwd):/app" -w /app php::php_version sh -c 'composer global require psy/psysh && ~/.composer/vendor/bin/psysh'

That's a heavier setup; the plain php -a is what most people want.

Run Composer without installing it

The Composer team publishes an image that bundles Composer with PHP:

bash
docker run --rm -v "$(pwd):/app" -w /app composer:latest install

docker run --rm -v "$(pwd):/app" -w /app composer:latest require monolog/monolog

docker run --rm -v "$(pwd):/app" -w /app composer:latest update

The composer image is itself based on the PHP image, so it picks up the PHP version from the tag (composer:2-php8.4, composer:2-php8.3, etc.).

To run Composer with one PHP version but actually run the resulting app against another (rare, useful for legacy migrations):

bash
docker run --rm -v "$(pwd):/app" -w /app composer:2-php8.1 install
docker run --rm -v "$(pwd):/app" -w /app php:8.4-cli php artisan migrate

PHP's built-in web server, in Docker

PHP ships a small built-in web server perfect for development:

bash
docker run --rm -p :host_port:8080 -v "$(pwd):/app" -w /app php::php_version php -S 0.0.0.0:8080

Visit http://localhost::host_port. The server serves files from the bind-mounted directory; edit a file on the host and reload to see the change.

Two notes:

  • -S 0.0.0.0:8080 not -S localhost:8080. Inside the container, localhost is the container's loopback, which the host can't reach. 0.0.0.0 listens on all interfaces, which is what you want when binding to a host port.
  • For Laravel/Symfony, use the framework's CLI server instead: php artisan serve --host=0.0.0.0 or symfony serve. Same idea, framework-aware routing.

Caching vendor/ between runs

If you're running Composer repeatedly, you don't want to re-download every package each time. Mount a named volume to cache Composer's global cache:

bash
docker run --rm \
  -v "$(pwd):/app" -w /app \
  -v composer-cache:/tmp \
  -e COMPOSER_CACHE_DIR=/tmp \
  composer:latest install

composer-cache is a named volume that persists across runs. The first install downloads everything; the second pulls from the cache and is fast.

For longer-lived development, you may prefer a Compose setup so the cache and the working directory are stable. Recipe in How to Dockerize a PHP / Laravel App.

Practical usage: testing one project against many PHP versions

Quick parallel-version sanity check before a PHP upgrade:

bash
for v in 7.4 8.1 8.2 8.3 8.4; do
  echo "=== PHP $v ==="
  docker run --rm -v "$(pwd):/app" -w /app "php:${v}-cli" php -l src/Lib.php
done

That runs the linter (php -l) on the same file against five PHP versions in turn. Replace with php tests/all.php or php vendor/bin/phpunit for a richer check. Each run is an isolated container; nothing leaks between them.

The Laravel community uses this pattern heavily for compatibility matrices, and it's how I check before bumping a project's PHP requirement.

Common pitfalls

  • -S localhost:8080 inside the container. Container's loopback is not your host's loopback. Bind to 0.0.0.0:8080 (and publish that port with -p).
  • Composer dependencies failing on Alpine. Some PHP extensions need musl-compatible libs that the Alpine image lacks. Switch to the Debian-based php:VERSION-cli if extensions fail mysteriously.
  • Files created by the container are owned by root. Add -u "$(id -u):$(id -g)" to run as your user, or sudo chown -R you:you . after the fact.
  • Forgetting -w /app. Your files are mounted at /app but the script can't find them because the container's working directory is somewhere else. Always pair -v "$(pwd):/app" with -w /app.
  • Mounting the project read-write when you only need read. For test runs, :ro on the mount prevents the container from writing anywhere on the host: -v "$(pwd):/app:ro".

FAQ

Yes. Each docker run is an isolated container; nothing on your host changes. docker run --rm php:7.4-cli php -v and docker run --rm php:8.4-cli php -v work in parallel without interfering. This is the main reason to use Docker for PHP — you can have every supported version available simultaneously, without juggling update-alternatives or phpenv.

Some, not all. The base image ships with the core extensions; less common ones (GD, intl, soap, gmp, etc.) need to be installed via the docker-php-ext-install helper, typically in a Dockerfile. For one-off CLI use that doesn't need them, the base image is enough. For a real app, build your own image with the extensions you need.

Three usual suspects: a missing PHP extension (the local install has it, the container doesn't), a different PHP version (check php -v in both), or a path that exists on the host but not inside the container's /app mount. Run docker run --rm php:VERSION-cli php -m to list the modules in the image.

There's an official composer image on Docker Hub. Tags like composer:2, composer:2-php8.4, composer:2-php8.3 let you pick the PHP version Composer runs against. Use it like any CLI tool: docker run --rm -v "$(pwd):/app" -w /app composer:2-php8.4 install.

The same pattern. docker run --rm -v "$(pwd):/app" -w /app php:8.4-cli php artisan migrate for Laravel; ... php bin/console doctrine:migrations:migrate for Symfony. For a longer-lived setup with a database container and a working PHP-FPM + Nginx stack, see How to Dockerize a PHP / Laravel App.

Sources

Authoritative references this article was fact-checked against.

TagsDockerPHPPHP 8.4ComposerDevelopmentDevOps

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Software Systems Architect · Senior Software Engineer · Engineering Leadership

Software systems architect and senior software engineer with more than two decades designing, building, and running production software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Now a CTO, though what I write here is drawn from the full arc of that work, across architecture, engineering, and operations, not any single job.

Keep reading

Related posts