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:
docker run --rm -v "$(pwd):/app" -w /app php:8.4-cli php script.phpFor an interactive REPL:
docker run --rm -it php:8.4-cli php -aFor a local PHP web server on port 8080 serving the current directory:
docker run --rm -p 8080:8080 -v "$(pwd):/app" -w /app php:8.4-cli php -S 0.0.0.0:8080Swap 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.
Set the PHP version and host port. The commands update as you type.
Jump to:
- Pick the right image tag
- Run a script
- Interactive REPL
- Run Composer without installing it
- PHP's built-in web server, in Docker
- Caching vendor/ between runs
- Practical usage: testing one project against many PHP versions
- Common pitfalls
- FAQ
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-inphp -Sserver.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:
| Tag | What you get |
|---|---|
php:8.4-cli | PHP 8.4 (current GA), CLI only |
php:8.3-cli | PHP 8.3, CLI only |
php:8.2-cli | PHP 8.2, still supported (security fixes only) |
php:8.1-cli | PHP 8.1, EOL late 2025 — use only for legacy testing |
php:7.4-cli | PHP 7.4, long EOL — explicitly opt-in for legacy work |
php:8.4-apache | PHP 8.4 + Apache for serving an app |
php:8.4-cli-alpine | Smaller Alpine variant |
Run a script
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.
# 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.phpInteractive REPL
docker run --rm -it php::php_version php -aThat 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:
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:
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 updateThe 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):
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 migratePHP's built-in web server, in Docker
PHP ships a small built-in web server perfect for development:
docker run --rm -p :host_port:8080 -v "$(pwd):/app" -w /app php::php_version php -S 0.0.0.0:8080Visit 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:8080not-S localhost:8080. Inside the container,localhostis the container's loopback, which the host can't reach.0.0.0.0listens 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.0orsymfony 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:
docker run --rm \
-v "$(pwd):/app" -w /app \
-v composer-cache:/tmp \
-e COMPOSER_CACHE_DIR=/tmp \
composer:latest installcomposer-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:
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
doneThat 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:8080inside the container. Container's loopback is not your host's loopback. Bind to0.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-cliif extensions fail mysteriously. - Files created by the container are owned by root. Add
-u "$(id -u):$(id -g)"to run as your user, orsudo chown -R you:you .after the fact. - Forgetting
-w /app. Your files are mounted at/appbut 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,
:roon 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.
- Official PHP image — Docker Hubhub.docker.com
- PHP supported versionsphp.net

