PHP con Docker. Introducción
Tras realizar el curso de postgrado Curso de Experto Desarrollo Ágil con Java, React y Docker de la Universitat Jaume I, hemos empezado a aplicar todas las ventajas que ofrece el desarrollo y despliegue de aplicaciones PHP con Docker. No voy a explicar todas las funcionalidades que ofrece Docker para centrarme en su aplicación en proyectos PHP. Además, para que no se alarguen demasiado el post, lo voy a dividir en tres partes, desarrollo, este post, test y despliegue.
Estado actual de PHP. La jungla.
En el estado actual de PHP, Docker viene como anillo al dedo. Como sabréis, con docker podemos hacer que la aplicación se ejecute en un contenedor con lo mínimo que necesite esta para ejecutarse. En el caso de PHP, el interprete del mismo, apache o nginx, algunas librerías como gd… Con esto, conseguimos que un desarrollador pueda ejecutarla y desarrollar la misma solamente levantando el contenedor.
Esto es una alegría para cualquier desarrollador de PHP, que ahora muy posiblemente tiene que convivir con, aparte de las distintas librerías que pueda necesitar cada aplicación por separado, todas las versiones que actualmente están manteniéndose en este lenguaje de desarrollo web. PHP 5.6, 7.0, 7.1, 7.2 7.3… E incluso, igual tiene algunas aplicaciones legacy con 5.3.
La aplicación
Para esta explicación he elegido una aplicación desarrollada con el framework Laravel 5.6, usando las migrations de la base de datos que nos permitirán, levantar una base de datos que conectaremos con nuestro contenedor para desarrollo, y otra in memory usando el motor sqlite, para ejecutar los tests en una base de datos temporal.
Docker Compose. Configuración para desarrollo
Como os he explicado en el apartado anterior, como muchas aplicaciones PHP, esta necesita una base de datos para ejecutarse, por lo que usamos docker compose para levantar 2 servicios e interconectarlos entre si bajo la misma red, la aplicación, y la base de datos.
Dockerfile
FROM php:7.2-apache
RUN apt-get update && apt-get install -y libmcrypt-dev git \
mysql-client zip unzip libmagickwand-dev --no-install-recommends \
&& pecl install imagick \
&& pecl install mcrypt-1.0.1 \
&& pecl install xdebug \
&& docker-php-ext-enable imagick \
&& docker-php-ext-enable mcrypt \
&& docker-php-ext-enable xdebug \
&& docker-php-ext-install pdo_mysql \
&& php -r "readfile('http://getcomposer.org/installer');" | php -- --install-dir=/usr/bin/ --filename=composer \
&& a2enmod rewrite \
&& service apache2 restart
WORKDIR /var/www
Como podréis ver, estamos partiendo de la imagen php:7.2-apache, instalamos todas las librerias que necesitamos, el gestor de paquetes PHP composer y finalmente habilitamos el módulo rewrite de apache. Este es el Dockerfile del servicio donde se ejecutará la aplicación.
docker-compose.yml
volumes:
my-datavolume:
driver: local
services:
php:
build:
dockerimage: Dockerfile
context: .
working_dir: /var/www
ports:
- 8081:80
volumes:
- .:/var/www
- ./000-default.conf:/etc/apache2/sites-available/000-default.conf:ro
- ~/.ssh:/root/.ssh
links:
- db
depends_on:
- db
db:
image: mysql
ports:
- 33066:3306
environment:
MYSQL_DATABASE: mydatabase
MYSQL_ROOT_PASSWORD: mypassword
volumes:
- my-datavolume:/var/lib/mysql
healthcheck:
test: mysql --user=root --password=mypassword -e "select 1"
interval: 3s
timeout: 10s
retries: 10
En este fichero de configuración de docker-compose, vemos que levantaremos dos servicios:
- la applicación PHP, que depende de la base de datos, porque la aplicación no funciona si no puede conectar de la misma, y usa el Dockerfile anteriormente explicado para crear el contenedor donde se ejecutará.
- La base de datos, a la que le hemos definido un volumen para que persista entre ejecuciones Como detalle, para asegurar que la base de datos ya está levantada, definimos un healthcheck que cada 3 segundos intentará una consulta simple en la misma. Usamos en el contenedor el fichero de configuración de apache, 000-default.conf, que tenemos en nuestro proyecto.
<VirtualHost *:80>
ServerAdmin webmaster@localhost
DocumentRoot /var/www/public
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
<Directory /var/www/public>
Options Indexes
FollowSymLinks
AllowOverride
All Require all granted
</Directory>
</VirtualHost>
Otra peculiaridad es que estamos compartiendo nuestra configuración ssh con el contenedor en la app porque tenemos paquetes composer privados en nuestro repositorio, y necesitamos acceso ssh para bajarlos.
Inicialización
Esta parte es algo común en proyectos PHP laravel, y no está relacionada con Docker, pero es un conjunto de acciones que deberemos realizar al clonar la primera vez el proyecto, por lo que lo hemos agrupado en un script init.sh.
#!/usr/bin/env bash
composer install
if [[ ! -f ./.env ]]; then
cp ./.env.example ./.env
php artisan key:generate
fi
php artisan migrate:fresh --seed
php artisan storage:link
chmod -R 775 storage
- Instalamos las dependencias
- Copiamos la configuración de entorno y generamos la key de la aplicación.
- Ejecutamos las migraciones con el parámetro –seed para que nos llene la base de datos de datos de prueba.
- Creamos el enlace simbólico de la carpeta storage.
- Damos permiso de escritura a apache a la carpeta storage.
Con todo esto, cuando un desarrollador se descarge el proyecto sólo tendrá que ejecutar dos comandos para empezar a trabajar, el segundo sólo la primera vez:
docker-compose up
docker-compose exec php ./init.bash
Test unitarios
Es conveniente ejecutar los test unitarios en nuestros PCs ya que son rápidos de ejecutar y nos permitirá subir código libre de errores al repositorio. Para ello con docker, podemos ejecutarlos como en cualquier proyecto, solamente teniendo en cuenta que en este caso este está dentro de contenedor en un entorno creado con docker-compose, por lo que podemos hacer lo siguiente:
docker-compose exec php vendor/bin/phpunit
Si usamos editores avanzados como PHPStorm, están preparados para ejecutar los test a través de contenedores. Al final ejecutan el mismo comando pero nos facilitan la labor pudiéndolo hacer con un clic.