Local PHP development has a consistent set of friction points: managing multiple PHP versions across projects, configuring extensions, keeping MySQL running and accessible, and ensuring the setup is reproducible when a new team member joins or a fresh machine needs to be configured.
Docker addresses all of these by packaging the application and its dependencies into containers that run identically regardless of the host machine. The PHP version, extensions, Apache configuration, and MySQL version are all defined in version-controlled files. Spinning up the environment is a single command.
This guide builds a working PHP and MySQL development environment using a
Dockerfilefor the PHP service and Docker Compose for multi-service orchestration.What this covers:
Installing Docker and verifying the setup
Writing a
Dockerfilefor PHP with ApacheConfiguring Docker Compose for PHP and MySQL
Starting, stopping, and inspecting containers
Managing credentials with a
.envfileVolume mounts for code changes and database persistence
A
.dockerignorefile and other practical considerations
Step 1: Verify Docker Is Installed
Check that both Docker and Docker Compose are available:
docker --version
docker compose versionIf either is missing, install Docker Desktop from docker.com. Docker Desktop includes both the Docker daemon and the Compose plugin.
Step 2: Create the Dockerfile
In the PHP project root, create a Dockerfile:
# Official PHP image with Apache
FROM php:8.2-apache
# Install PHP extensions for MySQL connectivity
RUN docker-php-ext-install pdo pdo_mysql mysqli
# Enable mod_rewrite for URL routing (required by most PHP frameworks)
RUN a2enmod rewrite
# Set working directory
WORKDIR /var/www/html
# Copy project files into the container
COPY . /var/www/html/This image includes Apache alongside PHP, so no separate web server container is needed for local development. The pdo, pdo_mysql, and mysqli extensions cover both PDO-based and procedural MySQL access patterns.
The COPY instruction comes after WORKDIR. When using volume mounts in development (covered in Step 4), the COPY is less critical because the host files overlay it — but keeping it in the Dockerfile ensures the image is self-contained if built without a volume mount.
Step 3: Create the Docker Compose File
Docker Compose defines the multi-service setup — PHP and MySQL — and their relationships:
services:
app:
build: .
ports:
- "8080:80"
volumes:
- .:/var/www/html
depends_on:
- db
environment:
MYSQL_HOST: db
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
volumes:
- db_data:/var/lib/mysql
volumes:
db_data:What each section does:
The app service builds the PHP container from the Dockerfile in the current directory, maps port 8080 on the host to port 80 in the container, and mounts the project directory so code changes are reflected immediately. The depends_on directive ensures the db service starts before app.
The db service runs MySQL 8.0 with a named volume (db_data) that persists the database between container restarts. Without the named volume, the database is lost every time the container stops.
MYSQL_HOST: db in the app service passes the database hostname to PHP. Inside Docker Compose, services reach each other by their service name, so the PHP application connects to db rather than localhost.
Note: the version field at the top of docker-compose.yml is deprecated in current versions of Docker Compose and can be omitted.
Step 4: Manage Credentials with a .env File
Docker Compose reads a .env file in the same directory automatically. Create one with the database credentials:
MYSQL_ROOT_PASSWORD=rootpassword
MYSQL_DATABASE=myapp
MYSQL_USER=appuser
MYSQL_PASSWORD=apppasswordAdd .env to .gitignore and commit a .env.example with placeholder values instead:
# .env.example
MYSQL_ROOT_PASSWORD=
MYSQL_DATABASE=
MYSQL_USER=
MYSQL_PASSWORD=The docker-compose.yml file already references these as ${VARIABLE_NAME}, so no further changes are needed. The actual values never appear in version control.
Step 5: Add a .dockerignore File
A .dockerignore file prevents unnecessary files from being copied into the container image during the COPY step. This keeps the image smaller and avoids including development artefacts:
.git
.env
.env.*
node_modules
vendor
*.log
*.cacheExcluding vendor is worth noting: if Composer dependencies are managed inside the container rather than on the host, the vendor directory will be generated during the build rather than copied from the host. For local development with volume mounts, the vendor directory on the host is used directly.
Step 6: Start the Environment
Build and start both services:
docker compose up -dThe first run builds the PHP image and pulls the MySQL image. Subsequent runs reuse the cached layers and start much faster.
Visit http://localhost:8080 to confirm the PHP application is running.
Useful commands for managing the environment:
# View logs for all services
docker compose logs -f
# View logs for a specific service
docker compose logs -f app
# Open a shell inside the PHP container
docker exec -it $(docker compose ps -q app) bash
# Stop containers without removing volumes
docker compose stop
# Stop containers and remove them (volumes are preserved)
docker compose down
# Stop containers and remove volumes (deletes the database)
docker compose down -vThe distinction between stop and down matters: stop halts the containers, down removes them. Neither removes named volumes unless -v is passed.
Step 7: Connect PHP to MySQL
Inside PHP, the database host is the service name defined in docker-compose.yml — db — not localhost. A basic PDO connection:
<?php
$host = getenv('MYSQL_HOST') ?: 'db';
$dbname = getenv('MYSQL_DATABASE');
$user = getenv('MYSQL_USER');
$pass = getenv('MYSQL_PASSWORD');
try {
$pdo = new PDO(
"mysql:host=$host;dbname=$dbname;charset=utf8mb4",
$user,
$pass,
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
echo "Connected successfully";
} catch (PDOException $e) {
echo "Connection failed: " . $e->getMessage();
}PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION converts PDO errors into exceptions, which is the recommended setting for development and production.
One common issue: MySQL takes a few seconds to initialize on first start, and PHP may try to connect before MySQL is ready. depends_on in Compose ensures the container starts but does not wait for MySQL to be accepting connections. A retry loop or a healthcheck on the db service handles this properly:
db:
image: mysql:8.0
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
timeout: 5s
retries: 10app:
depends_on:
db:
condition: service_healthyWith the healthcheck, Compose waits for MySQL to pass the health check before starting the app service.
Key Takeaways
A
Dockerfiledefines the PHP environment: the base image, extensions, and Apache configuration. Adocker-compose.ymlorchestrates PHP and MySQL together.The
appservice reaches MySQL using the service namedbas the host, notlocalhost.Named volumes (
db_data) persist the database between container restarts.docker compose down -vremoves them.Volume mounts (
.:/var/www/html) reflect code changes in the container instantly without rebuilding the image.Use a
.envfile for credentials and a.dockerignoreto exclude development artefacts from the image.The
depends_ondirective with a MySQL healthcheck prevents the PHP service from starting before MySQL is ready to accept connections.
Conclusion
A Docker-based PHP development environment solves the version management, extension configuration, and reproducibility problems that make local PHP setup time-consuming. The configuration is small, version-controlled, and portable: anyone with Docker installed can clone the repository and run docker compose up -d to get a working environment.
The setup described here is a solid foundation for local development. For production, the same Dockerfile can be extended with multi-stage builds, smaller base images, and production-specific Apache configuration.
Running into a specific Docker PHP configuration issue? Describe the error in the comments.




