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 KarunaratneIshan Karunaratne⏱️ 7 min readUpdated
Share thisCopied

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 arguments
docker run --rm -v "$(pwd):/app" -w /app php::php_version php script.php arg1 arg2

# Run as your user so created files aren't owned by root
docker 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

Sources

Authoritative references this article was fact-checked against.

TagsDockerPHPPHP 8.4ComposerDevelopmentDevOps

Found this useful? Pass it on.

Copied
Ishan Karunaratne

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts