Skip to main content

Buenas prácticas para crear un Dockerfile listo para producción para aplicaciones PHP

Escrito por
Headshot of James Walker

James Walker

blog feature multi namespace k8s controller

22 de agosto de 2023

0 minutos de lectura

Docker es una plataforma de contenedorización que agrupa tu código, sus dependencias y el entorno de ejecución en unidades independientes que funcionan de manera idéntica en distintos entornos.

Dockerizar una aplicación PHP simplifica su implementación al empaquetar el entorno de ejecución de PHP, un servidor web, tu código fuente y las dependencias de Composer en un contenedor. Empezar a usar Docker es fácil. Sin embargo, hay algunas dificultades que debes evitar antes de usarlo de forma segura en producción. Para prevenir problemas, es fundamental elegir una imagen base adecuada, seleccionar el entorno de ejecución de PHP correcto para tu carga de trabajo y reforzar la seguridad de la imagen. En este tutorial, aprenderás a escribir un Dockerfile confiable para aplicaciones PHP.

Cómo crear un Dockerfile listo para producción para PHP

Si estás leyendo esto, probablemente ya escribiste tu aplicación PHP y ahora quieres dockerizarla para implementarla. Estas son diez buenas prácticas para maximizar el rendimiento, la seguridad, la confiabilidad y la facilidad de desarrollo:

1. Usa una imagen base específica

La comunidad de Docker publica una imagen oficial de Docker para todas las versiones compatibles de PHP. Hay varias variantes de imagen disponibles, con distintas combinaciones de sistemas operativos base, versiones del lenguaje e integraciones con servidores web (como Apache o FastCGI Process Manager (FPM)).

Es importante seleccionar la imagen específica que mejor se adapte a tus requisitos. Evita usar etiquetas genéricas, como php:latest, porque podrían exponerte a cambios no deseados que rompan el código y a bibliotecas innecesarias que solo se requieren para un sistema operativo completo. Por ejemplo, la imagen php:apache incluye actualmente PHP 8.2, pero cambiará inmediatamente a PHP 9.0 cuando se lance esa versión. Si seleccionas php:8.2-apache, te aseguras de que tu imagen siempre ejecute PHP 8.2, aunque deje de ser la versión actual:

FROM php:8.2-apache
WORKDIR /var/www/html

COPY index.php .

El mismo principio se aplica a cualquier otra imagen de la que dependas, como composer para instalar dependencias. Selecciona la etiqueta de imagen que coincida con la versión principal de Composer que usas en tu máquina: composer:2 en lugar de composer:latest.

2. Usa variables de entorno para la configuración

Los contenedores Docker están diseñados para no conservar estado. Lo mejor es configurar tu aplicación mediante variables de entorno que estableces al iniciar el contenedor. Esto es preferible a depender de un archivo de configuración, que requeriría configurar siempre almacenamiento persistente para el contenedor mediante un volumen de Docker.

Las variables de entorno se establecen con la marca -e al iniciar un contenedor con docker run:

$ docker run -d -e DATABASE_HOST=172.26.0.2 php-app:latest

Tu código PHP puede obtener el valor de la variable llamando a la función getenv():

// 172.26.0.2
echo getenv("DATABASE_HOST");

Estas son variables de entorno de ejecución que se establecen en contenedores individuales. A veces, quizás quieras establecer una variable en tu Dockerfile para que se proporcione automáticamente a los contenedores. Para hacerlo, usa la instrucción ENV:

FROM php:8.2-apache
WORKDIR /var/www/html

ENV DATABASE_DRIVER=sqlite

COPY index.php .

También puedes definir variables de entorno durante la compilación y hacer referencia a ellas en otras instrucciones del Dockerfile mediante la palabra clave ARG:

FROM php:8.2-apache
WORKDIR /var/www/html

ARG DEFAULT_DATABASE_DRIVER

RUN echo '{"db":"$DEFAULT_DATABASE_DRIVER"}' > config.json
COPY index.php .

Para establecer el valor del argumento, usa la marca --build-arg al ejecutar docker build:

$ docker build --build-arg DEFAULT_DATABASE_DRIVER=sqlite -t php-app:latest .

