Docker + Laravel = ❤

laravel-in-docker


En este artículo, hablaré sobre mi experiencia de "envolver" una aplicación Laravel en un contenedor Docker para que los desarrolladores frontend y backend puedan trabajar localmente con ella, y lanzarla en producción fue lo más simple posible. Además, CI ejecutará automáticamente analizadores de código estático, pruebas de phpunit y phpunit imágenes.


"¿Y qué es, de hecho, la complejidad?" - Puedes decir, y estarás en parte en lo cierto. El hecho es que muchas discusiones en las comunidades de habla rusa e inglesa están dedicadas a este tema, y ​​dividiría condicionalmente casi todos los hilos estudiados en las siguientes categorías:


  • "Estoy usando Docker para el desarrollo local. Puse laradock y no sé los problemas". Genial, pero ¿qué pasa con el lanzamiento de automatización y producción?
  • "Recopilo un contenedor (monolito) basado en fedora:latest (~ 230 Mb), pongo todos los servicios (nginx, db, caché, etc.), ejecuto todo dentro del supervisor". También excelente, fácil de comenzar, pero ¿qué pasa con la ideología de "un contenedor - un proceso"? ¿Qué pasa con el equilibrio y la gestión de procesos? ¿Cuál es el tamaño de la imagen?
  • "Aquí hay piezas de configuraciones, sazona con extractos de scripts de sh, agrega valores mágicos del entorno, úsalo". Gracias, pero ¿qué pasa con al menos un ejemplo vivo que podría bifurcar y jugar completamente?

Todo lo que lees a continuación es una experiencia subjetiva que no pretende ser la verdad última. Si tiene adiciones o indicaciones de inexactitudes, bienvenido a los comentarios.


Para los impacientes: un enlace al repositorio , clone en el que puede iniciar la aplicación Laravel con un comando. Tampoco es difícil ejecutarlo en el mismo ranchero , "vincular" correctamente los contenedores, o usar la versión de supermercado docker-compose.yml como punto de partida.

Parte teórica


¿Qué herramientas usaremos en nuestro trabajo y en qué nos centraremos? En primer lugar, necesitamos instalar en el host:


  • docker : al momento de escribir, usé la versión 18.06.1-ce
  • docker-compose : se encarga de vincular contenedores y almacenar los valores de entorno necesarios; versión 1.22.0
  • make : puede que se sorprenda, pero encaja perfectamente en el contexto de trabajar con docker

Puede curl -fsSL get.docker.com | sudo sh docker en sistemas similares a debian con el comando curl -fsSL get.docker.com | sudo sh curl -fsSL get.docker.com | sudo sh , pero docker-compose mejor para instalar usando pip , ya que las versiones más recientes viven en sus repositorios (por lo general, apt muy retrasado).

Esto completa la lista de dependencias. Lo que usará para trabajar con el código fuente ( phpstorm , netbeans o dead vim ) depende de usted.


El siguiente es un control de calidad improvisado en el contexto del diseño de imagen (no tengo miedo de esta palabra) :


  • P: Imagen básica: ¿cuál es mejor elegir?


  • A: El que es "más delgado", sin lujos. Sobre la base de alpine (~ 5 Mb), puedes recolectar lo que tu corazón desee, pero lo más probable es que tengas que jugar con el conjunto de servicios desde la fuente. Como alternativa, jessie-slim (~ 30 Mb) . O use el que se usa con mayor frecuencia en sus proyectos.


  • P: ¿Por qué es importante el peso de la imagen?


  • R: Disminución en el volumen de tráfico, disminución en la probabilidad de un error al descargar (menos datos - menos probabilidad), disminución en el lugar consumido. La regla "La gravedad es confiable" (© "Snatch") no funciona muy bien aquí.


  • P: Pero mi amigo %friend_name% dice que una imagen "monolítica" con todas las dependencias es la mejor manera.


  • A: Solo cuentemos. La aplicación tiene 3 dependencias: PG, Redis, PHP. Y quería probar cómo se comportará en paquetes de diferentes versiones de estas dependencias. PG - versiones 9.6 y 10, Redis - 3.2 y 4.0, PHP - 7.0 y 7.2. En caso de que cada adicción sea una imagen separada, necesita 6 de ellas, que ni siquiera necesita recopilar, todo está listo y se encuentra en hub.docker.com . Si, por razones ideológicas, todas las dependencias están "empaquetadas" en un contenedor, debe volver a armarlo con plumas ... ¿8 veces? Ahora agregue la condición que todavía desea jugar con opcache . En el caso de descomposición, esto es simplemente un cambio en las etiquetas de las imágenes utilizadas. Un monolito es más fácil de ejecutar y mantener, pero es el camino a ninguna parte.


  • P: ¿Por qué es malo el supervisor en el contenedor?


  • A: Porque PID 1 . Si no desea una gran cantidad de problemas con los procesos zombie y tiene la capacidad de "agregar capacidad" de manera flexible cuando sea necesario, intente ejecutar un proceso por contenedor. Una excepción peculiar es nginx con sus trabajadores y php-fpm , que tienen la capacidad de producir procesos, pero tienen que soportar esto (además, no son malos para reaccionar a SIGTERM , "matando" a sus trabajadores de manera bastante correcta). Al lanzar a todos los demonios como supervisor, es casi seguro que te estás condenando a problemas. Aunque, en algunos casos, es difícil prescindir de él, pero estas ya son excepciones.



