Contenerizar una aplicación PHP con Docker garantiza que el entorno de desarrollo sea idéntico al de producción y elimina el clásico «en mi máquina funciona». Con php-fpm como motor de PHP, Nginx como servidor web y docker-compose para orquestar los servicios, tienes un stack moderno listo para escalar.
Estructura del proyecto
mi-app/
??? docker/
? ??? nginx/
? ? ??? default.conf
? ??? php/
? ??? Dockerfile
??? src/
? ??? index.php
??? docker-compose.yml
??? composer.json
Dockerfile para php:8.3-fpm
# docker/php/Dockerfile
FROM php:8.3-fpm
# Dependencias del sistema necesarias para las extensiones PHP
RUN apt-get update && apt-get install -y
libpng-dev
libjpeg-dev
libfreetype6-dev
libzip-dev
libonig-dev
libxml2-dev
git
unzip
&& rm -rf /var/lib/apt/lists/*
# Extensiones PHP
RUN docker-php-ext-configure gd
--with-freetype
--with-jpeg
&& docker-php-ext-install
pdo_mysql
mbstring
gd
zip
bcmath
opcache
xml
# Instalar Composer
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
# Directorio de trabajo
WORKDIR /var/www/html
# Copiar ficheros del proyecto
COPY . .
# Instalar dependencias de Composer
RUN composer install --no-dev --optimize-autoloader
# Permisos
RUN chown -R www-data:www-data /var/www/html/storage
&& chmod -R 775 /var/www/html/storage
EXPOSE 9000
CMD ["php-fpm"]
Configuración de Nginx como proxy
# docker/nginx/default.conf
server {
listen 80;
server_name _;
root /var/www/html/public;
index index.php;
# Servir ficheros estáticos directamente
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# Pasar PHP a php-fpm
location ~ .php$ {
fastcgi_pass php:9000; # nombre del servicio en docker-compose
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_read_timeout 300;
}
# Negar acceso a ficheros ocultos
location ~ /. {
deny all;
}
}
docker-compose.yml con MySQL 8
version: '3.9'
services:
php:
build:
context: .
dockerfile: docker/php/Dockerfile
volumes:
- .:/var/www/html
environment:
APP_ENV: ${APP_ENV:-development}
DB_HOST: db
DB_PORT: 3306
DB_NAME: ${DB_NAME:-mi_app}
DB_USER: ${DB_USER:-app}
DB_PASS: ${DB_PASS:-secret}
depends_on:
db:
condition: service_healthy
networks:
- app-net
nginx:
image: nginx:1.25-alpine
ports:
- "80:80"
- "443:443"
volumes:
- .:/var/www/html
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- php
networks:
- app-net
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: rootsecret
MYSQL_DATABASE: ${DB_NAME:-mi_app}
MYSQL_USER: ${DB_USER:-app}
MYSQL_PASSWORD: ${DB_PASS:-secret}
volumes:
- db-data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
timeout: 5s
retries: 10
networks:
- app-net
networks:
app-net:
volumes:
db-data:
Comandos habituales
# Construir y levantar en background
docker compose up -d --build
# Ver logs en tiempo real
docker compose logs -f php
# Ejecutar comandos en el contenedor PHP
docker compose exec php php artisan migrate
docker compose exec php composer install
# Abrir una shell en el contenedor
docker compose exec php bash
# Detener y eliminar contenedores
docker compose down
# Eliminar también los volúmenes (cuidado: borra la BD)
docker compose down -v
Dockerfile para desarrollo (hot-reload)
# docker/php/Dockerfile.dev
FROM php:8.3-fpm
# Instalar extensiones de desarrollo adicionales
RUN pecl install xdebug
&& docker-php-ext-enable xdebug
COPY docker/php/xdebug.ini /usr/local/etc/php/conf.d/xdebug.ini
# xdebug.ini
# xdebug.mode=debug
# xdebug.start_with_request=yes
# xdebug.client_host=host.docker.internal
# xdebug.client_port=9003
Buenas prácticas
- Usa imágenes
alpinedonde puedas (php:8.3-fpm-alpine) para reducir el tamaño de la imagen. - Separa el Dockerfile de desarrollo (con Xdebug, Composer) del de producción (sin herramientas de dev).
- Usa
healthchecken la BD para que el contenedor PHP no arranque antes de que MySQL esté listo. - No copies el directorio
vendor/ni.enven la imagen: usa.dockerignore. - En producción, construye la imagen con
composer install --no-dev --optimize-autoloadery copia solo los ficheros necesarios.