3. Evalúa PHP FPM frente a Nginx/Apache

FPM es una implementación alternativa de PHP FastCGI para integrar PHP con servidores web, incluidos Apache y Nginx. Una alternativa a FPM es usar el servidor web Apache con mod_php. En comparación con la alternativa de Apache, FPM ofrece una administración avanzada de procesos sin bloqueo, la posibilidad de crear distintos grupos de procesos de trabajo y un manejo de errores más eficaz. El uso de FPM suele proporcionar una mejora significativa del rendimiento en aplicaciones grandes.

La imagen base de Docker de PHP está disponible en variantes FPM y mod_php:

  • php:8.2-fpm: Esta imagen y las etiquetas similares exponen una conexión FPM.

  • php:8.2-apache: Estas imágenes incluyen una instalación de Apache y usan mod_php para procesar las solicitudes PHP.

Si usas Apache y estás conforme con mod_php, selecciona una de las imágenes base :apache. Puedes agregar tu código PHP al directorio /var/www/html y empezar a ejecutarlo de inmediato.

Para usar FPM, necesitarás un contenedor independiente que ejecute un servidor como Nginx. Debes configurarlo como un proxy inverso que reenvía las solicitudes PHP al puerto que expone el proceso FPM en tu contenedor PHP. El puerto predeterminado es 9000.

Usa FPM si quieres obtener el máximo rendimiento y no te preocupa administrar dos contenedores: uno de FPM y otro con un servidor web delante. Las imágenes basadas en Apache son un excelente punto de partida para comenzar rápidamente, sin configurar contenedores adicionales ni ajustar la configuración de los grupos de procesos de trabajo de FPM.

4. Desactiva los registros de depuración y la notificación de errores innecesarios en producción

Las aplicaciones de producción no deben divulgar públicamente información que pueda ser útil para un atacante. Desactiva la configuración display_errors y display_startup_errors de PHP para evitar que se muestren excepciones no controladas en tu sitio web.

Para cambiar estos valores, modifica el archivo php.ini dentro de tu contenedor.

Empieza por crear el archivo modificado:

display_errors = Off
display_startup_errors = Off

Luego, ajusta tu Dockerfile para copiar php.ini en la ubicación correcta del sistema de archivos del contenedor:

FROM php:8.2-apache
WORKDIR /var/www/html

COPY php.ini /usr/local/etc/php/php.ini
COPY index.php .

Quizás debas modificar el nivel de notificación de errores de PHP para usarlo en producción. Los errores de bajo nivel, como los avisos y las advertencias de funciones obsoletas, pueden llenar rápidamente los registros con información que no permite tomar medidas. Puedes establecer este valor dinámicamente al inicio del script según una variable de entorno:

if (getenv("IS_PRODUCTION")) {
    // Disable error reports which are not an immediate issue
    error_reporting(E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED);
}

5. Ejecuta tu contenedor PHP como usuario no root

De forma predeterminada, Docker ejecuta los contenedores como root. El proceso que se ejecuta dentro del contenedor tiene los mismos privilegios que el usuario root en tu host. Si alguien vulnera el contenedor y escapa de su aislamiento, los procesos maliciosos pueden ejecutar comandos arbitrarios en tu host.

Ejecutar tu contenedor como un usuario dedicado que no sea root ayuda a mitigar estos riesgos. Usa la instrucción USER en tu Dockerfile para cambiar a otro usuario a partir de ese punto:

FROM php:8.2-apache
WORKDIR /var/www/html
USER appuser

COPY index.php .

6. Configura verificaciones de estado para el contenedor PHP

Docker tiene un mecanismo de verificación de estado integrado que detecta cuándo fallan los contenedores. Puedes configurar Docker para que ejecute automáticamente un comando de verificación de estado cada treinta segundos. Si el comando termina con el código 0, se considera que el contenedor está en buen estado. En cambio, si termina con el código 1, significa que la aplicación falló.

Puedes configurar verificaciones de estado para tus aplicaciones mediante la instrucción HEALTHCHECK en tu Dockerfile. En un servicio PHP, puedes usar curl para enviar una solicitud de red a uno de los puntos de conexión de tu aplicación. Si curl termina con 0, significa que recibió una respuesta con un código de estado HTTP del rango 2xx (éxito):