Habiendo decidido los enfoques principales, pasemos a nuestra aplicación. Debe ser capaz de:


  • web|api : proporcione estática con nginx y genere contenido dinámico con fpm
  • scheduler : ejecuta el planificador de tareas nativo
  • queue - procesa trabajos desde colas

Un conjunto básico que se puede ampliar si es necesario. Ahora pasemos a las imágenes que tenemos que recopilar para que nuestra aplicación "despegue" (sus nombres en clave se dan entre paréntesis):


  • PHP + PHP-FPM ( aplicación ): el entorno en el que se ejecutará nuestro código. Dado que las versiones de PHP y FPM serán las mismas para nosotros, las recopilamos en una imagen. Por lo tanto, es más fácil de administrar con configuraciones, y la composición de los paquetes será idéntica. Por supuesto, FPM y los procesos de aplicación se ejecutarán en diferentes contenedores
  • nginx ( nginx ), que no molestaría con la entrega de configuraciones y módulos opcionales para nginx , recopilaremos una imagen separada con él. Dado que es un servicio separado, tiene su propio archivo acoplable y su contexto
  • Fuentes de la aplicación ( fuentes ): la fuente se entregará utilizando una imagen separada, montando el volume con ellas en un contenedor con la aplicación. La imagen base es alpine , en el interior solo hay fuentes con dependencias instaladas y recopiladas utilizando activos de paquete web (artefactos de compilación)

Otros servicios de desarrollo se lanzan en contenedores, extrayéndolos de hub.docker.com ; en producción, por otro lado, se ejecutan en servidores separados, agrupados. Todo lo que nos queda es decirle a la aplicación (a través del entorno) en qué direcciones / puertos y con qué detalles es necesario tocarlos. Aún más genial es usar el descubrimiento de servicios para este propósito, pero no en este momento.


Habiendo decidido la parte teórica, propongo pasar a la siguiente parte.


La parte práctica


Sugiero organizar los archivos en el repositorio de la siguiente manera:


 . ├── docker #    -   │  ├── app │  │  ├── Dockerfile │  │  └── ... │  ├── nginx │  │  ├── Dockerfile │  │  └── ... │  └── sources │    ├── Dockerfile │    └── ... ├── src #   │ ├── app │ ├── bootstrap │ ├── config │ ├── artisan │ └── ... ├── docker-compose.yml # Compose-    ├── Makefile ├── CHANGELOG.md └── README.md 

Puede familiarizarse con la estructura y los archivos haciendo clic en este enlace .

Para construir un servicio, puede usar el comando:


 $ docker build \ --tag %local_image_name% \ -f ./docker/%service_directory%/Dockerfile ./docker/%service_directory% 

La única diferencia será el ensamblaje de la imagen con las fuentes; para ello, el contexto de ensamblaje (argumento extremo) ./src establecerse en ./src .


Las reglas para nombrar imágenes en el registro local recomiendan usar las que docker-compose usa de manera predeterminada, a saber: %root_directory_name%_%service_name% . Si el directorio del proyecto se llama my-awesome-project , y el servicio se llama redis , entonces el nombre de la imagen (local) es mejor elegir my-awesome-project_redis respectivamente.


Para acelerar el proceso de compilación, puede indicarle a la ventana acoplable que use el caché de la imagen ensamblada previamente, y para esto, se --cache-from %full_registry_name% inicio --cache-from %full_registry_name% . Por lo tanto, el docker daemon se verá antes de comenzar una instrucción particular en el Dockerfile: ¿ha cambiado? Y si no (el hash converge), se saltará la instrucción, usando la capa ya preparada de la imagen, que le indicará que use como caché. Esto no es malo, por lo que reconstruirá el proceso, especialmente si nada ha cambiado :)

