Docker Compose LAMP Stack: Apache, MySQL, PHP Reference
Docker Compose reference: LAMP Stack (Apache, MySQL, PHP)
TL;DR
- Bottom line: A Docker Compose LAMP stack uses three services --
php:8.3-apachefor PHP+Apache,mysql:8.4for the database, and optionallyphpmyadminfor DB management -- connected via a Docker bridge network with named volumes for data persistence. - Key tool/command:
docker compose up -d --build - Watch out for: Using
localhostas the MySQL host inside PHP containers -- you must use the Docker Compose service name (e.g.,db) instead. - Works with: Docker Engine 24+, Docker Compose V2, PHP 8.0-8.3, MySQL 8.0/8.4/9.x, all major OS (Linux, macOS, Windows with WSL2).
Constraints
- NEVER use
MYSQL_ALLOW_EMPTY_PASSWORD=yesin production -- always set a strong root password or use Docker secrets - NEVER hardcode database passwords in
docker-compose.ymlfor production -- use.envfiles or Docker secrets (_FILEsuffix) - ALWAYS use named volumes for MySQL data persistence -- bind mounts risk permission issues and data loss
- NEVER expose MySQL port 3306 to
0.0.0.0in production -- bind to127.0.0.1or remove the port mapping entirely - ALWAYS use the Docker service name (e.g.,
db) as the MySQL hostname in PHP -- never uselocalhostor127.0.0.1from within containers
Quick Reference
Service Configuration Summary
| Service | Image | Ports | Volumes | Key Env |
|---|---|---|---|---|
| php-apache | php:8.3-apache | 8080:80 | ./src:/var/www/html | DB_HOST=db |
| db (MySQL) | mysql:8.4 | 127.0.0.1:3306:3306 (dev) | mysql_data:/var/lib/mysql | MYSQL_ROOT_PASSWORD, MYSQL_DATABASE, MYSQL_USER, MYSQL_PASSWORD |
| phpmyadmin | phpmyadmin:latest | 8081:80 | -- | PMA_HOST=db, PMA_PORT=3306 |
PHP Extension Install Helpers
| Helper Script | Purpose | Example |
|---|---|---|
docker-php-ext-install | Install bundled PHP extensions | docker-php-ext-install pdo_mysql mysqli |
docker-php-ext-configure | Configure extension before installing | docker-php-ext-configure gd --with-jpeg |
docker-php-ext-enable | Enable a PECL-installed extension | docker-php-ext-enable redis |
MySQL Initialization
| Method | Mount Path | Supported Extensions |
|---|---|---|
| SQL scripts | /docker-entrypoint-initdb.d/ | .sh, .sql, .sql.gz, .sql.bz2, .sql.xz, .sql.zst |
| Custom config | /etc/mysql/conf.d/ | .cnf files |
| Secrets | /run/secrets/<name> | Use MYSQL_ROOT_PASSWORD_FILE env var |
Docker Compose V2 Commands
| Action | Command |
|---|---|
| Start stack | docker compose up -d --build |
| Stop stack | docker compose down |
| Stop + delete volumes | docker compose down -v |
| View logs | docker compose logs -f [service] |
| Shell into container | docker compose exec php-apache bash |
| Rebuild single service | docker compose build php-apache |
Decision Tree
START: What kind of PHP + MySQL Docker setup do you need?
├── Development environment with hot-reload?
│ ├── YES → Use bind mount (./src:/var/www/html) + phpMyAdmin service
│ └── NO ↓
├── Production deployment?
│ ├── YES → Use multi-stage Dockerfile, COPY app code, Docker secrets, no phpMyAdmin
│ └── NO ↓
├── Need PHP-FPM (separate Apache/Nginx)?
│ ├── YES → Use php:8.3-fpm image + separate httpd or nginx container
│ └── NO ↓
├── Need multiple PHP versions side-by-side?
│ ├── YES → Create separate services with different php:X.Y-apache images
│ └── NO ↓
└── DEFAULT → Use php:8.3-apache all-in-one image with MySQL 8.4 LTS
Step-by-Step Guide
1. Create the project directory structure
Set up the directory layout for your LAMP project. [src4]
mkdir -p lamp-project/{src,mysql/init,php}
cd lamp-project
Verify: ls -la lamp-project/ -- all directories should exist.
2. Create the PHP Dockerfile
Build a custom image based on php:8.3-apache with required extensions. [src1]
# php/Dockerfile
FROM php:8.3-apache
RUN apt-get update && apt-get install -y \
libpng-dev libjpeg-dev libfreetype6-dev libzip-dev unzip \
&& rm -rf /var/lib/apt/lists/*
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) pdo_mysql mysqli gd zip opcache
RUN a2enmod rewrite headers expires
COPY php.ini /usr/local/etc/php/conf.d/custom.ini
WORKDIR /var/www/html
RUN chown -R www-data:www-data /var/www/html
Verify: docker compose build php-apache -- should complete without errors.
3. Create the PHP configuration file
Tune PHP settings for development. [src6]
; php/php.ini
display_errors = On
error_reporting = E_ALL
memory_limit = 256M
upload_max_filesize = 64M
post_max_size = 64M
opcache.enable = 1
opcache.validate_timestamps = 1
opcache.revalidate_freq = 0
Verify: docker compose exec php-apache php -i | grep display_errors -- should show On.
4. Create the docker-compose.yml
Define all services, networks, and volumes. [src4] [src5]
services:
php-apache:
build: ./php
ports: ["8080:80"]
volumes: ["./src:/var/www/html"]
depends_on:
db: { condition: service_healthy }
environment:
DB_HOST: db
DB_NAME: ${MYSQL_DATABASE}
networks: [lamp-network]
db:
image: mysql:8.4
ports: ["127.0.0.1:3306:3306"]
volumes:
- mysql_data:/var/lib/mysql
- ./mysql/init:/docker-entrypoint-initdb.d
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
networks: [lamp-network]
phpmyadmin:
image: phpmyadmin:latest
ports: ["8081:80"]
environment:
PMA_HOST: db
PMA_PORT: 3306
depends_on:
db: { condition: service_healthy }
networks: [lamp-network]
networks:
lamp-network:
volumes:
mysql_data:
Verify: docker compose config -- should print resolved config without errors.
5. Create the .env file
Store credentials outside the compose file. [src7]
# .env
MYSQL_ROOT_PASSWORD=change_me_root_2026
MYSQL_DATABASE=lamp_app
MYSQL_USER=lamp_user
MYSQL_PASSWORD=change_me_user_2026
Verify: docker compose config | grep MYSQL_DATABASE -- should show lamp_app.
6. Create initialization SQL and test PHP
Pre-populate the database and verify connectivity. [src2]
-- mysql/init/01-create-tables.sql
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Verify: docker compose exec db mysql -u lamp_user -p lamp_app -e "SHOW TABLES;"
7. Start the stack
Build and launch all containers. [src4]
docker compose up -d --build
Verify: docker compose ps -- all services should show running (healthy). Visit http://localhost:8080.
Code Examples
PHP PDO: Secure database connection
<?php
// Input: Environment variables DB_HOST, DB_NAME, DB_USER, DB_PASS
// Output: PDO connection object or exception
function createDbConnection(): PDO {
$dsn = sprintf('mysql:host=%s;dbname=%s;charset=utf8mb4',
getenv('DB_HOST') ?: 'db',
getenv('DB_NAME') ?: 'lamp_app'
);
return new PDO($dsn, getenv('DB_USER'), getenv('DB_PASS'), [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
}
Docker Compose: Production-hardened configuration
# No phpMyAdmin, no exposed DB port, Docker secrets for passwords
services:
php-apache:
build: ./php
ports: ["80:80", "443:443"]
volumes: ["app_code:/var/www/html:ro"]
restart: always
db:
image: mysql:8.4
volumes: ["mysql_data:/var/lib/mysql"]
environment:
MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db_root_password
secrets: [db_root_password]
restart: always
secrets:
db_root_password:
file: ./secrets/db_root_password.txt
Bash: Health check and restart script
#!/bin/bash
# Input: Running Docker Compose LAMP stack
# Output: Status report + restart if unhealthy
cd /path/to/lamp-project
if ! docker compose ps --status running | grep -q "lamp-php"; then
echo "PHP-Apache is down, restarting..."
docker compose restart php-apache
fi
echo "Stack status:"
docker compose ps
Anti-Patterns
Wrong: Using localhost as MySQL host in PHP containers
// BAD -- 'localhost' refers to the container's own loopback, not the MySQL container
$pdo = new PDO('mysql:host=localhost;dbname=mydb', 'user', 'pass');
// Error: SQLSTATE[HY000] [2002] Connection refused
Correct: Use the Docker Compose service name
// GOOD -- 'db' is the service name in docker-compose.yml
$pdo = new PDO('mysql:host=db;dbname=mydb', 'user', 'pass');
// Docker's internal DNS resolves 'db' to the MySQL container's IP
Wrong: Hardcoding credentials in docker-compose.yml
# BAD -- credentials visible in version control
services:
db:
image: mysql:8.4
environment:
MYSQL_ROOT_PASSWORD: supersecret123
Correct: Use .env file or Docker secrets
# GOOD -- credentials loaded from .env file (gitignored)
services:
db:
image: mysql:8.4
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
Wrong: Using bind mount for MySQL data
# BAD -- bind mount causes permission issues and is not portable
services:
db:
volumes:
- ./mysql_data:/var/lib/mysql
Correct: Use named volumes for database data
# GOOD -- named volumes are managed by Docker, portable and reliable
services:
db:
volumes:
- mysql_data:/var/lib/mysql
volumes:
mysql_data:
Wrong: No health check -- PHP starts before MySQL is ready
# BAD -- depends_on without health check only waits for container start
services:
php-apache:
depends_on:
- db
Correct: Use healthcheck with condition
# GOOD -- PHP waits until MySQL passes health check
services:
php-apache:
depends_on:
db: { condition: service_healthy }
db:
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
retries: 5
start_period: 30s
Common Pitfalls
- MySQL "Connection refused" on first start: MySQL takes 10-30 seconds to initialize. Fix: Add a
healthcheckand usedepends_onwithcondition: service_healthy. [src2] - PHP extensions missing: The base
php:apacheimage has minimal extensions. Fix: Usedocker-php-ext-install pdo_mysql mysqliin your Dockerfile. [src1] - Apache mod_rewrite not working: Default Apache config has
AllowOverride None. Fix: AddRUN a2enmod rewritein Dockerfile and configure.htaccess. [src4] - Data loss on
docker compose down -v: The-vflag deletes named volumes. Fix: Usedocker compose downwithout-vfor normal stops. [src2] - File permission issues with bind mounts: Files inside container owned by
www-data(UID 33). Fix: Setuser: "${UID}:${GID}"in compose or usechownin Dockerfile. [src6] - MySQL init scripts not running: Scripts in
/docker-entrypoint-initdb.d/only run on first start. Fix: Rundocker compose down -vto force re-initialization. [src2] - OPcache serving stale PHP files: OPcache caches compiled PHP. Fix: Set
opcache.validate_timestamps=1andopcache.revalidate_freq=0for development. [src6]
Diagnostic Commands
# Check all container status
docker compose ps
# View real-time logs for all services
docker compose logs -f
# Test MySQL connectivity from PHP container
docker compose exec php-apache php -r "new PDO('mysql:host=db;dbname=lamp_app', 'lamp_user', 'password');"
# Check installed PHP extensions
docker compose exec php-apache php -m
# Check Apache modules
docker compose exec php-apache apache2ctl -M
# Connect to MySQL CLI
docker compose exec db mysql -u root -p
# Inspect network between containers
docker compose exec php-apache ping -c 3 db
# Check disk usage of named volumes
docker system df -v | grep mysql_data
# Rebuild without cache
docker compose build --no-cache php-apache
Version History & Compatibility
| Component | Version | Status | Notes |
|---|---|---|---|
| PHP | 8.3.x | Current (Active) | Latest features, JIT improvements |
| PHP | 8.2.x | Active until Dec 2025 | Stable, widely used |
| PHP | 8.1.x | Security-only until Dec 2025 | Fibers, enums |
| PHP | 8.0.x | EOL (Nov 2023) | Upgrade recommended |
| MySQL | 8.4 LTS | Current LTS | Recommended for stability |
| MySQL | 8.0.x | GA until Apr 2026 | Upgrade to 8.4 recommended |
| MySQL | 9.x | Innovation | Shorter support cycle |
| Docker Compose | V2 | Current | V1 (docker-compose) is deprecated |
| phpMyAdmin | 5.2.x | Current | Supports PHP 8.1-8.3, MySQL 5.5+ |
When to Use / When Not to Use
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Local PHP/MySQL development environment | Production deployment at scale | Kubernetes + managed database |
| Rapid prototyping of PHP web apps | You need Nginx instead of Apache | Docker LEMP stack |
| CI/CD pipeline testing with real MySQL | You need PostgreSQL | Docker Compose with postgres image |
| Team onboarding (consistent dev environment) | Your app is not PHP | Framework-specific Docker setup |
| Legacy PHP app containerization | High-availability clustering needed | Docker Swarm or Kubernetes |
Important Caveats
- Docker Compose V1 (
docker-composewith hyphen) is deprecated -- use V2 (docker composewith space); thedepends_on.conditionsyntax requires V2 - MySQL 8.4+ uses
caching_sha2_passwordas default auth plugin -- older PHP MySQL clients may needmysql_native_password - On macOS and Windows (Docker Desktop), bind mount performance is significantly slower than on Linux -- use named volumes or
cached/delegatedmount options - The
php:8.3-apacheimage is ~400MB (Debian Bookworm) -- for smaller images, considerphp:8.3-alpine(but requires different package managers) - MySQL init scripts in
/docker-entrypoint-initdb.d/run alphabetically -- prefix files with numbers (01-, 02-) to control order - Docker Desktop on Windows requires WSL2 backend for best performance with file-intensive PHP workloads