FROM php:8.2-apache
WORKDIR /var/www/html
USER appuser

HEALTHCHECK CMD curl --fail http://localhost/index.php || exit 1
COPY index.php .

El estado de tus contenedores se muestra en el resultado del comando docker ps:

$ docker ps
CONTAINER ID	IMAGE  		COMMAND              		CREATED    	STATUS
335889ed4698	php-demo-app		"docker-php-entrypoi…"   	2 mins ago 	Up 2 mins (healthy)

Cuando falla una verificación de estado, el estado del contenedor aparecerá como unhealthy. Los orquestadores de contenedores, como Kubernetes, usan esta señal para reiniciar o reemplazar automáticamente los contenedores que fallan.

7. Limita la cantidad de puertos que usas

Evita exponer puertos innecesarios en tus contenedores. Solo debes exponer los puertos a los que necesitan acceder procesos externos. Son los puertos a los que se conectarán otros contenedores y usuarios públicos.

En las aplicaciones PHP, suele ser el puerto 80 cuando usas una imagen con un servidor web incluido, como php:8.2-apache. En su lugar, las imágenes basadas en FPM deben exponer el puerto 9000, para que se conecte un contenedor independiente con el servidor web. El puerto FPM no debe estar abierto al público.

Del mismo modo, presta atención a la información que expones mediante las variables de entorno. Elimina las variables que no uses para reducir el riesgo de divulgación. Herramientas como docker inspect y cualquier software malicioso dentro del contenedor siempre pueden acceder a la lista completa de variables que estableciste.

8. Actualiza las dependencias con regularidad

Debes actualizar con regularidad todos los componentes de tu stack para aplicar nuevas correcciones de errores y actualizaciones de seguridad. Esto incluye lo siguiente:

  • Actualizaciones del sistema operativo base de tu imagen de Docker.

  • Actualizaciones de la versión de PHP y de las extensiones que usas.

  • Paquetes de Composer que instalaste como dependencias en tu proyecto.

Esto es fundamental para protegerte de nuevas vulnerabilidades. Debes volver a crear tu imagen con docker build --pull cada vez que la compiles para obtener actualizaciones. Así se descargará la versión más reciente de tu imagen base, incluidos los paquetes actualizados del sistema operativo y las versiones de PHP.

También debes ejecutar composer update periódicamente en tu proyecto para obtener las correcciones más recientes para tus dependencias. Puedes usar herramientas como Snyk para encontrar dependencias desactualizadas o vulnerables y obtener recomendaciones para actualizarlas a una versión corregida. Después de aplicar las actualizaciones de paquetes con Composer, vuelve a crear tu imagen de Docker para que las implementaciones en producción usen las nuevas versiones.

9. Limita las capacidades del contenedor a las mínimas necesarias

Las capacidades del kernel de Linux restringen las acciones potencialmente sensibles que pueden realizar los procesos. Docker ya ejecuta los contenedores con un conjunto sin privilegios de capacidades, pero estas siguen siendo excesivas para las cargas de trabajo PHP habituales. Por ejemplo, la configuración predeterminada permite que los procesos del contenedor cambien los permisos de archivos, eliminen procesos y manipulen los ID de usuario.

Puedes agregar y quitar capacidades del contenedor con las marcas --cap-add y --cap-drop de docker run. Lo más seguro es quitar todas las capacidades con --cap-drop=all y luego volver a agregar las específicas que necesite tu aplicación.

En el siguiente ejemplo, el contenedor solo recibe la capacidad CHOWN. Puede cambiar la propiedad de archivos y carpetas, pero no podrá realizar ninguna otra acción privilegiada:

$ docker run -d --cap-drop=all --cap-add=CHOWN php-app:latest

10. Usa compilaciones en varias etapas para separar la compilación del entorno de ejecución

Las compilaciones en varias etapas son un mecanismo que facilita la escritura y el mantenimiento de canalizaciones de compilación más complejas. Permiten incluir varias imágenes base en el proceso de compilación y, a la vez, mantener un resultado pequeño y preciso.