Preste atención a los scripts de ENTRYPOINT para iniciar contenedores de aplicaciones.

La imagen del entorno para iniciar la aplicación (aplicación) se recopiló teniendo en cuenta el hecho de que funcionará no solo en la producción, sino también a nivel local, los desarrolladores deben interactuar con ella de manera efectiva. La instalación y eliminación de las dependencias del composer , la ejecución de pruebas unit , los registros de tail y el uso de alias familiares ( php /app/artisanart , composerc ) deben realizarse sin molestias. Además, también se utilizará para ejecutar pruebas unit y analizadores de código estático ( phpstan en nuestro caso) en CI. Es por eso que su Dockerfile, por ejemplo, contiene la xdebug instalación xdebug , pero el módulo en sí no está habilitado (solo se habilita mediante CI).


También para el composer el paquete hirak/prestissimo se hirak/prestissimo , lo que aumenta enormemente la instalación de todas las dependencias.

En producción, montamos el contenido del directorio /src de la imagen con las fuentes (fuentes) dentro del directorio /app . Para el desarrollo, "desplazamos" el directorio local con las fuentes de la aplicación ( -v "$(pwd)/src:/app:rw" ).


Y aquí radica una complejidad: estos son los derechos de acceso a los archivos que se crean desde el contenedor. El hecho es que, por defecto, los procesos que se ejecutan dentro del contenedor comienzan desde la raíz ( root:root ), los archivos creados por estos procesos (caché, registros, sesiones, etc.), también, y como resultado, no tiene nada "localmente" con ellos puede hacerlo sin ejecutar sudo chown -R $(id -u):$(id -g) /path/to/sources .


Como una solución, use fixuid , pero esta solución es sencilla. La mejor forma me pareció USER_ID local y su GROUP_ID dentro del contenedor, y comenzar los procesos con estos valores . De forma predeterminada, al sustituir los valores 1000:1000 (los valores predeterminados para el primer usuario local) se eliminó la llamada $(id -u):$(id -g) , y si es necesario, siempre puede anularlos ( $ USER_ID=666 docker-compose up -d ) o coloque el archivo docker-compose en el archivo .env .


Además, cuando php-fpm inicie localmente php-fpm no olvide deshabilitar opcache de lo contrario, habrá un montón de "¡sí, qué demonios!" se le proporcionará


Para una conexión "directa" a redis y postgres, arrojé puertos adicionales "fuera" ( 15432 y 15432 respectivamente), por lo que no hay problemas con "conectarse y ver qué y cómo es realmente" en principio.


Mantengo el contenedor con la app nombre en código ejecutándose ( --command keep-alive.sh ) con el fin de tener un acceso conveniente a la aplicación.


Estos son algunos ejemplos de resolución de problemas cotidianos con docker-compose :


OperaciónComando en ejecución
Instalar paquete composer$ docker-compose exec app composer require package/name
Ejecutando phpunit$ docker-compose exec app php ./vendor/bin/phpunit --no-coverage
Instalar todas las dependencias de nodo$ docker-compose run --rm node npm install
Instalar paquete de nodos$ docker-compose run --rm node npm i package_name
Lanzar una reconstrucción en vivo de activos$ docker-compose run --rm node npm run watch

Puede encontrar todos los detalles de inicio en el archivo docker-compose.yml .


Choi make vivo!


Escribir los mismos comandos cada vez se vuelve aburrido después de la segunda vez, y dado que los programadores son criaturas perezosas por naturaleza, entremos en su "automatización". Mantener un conjunto de scripts es una opción, pero no tan atractiva como un solo Makefile , especialmente porque su aplicabilidad en el desarrollo moderno está muy subestimada.


Puede encontrar el manual completo en ruso en este enlace .

Veamos cómo se ve la ejecución de ejecución en la raíz del repositorio:


 [user@host ~/projects/app] $ make help Show this help app-pull Application - pull latest Docker image (from remote registry) app Application - build Docker image locally app-push Application - tag and push Docker image into remote registry sources-pull Sources - pull latest Docker image (from remote registry) sources Sources - build Docker image locally sources-push Sources - tag and push Docker image into remote registry nginx-pull Nginx - pull latest Docker image (from remote registry) nginx Nginx - build Docker image locally nginx-push Nginx - tag and push Docker image into remote registry pull Pull all Docker images (from remote registry) build Build all Docker images push Tag and push all Docker images into remote registry login Log in to a remote Docker registry clean Remove images from local registry --------------- --------------- up Start all containers (in background) for development down Stop all started for development containers restart Restart all started for development containers shell Start shell into application container install Install application dependencies into application container watch Start watching assets for changes (node) init Make full application initialization (install, seed, build assets) test Execute application tests Allowed for overriding next properties: PULL_TAG - Tag for pulling images before building own ('latest' by default) PUBLISH_TAGS - Tags list for building and pushing into remote registry (delimiter - single space, 'latest' by default) Usage example: make PULL_TAG='v1.2.3' PUBLISH_TAGS='latest v1.2.3 test-tag' app-push 

