En nuestro día a día es bastante frecuente necesitar un entorno de desarrollo y de producción para trabajar con nuestro lenguaje de desarrollo favorito. Es una tarea tediosa, repetitiva y que gracias a Docker podemos automatizar y replicar con suma facilidad.
Esta publicación se ha diseñado como una guía práctica para crear entornos de desarollo y de producción usando Docker y en el que se se podrá...
- Entender cómo crear imágenes personalizadas de Docker.
- Hacer uso de los multi-stage builds para construir imágenes Docker optimizadas.
- Comprender las diferencias a la hora de crear imágenes de desarrollo y de producción.
Esta formación está orientada principalmente a desarrolladores/as de software que necesiten crear entornos de desarrollo para la crear pruebas de concepto, side projects, etc.
La metodología de esta guía se basa en:
- Teoría y conceptos, con explicaciones claras y concisas de los puntos clave.
- Ejemplo de implementación.
Para aprovechar al máximo este recurso se necesita:
- Un equipo con acceso a Internet.
- Privilegios de administrador para instalar software.
- Conocimientos básicos de desarrollo web.
- Conocimientos básicos de Docker.
Si no tienes los conceptos básicos de Docker, no te preocupes ya que aquí tienes una guía de introducción a Docker
Un entorno de trabajo es, grosso modo, una virtualización de un ordenador que tiene la configuración y recursos necesarios para poder desempeñar una tarea determinada.
En dicho entorno podremos hacer uso de tecnologías, librerías o extensiones que no tenemos (o no queremos tener) en nuestro host. Bien sea porque no tenemos permisos para poder instalarlas o porque la configuración en nuestro host podría poner en peligro la integridad de nuestro equipo.
Un caso de uso típico es el de probar una aplicación con diferentes versiones del lenguaje de desarrollo. Configurar el host con dos o más versiones específicas de un mismo lenguaje de programación y crear un mecanismo para activarlas/desactivarlas bajo demanda sin que afecte al buen funcionamiento de nuestra máquina, es una tarea que requiere de cierta destreza, de tiempo y de muchos intentos.
Con Docker esta operativa se simplifica notablemente. Más aún si se quiere replicar entre los miembros del departamento de desarrollo.
En términos generales el entorno de desarrollo y el de producción deben ser lo más parecidos posibles pero no deben de ser exactamente iguales puesto que en desarrollo hacemos uso de mecanismos para la depuración que en producción no deben de estar presentes bajo ningún concepto.
Otro aspecto no menos importante es el de la seguridad: en desarrollo queremos editar el código fuente de nuestra aplicación, con nuestro IDE favorito y que, automáticamente, los cambios se repliquen en el contenedor para poder probar dichos cambios. Esta funcionalidad se consigue haciendo uso de los puntos de montaje entre el host y el contenedor pero, en producción, no queremos que nadie modifique nuestra aplicación. Ni siquiera que tenga acceso al código fuente.
Veamos algunas de las principales diferencias entre el entorno de producción y desarrollo:
Como podemos ver, aunque los entornos de desarrollo y producción cuenten con la misma versión de PHP, dependencias del sistema operativo... hay diferencias muy evidentes:
el entorno de desarrollo debe configurarse de tal modo que facilite el desarrollo y la depuración, mientras que en producción la aplicación debe configurarse para un uso óptimo y eficiente sin extensiones ni configuraciones innecesarias que pueden afectar al rendimiento y/o la seguridad de la infraestructura.
Veamos con un ejemplo práctico cómo crear un entorno de trabajo.
Para facilitar la creación de nuestro Dockerfile haremos uso de un diagrama de flujo donde podemos representar las acciones necesarias como estados independientes que nos permitan abstraer subtareas y poderlas reutilizar en otros casos.
Veamos este diagrama cómo podemos trasladarlo a un Dockerfile...
Cada estado del diagrama corresponde a un stage de nuestro Dockerfile.
Un stage es, por así decirlo, un hilo independiente y efímero de construcción de nuestra imágen. Es decir, un subproceso temporal que nos permite compilar extensiones y poder extraer el resultado de dicho proceso para aplicarlo donde lo consideremos oportuno y que, al final de construcción de nuestra imágen principal, dichos stages se eliminan automáticamente.
La ventaja principal de utilizar stages es que nos permite reducir el tamaño de nuestra imágen.
Al usar hilos de construcción independientes, todas aquellas extensiones o librerías que necesitemos compilar se hacen en un subproceso aislado en el que, una vez copiamos el resultado a nuestra imágen principal, todas las dependencias de compilación (compilador, ficheros de idiomas, librerías compartidas...) son eliminadas, puesto que no son necesarias.
# syntax=docker/dockerfile:1
FROM php:8.4.10-fpm-alpine AS base-image
FROM base-image AS common
RUN apk update && apk add --no-cache fcgi libzip
WORKDIR /var/www/html
FROM base-image AS extensions-builder-required
RUN docker-php-ext-install zip
FROM extensions-builder-required AS extensions-builder-development
RUN docker-php-ext-install xdebug
FROM common AS build-development
RUN apk update && apk add --no-cache envsubst git ncurses util-linux
COPY --from=extensions-builder-development /usr/local/lib/php/extensions/*/* /usr/local/lib/php/extensions/no-debug-non-zts-20240924/
COPY --from=extensions-builder-development /usr/local/etc/php/conf.d/* /usr/local/etc/php/conf.d/
COPY --from=composer /usr/bin/composer /usr/bin/composer
...
Veamos qué hace cada stage...
- ⇝ base-image
- Este stage está definido en la línea 3 de nuestro Dockerfile.
- Aquí sólo definimos qué versión de PHP vamos a utilizar para construir nuestro entorno.
- ⇝ common
- Este stage está definido en la línea 5 de nuestro Dockerfile.
- Aquí definimos las dependencias comunes tanto para desarrollo como para producción.
- ⇝ extensions-builder-required
- Este stage está definido en la línea 11 de nuestro Dockerfile.
- En este stage instalamos las extensiones de PHP comunes tanto para desarrollo como a producción.
- ⇝ extensions-builder-development
- Este stage está definido en la línea 15 de nuestro Dockerfile.
- En este stage instalamos las extensiones de PHP exclusivas para desarrollo.
- ⇝ build-development
- Este stage está definido en la línea 19 de nuestro Dockerfile.
- Este es el stage encargado de construir nuestra imagen de desarrollo, importando las extensiones compiladas desde extensions-builder-development, además de instalar y configurar todo lo referente a permisos y dependencias de desarrollo.
...
FROM composer AS optimize-php-dependencies
COPY src/composer.json /app/
COPY src/composer.lock /app/
RUN composer install --ignore-platform-reqs --no-ansi --no-autoloader --no-interaction --no-scripts --prefer-dist --no-dev
COPY src/app /app/app
RUN composer dump-autoload --optimize --classmap-authoritative
FROM common AS build-production
COPY --from=extensions-builder-development /usr/local/lib/php/extensions/*/* /usr/local/lib/php/extensions/no-debug-non-zts-20240924/
COPY --from=extensions-builder-development /usr/local/etc/php/conf.d/* /usr/local/etc/php/conf.d/
COPY --from=optimize-php-dependencies --chown=www-data:www-data /app /var/www/html
...
Veamos qué hace cada stage...
- ⇝ optimize-php-dependencies
- Este stage está definido en la línea 109 de nuestro Dockerfile.
- A diferencia del entorno de desarrollo, en el que nuestra aplicación se monta dentro del contenedor mediante un punto de montaje, en producción esta configuración no aplica.
En producción nuestra aplicación está embebida, es decir, se copia un snapshot de nuestra aplicación dentro del contenedor limitando así que cualquier elemento externo pueda acceder o modificar al código de nuestra aplicación.
- Para ello este stage se crea desde la imágen pública de Composer y, en ese subproceso efímero y volátil, se copia nuestra aplicación de manera optimizada: se copian las dependencias, se instalan desde la versión de distribución y no se instala ninguna dependencia de desarrollo. Además se genera un árbol de dependencias para PHP de manera optimizado.
- ⇝ build-production
- Este stage está definido en la línea 120 de nuestro Dockerfile.
- Este es el stage encargado de construir nuestra imagen de producción, importando las extensiones compiladas desde extensions-builder-required, además importar nuestra aplicación desde el stage de optimize-php-dependencies además de instalar y configurar todo lo necesario en el entorno de producción.
Puedes ver el código fuente en el siguiente repositorio https://github.com/AlcidesRC/dockerized-php
Este repositorio está en constante evolución ya que es el repositorio que uso de base para mis pruebas de concepto, side projects y aplicaciones que llevo a producción.
Es muy probable que te surjan dudas sobre los cómo y los porqués de hacer ciertas cosas en dicho repositorio... si ese es el caso, no dudes en ponerte en contacto conmigo que estaré encantado de aclararte cualquier duda.