El patrón de varias etapas es ideal para los Dockerfiles de PHP porque, por lo general, querrás instalar las dependencias de Composer durante la compilación. Las imágenes oficiales de Docker de PHP no incluyen Composer; el proyecto publica sus propias imágenes. Con una compilación en varias etapas, puedes hacer referencia al binario de Composer directamente en tu Dockerfile:

FROM php:8.2-apache
WORKDIR /var/www/html

COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
COPY composer.json .
COPY composer.lock .
RUN composer install --no-dev
RUN rm /usr/bin/composer

COPY index.php .

La imagen composer:2 se descartará cuando finalice la compilación. Este enfoque mantiene una buena separación entre la compilación y el entorno de ejecución sin aumentar el tamaño de la imagen final.

Cómo identificar y corregir vulnerabilidades con Snyk

Usar Docker puede mejorar la seguridad al proporcionar cierto nivel de aislamiento entre tus aplicaciones PHP y sus entornos host. Sin embargo, las imágenes de contenedor suelen contener vulnerabilidades de seguridad debido a paquetes desactualizados del sistema operativo y dependencias de Composer:

Imagen principal del sitio web de Snyk con el título «Amado por desarrolladores, confiable para los equipos de seguridad», una interfaz de análisis de seguridad y una ilustración estilizada de un perro de Snyk

Analizar tu imagen en busca de vulnerabilidades es un paso fundamental antes de implementarla en producción. Puedes usar Snyk para analizar tu imagen de Docker PHP, e identificar y corregir vulnerabilidades. La Snyk Vulnerability Database incluye registros de todos los sistemas operativos y dependencias populares, incluidos los paquetes de PHP publicados en Packagist.

Para empezar a identificar vulnerabilidades con Snyk, crea un archivo PHP sencillo y guárdalo como index.php:

<?php

echo "Hello World";

?>

A continuación, escribe un Dockerfile para tu aplicación:

FROM php:8.2-apache
WORKDIR /var/www/html

COPY index.php

Crea la imagen con Docker:

$ docker build -t php-app:latest .

Luego, ve al sitio web de Snyk y haz clic en Comenzar gratis para crear tu cuenta. Sigue los pasos para configurarla e instala la CLI de Snyk. También necesitarás tener instalado Node.js y npm:

$ npm install -g snyk

Luego, ejecuta el comando snyk auth. Se abrirá una nueva pestaña del navegador donde podrás iniciar sesión en Snyk y autenticar la CLI. Después, vuelve a la terminal.

Para analizar tu imagen, ejecuta el siguiente comando:

$ snyk container test php-app:latest

El análisis puede tardar unos momentos. Luego, los resultados aparecerán en la terminal. Verás una lista de todas las vulnerabilidades detectadas, incluido el paquete afectado, las versiones vulnerables y un enlace para obtener más información:

Informe de terminal que muestra 110 vulnerabilidades en una imagen Docker de PHP Apache, incluidas dos vulnerabilidades críticas y recomendaciones de imágenes base alternativas.

El resumen al final muestra la cantidad total de problemas encontrados y sugiere imágenes base alternativas con menos vulnerabilidades.

Cómo corregir las vulnerabilidades detectadas

Si se encuentran vulnerabilidades, evalúa cada una para determinar si debes tomar medidas. No todas necesariamente representarán un problema para tu caso de uso. Concéntrate primero en resolver los problemas críticos, altos y medios.

A menudo, la forma más eficaz de empezar es reconstruir tu imagen con una versión actualizada de la imagen base. Las imágenes populares de la comunidad, como php:8.2-fpm, se vuelven a publicar periódicamente con nuevas versiones que incluyen correcciones de vulnerabilidades. Después de reconstruir la imagen, repite el análisis para ver cómo afecta el resultado.

El informe podría enumerar muchas vulnerabilidades en componentes de bajo nivel del sistema operativo. Considera usar una imagen base más pequeña y minimalista para reducir la superficie de ataque general. Las variantes de Alpine Linux, como php:fpm-alpine, incluyen muchos menos paquetes, lo que suele significar menos vulnerabilidades.