Es muy bueno en objetivos adictivos. Por ejemplo, para ejecutar watch ( docker-compose run --rm node npm run watch ), necesita que la aplicación sea "activada", solo necesita especificar el objetivo up como dependiente, y no tiene que preocuparse por olvidar hacer esto antes de llamar a watch - make sí mismo hará todo por ti. Lo mismo se aplica a la ejecución de pruebas y analizadores estáticos, por ejemplo, antes de realizar cambios: ejecute una make test y ¡toda la magia sucederá por usted!


No hace falta decir que no tiene que preocuparse por ensamblar imágenes, descargarlas, especificar - --cache-from y casi todo.


Puede ver el contenido del Makefile en este enlace .


Parte auto


Pasemos a la parte final de este artículo: esta es la automatización del proceso de actualización de imágenes en el Registro de Docker. Aunque en mi ejemplo se usa GitLab CI, para transferir la idea a otro servicio de integración, creo que será bastante posible.


En primer lugar, determinaremos el nombre de las etiquetas de imagen utilizadas:


Nombre de etiquetaDestino
latestImágenes recopiladas de la rama master .
El estado del código es el más reciente, pero aún no está listo para entrar en el lanzamiento.
some-branch-nameImágenes recopiladas en el brunch de some-branch-name .
Por lo tanto, podemos "implementar" cambios en cualquier entorno que se implementaron solo en el marco de un brunch específico, incluso antes de fusionarlos con la luz master : es suficiente para "estirar" las imágenes con esta etiqueta.
Y, sí, ¡los cambios pueden referirse tanto al código como a las imágenes de todos los servicios en general!
vX.XXEn realidad, el lanzamiento de la aplicación (se usa para implementar una versión específica)
stableAlias, para la etiqueta con la última versión (se usa para implementar la última versión estable)

El lanzamiento se lleva a cabo mediante la publicación de una etiqueta en un formato vX.XX .


Para acelerar la compilación, docker build el almacenamiento en caché de los directorios ./src/vendor y ./src/node_modules + --cache-from para la docker build , y consta de las siguientes etapas:


Nombre del escenarioDestino
prepareLa fase preparatoria: el ensamblaje de imágenes de todos los servicios, excepto la imagen con la fuente
testProbar la aplicación (ejecutando phpunit , analizadores de código estático) usando imágenes recopiladas en la etapa de preparación
buildInstalar todas las dependencias del composer ( --no-dev ), ensamblar assets webpack y webpack imagen con el código fuente, incluidos los artefactos recibidos ( vendor/* , app.js , app.css )

captura de pantalla de tuberías


El ensamblaje en la rama master produce push con las latest etiquetas y master

En promedio, todas las etapas del ensamblaje toman 4 minutos , lo cual es un resultado bastante bueno (la ejecución paralela de tareas es nuestro todo).


Puede familiarizarse con el contenido de la configuración ( .gitlab-ci.yml ) del recopilador en este enlace .


En lugar de una conclusión


Como puede ver, organizar el trabajo con una aplicación php (usando Laravel como ejemplo) usando Docker no es tan difícil. Como prueba, puede bifurcar el repositorio y reemplazar todos los casos de tarampampam/laravel-in-docker con los suyos: intente todo "en vivo" por su cuenta.


Para el lanzamiento local: ejecute solo 2 comandos:


 $ git clone https://gitlab.com/tarampampam/laravel-in-docker.git ./laravel-in-docker && cd $_ $ make init 

Luego abra http://127.0.0.1:9999 en su navegador favorito.


... aprovechando la oportunidad


En este momento estoy trabajando en el "autocódigo" del proyecto TL, y estamos buscando desarrolladores de php talentosos y administradores de sistemas (la oficina de desarrollo se encuentra en Ekaterimburgo). Si te consideras el primero o el segundo, escribe nuestra carta de RR. HH. Con el texto "Quiero ser un equipo de desarrollo, resume:% link_on_summary%" al correo electrónico hr@avtocod.ru , te ayudamos con la reubicación.

Source: https://habr.com/ru/post/es425101/


All Articles