DDEV works great day to day, but what if you need a custom configuration DDEV does not support? Or you want to understand what is happening under the hood? I show how to build a PHP environment from scratch with pure Docker and docker-compose – nginx, PHP-FPM, MySQL and Redis with no intermediate tools.
Project structure
project/
docker/
nginx/
default.conf
php/
Dockerfile
php.ini
src/ <- application code
docker-compose.yml
Dockerfile for PHP-FPM
FROM php:7.4-fpm
RUN apt-get update && apt-get install -y \
libpng-dev libjpeg-dev libfreetype6-dev libzip-dev libicu-dev \
libxslt-dev libonig-dev git unzip \
&& rm -rf /var/lib/apt/lists/*
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) \
gd pdo_mysql zip intl xsl soap bcmath sockets opcache
RUN pecl install xdebug-2.9.8 && docker-php-ext-enable xdebug
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html
RUN usermod -u 1000 www-data
USER www-data
PHP configuration – php.ini
[PHP] memory_limit = 2G max_execution_time = 600 upload_max_filesize = 64M post_max_size = 64M date.timezone = Europe/Warsaw [opcache] opcache.enable = 1 opcache.memory_consumption = 256 opcache.max_accelerated_files = 60000 opcache.validate_timestamps = 1 [xdebug] xdebug.mode = debug xdebug.client_host = host.docker.internal xdebug.client_port = 9003 xdebug.start_with_request = no xdebug.idekey = PHPSTORM
nginx configuration
upstream fastcgi_backend {
server php:9000;
}
server {
listen 80;
server_name localhost;
root /var/www/html/pub;
index index.php;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ ^/index\.php$ {
fastcgi_pass fastcgi_backend;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_param MAGE_MODE developer;
}
location ~ /\. {
deny all;
}
}
docker-compose.yml – putting it all together
version: '3.8'
services:
nginx:
image: nginx:1.18-alpine
ports:
- "80:80"
volumes:
- ./src:/var/www/html
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
depends_on: [php]
php:
build:
context: .
dockerfile: docker/php/Dockerfile
volumes:
- ./src:/var/www/html
- ./docker/php/php.ini:/usr/local/etc/php/conf.d/custom.ini
environment:
- XDEBUG_MODE=off
depends_on: [db, redis]
db:
image: mariadb:10.4
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: magento
MYSQL_USER: magento
MYSQL_PASSWORD: magento
volumes:
- db_data:/var/lib/mysql
ports:
- "3306:3306"
redis:
image: redis:6-alpine
ports:
- "6379:6379"
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:7.6.2
environment:
- discovery.type=single-node
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
ports:
- "9200:9200"
volumes:
- es_data:/usr/share/elasticsearch/data
volumes:
db_data:
es_data:
Basic commands
# Build images and start docker-compose up -d --build # Enter the PHP container docker-compose exec php bash # Run a Magento command docker-compose exec php bin/magento setup:upgrade # Enable Xdebug for a session docker-compose exec php bash -c "XDEBUG_MODE=debug php bin/magento ..." # Nginx logs docker-compose logs -f nginx # Stop everything docker-compose down # Stop and remove volumes (clean reset) docker-compose down -v
Xdebug 3.x and PHPStorm
Xdebug 3.x renamed configuration directives from 2.x. Instead of remote_enable we now have xdebug.mode. In PHPStorm set up a PHP server with a path mapping between your local ./src directory and /var/www/html inside the container. Without this mapping breakpoints will not hit the right lines.
When to use pure Docker instead of DDEV?
Pure Docker makes sense when: you need custom service versions DDEV does not support, you are building a CI/CD environment that should be identical to local, you need full control over container networking, or the project has specific infrastructure requirements (e.g. custom reverse proxy, multiple domains on one stack).
Summary
Pure Docker requires more configuration work than DDEV, but gives full control and deeper understanding of the environment. It is worth going through this process at least once – then the DDEV abstraction stops being a black box and becomes a conscious convenience choice.