Al momento de escribir este artículo, la imagen php:8.2-fpm tiene noventa y ocho vulnerabilidades detectadas, por ejemplo:

Salida de terminal que muestra un análisis de una imagen de Docker con PHP 8.2 que detectó 98 vulnerabilidades en las dependencias: 1 crítica, 2 altas, 1 media y 94 bajas.

La variante php:8.2-fpm-alpine solo tiene diez porque incluye muchos menos paquetes del sistema operativo:

Salida de la terminal que muestra una imagen de Docker de PHP 8.2 Alpine con 10 vulnerabilidades en las dependencias, incluidas de gravedad crítica, alta, media y baja.

Es posible que algunas vulnerabilidades no se puedan resolver en tu proyecto. Por ejemplo, los problemas en paquetes del sistema operativo podrían no afectar tu carga de trabajo, o quizá no haya un parche disponible. Puedes descartar una vulnerabilidad de un informe de Snyk pasando su ID al comando snyk ignore. Puedes encontrar el ID en la parte final de la URL de la vulnerabilidad, como SNYK-ALPINE317-CURL-3320725 en https://security.snyk.io/vuln/SNYK-ALPINE317-CURL-3320725:

$ snyk ignore --id=SNYK-ALPINE317-CURL-3320725

Por último, recuerda que la seguridad de los contenedores depende de varios componentes. Necesitas una imagen base segura, una versión actualizada y con soporte del entorno de ejecución de PHP, además de versiones con parches de tus dependencias, los componentes de la cadena de herramientas y el código de la aplicación. Analizar periódicamente tu imagen en busca de vulnerabilidades te permitirá conocer su nivel de seguridad.

Resumen

Después de leer este artículo, deberías tener una imagen de Docker lista para producción con la que ejecutar tu aplicación PHP. Esta guía abordó algunas prácticas recomendadas para escribir tu Dockerfile y usar las imágenes que compilas, como elegir una imagen base específica, usar compilaciones de varias etapas para instalar dependencias y analizar en busca de vulnerabilidades para asegurarte de que tus cargas de trabajo sean aptas para implementarse en producción.

Si quieres más recomendaciones sobre imágenes de Docker y PHP, consulta la documentación de Docker y los demás artículos disponibles en el blog de Snyk.

Snyk es una plataforma de seguridad diseñada para desarrolladores y para cualquier persona responsable de proteger el código. Puede analizar tus imágenes de Docker para detectar vulnerabilidades en tus dependencias, paquetes del sistema operativo y código PHP. Snyk también ofrece un complemento para IDE que realiza análisis estáticos para detectar vulnerabilidades en cuanto aparecen. Regístrate ahora para encontrar y corregir vulnerabilidades automáticamente en tus imágenes de contenedores PHP.

Seguridad de contenedores centrada en los desarrolladores

Snyk encuentra y corrige automáticamente vulnerabilidades en imágenes de contenedores y cargas de trabajo de Kubernetes.

Leer más

feature customer snowflake
Article

Seguridad desde el inicio: presentamos la integración de Snyk Studio con Snowflake Cortex Code

Snyk Studio se integra con Snowflake Cortex Code para analizar código generado por IA, dependencias y contenedores en busca de vulnerabilidades durante el desarrollo.

blog feature pypi spoof
Article

Toda la Snyk AI Security Platform, gratis para quienes mantienen proyectos de código abierto

Quienes mantienen proyectos de código abierto reciben reportes de vulnerabilidades reales sin parar y necesitan ayuda para priorizarlas, corregirlas y publicar soluciones más rápido. El Secure Developer Program de Snyk ofrece a los proyectos que cumplen los requisitos acceso gratis a Snyk AI Security Platform.

feature insights announcement
Blog

Compromiso de la cadena de suministro de node-gyp: un gusano de npm que se propaga solo y se oculta en binding.gyp

Un nuevo gusano de npm abusa de binding.gyp para activar node-gyp durante la instalación y permitir que paquetes maliciosos ejecuten código sin scripts del ciclo de vida. Roba credenciales, persiste en GitHub y se propaga entre mantenedores.