<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="es">
		<id>https://1984.lsi.us.es/wiki-egc/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Drorganvidez</id>
		<title>Wiki de EGC - Contribuciones del usuario [es]</title>
		<link rel="self" type="application/atom+xml" href="https://1984.lsi.us.es/wiki-egc/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Drorganvidez"/>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php/Especial:Contribuciones/Drorganvidez"/>
		<updated>2026-04-10T01:50:43Z</updated>
		<subtitle>Contribuciones del usuario</subtitle>
		<generator>MediaWiki 1.29.0</generator>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Teor%C3%ADa_-_25/26&amp;diff=10224</id>
		<title>Teoría - 25/26</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Teor%C3%ADa_-_25/26&amp;diff=10224"/>
				<updated>2025-11-19T17:31:12Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[Página_Principal]] -&amp;gt; [[2025/2026]] -&amp;gt; [[Teoría - 25/26]]&lt;br /&gt;
&lt;br /&gt;
* Tema 0: [https://hdvirtual.us.es/discovirt/index.php/s/D2EmT4m8yaFsQro Presentación de la asignatura]&lt;br /&gt;
* Tema 1: [[Archivo:Gestion_de_equipos.pdf]]&lt;br /&gt;
* Tema 2: [[Archivo:IntegracionContinua.pdf]]&lt;br /&gt;
* Tema 3: [[Archivo:GestionDelCodigoFuente2526.pdf]]&lt;br /&gt;
* Tema 4: [https://hdvirtual.us.es/discovirt/index.php/s/FHi8882THcY3EN6 Pruebas de software]&lt;br /&gt;
* Tema 5: [https://hdvirtual.us.es/discovirt/index.php/s/qpKPcLS8Edmtdd9 Gestión de tareas e incidencias en el ciclo de integración continua]&lt;br /&gt;
* Tema 6: [https://hdvirtual.us.es/discovirt/index.php/s/dHrd8zwgAARePMK Procesamiento y compilación aisladas]&lt;br /&gt;
* Tema 7: [https://hdvirtual.us.es/discovirt/index.php/s/eSS347RqjiCD8QX FLOSS]&lt;br /&gt;
* Tema 8: [https://hdvirtual.us.es/discovirt/index.php/s/fcA7fi3edB8nCr7 Diseño de Patrones y Frameworks (PDF)][https://egcetsii.github.io/2526-frameworks/1 Frameworks (versión online)]&lt;br /&gt;
* Tema 9: [ Líneas de Producto Software]&lt;br /&gt;
** Test de Pensamiento en Variabilidad (15 minutos): [https://bit.ly/tvt_egc https://bit.ly/tvt_egc]&lt;br /&gt;
* InnoSoft: [[Archivo:Innosoft.pdf]]&lt;br /&gt;
&lt;br /&gt;
Material adicional (no entra en la prueba de teoría): &lt;br /&gt;
&lt;br /&gt;
* [https://hdvirtual.us.es/discovirt/index.php/s/5BJCLtRSdpFFAXF Presentación de Andreas Zeller sobre Software Testing]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!--&lt;br /&gt;
&lt;br /&gt;
* Tema 7: [https://hdvirtual.us.es/discovirt/index.php/s/WAgPgBbN492im8q Software Product Lines (SPL)]&lt;br /&gt;
* Tema 8: [https://hdvirtual.us.es/discovirt/index.php/s/KoqontffP3YAGtN Introducción a Kubernetes] (impartido por invitados externos)&lt;br /&gt;
* Tema 9: [https://hdvirtual.us.es/discovirt/index.php/s/KaYc8HbdewYFxa2 El futuro de la profesión], [https://forms.office.com/e/X5nXMBLkEQ Encuesta sobre forma de aprender]&lt;br /&gt;
--&amp;gt;&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Proyecto_-_25/26&amp;diff=10197</id>
		<title>Proyecto - 25/26</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Proyecto_-_25/26&amp;diff=10197"/>
				<updated>2025-11-05T11:38:04Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: /* 🆕 Integraciones entre equipos */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[Página_Principal]] -&amp;gt; [[2025/2026]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''ESTA PÁGINA ESTÁ EN CONSTRUCCIÓN'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
= Material y enlaces relacionados con el proyecto =&lt;br /&gt;
&lt;br /&gt;
== Documentación general ==&lt;br /&gt;
&lt;br /&gt;
* Documento de descripción general del proyecto: [[Guía general del proyecto en equipo]]&lt;br /&gt;
* Inscripción de los equipos (leer instrucciones más abajo sobre el [[Proyecto_-_25/26#Milestones | M0]])&lt;br /&gt;
&lt;br /&gt;
= Tipos de proyectos según nota a la que aspira=&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;margin:auto&amp;quot;&lt;br /&gt;
|+ Tabla resumen de los tipos de proyectos&lt;br /&gt;
|-&lt;br /&gt;
! Tipos de proyectos !! Máxima nota a la que aspira !! Necesita coordinación&lt;br /&gt;
|-&lt;br /&gt;
| UVLHub single || 8 || NO&lt;br /&gt;
|-&lt;br /&gt;
| UVLHub equipo|| 10 || SI, 2 o más equipos&lt;br /&gt;
|-&lt;br /&gt;
| Innosoft || 10 || NO, pero podría hacerse&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Se podrá elegir entre los siguientes tipos de proyectos: &lt;br /&gt;
== Proyectos UVLHub ==&lt;br /&gt;
* Portal de Github con el código: https://github.com/EGCETSII/UVLHub&lt;br /&gt;
* Presentación de [https://hdvirtual.us.es/discovirt/index.php/s/eTfYiHSbMZ2aenp &amp;quot;UVLHub&amp;quot;]&lt;br /&gt;
* Sistema desplegado [https://egc.uvlhub.io &amp;quot;UVLHub&amp;quot;]&lt;br /&gt;
* Breve guía para introducir cambios (es un inicio, el resto de material se ve en prácticas a lo largo del curso): https://docs.uvlhub.io/tutorials/crud_tutorial &lt;br /&gt;
&lt;br /&gt;
* Subtipos de proyectos UVLHub: &lt;br /&gt;
** UVLHub-single: un proyecto de un solo equipo derivado de UVLHub en el que no haya integración con otros equipos. &lt;br /&gt;
** UVLHub-equipo: Un proyecto de menos de 4 y más de 1 equipo derivado de UVLHub en el que todos los equipos estén integrados entre sí.&lt;br /&gt;
=== ¿Qué cambios se le puede hacer a UVLHub?===&lt;br /&gt;
* Se pueden hacer muchos cambios a UVLHub. Los cambio se basan en las [https://github.com/EGCETSII/uvlhub/issues issues] que están publicadas. Se pueden hacer otras sugerencias de cambio, pero deberán estar bien justificadas y aprobadas por su tutor. Los cambios están clasificados por su nivel de dificultad estimada (H = High; M = Medium;  L= Low).&lt;br /&gt;
* Hay dos cambios que tienen que hacer todos los equipos y que son adicionales a los demás que se definen en el siguiente apartado: &lt;br /&gt;
** WI de '''fakenodo''': para no conectarse con Zenodo y simular la llamada a una API ficticia. Es importante que entienda que NO se trata de hacer una réplica de Zenodo ([https://github.com/EGCETSII/uvlhub/issues/103 más información]).&lt;br /&gt;
** WI de '''newdataset''': para crear un nuevo hub que gestione otro tipo de datos distinto a UVL ([https://github.com/EGCETSII/uvlhub/issues/104 más información]).&lt;br /&gt;
* Además de los dos anteriores, se '''le asignarán''' tantos &amp;quot;Work Items&amp;quot; (WIs) como miembros tenga el proyecto. Los WIs que se le asignen serán en términos de dificultad al menos 2 WIs H, 2 WIs M y 2 WIs L. Si se tiene menos componentes, se irán eliminando de más simples a más complejos, por ejemplo, si termino entregando el proyecto con solo 4 componentes en el equipo, tendré que entregar, 2 WIs H y 2 WIs M; si fueran 5 componentes en el equipo 2WI h, 2 WIs M y 1 WI L. &lt;br /&gt;
* Puede haber WIs que, depende de cómo se aborden puedan ser divididos en varios WIs. Para ello, debe contar con el visto bueno de su tutor/a de proyecto.&lt;br /&gt;
* Puede haber WIs que, depende de cómo se aborden puedan bajar su dificultad o también subirla. Para ello, debe contar con el visto bueno de su tutor/a de proyecto usando para ello las tutorías o días de seguimiento.&lt;br /&gt;
* La asignación del los WIs se hará cuando se publiquen los equipos&lt;br /&gt;
&lt;br /&gt;
=== 🆕 Integraciones entre equipos ===&lt;br /&gt;
En los proyectos de tipo '''UVLHub-equipo''' es conveniente mantener una integración continua entre los equipos. &lt;br /&gt;
&lt;br /&gt;
No es recomendable que cada equipo trabaje de forma aislada para luego unir el código al final. Todos los equipos deberían coordinar sus ramas, integraciones y despliegues '''desde el inicio del proyecto''', mejorando  la coherencia del sistema y evitando conflictos de última hora, mediante el uso de workflows de integración y despliegue continuos y un pipeline automatizado y eficiente, tal y como se ha trabajado en clase.&lt;br /&gt;
&lt;br /&gt;
== Proyectos &amp;quot;InnoSoft&amp;quot;==&lt;br /&gt;
&lt;br /&gt;
* Un proyecto de uno o varios equipos relacionados con la automatización de las [[Innosoft - 25/26| jornadas]] (InnoSoft),[[Proyecto InnoSoft | más información]].&lt;br /&gt;
&lt;br /&gt;
= Equipos y proyectos =&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
M0: Este es el listado de proyectos y personas inscritas con la fecha límite establecida. Se señalan casos especiales según el color de algunas celdas.&lt;br /&gt;
&lt;br /&gt;
Sobre los WIs que cada equipo/proyecto desarrollará, se mantiene para el M1 lo que haya elegido el equipo pero se podrá determinar sugerencias de cambios por parte del tutor/a si fuera necesario.  &lt;br /&gt;
&lt;br /&gt;
* [https://hdvirtual.us.es/discovirt/index.php/s/2DyPwLqjp8r5x9g Listado de equipos y proyectos M0 (07/10/2025)]&lt;br /&gt;
* [https://hdvirtual.us.es/discovirt/index.php/s/BjbqA3QjfLccfQY Listado de equipos y proyectos M0 - '''final''' (16/10/2025)]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Si algún proyecto no se ha inscrito a tiempo podrá continuar con el proyecto pero no podrá presentarse a M1. Debe estar atento para una inscripción una vez haya pasado M1 (contacte con el coordinador de proyectos). Si alguna persona todavía no tiene proyecto, debe ponerse urgentemente en contacto con alguno de los proyectos que todavía tienen plazas libres/vacantes o, en su defecto, ponerse en contacto con el coordinador de proyectos (David Benavides) para solucionar la incidencia. &lt;br /&gt;
&lt;br /&gt;
Cualquier otra duda, puede dirigirse al coordinador de la asignatura.&lt;br /&gt;
&lt;br /&gt;
= Fechas Importantes =&lt;br /&gt;
&lt;br /&gt;
== Seguimientos ==&lt;br /&gt;
* [[Seguimiento Gestión del código | Seguimiento de gestión del código fuente (+info)]]  &lt;br /&gt;
* [[Seguimiento de pruebas | Seguimiento de pruebas (+info)]]  &lt;br /&gt;
* [[Seguimiento de integración/despliegue | Seguimiento de integración/despliegue (+info)]]&lt;br /&gt;
&lt;br /&gt;
== Milestones ==&lt;br /&gt;
* M0: [[M0-25_26 | Inscripción de los equipos (+info)]] &lt;br /&gt;
** V 3 de Octubre (fecha límite para rellenar el formulario de inscripción)&lt;br /&gt;
* M1: [[M1-25_26 | Sistema funcionando y pruebas (+info)]]&lt;br /&gt;
** M 21 de Octubre&lt;br /&gt;
*M2: [[M2-25_26 | Sistema funcionando y con incrementos (+info)]] &lt;br /&gt;
** M 11 de Noviembre&lt;br /&gt;
* M3: [[M3-25_26 | Entrega de proyectos y defensas (+info)]] &lt;br /&gt;
** M 16 Diciembre&lt;br /&gt;
** J 18 Diciembre&lt;br /&gt;
&lt;br /&gt;
= Calificaciones de los proyectos y los equipos =&lt;br /&gt;
&lt;br /&gt;
== Indicaciones sobre requisitos mínimos para superar el proyecto ==&lt;br /&gt;
* [[Guia-proyecto | Guía para conocer los requisitos mínimos para superar el proyecto]]&lt;br /&gt;
&lt;br /&gt;
== Cálculo de la nota del proyecto == &lt;br /&gt;
&lt;br /&gt;
Aunque la nota del proyecto está basado en el trabajo en equipo, la nota del proyecto es individual, es decir, lo ideal es que todos los componentes del equipo tengan la misma nota, pues eso sería una buena muestra de que el equipo ha trabajado de manera coordinada y equilibrada, pero '''puede darse el caso de que cada miembro del equipo tenga una nota distinta'''. &lt;br /&gt;
&lt;br /&gt;
El proyecto es parte de la evaluación continua de la asignatura y tiene el peso indicado en el proyecto docente de la asignatura. Para calcular la nota del proyecto se tienen en cuenta varios aspectos: &lt;br /&gt;
&lt;br /&gt;
* La inscripción del proyecto a tiempo (M0)&lt;br /&gt;
* La asistencia a las clases de seguimiento (seguimiento de proyectos en gestión del código fuente, en pruebas, en integración/despliegue)&lt;br /&gt;
* Las entregas y desempeño en los distintos milestones (M1, M2, M3)&lt;br /&gt;
&lt;br /&gt;
El total de la nota se calculará de la siguiente manera: &lt;br /&gt;
&lt;br /&gt;
Nota parcial del proyecto: &lt;br /&gt;
* El M3 tendrá una nota para cada integrante del proyecto&lt;br /&gt;
* El M2 podrá beneficiar hasta en 1 punto dicha nota. Si todo sale bien, podrá haber un incremento de hasta 1 punto en la nota. Si no sale bien, podrá haber hasta -1 punto en la nota. &lt;br /&gt;
* El M1 podrá beneficiar hasta en 1 punto dicha nota. Si todo sale bien, podrá haber un incremento de hasta 1 punto en la nota. Si no sale bien, podrá haber hasta -1 punto en la nota. &lt;br /&gt;
&lt;br /&gt;
Ejemplo: si una persona saca un 8 en M3 pero en M2 y M1 todo fue bien, su nota será entre 8 y 10. Sin embargo, si la misma persona tuvo algún percance en M1 o M2, podría sacar hasta un 6 en esta nota parcial.&lt;br /&gt;
&lt;br /&gt;
Penalizaciones por ausencias a seguimiento y puntualidad: &lt;br /&gt;
* La no asistencia a las sesiones de seguimiento penalizará un 5% cada una sobre la nota parcial del proyecto  (hasta un máximo de un 15%). '''¡OJO!''', eso se refiere a las sesiones de seguimiento, no a los milestones que tienen su impacto en la nota parcial del proyecto como se ha descrito en el punto anterior. &lt;br /&gt;
* La no inscripción a tiempo del equipo del proyecto penalizará un 5% sobre la nota parcial del proyecto.&lt;br /&gt;
&lt;br /&gt;
Nota final del proyecto: &lt;br /&gt;
&lt;br /&gt;
* La nota final del proyecto se calculará con la fórmula: &lt;br /&gt;
&lt;br /&gt;
'''NOTA FINAL DEL PROYECTO''' = Nota parcial del proyecto * [(100 - suma(penalizaciones_por_ausencia_y_puntualidad))/100]&lt;br /&gt;
&lt;br /&gt;
Por ejemplo, si una persona tiene una nota parcial del proyecto de 9 y no ha tenido ningún tipo de penalización por ausencia y puntualidad, su nota final del proyecto  se quedará en un 9. Si la misma persona con una nota de un 9 ha tenido alguna penalización por ausencia o puntualidad, su nota podrá bajar hasta en un 20%, es decir, hasta un 7,2.&lt;br /&gt;
&lt;br /&gt;
Tenga en cuenta que esta nota es la que se calcula para el proyecto pero después, dependiendo del proyecto, esta nota puede tener un límite (por ejemplo, de un 8 en el caso de proyectos UVLHub single).&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Proyecto_-_25/26&amp;diff=10196</id>
		<title>Proyecto - 25/26</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Proyecto_-_25/26&amp;diff=10196"/>
				<updated>2025-11-05T11:32:50Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: /* Proyectos UVLHub */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[Página_Principal]] -&amp;gt; [[2025/2026]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''ESTA PÁGINA ESTÁ EN CONSTRUCCIÓN'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
= Material y enlaces relacionados con el proyecto =&lt;br /&gt;
&lt;br /&gt;
== Documentación general ==&lt;br /&gt;
&lt;br /&gt;
* Documento de descripción general del proyecto: [[Guía general del proyecto en equipo]]&lt;br /&gt;
* Inscripción de los equipos (leer instrucciones más abajo sobre el [[Proyecto_-_25/26#Milestones | M0]])&lt;br /&gt;
&lt;br /&gt;
= Tipos de proyectos según nota a la que aspira=&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;margin:auto&amp;quot;&lt;br /&gt;
|+ Tabla resumen de los tipos de proyectos&lt;br /&gt;
|-&lt;br /&gt;
! Tipos de proyectos !! Máxima nota a la que aspira !! Necesita coordinación&lt;br /&gt;
|-&lt;br /&gt;
| UVLHub single || 8 || NO&lt;br /&gt;
|-&lt;br /&gt;
| UVLHub equipo|| 10 || SI, 2 o más equipos&lt;br /&gt;
|-&lt;br /&gt;
| Innosoft || 10 || NO, pero podría hacerse&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Se podrá elegir entre los siguientes tipos de proyectos: &lt;br /&gt;
== Proyectos UVLHub ==&lt;br /&gt;
* Portal de Github con el código: https://github.com/EGCETSII/UVLHub&lt;br /&gt;
* Presentación de [https://hdvirtual.us.es/discovirt/index.php/s/eTfYiHSbMZ2aenp &amp;quot;UVLHub&amp;quot;]&lt;br /&gt;
* Sistema desplegado [https://egc.uvlhub.io &amp;quot;UVLHub&amp;quot;]&lt;br /&gt;
* Breve guía para introducir cambios (es un inicio, el resto de material se ve en prácticas a lo largo del curso): https://docs.uvlhub.io/tutorials/crud_tutorial &lt;br /&gt;
&lt;br /&gt;
* Subtipos de proyectos UVLHub: &lt;br /&gt;
** UVLHub-single: un proyecto de un solo equipo derivado de UVLHub en el que no haya integración con otros equipos. &lt;br /&gt;
** UVLHub-equipo: Un proyecto de menos de 4 y más de 1 equipo derivado de UVLHub en el que todos los equipos estén integrados entre sí.&lt;br /&gt;
=== ¿Qué cambios se le puede hacer a UVLHub?===&lt;br /&gt;
* Se pueden hacer muchos cambios a UVLHub. Los cambio se basan en las [https://github.com/EGCETSII/uvlhub/issues issues] que están publicadas. Se pueden hacer otras sugerencias de cambio, pero deberán estar bien justificadas y aprobadas por su tutor. Los cambios están clasificados por su nivel de dificultad estimada (H = High; M = Medium;  L= Low).&lt;br /&gt;
* Hay dos cambios que tienen que hacer todos los equipos y que son adicionales a los demás que se definen en el siguiente apartado: &lt;br /&gt;
** WI de '''fakenodo''': para no conectarse con Zenodo y simular la llamada a una API ficticia. Es importante que entienda que NO se trata de hacer una réplica de Zenodo ([https://github.com/EGCETSII/uvlhub/issues/103 más información]).&lt;br /&gt;
** WI de '''newdataset''': para crear un nuevo hub que gestione otro tipo de datos distinto a UVL ([https://github.com/EGCETSII/uvlhub/issues/104 más información]).&lt;br /&gt;
* Además de los dos anteriores, se '''le asignarán''' tantos &amp;quot;Work Items&amp;quot; (WIs) como miembros tenga el proyecto. Los WIs que se le asignen serán en términos de dificultad al menos 2 WIs H, 2 WIs M y 2 WIs L. Si se tiene menos componentes, se irán eliminando de más simples a más complejos, por ejemplo, si termino entregando el proyecto con solo 4 componentes en el equipo, tendré que entregar, 2 WIs H y 2 WIs M; si fueran 5 componentes en el equipo 2WI h, 2 WIs M y 1 WI L. &lt;br /&gt;
* Puede haber WIs que, depende de cómo se aborden puedan ser divididos en varios WIs. Para ello, debe contar con el visto bueno de su tutor/a de proyecto.&lt;br /&gt;
* Puede haber WIs que, depende de cómo se aborden puedan bajar su dificultad o también subirla. Para ello, debe contar con el visto bueno de su tutor/a de proyecto usando para ello las tutorías o días de seguimiento.&lt;br /&gt;
* La asignación del los WIs se hará cuando se publiquen los equipos&lt;br /&gt;
&lt;br /&gt;
=== 🆕 Integraciones entre equipos ===&lt;br /&gt;
* En los proyectos de tipo '''UVLHub-equipo''' es necesario mantener una integración continua entre los equipos. No es válido que cada equipo trabaje de forma aislada para luego unir el código al final. Todos los equipos deben coordinar sus ramas, integraciones y despliegues '''desde el inicio del proyecto''', garantizando la coherencia del sistema y evitando conflictos de última hora, mediante el uso de workflows de integración y despliegue continuos y un pipeline automatizado y eficiente, tal y como se ha trabajado en clase.&lt;br /&gt;
&lt;br /&gt;
== Proyectos &amp;quot;InnoSoft&amp;quot;==&lt;br /&gt;
&lt;br /&gt;
* Un proyecto de uno o varios equipos relacionados con la automatización de las [[Innosoft - 25/26| jornadas]] (InnoSoft),[[Proyecto InnoSoft | más información]].&lt;br /&gt;
&lt;br /&gt;
= Equipos y proyectos =&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
M0: Este es el listado de proyectos y personas inscritas con la fecha límite establecida. Se señalan casos especiales según el color de algunas celdas.&lt;br /&gt;
&lt;br /&gt;
Sobre los WIs que cada equipo/proyecto desarrollará, se mantiene para el M1 lo que haya elegido el equipo pero se podrá determinar sugerencias de cambios por parte del tutor/a si fuera necesario.  &lt;br /&gt;
&lt;br /&gt;
* [https://hdvirtual.us.es/discovirt/index.php/s/2DyPwLqjp8r5x9g Listado de equipos y proyectos M0 (07/10/2025)]&lt;br /&gt;
* [https://hdvirtual.us.es/discovirt/index.php/s/BjbqA3QjfLccfQY Listado de equipos y proyectos M0 - '''final''' (16/10/2025)]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Si algún proyecto no se ha inscrito a tiempo podrá continuar con el proyecto pero no podrá presentarse a M1. Debe estar atento para una inscripción una vez haya pasado M1 (contacte con el coordinador de proyectos). Si alguna persona todavía no tiene proyecto, debe ponerse urgentemente en contacto con alguno de los proyectos que todavía tienen plazas libres/vacantes o, en su defecto, ponerse en contacto con el coordinador de proyectos (David Benavides) para solucionar la incidencia. &lt;br /&gt;
&lt;br /&gt;
Cualquier otra duda, puede dirigirse al coordinador de la asignatura.&lt;br /&gt;
&lt;br /&gt;
= Fechas Importantes =&lt;br /&gt;
&lt;br /&gt;
== Seguimientos ==&lt;br /&gt;
* [[Seguimiento Gestión del código | Seguimiento de gestión del código fuente (+info)]]  &lt;br /&gt;
* [[Seguimiento de pruebas | Seguimiento de pruebas (+info)]]  &lt;br /&gt;
* [[Seguimiento de integración/despliegue | Seguimiento de integración/despliegue (+info)]]&lt;br /&gt;
&lt;br /&gt;
== Milestones ==&lt;br /&gt;
* M0: [[M0-25_26 | Inscripción de los equipos (+info)]] &lt;br /&gt;
** V 3 de Octubre (fecha límite para rellenar el formulario de inscripción)&lt;br /&gt;
* M1: [[M1-25_26 | Sistema funcionando y pruebas (+info)]]&lt;br /&gt;
** M 21 de Octubre&lt;br /&gt;
*M2: [[M2-25_26 | Sistema funcionando y con incrementos (+info)]] &lt;br /&gt;
** M 11 de Noviembre&lt;br /&gt;
* M3: [[M3-25_26 | Entrega de proyectos y defensas (+info)]] &lt;br /&gt;
** M 16 Diciembre&lt;br /&gt;
** J 18 Diciembre&lt;br /&gt;
&lt;br /&gt;
= Calificaciones de los proyectos y los equipos =&lt;br /&gt;
&lt;br /&gt;
== Indicaciones sobre requisitos mínimos para superar el proyecto ==&lt;br /&gt;
* [[Guia-proyecto | Guía para conocer los requisitos mínimos para superar el proyecto]]&lt;br /&gt;
&lt;br /&gt;
== Cálculo de la nota del proyecto == &lt;br /&gt;
&lt;br /&gt;
Aunque la nota del proyecto está basado en el trabajo en equipo, la nota del proyecto es individual, es decir, lo ideal es que todos los componentes del equipo tengan la misma nota, pues eso sería una buena muestra de que el equipo ha trabajado de manera coordinada y equilibrada, pero '''puede darse el caso de que cada miembro del equipo tenga una nota distinta'''. &lt;br /&gt;
&lt;br /&gt;
El proyecto es parte de la evaluación continua de la asignatura y tiene el peso indicado en el proyecto docente de la asignatura. Para calcular la nota del proyecto se tienen en cuenta varios aspectos: &lt;br /&gt;
&lt;br /&gt;
* La inscripción del proyecto a tiempo (M0)&lt;br /&gt;
* La asistencia a las clases de seguimiento (seguimiento de proyectos en gestión del código fuente, en pruebas, en integración/despliegue)&lt;br /&gt;
* Las entregas y desempeño en los distintos milestones (M1, M2, M3)&lt;br /&gt;
&lt;br /&gt;
El total de la nota se calculará de la siguiente manera: &lt;br /&gt;
&lt;br /&gt;
Nota parcial del proyecto: &lt;br /&gt;
* El M3 tendrá una nota para cada integrante del proyecto&lt;br /&gt;
* El M2 podrá beneficiar hasta en 1 punto dicha nota. Si todo sale bien, podrá haber un incremento de hasta 1 punto en la nota. Si no sale bien, podrá haber hasta -1 punto en la nota. &lt;br /&gt;
* El M1 podrá beneficiar hasta en 1 punto dicha nota. Si todo sale bien, podrá haber un incremento de hasta 1 punto en la nota. Si no sale bien, podrá haber hasta -1 punto en la nota. &lt;br /&gt;
&lt;br /&gt;
Ejemplo: si una persona saca un 8 en M3 pero en M2 y M1 todo fue bien, su nota será entre 8 y 10. Sin embargo, si la misma persona tuvo algún percance en M1 o M2, podría sacar hasta un 6 en esta nota parcial.&lt;br /&gt;
&lt;br /&gt;
Penalizaciones por ausencias a seguimiento y puntualidad: &lt;br /&gt;
* La no asistencia a las sesiones de seguimiento penalizará un 5% cada una sobre la nota parcial del proyecto  (hasta un máximo de un 15%). '''¡OJO!''', eso se refiere a las sesiones de seguimiento, no a los milestones que tienen su impacto en la nota parcial del proyecto como se ha descrito en el punto anterior. &lt;br /&gt;
* La no inscripción a tiempo del equipo del proyecto penalizará un 5% sobre la nota parcial del proyecto.&lt;br /&gt;
&lt;br /&gt;
Nota final del proyecto: &lt;br /&gt;
&lt;br /&gt;
* La nota final del proyecto se calculará con la fórmula: &lt;br /&gt;
&lt;br /&gt;
'''NOTA FINAL DEL PROYECTO''' = Nota parcial del proyecto * [(100 - suma(penalizaciones_por_ausencia_y_puntualidad))/100]&lt;br /&gt;
&lt;br /&gt;
Por ejemplo, si una persona tiene una nota parcial del proyecto de 9 y no ha tenido ningún tipo de penalización por ausencia y puntualidad, su nota final del proyecto  se quedará en un 9. Si la misma persona con una nota de un 9 ha tenido alguna penalización por ausencia o puntualidad, su nota podrá bajar hasta en un 20%, es decir, hasta un 7,2.&lt;br /&gt;
&lt;br /&gt;
Tenga en cuenta que esta nota es la que se calcula para el proyecto pero después, dependiendo del proyecto, esta nota puede tener un límite (por ejemplo, de un 8 en el caso de proyectos UVLHub single).&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10187</id>
		<title>Tutorial-docker</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10187"/>
				<updated>2025-10-28T09:43:13Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: /* Construir la imagen */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Ejercicio 1: Empezamos desde cero =&lt;br /&gt;
&lt;br /&gt;
El objetivo es eliminar toda la configuración previa y crear desde cero la estructura de carpetas que contendrá nuestros futuros archivos Docker.&lt;br /&gt;
&lt;br /&gt;
== Eliminar la configuración existente ==&lt;br /&gt;
Antes de comenzar, asegúrate de borrar cualquier carpeta Docker existente en el proyecto. En este caso, le haremos una copia de seguridad para poder volver a ella.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mv docker docker.bk&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
También debes asegurarte que trabajas con las variables de entorno adecuadas:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
cp .env.docker.example .env&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
⚠️ Este paso es importante: partimos completamente desde cero para entender cada parte del sistema.&lt;br /&gt;
&lt;br /&gt;
== Instalar Docker y Docker Compose (Linux) ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo apt update&lt;br /&gt;
sudo apt install -y apt-transport-https ca-certificates curl software-properties-common&lt;br /&gt;
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg&lt;br /&gt;
echo &amp;quot;deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable&amp;quot; | sudo tee /etc/apt/sources.list.d/docker.list &amp;gt; /dev/null&lt;br /&gt;
sudo apt update&lt;br /&gt;
sudo apt install -y docker-ce&lt;br /&gt;
sudo usermod -aG docker ${USER}&lt;br /&gt;
mkdir -p ~/.docker/cli-plugins/&lt;br /&gt;
LATEST_VERSION=$(curl -s https://api.github.com/repos/docker/compose/releases/latest | grep '&amp;quot;tag_name&amp;quot;:' | sed -E 's/.*&amp;quot;([^&amp;quot;]+)&amp;quot;.*/\1/')&lt;br /&gt;
curl -SL &amp;quot;https://github.com/docker/compose/releases/download/${LATEST_VERSION}/docker-compose-$(uname -s)-$(uname -m)&amp;quot; -o ~/.docker/cli-plugins/docker-compose&lt;br /&gt;
chmod +x ~/.docker/cli-plugins/docker-compose&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Es posible que tengas que reiniciar la sesión de usuario para poder usar docker sin necesidad de sudo.&lt;br /&gt;
&lt;br /&gt;
== Crear un Dockerfile mínimo ==&lt;br /&gt;
&lt;br /&gt;
Ahora crea nuevamente la carpeta base donde almacenaremos los archivos relacionados con Docker:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt; mkdir -p docker/images &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Creamos un Dockerfile vacío&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
touch docker/images/Dockerfile&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vamos a explicar los distintos apartados:&lt;br /&gt;
&lt;br /&gt;
=== FROM ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Indica la imagen base sobre la que se construirá la tuya.&lt;br /&gt;
En este caso, parte de una imagen ligera que ya tiene Python 3.12 instalado.&lt;br /&gt;
&lt;br /&gt;
=== WORKDIR ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta línea define el directorio de trabajo dentro del contenedor.&lt;br /&gt;
A partir de aquí, todos los comandos que aparezcan después (COPY, RUN, CMD, etc.) se ejecutarán dentro de /app.&lt;br /&gt;
&lt;br /&gt;
'''Ojo: /app no es la carpeta app/ de tu proyecto local.'''. Son cosas distintas.&lt;br /&gt;
&lt;br /&gt;
En tu máquina puede existir algo como:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
uvlhub&lt;br /&gt;
   app&lt;br /&gt;
   core&lt;br /&gt;
   requirements.txt &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Pero dentro del contenedor la estructura será distinta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
/ &lt;br /&gt;
  app/ &lt;br /&gt;
    app/ ← aquí dentro se habrá copiado tu carpeta local &amp;quot;app&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Cuando más adelante uses:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Docker copiará todos los archivos de tu proyecto dentro de la carpeta /app del contenedor.&lt;br /&gt;
Por eso el código quedará en /app/app/.&lt;br /&gt;
&lt;br /&gt;
=== COPY ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Copia todos los archivos del proyecto (del host) dentro del contenedor, en /app.&lt;br /&gt;
El primer punto es el origen; el segundo, el destino.&lt;br /&gt;
&lt;br /&gt;
=== RUN ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ejecuta comandos durante la construcción de la imagen.&lt;br /&gt;
Aquí instalamos las dependencias necesarias desde requirements.txt.&lt;br /&gt;
El resultado de este paso quedará guardado en la imagen.&lt;br /&gt;
&lt;br /&gt;
=== EXPOSE ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Documenta el puerto que la aplicación usa dentro del contenedor.&lt;br /&gt;
Esto no lo abre al exterior; simplemente indica que la app escucha en el 5000.&lt;br /&gt;
&lt;br /&gt;
=== CMD ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Define el comando que se ejecutará cuando el contenedor se inicie.&lt;br /&gt;
En este caso, ejecutará la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
=== RUN vs CMD ===&lt;br /&gt;
&lt;br /&gt;
Estas dos instrucciones parecen similares, pero hacen cosas muy distintas.&lt;br /&gt;
&lt;br /&gt;
==== RUN ====&lt;br /&gt;
Se ejecuta durante la construcción de la imagen (en el momento del docker build).&lt;br /&gt;
Cada RUN crea una nueva capa dentro de la imagen.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando instala las dependencias una sola vez, cuando se construye la imagen.&lt;br /&gt;
El resultado (las librerías instaladas) queda grabado dentro de la imagen final.&lt;br /&gt;
&lt;br /&gt;
Si después lanzas un contenedor nuevo a partir de esa imagen, las dependencias ya estarán instaladas y no se volverán a ejecutar.&lt;br /&gt;
&lt;br /&gt;
==== CMD ====&lt;br /&gt;
Se ejecuta cuando se inicia el contenedor (en el docker run).&lt;br /&gt;
Define el proceso principal que se ejecutará dentro del contenedor mientras esté activo.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando no se ejecuta durante la construcción, sino cada vez que arrancas el contenedor.&lt;br /&gt;
Si detienes el contenedor, este proceso también se detiene.&lt;br /&gt;
&lt;br /&gt;
==== Diferencia resumida ====&lt;br /&gt;
&lt;br /&gt;
RUN → se ejecuta al construir la imagen (fase de docker build).&lt;br /&gt;
&lt;br /&gt;
CMD → se ejecuta al iniciar el contenedor (fase de docker run).&lt;br /&gt;
&lt;br /&gt;
Solo puede haber un CMD por Dockerfile.&lt;br /&gt;
Si hay más de uno, solo se usará el último.&lt;br /&gt;
&lt;br /&gt;
== Dockerfile completo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
COPY . .&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto tiene que ir en un Dockerfile en docker/images&lt;br /&gt;
&lt;br /&gt;
== Construir la imagen ==&lt;br /&gt;
&lt;br /&gt;
Antes de construir la imagen, asegúrate de que ignoramos el módulo webhook:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
echo &amp;quot;webhook&amp;quot; &amp;gt; .moduleignore&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Desde la raíz del proyecto (no dentro de docker/images):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker build -t uvlhub:dev -f docker/images/Dockerfile .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Ejecutar el contenedor ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Qué es lo que observas?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 2: Conectando servicios =&lt;br /&gt;
&lt;br /&gt;
¡Vaya! ¡No funciona! Can't connect to MySQL server on 'db'... ¡Ah, claro, intenta conectarse a la base de datos, que no lo tenemos! Pero claro, en un contenedor no es recomendable tener la app y la base de datos conviviendo...&lt;br /&gt;
&lt;br /&gt;
== Crear contenedor MariaDB ==&lt;br /&gt;
&lt;br /&gt;
Antes que nada, haz Ctrl + C para parar el contenedor web. En este ejercicio aprenderás a lanzar un contenedor de MariaDB 12.0.2 de forma manual, configurando todas las variables necesarias para tu aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
El objetivo es entender la complejidad de hacerlo “a mano”, antes de automatizarlo con docker-compose.&lt;br /&gt;
&lt;br /&gt;
== Descargar la imagen oficial ==&lt;br /&gt;
Primero descarga la versión exacta que usaremos:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker pull mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Crear el contenedor de MariaDB ==&lt;br /&gt;
Lanza un contenedor con todas las variables necesarias para que Flask pueda conectarse correctamente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d --name mariadb_container \&lt;br /&gt;
 -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
 -e FLASK_ENV=development \&lt;br /&gt;
 -e DOMAIN=localhost \&lt;br /&gt;
 -e MARIADB_HOSTNAME=db \&lt;br /&gt;
 -e MARIADB_PORT=3306 \&lt;br /&gt;
 -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
 -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
 -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
 -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
 -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
 -e WORKING_DIR=/app/ \&lt;br /&gt;
 -p 3306:3306 mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Explicación:&lt;br /&gt;
&lt;br /&gt;
* -d: ejecuta el contenedor en segundo plano (modo detached).&lt;br /&gt;
* --name: asigna un nombre identificable al contenedor.&lt;br /&gt;
* -e: define variables de entorno dentro del contenedor.&lt;br /&gt;
* -p 3306:3306: publica el puerto de MariaDB en tu máquina local.&lt;br /&gt;
* mariadb:12.0.2: imagen exacta que vamos a usar.&lt;br /&gt;
&lt;br /&gt;
Aquí ya, de entrada, vemos que esto es más feo que una nevera por detrás. Para empezar, estamos pasando a lo loco un montón de variables que difílmente será replicable en producción, por contener información sensible. Aparte, si metemos nuevas variables, ese comando queda inservible. Necesitaremos una solución más ágil para este problema&lt;br /&gt;
&lt;br /&gt;
== Comprobar que está corriendo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver algo así:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES&lt;br /&gt;
abcd1234efgh mariadb:12.0.2 &amp;quot;docker-entrypoint.s…&amp;quot; Up 5 seconds 0.0.0.0:3306-&amp;gt;3306/tcp mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Conectarse a la base de datos ==&lt;br /&gt;
Abre una consola dentro del contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it mariadb_container bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y dentro, accede al cliente de MariaDB:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mariadb -u root -p&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Contraseña: uvlhubdb_root_password&lt;br /&gt;
&lt;br /&gt;
== Verificar la base de datos ==&lt;br /&gt;
Una vez dentro del cliente MySQL, ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
SHOW DATABASES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver que existen la bases de datos uvlhubdb porque pedimos crearla a la hora de levantar el contenedor. ¿Qué tiene la base de datos dentro? Vamos a verlo...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
USE uvlhubdb;&lt;br /&gt;
SHOW TABLES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Un total de... 0 tablas... ¡Claro! Es que solo hemos creado la base en sí, pero no las tablas. ¿Con un script SQL tal vez? ¡Espera, espera, si tenemos las migraciones! Pero claro, para eso necesito el contenedor de la app levantado. Sal de la consola SQL y del contenedor de MariaDB. Bien, ahora en el host...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta vez con el flag &amp;quot;-d&amp;quot; para que corra en segundo plano. Ahora, accedamos al contenedor y ejecutemos las migraciones. Pero... ¿cómo se llama mi contenedor?&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate que Docker asigna un nombre aleatorio a tu contenedor web porque no definiste ninguno. Una vez identificado el nombre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it &amp;lt;nombre&amp;gt; bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
What? ¿Qué pasa? ¿Otra vez error de conexión con la base? Si tenemos el contenedor funcionando, esto es un jaleo...&lt;br /&gt;
&lt;br /&gt;
== Redes internas ==&lt;br /&gt;
&lt;br /&gt;
Ahora mismo tienes dos contenedores aislados, el de la app y el la base de datos. Por defecto, cada contenedor tiene su propia red interna y no puede ver a los demás por nombre. El hostname '''db''' que usa Flask no existe en su DNS interno.&lt;br /&gt;
&lt;br /&gt;
=== Conectar servicios ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una red interna que compartan ambos contenedores&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker network create uvlhub_network&lt;br /&gt;
docker network ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vemos que ya aparece nuestra red '''uvlhub_network'''. Ahora, conectemos el contenedor de MariaDB a la red:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Claro, si te fijas, ahora hay que añadir, además de toda esa configuración engorrosa, que se añada en la misma red y que además tenga de hostname &amp;quot;db&amp;quot; para que el contenedor web entienda qué es &amp;quot;db&amp;quot;. Tenemos que hacer lo mismo para el contenedor web:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop &amp;lt;nombre&amp;gt;&lt;br /&gt;
docker rm &amp;lt;nombre&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y lo levantamos de nuevo, pero ya en la nueva red. De paso, le ponemos un nombre para identificarlo mejor.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 --name web_app_container --network uvlhub_network -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y ahora ya podemos acceder al contenedor por su nombre y ejecutar las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Mejor?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 3: Persistencia =&lt;br /&gt;
&lt;br /&gt;
Vamos a simular que hemos desplegado justo esto en producción y el servidor se reinicia por un corte de luz. Reiniciemos a mano cada contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker restart web_app_container&lt;br /&gt;
docker restart mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. What? ¿Cómo? ¿Qué ha pasado? ¿Otra vez problema de conexión? Ve dándole a F5... F5... ¡bingo! Ahora sí sale, pero menuda aleatoriedad, ¿no? Claro, es que ahora caes en que una cosa es que el contenedor haya arrancado y otra que esté listo.&lt;br /&gt;
&lt;br /&gt;
Aparte, tenemos otro problema. ¿Qué pasa con la persistencia? Porque si borro el contenedor de MariaDB, todos los datos que estén en su interior también se pierden. Y si estamos en producción, lío asegurado.&lt;br /&gt;
&lt;br /&gt;
== Volúmenes ==&lt;br /&gt;
&lt;br /&gt;
Cada vez que eliminas el contenedor de MariaDB, todas las bases de datos se pierden. Esto ocurre porque los datos se guardan dentro del sistema de archivos del contenedor, y al borrarlo desaparecen con él.&lt;br /&gt;
&lt;br /&gt;
En este ejercicio creas un volumen de Docker para que los datos persistan entre ejecuciones.&lt;br /&gt;
&lt;br /&gt;
=== Crear un volumen ===&lt;br /&gt;
Un volumen es una carpeta gestionada por Docker que vive fuera del ciclo de vida de los contenedores.&lt;br /&gt;
&lt;br /&gt;
Ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume create mariadb_data&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba que existe:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
La salida muestra algo como:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
DRIVER    VOLUME NAME&lt;br /&gt;
local     mariadb_data&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Crear un contenedor de MariaDB con el volumen ===&lt;br /&gt;
Lanza el contenedor y conecta el volumen a la ruta donde MariaDB guarda los datos (/var/lib/mysql):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -v mariadb_data:/var/lib/mysql \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Qué significa &amp;quot;:&amp;quot; en los volúmenes ===&lt;br /&gt;
&lt;br /&gt;
Cuando usas la opción '''-v''' o '''--volume''' en Docker, el signo dos puntos (''':''') separa dos rutas:&lt;br /&gt;
&lt;br /&gt;
'''-v origen:destino'''&lt;br /&gt;
&lt;br /&gt;
origen → es la ruta o el volumen en tu máquina (el host).&lt;br /&gt;
&lt;br /&gt;
destino → es la ruta dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 4: Orquestación de servicios =&lt;br /&gt;
&lt;br /&gt;
Hasta ahora hemos creado y conectado los contenedores manualmente.&lt;br /&gt;
Ahora vas a usar Docker Compose para definir todo en un solo archivo.&lt;br /&gt;
&lt;br /&gt;
Lo primero es parar y eliminar los contenedores previos para que no interfieran:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop $(docker ps -a -q)&lt;br /&gt;
docker rm $(docker ps -a -q)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Orquestación básica ==&lt;br /&gt;
&lt;br /&gt;
Crea un archivo llamado docker-compose.yml en la carpeta '''docker''' con este contenido:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    container_name: web_app_container&lt;br /&gt;
    env_file:&lt;br /&gt;
      - ../.env&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;5000&amp;quot;&lt;br /&gt;
    depends_on:&lt;br /&gt;
      - db&lt;br /&gt;
    build:&lt;br /&gt;
      context: ../&lt;br /&gt;
      dockerfile: docker/images/Dockerfile&lt;br /&gt;
    networks:&lt;br /&gt;
      - uvlhub_network&lt;br /&gt;
&lt;br /&gt;
  db:&lt;br /&gt;
    container_name: mariadb_container&lt;br /&gt;
    image: mariadb:12.0.2&lt;br /&gt;
    env_file:&lt;br /&gt;
      - ../.env&lt;br /&gt;
    volumes:&lt;br /&gt;
      - db_data:/var/lib/mysql&lt;br /&gt;
    networks:&lt;br /&gt;
      - uvlhub_network&lt;br /&gt;
&lt;br /&gt;
volumes:&lt;br /&gt;
  db_data:&lt;br /&gt;
&lt;br /&gt;
networks:&lt;br /&gt;
  uvlhub_network:&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate que esta orquestación de servicios me permite definir el nombre de cada servicio, su dependencia, sus puertos, sus volúmenes, redes... todo lo que hacíamos de forma manual y propenso a errores queda ahora más fácil de gestionar. Además, ahora ambos contenedores comparten el mismo archivo .env de variables de entorno.&lt;br /&gt;
&lt;br /&gt;
Bien, ahora vamos a levantar estos contenedores y acceder al contenedor web:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Inmediatamente después, ejecuta las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué ha ocurrido? Depende del tiempo que hayas tardado, puede que haya saltado un error o puede que haya funcionado. Puede que hayas intentado varias veces el comando anterior hasta que ha funcionado. ¿Por qué crees que ocurre esta aparente aleatoriedad?&lt;br /&gt;
&lt;br /&gt;
== Dependencias entre contenedores y entrypoints ==&lt;br /&gt;
&lt;br /&gt;
Fíjate que tenemos un &amp;quot;depends on&amp;quot; en el servicio web. Significa que el servicio &amp;quot;db&amp;quot; debe iniciarse antes que el servicio web. Pero, cuidado, ahí está la trampa, porque &amp;quot;servicio iniciado != servicio preparado&amp;quot;. El servicio de MariaDB tiene que arrancar, configurarse, etc...&lt;br /&gt;
&lt;br /&gt;
Cuando hicimos el comando COPY en nuestro Dockerfile, también copiamos la carpeta '''scripts'''. Uno de los scripts es '''wait-for-db.sh''', que realiza continuamente llamadas a MariaDB hasta que este servicio está listo para recibir peticiones. Ábrelo y estúdialo.&lt;br /&gt;
&lt;br /&gt;
Añade al servicio web de tu docker compose las siguientes líneas:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
entrypoint: [&amp;quot;/app/scripts/wait-for-db.sh&amp;quot;]&lt;br /&gt;
command: [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A su vez, del Dockerfile, hay que eliminar la línea de CMD que arrancaba nuestro servidor Flask.&lt;br /&gt;
&lt;br /&gt;
Luego, reinicializa todos los servicios:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. What? ¿No abre? Vamos a investigarlo, veamos el logs:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker logs web_app_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Nos debe salir un mensaje eterno de &amp;quot;/app/scripts/wait-for-db.sh: line 18: mariadb: command not found&amp;quot;. Esto ocurre porque en la imagen de nuestro servicio web NO TENEMOS el cliente de MariaDB. Debemos indicar entonces que lo instale. Añade justo debajo de FROM lo siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y --no-install-recommends mariadb-client &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Al ser un contenedor basado en Linux, y accediendo como root, es como si actualizáramos todos los paquetes y luego instalásemos el cliente de MariaDB. Intentémoslo otra vez:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. ¡Jopelines! ¿Tampoco? Si haces un &amp;quot;docker ps&amp;quot; verás el problema. El contenedor web no tiene expuesto ningún puerto.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    (...)&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;5000&amp;quot;&lt;br /&gt;
    (...)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;quot;Expose&amp;quot; hace visible el puerto '''entre contenedores''', pero '''NO entre tu contenedor y el host'''. Para eso usamos la directiva &amp;quot;ports&amp;quot;, que se encarga del mapping. En nuestro caso, será:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    (...)&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;5000:5000&amp;quot;&lt;br /&gt;
    (...)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
En ocasiones, si los dos puertos coinciden, se puede dejar uno solo con &amp;quot;5000&amp;quot; pero dejaremos los dos. El de la izquierda siempre es el puerto externo (host), el de la derecha es el puerto interno (contenedor). Así que cambia ese detalle y por última vez:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Recuerda ejecutar las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
exit&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que si falla, es porque la base aún no está lista, para eso tenemos el script... Y, ya que estamos, ¿se te ocurre alguna forma de automatizar este proceso? Es decir, que yo levante los contenedores y realice también las migraciones de la base de datos.&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. ¡Ahora sí!&lt;br /&gt;
&lt;br /&gt;
== Trabajando con CMD, entrypoints y commands ==&lt;br /&gt;
&lt;br /&gt;
Piensa que al arrancar un contenedor Docker, siempre se ejecuta un único comando final.&lt;br /&gt;
Lo que hacen ENTRYPOINT, CMD y command (de Compose) es decidir qué comando será ese y con qué argumentos.&lt;br /&gt;
&lt;br /&gt;
=== CMD → “qué hacer por defecto” ===&lt;br /&gt;
&lt;br /&gt;
* Está en el Dockerfile.&lt;br /&gt;
* Es el comando que Docker ejecuta si no se indica otra cosa. En nuestro caso:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== ENTRYPOINT → “qué se ejecuta siempre” ===&lt;br /&gt;
&lt;br /&gt;
* También va en el Dockerfile, o se puede sobreescribir en el docker-compose.yml.&lt;br /&gt;
* Define el programa o script principal que siempre se ejecuta.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
ENTRYPOINT [&amp;quot;/app/scripts/wait-for-db.sh&amp;quot;]&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Normalmente, el entrypoint se combina con CMD en modo &amp;quot;eh, ejecuta esto (entrypoint) y luego este comando final (cmd)&lt;br /&gt;
&lt;br /&gt;
Los entrypoints son muy útiles, sobre todo en los docker-compose's, porque puedo redefinir cómo quiero que inicie un contenedor en concreto '''SIN TENER QUE HACER UN BUILD DE NUEVO DE LA IMAGEN DE DOCKER'''. A diferencia de CMD, que viene predefinido, un entrypoint en mi docker-compose me da más versatilidad&lt;br /&gt;
&lt;br /&gt;
=== command (de docker-compose.yml) → “sobrescribe el CMD” ===&lt;br /&gt;
&lt;br /&gt;
En docker-compose.yml, el campo command: reemplaza al CMD del Dockerfile,pero no al ENTRYPOINT.&lt;br /&gt;
&lt;br /&gt;
== Combinación de elementos ==&lt;br /&gt;
&lt;br /&gt;
Es importante entender cómo Docker trabaja con la orquestación y con los contenedores:&lt;br /&gt;
&lt;br /&gt;
* Lee el YAML como un diccionario&lt;br /&gt;
* Ejecuta todo lo que haya en un Dockerfile EXCEPTUANDO directivas ENTRYPOINT y CMD&lt;br /&gt;
* Monta el volumen o los volúmenes de ese contenedor (si procede)&lt;br /&gt;
&lt;br /&gt;
Y ahora viene el detalle:&lt;br /&gt;
&lt;br /&gt;
* Si estamos levantando un contenedor a través de docker-compose, va a ejecutar los entrypoints y commands definidos en ese docker-compose.&lt;br /&gt;
* Un entrypoint definido en un docker-compose va a sustituir a un entrypoint definido en el Dockerfile&lt;br /&gt;
* Un command definido en un docker-compose va a sustituir a un CMD definido en el Dockerfile.&lt;br /&gt;
&lt;br /&gt;
Como consejo:&lt;br /&gt;
&lt;br /&gt;
* Definimos un entrypoint en el docker-compose. Esto es así porque está fuera de la imagen de Docker y nos permite redefinir el inicio del servicio sin modificar la imagen&lt;br /&gt;
* Eventualmente, definimos un CMD donde indicamos el comando final de ese contenedor al iniciar, que suele ser el arranque de Flask Server o de Gunicorn, dependiendo de si estamos en desarrollo o producción.&lt;br /&gt;
&lt;br /&gt;
No obstante, como nuestros entrypoints son todos scripts en bash, podemos definir en el mismo el arranque final (comando final) y nos olvidamos de CMD.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 5: Volumen de trabajo =&lt;br /&gt;
&lt;br /&gt;
== Problema de desarrollo ==&lt;br /&gt;
&lt;br /&gt;
Vale, ya parece que le hemos pillado el tranquillo a Docker y a Docker Compose. Ahora vamos a hacer algo como desarrolladores. Ve al archivo &amp;quot;app/templates/base_templates.html&amp;quot; y cambia la línea 22:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;title&amp;gt;{{ FLASK_APP_NAME }} - Repository of feature models in UVL &amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
por:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;title&amp;gt;Hello world! &amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. Fíjate en el nombre de la pestaña del navegador, ¿ha cambiado? ¿No? Tsss, esto de los contenedores... bueno, va, vamos a reiniciarlo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker restart web_app_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. ¿Tampoco? Anda que vaya idea la de usar Docker... Claro, es a la hora de construir la imagen, hicimos &amp;quot;COPY . .&amp;quot;, pero no hemos reconstruido nada, así que...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml build --no-cache web&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --force-recreate web&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Importante señalar que le pedimos que ignore la caché para que fuerce a copiar de nuevo todo el contenido (incluyendo el cambio de línea) al interior del contenedor.&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. ¡Ahora sí! Pero... ¿tengo que reconstruir TODO el contenedor en cada cambio línea? ¿Y eso es trabajar de forma ágil?&lt;br /&gt;
&lt;br /&gt;
== Crear volumen de trabajo (bind mount) ==&lt;br /&gt;
&lt;br /&gt;
Evidentemente, eso no tiene sentido, tenemos que usar un volumen de trabajo que nos ayude a sincronizar el código de nuestro host con el código interno del contenedor. Luego en nuestro docker-compose:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  web:&lt;br /&gt;
    (....)&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;5000:5000&amp;quot;&lt;br /&gt;
    volumes:&lt;br /&gt;
      - ../:/app&lt;br /&gt;
    (....)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
La línea&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
- ../:/app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Monta tu carpeta local del proyecto (../) dentro del contenedor, (/app). ¿Y por qué /app? Porque fue el espacio de trabajo que definimos en el Dockerfile. Como ves, la congruencia entre los Dockerfile y el docker-compose es importante.&lt;br /&gt;
&lt;br /&gt;
Recarguemos de nuevo nustra configuración:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ahora ya podemos trabajar con el código en host sin tener que reconstruir la imagen a cada cambio de línea.&lt;br /&gt;
&lt;br /&gt;
== Cuidado con COPY y los bind mounts ==&lt;br /&gt;
&lt;br /&gt;
Un '''bind mount''' (o “montaje de enlace”) es un tipo de volumen en el que Docker conecta directamente una carpeta de tu máquina (el host) con una carpeta dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
Cuando usas un bind mount (por ejemplo ../:/app), Docker monta tu carpeta local sobre la ruta /app del contenedor.&lt;br /&gt;
Eso significa que todo lo que estaba en /app dentro de la imagen desaparece al arrancar el contenedor, porque queda “oculto” por el volumen.&lt;br /&gt;
&lt;br /&gt;
Ahora bien, durante la construcción de la imagen usamos la directiva COPY. ¿Por qué? Porque necesitamos el archivo requirements.txt para instalar las dependencias de Python del proyecto. Pero... ¿realmente necesitamos copiar TODO? No, claro que no, podemos optar por copiar exclusivamente aquellos archivos que nos hagan falta durante '''LA CONSTRUCCIÓN''' de la imagen. El bind mount luego lo machacará.&lt;br /&gt;
&lt;br /&gt;
'''Muy importante:''' los bind mounts, al igual que cualquier otro volumen, están disponibles '''DESPUÉS''' de la construcción de la imagen. Es un error común creer que porque definamos un bind mount en un docker-compose, esos archivos aparecen ya para el contenedor. '''Esto es una fuente inagotable de problemas en la práctica'''.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 6: Distintas configuraciones de despliegue =&lt;br /&gt;
&lt;br /&gt;
Volvamos al punto de partida:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
rm -r docker&lt;br /&gt;
mv docker.bk docker&lt;br /&gt;
docker stop $(docker ps -a -q)&lt;br /&gt;
docker rm $(docker ps -a -q)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta práctica ha sido engorrosa a propósito para quitar esa &amp;quot;magia&amp;quot; que existe cuando lanzas un comando simple de orquestación y todo se levanta y funciona perfectamente. Esa situación está muy bien para desarrollar, pero no para aprender.&lt;br /&gt;
&lt;br /&gt;
Es importante que analices que existen distintos escenarios:&lt;br /&gt;
&lt;br /&gt;
* Hay un docker-compose para desarrollo.&lt;br /&gt;
* Hay 3 docker-compose para producción.&lt;br /&gt;
* Hay 6 Dockerfiles distintos&lt;br /&gt;
* Hay 3 entrypoints distintos&lt;br /&gt;
* Hay una carpeta llamada letsencrypt y otra llamada nginx. Dentro de nginx hay dos archivos de configuración dependiendo de si estamos en dev o en prod.&lt;br /&gt;
&lt;br /&gt;
== Autoevaluación ==&lt;br /&gt;
&lt;br /&gt;
Estudia bien cada archivo. Tómate tu tiempo e intenta responder a estas preguntas:&lt;br /&gt;
&lt;br /&gt;
* ¿Por qué en el docker-compose de dev NO se usa &amp;quot;ports&amp;quot; para enlazar con el host? (pista: nginx)&lt;br /&gt;
* ¿Qué es nginx y por qué es útil aquí?&lt;br /&gt;
* ¿Cuántos bind mounts usa el contenedor web en desarrollo? ¿Para qué sirve el bind mount del propio Docker?&lt;br /&gt;
* ¿Qué diferencia hay entre el entrypoint de desarrollo y el entrypoint de producción?&lt;br /&gt;
* ¿Por qué el servicio web en producción está montando todos esos bind mounts por separado? ¿No sería más sencillo hacer un volumen de todo el código?&lt;br /&gt;
&lt;br /&gt;
== Ejercicio propuesto ==&lt;br /&gt;
&lt;br /&gt;
* Levanta el entorno de Docker de desarrollo de uvlhub, siguendo la documentación oficial. Comprueba que puedes acceder a la app a través de localhost.&lt;br /&gt;
* Intenta correr los test de Selenium [https://docs.uvlhub.io/rosemary/testing/gui_tests#docker-environment-selenium-grid https://docs.uvlhub.io/rosemary/testing/gui_tests#docker-environment-selenium-grid]. Ten en cuenta que la forma de ejecutar y de visualizar los test de Selenium es distinta en un entorno Docker. Estudia bien la documentación, te hará falta para el desarrollo.&lt;br /&gt;
&lt;br /&gt;
== Comentarios finales ==&lt;br /&gt;
&lt;br /&gt;
=== Cuanto más se parezca el entorno de desarrollo al de producción, menos errores habrá ===&lt;br /&gt;
&lt;br /&gt;
En desarrollo es tentador usar configuraciones cómodas: servidor de Flask con --reload, bind mounts, base de datos vacía, logs en consola, etc.&lt;br /&gt;
Pero cuanto más difiere ese entorno de producción, más probable es que algo falle al desplegar.&lt;br /&gt;
&lt;br /&gt;
'''El código no falla por cambiar de servidor, sino porque los entornos se comportan distinto.'''&lt;br /&gt;
&lt;br /&gt;
Ejemplos clásicos:&lt;br /&gt;
&lt;br /&gt;
* En desarrollo usas SQLite, en producción MariaDB → aparecen errores de compatibilidad SQL.&lt;br /&gt;
* En desarrollo Flask sirve los archivos estáticos, en producción lo hace Nginx → las rutas cambian.&lt;br /&gt;
* En desarrollo ejecutas '''flask run''', en producción '''gunicorn''' → los hilos, workers o el reloader funcionan distinto.&lt;br /&gt;
&lt;br /&gt;
Por eso, el objetivo ideal es que tu entorno de desarrollo (dev), de preproducción (staging) y de producción (prod) sean idénticos salvo por las credenciales.&lt;br /&gt;
&lt;br /&gt;
Docker y docker-compose te permiten lograr eso: puedes usar la misma imagen base, cambiando solo el docker-compose (volúmenes, puertos, etc.).&lt;br /&gt;
&lt;br /&gt;
=== Diferencia entre el servidor de Flask y Gunicorn ===&lt;br /&gt;
&lt;br /&gt;
Flask trae incorporado un servidor de desarrollo.&lt;br /&gt;
Es muy útil para depurar porque:&lt;br /&gt;
&lt;br /&gt;
* recarga automáticamente el código (--reload),&lt;br /&gt;
* muestra errores detallados en el navegador,&lt;br /&gt;
* solo ejecuta un proceso.&lt;br /&gt;
&lt;br /&gt;
Sin embargo, no está pensado para producción:&lt;br /&gt;
&lt;br /&gt;
* no maneja múltiples conexiones concurrentes,&lt;br /&gt;
* no tiene control de procesos ni balanceo de carga,&lt;br /&gt;
* no implementa estándares de rendimiento o seguridad (como WSGI robusto).&lt;br /&gt;
&lt;br /&gt;
Ahí entra Gunicorn (Green Unicorn), que sí es un servidor WSGI  de producción (Web Server Gateway Interface, estándar de comunicación entre aplicaciones web Python y servidores web):&lt;br /&gt;
&lt;br /&gt;
* puede lanzar varios workers en paralelo,&lt;br /&gt;
* maneja múltiples peticiones concurrentes,&lt;br /&gt;
* se integra fácilmente con Nginx para servir contenido estático o HTTPS.&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10183</id>
		<title>Tutorial-docker</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10183"/>
				<updated>2025-10-26T16:27:54Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: /* Ejercicio 6: Distintas configuraciones de despliegue */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Ejercicio 1: Empezamos desde cero =&lt;br /&gt;
&lt;br /&gt;
El objetivo es eliminar toda la configuración previa y crear desde cero la estructura de carpetas que contendrá nuestros futuros archivos Docker.&lt;br /&gt;
&lt;br /&gt;
== Eliminar la configuración existente ==&lt;br /&gt;
Antes de comenzar, asegúrate de borrar cualquier carpeta Docker existente en el proyecto. En este caso, le haremos una copia de seguridad para poder volver a ella.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mv docker docker.bk&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
También debes asegurarte que trabajas con las variables de entorno adecuadas:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
cp .env.docker.example .env&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
⚠️ Este paso es importante: partimos completamente desde cero para entender cada parte del sistema.&lt;br /&gt;
&lt;br /&gt;
== Instalar Docker y Docker Compose (Linux) ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo apt update&lt;br /&gt;
sudo apt install -y apt-transport-https ca-certificates curl software-properties-common&lt;br /&gt;
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg&lt;br /&gt;
echo &amp;quot;deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable&amp;quot; | sudo tee /etc/apt/sources.list.d/docker.list &amp;gt; /dev/null&lt;br /&gt;
sudo apt update&lt;br /&gt;
sudo apt install -y docker-ce&lt;br /&gt;
sudo usermod -aG docker ${USER}&lt;br /&gt;
mkdir -p ~/.docker/cli-plugins/&lt;br /&gt;
LATEST_VERSION=$(curl -s https://api.github.com/repos/docker/compose/releases/latest | grep '&amp;quot;tag_name&amp;quot;:' | sed -E 's/.*&amp;quot;([^&amp;quot;]+)&amp;quot;.*/\1/')&lt;br /&gt;
curl -SL &amp;quot;https://github.com/docker/compose/releases/download/${LATEST_VERSION}/docker-compose-$(uname -s)-$(uname -m)&amp;quot; -o ~/.docker/cli-plugins/docker-compose&lt;br /&gt;
chmod +x ~/.docker/cli-plugins/docker-compose&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Es posible que tengas que reiniciar la sesión de usuario para poder usar docker sin necesidad de sudo.&lt;br /&gt;
&lt;br /&gt;
== Crear un Dockerfile mínimo ==&lt;br /&gt;
&lt;br /&gt;
Ahora crea nuevamente la carpeta base donde almacenaremos los archivos relacionados con Docker:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt; mkdir -p docker/images &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Creamos un Dockerfile vacío&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
touch docker/images/Dockerfile&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vamos a explicar los distintos apartados:&lt;br /&gt;
&lt;br /&gt;
=== FROM ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Indica la imagen base sobre la que se construirá la tuya.&lt;br /&gt;
En este caso, parte de una imagen ligera que ya tiene Python 3.12 instalado.&lt;br /&gt;
&lt;br /&gt;
=== WORKDIR ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta línea define el directorio de trabajo dentro del contenedor.&lt;br /&gt;
A partir de aquí, todos los comandos que aparezcan después (COPY, RUN, CMD, etc.) se ejecutarán dentro de /app.&lt;br /&gt;
&lt;br /&gt;
'''Ojo: /app no es la carpeta app/ de tu proyecto local.'''. Son cosas distintas.&lt;br /&gt;
&lt;br /&gt;
En tu máquina puede existir algo como:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
uvlhub&lt;br /&gt;
   app&lt;br /&gt;
   core&lt;br /&gt;
   requirements.txt &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Pero dentro del contenedor la estructura será distinta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
/ &lt;br /&gt;
  app/ &lt;br /&gt;
    app/ ← aquí dentro se habrá copiado tu carpeta local &amp;quot;app&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Cuando más adelante uses:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Docker copiará todos los archivos de tu proyecto dentro de la carpeta /app del contenedor.&lt;br /&gt;
Por eso el código quedará en /app/app/.&lt;br /&gt;
&lt;br /&gt;
=== COPY ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Copia todos los archivos del proyecto (del host) dentro del contenedor, en /app.&lt;br /&gt;
El primer punto es el origen; el segundo, el destino.&lt;br /&gt;
&lt;br /&gt;
=== RUN ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ejecuta comandos durante la construcción de la imagen.&lt;br /&gt;
Aquí instalamos las dependencias necesarias desde requirements.txt.&lt;br /&gt;
El resultado de este paso quedará guardado en la imagen.&lt;br /&gt;
&lt;br /&gt;
=== EXPOSE ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Documenta el puerto que la aplicación usa dentro del contenedor.&lt;br /&gt;
Esto no lo abre al exterior; simplemente indica que la app escucha en el 5000.&lt;br /&gt;
&lt;br /&gt;
=== CMD ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Define el comando que se ejecutará cuando el contenedor se inicie.&lt;br /&gt;
En este caso, ejecutará la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
=== RUN vs CMD ===&lt;br /&gt;
&lt;br /&gt;
Estas dos instrucciones parecen similares, pero hacen cosas muy distintas.&lt;br /&gt;
&lt;br /&gt;
==== RUN ====&lt;br /&gt;
Se ejecuta durante la construcción de la imagen (en el momento del docker build).&lt;br /&gt;
Cada RUN crea una nueva capa dentro de la imagen.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando instala las dependencias una sola vez, cuando se construye la imagen.&lt;br /&gt;
El resultado (las librerías instaladas) queda grabado dentro de la imagen final.&lt;br /&gt;
&lt;br /&gt;
Si después lanzas un contenedor nuevo a partir de esa imagen, las dependencias ya estarán instaladas y no se volverán a ejecutar.&lt;br /&gt;
&lt;br /&gt;
==== CMD ====&lt;br /&gt;
Se ejecuta cuando se inicia el contenedor (en el docker run).&lt;br /&gt;
Define el proceso principal que se ejecutará dentro del contenedor mientras esté activo.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando no se ejecuta durante la construcción, sino cada vez que arrancas el contenedor.&lt;br /&gt;
Si detienes el contenedor, este proceso también se detiene.&lt;br /&gt;
&lt;br /&gt;
==== Diferencia resumida ====&lt;br /&gt;
&lt;br /&gt;
RUN → se ejecuta al construir la imagen (fase de docker build).&lt;br /&gt;
&lt;br /&gt;
CMD → se ejecuta al iniciar el contenedor (fase de docker run).&lt;br /&gt;
&lt;br /&gt;
Solo puede haber un CMD por Dockerfile.&lt;br /&gt;
Si hay más de uno, solo se usará el último.&lt;br /&gt;
&lt;br /&gt;
== Dockerfile completo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
COPY . .&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto tiene que ir en un Dockerfile en docker/images&lt;br /&gt;
&lt;br /&gt;
== Construir la imagen ==&lt;br /&gt;
Desde la raíz del proyecto (no dentro de docker/images):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker build -t uvlhub:dev -f docker/images/Dockerfile .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Ejecutar el contenedor ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Qué es lo que observas?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 2: Conectando servicios =&lt;br /&gt;
&lt;br /&gt;
¡Vaya! ¡No funciona! Can't connect to MySQL server on 'db'... ¡Ah, claro, intenta conectarse a la base de datos, que no lo tenemos! Pero claro, en un contenedor no es recomendable tener la app y la base de datos conviviendo...&lt;br /&gt;
&lt;br /&gt;
== Crear contenedor MariaDB ==&lt;br /&gt;
&lt;br /&gt;
Antes que nada, haz Ctrl + C para parar el contenedor web. En este ejercicio aprenderás a lanzar un contenedor de MariaDB 12.0.2 de forma manual, configurando todas las variables necesarias para tu aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
El objetivo es entender la complejidad de hacerlo “a mano”, antes de automatizarlo con docker-compose.&lt;br /&gt;
&lt;br /&gt;
== Descargar la imagen oficial ==&lt;br /&gt;
Primero descarga la versión exacta que usaremos:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker pull mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Crear el contenedor de MariaDB ==&lt;br /&gt;
Lanza un contenedor con todas las variables necesarias para que Flask pueda conectarse correctamente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d --name mariadb_container \&lt;br /&gt;
 -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
 -e FLASK_ENV=development \&lt;br /&gt;
 -e DOMAIN=localhost \&lt;br /&gt;
 -e MARIADB_HOSTNAME=db \&lt;br /&gt;
 -e MARIADB_PORT=3306 \&lt;br /&gt;
 -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
 -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
 -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
 -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
 -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
 -e WORKING_DIR=/app/ \&lt;br /&gt;
 -p 3306:3306 mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Explicación:&lt;br /&gt;
&lt;br /&gt;
* -d: ejecuta el contenedor en segundo plano (modo detached).&lt;br /&gt;
* --name: asigna un nombre identificable al contenedor.&lt;br /&gt;
* -e: define variables de entorno dentro del contenedor.&lt;br /&gt;
* -p 3306:3306: publica el puerto de MariaDB en tu máquina local.&lt;br /&gt;
* mariadb:12.0.2: imagen exacta que vamos a usar.&lt;br /&gt;
&lt;br /&gt;
Aquí ya, de entrada, vemos que esto es más feo que una nevera por detrás. Para empezar, estamos pasando a lo loco un montón de variables que difílmente será replicable en producción, por contener información sensible. Aparte, si metemos nuevas variables, ese comando queda inservible. Necesitaremos una solución más ágil para este problema&lt;br /&gt;
&lt;br /&gt;
== Comprobar que está corriendo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver algo así:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES&lt;br /&gt;
abcd1234efgh mariadb:12.0.2 &amp;quot;docker-entrypoint.s…&amp;quot; Up 5 seconds 0.0.0.0:3306-&amp;gt;3306/tcp mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Conectarse a la base de datos ==&lt;br /&gt;
Abre una consola dentro del contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it mariadb_container bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y dentro, accede al cliente de MariaDB:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mariadb -u root -p&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Contraseña: uvlhubdb_root_password&lt;br /&gt;
&lt;br /&gt;
== Verificar la base de datos ==&lt;br /&gt;
Una vez dentro del cliente MySQL, ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
SHOW DATABASES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver que existen la bases de datos uvlhubdb porque pedimos crearla a la hora de levantar el contenedor. ¿Qué tiene la base de datos dentro? Vamos a verlo...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
USE uvlhubdb;&lt;br /&gt;
SHOW TABLES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Un total de... 0 tablas... ¡Claro! Es que solo hemos creado la base en sí, pero no las tablas. ¿Con un script SQL tal vez? ¡Espera, espera, si tenemos las migraciones! Pero claro, para eso necesito el contenedor de la app levantado. Sal de la consola SQL y del contenedor de MariaDB. Bien, ahora en el host...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta vez con el flag &amp;quot;-d&amp;quot; para que corra en segundo plano. Ahora, accedamos al contenedor y ejecutemos las migraciones. Pero... ¿cómo se llama mi contenedor?&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate que Docker asigna un nombre aleatorio a tu contenedor web porque no definiste ninguno. Una vez identificado el nombre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it &amp;lt;nombre&amp;gt; bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
What? ¿Qué pasa? ¿Otra vez error de conexión con la base? Si tenemos el contenedor funcionando, esto es un jaleo...&lt;br /&gt;
&lt;br /&gt;
== Redes internas ==&lt;br /&gt;
&lt;br /&gt;
Ahora mismo tienes dos contenedores aislados, el de la app y el la base de datos. Por defecto, cada contenedor tiene su propia red interna y no puede ver a los demás por nombre. El hostname '''db''' que usa Flask no existe en su DNS interno.&lt;br /&gt;
&lt;br /&gt;
=== Conectar servicios ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una red interna que compartan ambos contenedores&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker network create uvlhub_network&lt;br /&gt;
docker network ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vemos que ya aparece nuestra red '''uvlhub_network'''. Ahora, conectemos el contenedor de MariaDB a la red:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Claro, si te fijas, ahora hay que añadir, además de toda esa configuración engorrosa, que se añada en la misma red y que además tenga de hostname &amp;quot;db&amp;quot; para que el contenedor web entienda qué es &amp;quot;db&amp;quot;. Tenemos que hacer lo mismo para el contenedor web:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop &amp;lt;nombre&amp;gt;&lt;br /&gt;
docker rm &amp;lt;nombre&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y lo levantamos de nuevo, pero ya en la nueva red. De paso, le ponemos un nombre para identificarlo mejor.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 --name web_app_container --network uvlhub_network -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y ahora ya podemos acceder al contenedor por su nombre y ejecutar las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Mejor?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 3: Persistencia =&lt;br /&gt;
&lt;br /&gt;
Vamos a simular que hemos desplegado justo esto en producción y el servidor se reinicia por un corte de luz. Reiniciemos a mano cada contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker restart web_app_container&lt;br /&gt;
docker restart mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. What? ¿Cómo? ¿Qué ha pasado? ¿Otra vez problema de conexión? Ve dándole a F5... F5... ¡bingo! Ahora sí sale, pero menuda aleatoriedad, ¿no? Claro, es que ahora caes en que una cosa es que el contenedor haya arrancado y otra que esté listo.&lt;br /&gt;
&lt;br /&gt;
Aparte, tenemos otro problema. ¿Qué pasa con la persistencia? Porque si borro el contenedor de MariaDB, todos los datos que estén en su interior también se pierden. Y si estamos en producción, lío asegurado.&lt;br /&gt;
&lt;br /&gt;
== Volúmenes ==&lt;br /&gt;
&lt;br /&gt;
Cada vez que eliminas el contenedor de MariaDB, todas las bases de datos se pierden. Esto ocurre porque los datos se guardan dentro del sistema de archivos del contenedor, y al borrarlo desaparecen con él.&lt;br /&gt;
&lt;br /&gt;
En este ejercicio creas un volumen de Docker para que los datos persistan entre ejecuciones.&lt;br /&gt;
&lt;br /&gt;
=== Crear un volumen ===&lt;br /&gt;
Un volumen es una carpeta gestionada por Docker que vive fuera del ciclo de vida de los contenedores.&lt;br /&gt;
&lt;br /&gt;
Ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume create mariadb_data&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba que existe:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
La salida muestra algo como:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
DRIVER    VOLUME NAME&lt;br /&gt;
local     mariadb_data&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Crear un contenedor de MariaDB con el volumen ===&lt;br /&gt;
Lanza el contenedor y conecta el volumen a la ruta donde MariaDB guarda los datos (/var/lib/mysql):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -v mariadb_data:/var/lib/mysql \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Qué significa &amp;quot;:&amp;quot; en los volúmenes ===&lt;br /&gt;
&lt;br /&gt;
Cuando usas la opción '''-v''' o '''--volume''' en Docker, el signo dos puntos (''':''') separa dos rutas:&lt;br /&gt;
&lt;br /&gt;
'''-v origen:destino'''&lt;br /&gt;
&lt;br /&gt;
origen → es la ruta o el volumen en tu máquina (el host).&lt;br /&gt;
&lt;br /&gt;
destino → es la ruta dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 4: Orquestación de servicios =&lt;br /&gt;
&lt;br /&gt;
Hasta ahora hemos creado y conectado los contenedores manualmente.&lt;br /&gt;
Ahora vas a usar Docker Compose para definir todo en un solo archivo.&lt;br /&gt;
&lt;br /&gt;
Lo primero es parar y eliminar los contenedores previos para que no interfieran:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop $(docker ps -a -q)&lt;br /&gt;
docker rm $(docker ps -a -q)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Orquestación básica ==&lt;br /&gt;
&lt;br /&gt;
Crea un archivo llamado docker-compose.yml en la carpeta '''docker''' con este contenido:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    container_name: web_app_container&lt;br /&gt;
    env_file:&lt;br /&gt;
      - ../.env&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;5000&amp;quot;&lt;br /&gt;
    depends_on:&lt;br /&gt;
      - db&lt;br /&gt;
    build:&lt;br /&gt;
      context: ../&lt;br /&gt;
      dockerfile: docker/images/Dockerfile&lt;br /&gt;
    networks:&lt;br /&gt;
      - uvlhub_network&lt;br /&gt;
&lt;br /&gt;
  db:&lt;br /&gt;
    container_name: mariadb_container&lt;br /&gt;
    image: mariadb:12.0.2&lt;br /&gt;
    env_file:&lt;br /&gt;
      - ../.env&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;3306:3306&amp;quot;&lt;br /&gt;
    volumes:&lt;br /&gt;
      - db_data:/var/lib/mysql&lt;br /&gt;
    networks:&lt;br /&gt;
      - uvlhub_network&lt;br /&gt;
&lt;br /&gt;
volumes:&lt;br /&gt;
  db_data:&lt;br /&gt;
&lt;br /&gt;
networks:&lt;br /&gt;
  uvlhub_network:&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate que esta orquestación de servicios me permite definir el nombre de cada servicio, su dependencia, sus puertos, sus volúmenes, redes... todo lo que hacíamos de forma manual y propenso a errores queda ahora más fácil de gestionar. Además, ahora ambos contenedores comparten el mismo archivo .env de variables de entorno.&lt;br /&gt;
&lt;br /&gt;
Bien, ahora vamos a levantar estos contenedores y acceder al contenedor web:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Inmediatamente después, ejecuta las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué ha ocurrido? Depende del tiempo que hayas tardado, puede que haya saltado un error o puede que haya funcionado. Puede que hayas intentado varias veces el comando anterior hasta que ha funcionado. ¿Por qué crees que ocurre esta aparente aleatoriedad?&lt;br /&gt;
&lt;br /&gt;
== Dependencias entre contenedores y entrypoints ==&lt;br /&gt;
&lt;br /&gt;
Fíjate que tenemos un &amp;quot;depends on&amp;quot; en el servicio web. Significa que el servicio &amp;quot;db&amp;quot; debe iniciarse antes que el servicio web. Pero, cuidado, ahí está la trampa, porque &amp;quot;servicio iniciado != servicio preparado&amp;quot;. El servicio de MariaDB tiene que arrancar, configurarse, etc...&lt;br /&gt;
&lt;br /&gt;
Cuando hicimos el comando COPY en nuestro Dockerfile, también copiamos la carpeta '''scripts'''. Uno de los scripts es '''wait-for-db.sh''', que realiza continuamente llamadas a MariaDB hasta que este servicio está listo para recibir peticiones. Ábrelo y estúdialo.&lt;br /&gt;
&lt;br /&gt;
Añade al servicio web de tu docker compose las siguientes líneas:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
entrypoint: [&amp;quot;/app/scripts/wait-for-db.sh&amp;quot;]&lt;br /&gt;
command: [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A su vez, del Dockerfile, hay que eliminar la línea de CMD que arrancaba nuestro servidor Flask.&lt;br /&gt;
&lt;br /&gt;
Luego, reinicializa todos los servicios:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. What? ¿No abre? Vamos a investigarlo, veamos el logs:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker logs web_app_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Nos debe salir un mensaje eterno de &amp;quot;/app/scripts/wait-for-db.sh: line 18: mariadb: command not found&amp;quot;. Esto ocurre porque en la imagen de nuestro servicio web NO TENEMOS el cliente de MariaDB. Debemos indicar entonces que lo instale. Añade justo debajo de FROM lo siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y --no-install-recommends mariadb-client &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Al ser un contenedor basado en Linux, y accediendo como root, es como si actualizáramos todos los paquetes y luego instalásemos el cliente de MariaDB. Intentémoslo otra vez:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. ¡Jopelines! ¿Tampoco? Si haces un &amp;quot;docker ps&amp;quot; verás el problema. El contenedor web no tiene expuesto ningún puerto.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    (...)&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;5000&amp;quot;&lt;br /&gt;
    (...)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;quot;Expose&amp;quot; hace visible el puerto '''entre contenedores''', pero '''NO entre tu contenedor y el host'''. Para eso usamos la directiva &amp;quot;ports&amp;quot;, que se encarga del mapping. En nuestro caso, será:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    (...)&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;5000:5000&amp;quot;&lt;br /&gt;
    (...)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
En ocasiones, si los dos puertos coinciden, se puede dejar uno solo con &amp;quot;5000&amp;quot; pero dejaremos los dos. El de la izquierda siempre es el puerto externo (host), el de la derecha es el puerto interno (contenedor). Así que cambia ese detalle y por última vez:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Recuerda ejecutar las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
exit&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que si falla, es porque la base aún no está lista, para eso tenemos el script... Y, ya que estamos, ¿se te ocurre alguna forma de automatizar este proceso? Es decir, que yo levante los contenedores y realice también las migraciones de la base de datos.&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. ¡Ahora sí!&lt;br /&gt;
&lt;br /&gt;
== Trabajando con CMD, entrypoints y commands ==&lt;br /&gt;
&lt;br /&gt;
Piensa que al arrancar un contenedor Docker, siempre se ejecuta un único comando final.&lt;br /&gt;
Lo que hacen ENTRYPOINT, CMD y command (de Compose) es decidir qué comando será ese y con qué argumentos.&lt;br /&gt;
&lt;br /&gt;
=== CMD → “qué hacer por defecto” ===&lt;br /&gt;
&lt;br /&gt;
* Está en el Dockerfile.&lt;br /&gt;
* Es el comando que Docker ejecuta si no se indica otra cosa. En nuestro caso:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== ENTRYPOINT → “qué se ejecuta siempre” ===&lt;br /&gt;
&lt;br /&gt;
* También va en el Dockerfile, o se puede sobreescribir en el docker-compose.yml.&lt;br /&gt;
* Define el programa o script principal que siempre se ejecuta.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
ENTRYPOINT [&amp;quot;/app/scripts/wait-for-db.sh&amp;quot;]&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Normalmente, el entrypoint se combina con CMD en modo &amp;quot;eh, ejecuta esto (entrypoint) y luego este comando final (cmd)&lt;br /&gt;
&lt;br /&gt;
Los entrypoints son muy útiles, sobre todo en los docker-compose's, porque puedo redefinir cómo quiero que inicie un contenedor en concreto '''SIN TENER QUE HACER UN BUILD DE NUEVO DE LA IMAGEN DE DOCKER'''. A diferencia de CMD, que viene predefinido, un entrypoint en mi docker-compose me da más versatilidad&lt;br /&gt;
&lt;br /&gt;
=== command (de docker-compose.yml) → “sobrescribe el CMD” ===&lt;br /&gt;
&lt;br /&gt;
En docker-compose.yml, el campo command: reemplaza al CMD del Dockerfile,pero no al ENTRYPOINT.&lt;br /&gt;
&lt;br /&gt;
== Combinación de elementos ==&lt;br /&gt;
&lt;br /&gt;
Es importante entender cómo Docker trabaja con la orquestación y con los contenedores:&lt;br /&gt;
&lt;br /&gt;
* Lee el YAML como un diccionario&lt;br /&gt;
* Ejecuta todo lo que haya en un Dockerfile EXCEPTUANDO directivas ENTRYPOINT y CMD&lt;br /&gt;
* Monta el volumen o los volúmenes de ese contenedor (si procede)&lt;br /&gt;
&lt;br /&gt;
Y ahora viene el detalle:&lt;br /&gt;
&lt;br /&gt;
* Si estamos levantando un contenedor a través de docker-compose, va a ejecutar los entrypoints y commands definidos en ese docker-compose.&lt;br /&gt;
* Un entrypoint definido en un docker-compose va a sustituir a un entrypoint definido en el Dockerfile&lt;br /&gt;
* Un command definido en un docker-compose va a sustituir a un CMD definido en el Dockerfile.&lt;br /&gt;
&lt;br /&gt;
Como consejo:&lt;br /&gt;
&lt;br /&gt;
* Definimos un entrypoint en el docker-compose. Esto es así porque está fuera de la imagen de Docker y nos permite redefinir el inicio del servicio sin modificar la imagen&lt;br /&gt;
* Eventualmente, definimos un CMD donde indicamos el comando final de ese contenedor al iniciar, que suele ser el arranque de Flask Server o de Gunicorn, dependiendo de si estamos en desarrollo o producción.&lt;br /&gt;
&lt;br /&gt;
No obstante, como nuestros entrypoints son todos scripts en bash, podemos definir en el mismo el arranque final (comando final) y nos olvidamos de CMD.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 5: Volumen de trabajo =&lt;br /&gt;
&lt;br /&gt;
== Problema de desarrollo ==&lt;br /&gt;
&lt;br /&gt;
Vale, ya parece que le hemos pillado el tranquillo a Docker y a Docker Compose. Ahora vamos a hacer algo como desarrolladores. Ve al archivo &amp;quot;app/templates/base_templates.html&amp;quot; y cambia la línea 22:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;title&amp;gt;{{ FLASK_APP_NAME }} - Repository of feature models in UVL &amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
por:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;title&amp;gt;Hello world! &amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. Fíjate en el nombre de la pestaña del navegador, ¿ha cambiado? ¿No? Tsss, esto de los contenedores... bueno, va, vamos a reiniciarlo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker restart web_app_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. ¿Tampoco? Anda que vaya idea la de usar Docker... Claro, es a la hora de construir la imagen, hicimos &amp;quot;COPY . .&amp;quot;, pero no hemos reconstruido nada, así que...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml build --no-cache web&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --force-recreate web&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Importante señalar que le pedimos que ignore la caché para que fuerce a copiar de nuevo todo el contenido (incluyendo el cambio de línea) al interior del contenedor.&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. ¡Ahora sí! Pero... ¿tengo que reconstruir TODO el contenedor en cada cambio línea? ¿Y eso es trabajar de forma ágil?&lt;br /&gt;
&lt;br /&gt;
== Crear volumen de trabajo (bind mount) ==&lt;br /&gt;
&lt;br /&gt;
Evidentemente, eso no tiene sentido, tenemos que usar un volumen de trabajo que nos ayude a sincronizar el código de nuestro host con el código interno del contenedor. Luego en nuestro docker-compose:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  web:&lt;br /&gt;
    (....)&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;5000:5000&amp;quot;&lt;br /&gt;
    volumes:&lt;br /&gt;
      - ../:/app&lt;br /&gt;
    (....)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
La línea&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
- ../:/app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Monta tu carpeta local del proyecto (../) dentro del contenedor, (/app). ¿Y por qué /app? Porque fue el espacio de trabajo que definimos en el Dockerfile. Como ves, la congruencia entre los Dockerfile y el docker-compose es importante.&lt;br /&gt;
&lt;br /&gt;
Recarguemos de nuevo nustra configuración:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ahora ya podemos trabajar con el código en host sin tener que reconstruir la imagen a cada cambio de línea.&lt;br /&gt;
&lt;br /&gt;
== Cuidado con COPY y los bind mounts ==&lt;br /&gt;
&lt;br /&gt;
Un '''bind mount''' (o “montaje de enlace”) es un tipo de volumen en el que Docker conecta directamente una carpeta de tu máquina (el host) con una carpeta dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
Cuando usas un bind mount (por ejemplo ../:/app), Docker monta tu carpeta local sobre la ruta /app del contenedor.&lt;br /&gt;
Eso significa que todo lo que estaba en /app dentro de la imagen desaparece al arrancar el contenedor, porque queda “oculto” por el volumen.&lt;br /&gt;
&lt;br /&gt;
Ahora bien, durante la construcción de la imagen usamos la directiva COPY. ¿Por qué? Porque necesitamos el archivo requirements.txt para instalar las dependencias de Python del proyecto. Pero... ¿realmente necesitamos copiar TODO? No, claro que no, podemos optar por copiar exclusivamente aquellos archivos que nos hagan falta durante '''LA CONSTRUCCIÓN''' de la imagen. El bind mount luego lo machacará.&lt;br /&gt;
&lt;br /&gt;
'''Muy importante:''' los bind mounts, al igual que cualquier otro volumen, están disponibles '''DESPUÉS''' de la construcción de la imagen. Es un error común creer que porque definamos un bind mount en un docker-compose, esos archivos aparecen ya para el contenedor. '''Esto es una fuente inagotable de problemas en la práctica'''.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 6: Distintas configuraciones de despliegue =&lt;br /&gt;
&lt;br /&gt;
Volvamos al punto de partida:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
rm -r docker&lt;br /&gt;
mv docker.bk docker&lt;br /&gt;
docker stop $(docker ps -a -q)&lt;br /&gt;
docker rm $(docker ps -a -q)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta práctica ha sido engorrosa a propósito para quitar esa &amp;quot;magia&amp;quot; que existe cuando lanzas un comando simple de orquestación y todo se levanta y funciona perfectamente. Esa situación está muy bien para desarrollar, pero no para aprender.&lt;br /&gt;
&lt;br /&gt;
Es importante que analices que existen distintos escenarios:&lt;br /&gt;
&lt;br /&gt;
* Hay un docker-compose para desarrollo.&lt;br /&gt;
* Hay 3 docker-compose para producción.&lt;br /&gt;
* Hay 6 Dockerfiles distintos&lt;br /&gt;
* Hay 3 entrypoints distintos&lt;br /&gt;
* Hay una carpeta llamada letsencrypt y otra llamada nginx. Dentro de nginx hay dos archivos de configuración dependiendo de si estamos en dev o en prod.&lt;br /&gt;
&lt;br /&gt;
== Autoevaluación ==&lt;br /&gt;
&lt;br /&gt;
Estudia bien cada archivo. Tómate tu tiempo e intenta responder a estas preguntas:&lt;br /&gt;
&lt;br /&gt;
* ¿Por qué en el docker-compose de dev NO se usa &amp;quot;ports&amp;quot; para enlazar con el host? (pista: nginx)&lt;br /&gt;
* ¿Qué es nginx y por qué es útil aquí?&lt;br /&gt;
* ¿Cuántos bind mounts usa el contenedor web en desarrollo? ¿Para qué sirve el bind mount del propio Docker?&lt;br /&gt;
* ¿Qué diferencia hay entre el entrypoint de desarrollo y el entrypoint de producción?&lt;br /&gt;
* ¿Por qué el servicio web en producción está montando todos esos bind mounts por separado? ¿No sería más sencillo hacer un volumen de todo el código?&lt;br /&gt;
&lt;br /&gt;
== Ejercicio propuesto ==&lt;br /&gt;
&lt;br /&gt;
* Levanta el entorno de Docker de desarrollo de uvlhub, siguendo la documentación oficial. Comprueba que puedes acceder a la app a través de localhost.&lt;br /&gt;
* Intenta correr los test de Selenium [https://docs.uvlhub.io/rosemary/testing/gui_tests#docker-environment-selenium-grid https://docs.uvlhub.io/rosemary/testing/gui_tests#docker-environment-selenium-grid]. Ten en cuenta que la forma de ejecutar y de visualizar los test de Selenium es distinta en un entorno Docker. Estudia bien la documentación, te hará falta para el desarrollo.&lt;br /&gt;
&lt;br /&gt;
== Comentarios finales ==&lt;br /&gt;
&lt;br /&gt;
=== Cuanto más se parezca el entorno de desarrollo al de producción, menos errores habrá ===&lt;br /&gt;
&lt;br /&gt;
En desarrollo es tentador usar configuraciones cómodas: servidor de Flask con --reload, bind mounts, base de datos vacía, logs en consola, etc.&lt;br /&gt;
Pero cuanto más difiere ese entorno de producción, más probable es que algo falle al desplegar.&lt;br /&gt;
&lt;br /&gt;
'''El código no falla por cambiar de servidor, sino porque los entornos se comportan distinto.'''&lt;br /&gt;
&lt;br /&gt;
Ejemplos clásicos:&lt;br /&gt;
&lt;br /&gt;
* En desarrollo usas SQLite, en producción MariaDB → aparecen errores de compatibilidad SQL.&lt;br /&gt;
* En desarrollo Flask sirve los archivos estáticos, en producción lo hace Nginx → las rutas cambian.&lt;br /&gt;
* En desarrollo ejecutas '''flask run''', en producción '''gunicorn''' → los hilos, workers o el reloader funcionan distinto.&lt;br /&gt;
&lt;br /&gt;
Por eso, el objetivo ideal es que tu entorno de desarrollo (dev), de preproducción (staging) y de producción (prod) sean idénticos salvo por las credenciales.&lt;br /&gt;
&lt;br /&gt;
Docker y docker-compose te permiten lograr eso: puedes usar la misma imagen base, cambiando solo el docker-compose (volúmenes, puertos, etc.).&lt;br /&gt;
&lt;br /&gt;
=== Diferencia entre el servidor de Flask y Gunicorn ===&lt;br /&gt;
&lt;br /&gt;
Flask trae incorporado un servidor de desarrollo.&lt;br /&gt;
Es muy útil para depurar porque:&lt;br /&gt;
&lt;br /&gt;
* recarga automáticamente el código (--reload),&lt;br /&gt;
* muestra errores detallados en el navegador,&lt;br /&gt;
* solo ejecuta un proceso.&lt;br /&gt;
&lt;br /&gt;
Sin embargo, no está pensado para producción:&lt;br /&gt;
&lt;br /&gt;
* no maneja múltiples conexiones concurrentes,&lt;br /&gt;
* no tiene control de procesos ni balanceo de carga,&lt;br /&gt;
* no implementa estándares de rendimiento o seguridad (como WSGI robusto).&lt;br /&gt;
&lt;br /&gt;
Ahí entra Gunicorn (Green Unicorn), que sí es un servidor WSGI  de producción (Web Server Gateway Interface, estándar de comunicación entre aplicaciones web Python y servidores web):&lt;br /&gt;
&lt;br /&gt;
* puede lanzar varios workers en paralelo,&lt;br /&gt;
* maneja múltiples peticiones concurrentes,&lt;br /&gt;
* se integra fácilmente con Nginx para servir contenido estático o HTTPS.&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10182</id>
		<title>Tutorial-docker</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10182"/>
				<updated>2025-10-26T16:23:25Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: /* Dependencias entre contenedores y entrypoints */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Ejercicio 1: Empezamos desde cero =&lt;br /&gt;
&lt;br /&gt;
El objetivo es eliminar toda la configuración previa y crear desde cero la estructura de carpetas que contendrá nuestros futuros archivos Docker.&lt;br /&gt;
&lt;br /&gt;
== Eliminar la configuración existente ==&lt;br /&gt;
Antes de comenzar, asegúrate de borrar cualquier carpeta Docker existente en el proyecto. En este caso, le haremos una copia de seguridad para poder volver a ella.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mv docker docker.bk&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
También debes asegurarte que trabajas con las variables de entorno adecuadas:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
cp .env.docker.example .env&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
⚠️ Este paso es importante: partimos completamente desde cero para entender cada parte del sistema.&lt;br /&gt;
&lt;br /&gt;
== Instalar Docker y Docker Compose (Linux) ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo apt update&lt;br /&gt;
sudo apt install -y apt-transport-https ca-certificates curl software-properties-common&lt;br /&gt;
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg&lt;br /&gt;
echo &amp;quot;deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable&amp;quot; | sudo tee /etc/apt/sources.list.d/docker.list &amp;gt; /dev/null&lt;br /&gt;
sudo apt update&lt;br /&gt;
sudo apt install -y docker-ce&lt;br /&gt;
sudo usermod -aG docker ${USER}&lt;br /&gt;
mkdir -p ~/.docker/cli-plugins/&lt;br /&gt;
LATEST_VERSION=$(curl -s https://api.github.com/repos/docker/compose/releases/latest | grep '&amp;quot;tag_name&amp;quot;:' | sed -E 's/.*&amp;quot;([^&amp;quot;]+)&amp;quot;.*/\1/')&lt;br /&gt;
curl -SL &amp;quot;https://github.com/docker/compose/releases/download/${LATEST_VERSION}/docker-compose-$(uname -s)-$(uname -m)&amp;quot; -o ~/.docker/cli-plugins/docker-compose&lt;br /&gt;
chmod +x ~/.docker/cli-plugins/docker-compose&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Es posible que tengas que reiniciar la sesión de usuario para poder usar docker sin necesidad de sudo.&lt;br /&gt;
&lt;br /&gt;
== Crear un Dockerfile mínimo ==&lt;br /&gt;
&lt;br /&gt;
Ahora crea nuevamente la carpeta base donde almacenaremos los archivos relacionados con Docker:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt; mkdir -p docker/images &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Creamos un Dockerfile vacío&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
touch docker/images/Dockerfile&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vamos a explicar los distintos apartados:&lt;br /&gt;
&lt;br /&gt;
=== FROM ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Indica la imagen base sobre la que se construirá la tuya.&lt;br /&gt;
En este caso, parte de una imagen ligera que ya tiene Python 3.12 instalado.&lt;br /&gt;
&lt;br /&gt;
=== WORKDIR ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta línea define el directorio de trabajo dentro del contenedor.&lt;br /&gt;
A partir de aquí, todos los comandos que aparezcan después (COPY, RUN, CMD, etc.) se ejecutarán dentro de /app.&lt;br /&gt;
&lt;br /&gt;
'''Ojo: /app no es la carpeta app/ de tu proyecto local.'''. Son cosas distintas.&lt;br /&gt;
&lt;br /&gt;
En tu máquina puede existir algo como:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
uvlhub&lt;br /&gt;
   app&lt;br /&gt;
   core&lt;br /&gt;
   requirements.txt &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Pero dentro del contenedor la estructura será distinta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
/ &lt;br /&gt;
  app/ &lt;br /&gt;
    app/ ← aquí dentro se habrá copiado tu carpeta local &amp;quot;app&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Cuando más adelante uses:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Docker copiará todos los archivos de tu proyecto dentro de la carpeta /app del contenedor.&lt;br /&gt;
Por eso el código quedará en /app/app/.&lt;br /&gt;
&lt;br /&gt;
=== COPY ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Copia todos los archivos del proyecto (del host) dentro del contenedor, en /app.&lt;br /&gt;
El primer punto es el origen; el segundo, el destino.&lt;br /&gt;
&lt;br /&gt;
=== RUN ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ejecuta comandos durante la construcción de la imagen.&lt;br /&gt;
Aquí instalamos las dependencias necesarias desde requirements.txt.&lt;br /&gt;
El resultado de este paso quedará guardado en la imagen.&lt;br /&gt;
&lt;br /&gt;
=== EXPOSE ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Documenta el puerto que la aplicación usa dentro del contenedor.&lt;br /&gt;
Esto no lo abre al exterior; simplemente indica que la app escucha en el 5000.&lt;br /&gt;
&lt;br /&gt;
=== CMD ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Define el comando que se ejecutará cuando el contenedor se inicie.&lt;br /&gt;
En este caso, ejecutará la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
=== RUN vs CMD ===&lt;br /&gt;
&lt;br /&gt;
Estas dos instrucciones parecen similares, pero hacen cosas muy distintas.&lt;br /&gt;
&lt;br /&gt;
==== RUN ====&lt;br /&gt;
Se ejecuta durante la construcción de la imagen (en el momento del docker build).&lt;br /&gt;
Cada RUN crea una nueva capa dentro de la imagen.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando instala las dependencias una sola vez, cuando se construye la imagen.&lt;br /&gt;
El resultado (las librerías instaladas) queda grabado dentro de la imagen final.&lt;br /&gt;
&lt;br /&gt;
Si después lanzas un contenedor nuevo a partir de esa imagen, las dependencias ya estarán instaladas y no se volverán a ejecutar.&lt;br /&gt;
&lt;br /&gt;
==== CMD ====&lt;br /&gt;
Se ejecuta cuando se inicia el contenedor (en el docker run).&lt;br /&gt;
Define el proceso principal que se ejecutará dentro del contenedor mientras esté activo.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando no se ejecuta durante la construcción, sino cada vez que arrancas el contenedor.&lt;br /&gt;
Si detienes el contenedor, este proceso también se detiene.&lt;br /&gt;
&lt;br /&gt;
==== Diferencia resumida ====&lt;br /&gt;
&lt;br /&gt;
RUN → se ejecuta al construir la imagen (fase de docker build).&lt;br /&gt;
&lt;br /&gt;
CMD → se ejecuta al iniciar el contenedor (fase de docker run).&lt;br /&gt;
&lt;br /&gt;
Solo puede haber un CMD por Dockerfile.&lt;br /&gt;
Si hay más de uno, solo se usará el último.&lt;br /&gt;
&lt;br /&gt;
== Dockerfile completo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
COPY . .&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto tiene que ir en un Dockerfile en docker/images&lt;br /&gt;
&lt;br /&gt;
== Construir la imagen ==&lt;br /&gt;
Desde la raíz del proyecto (no dentro de docker/images):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker build -t uvlhub:dev -f docker/images/Dockerfile .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Ejecutar el contenedor ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Qué es lo que observas?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 2: Conectando servicios =&lt;br /&gt;
&lt;br /&gt;
¡Vaya! ¡No funciona! Can't connect to MySQL server on 'db'... ¡Ah, claro, intenta conectarse a la base de datos, que no lo tenemos! Pero claro, en un contenedor no es recomendable tener la app y la base de datos conviviendo...&lt;br /&gt;
&lt;br /&gt;
== Crear contenedor MariaDB ==&lt;br /&gt;
&lt;br /&gt;
Antes que nada, haz Ctrl + C para parar el contenedor web. En este ejercicio aprenderás a lanzar un contenedor de MariaDB 12.0.2 de forma manual, configurando todas las variables necesarias para tu aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
El objetivo es entender la complejidad de hacerlo “a mano”, antes de automatizarlo con docker-compose.&lt;br /&gt;
&lt;br /&gt;
== Descargar la imagen oficial ==&lt;br /&gt;
Primero descarga la versión exacta que usaremos:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker pull mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Crear el contenedor de MariaDB ==&lt;br /&gt;
Lanza un contenedor con todas las variables necesarias para que Flask pueda conectarse correctamente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d --name mariadb_container \&lt;br /&gt;
 -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
 -e FLASK_ENV=development \&lt;br /&gt;
 -e DOMAIN=localhost \&lt;br /&gt;
 -e MARIADB_HOSTNAME=db \&lt;br /&gt;
 -e MARIADB_PORT=3306 \&lt;br /&gt;
 -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
 -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
 -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
 -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
 -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
 -e WORKING_DIR=/app/ \&lt;br /&gt;
 -p 3306:3306 mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Explicación:&lt;br /&gt;
&lt;br /&gt;
* -d: ejecuta el contenedor en segundo plano (modo detached).&lt;br /&gt;
* --name: asigna un nombre identificable al contenedor.&lt;br /&gt;
* -e: define variables de entorno dentro del contenedor.&lt;br /&gt;
* -p 3306:3306: publica el puerto de MariaDB en tu máquina local.&lt;br /&gt;
* mariadb:12.0.2: imagen exacta que vamos a usar.&lt;br /&gt;
&lt;br /&gt;
Aquí ya, de entrada, vemos que esto es más feo que una nevera por detrás. Para empezar, estamos pasando a lo loco un montón de variables que difílmente será replicable en producción, por contener información sensible. Aparte, si metemos nuevas variables, ese comando queda inservible. Necesitaremos una solución más ágil para este problema&lt;br /&gt;
&lt;br /&gt;
== Comprobar que está corriendo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver algo así:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES&lt;br /&gt;
abcd1234efgh mariadb:12.0.2 &amp;quot;docker-entrypoint.s…&amp;quot; Up 5 seconds 0.0.0.0:3306-&amp;gt;3306/tcp mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Conectarse a la base de datos ==&lt;br /&gt;
Abre una consola dentro del contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it mariadb_container bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y dentro, accede al cliente de MariaDB:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mariadb -u root -p&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Contraseña: uvlhubdb_root_password&lt;br /&gt;
&lt;br /&gt;
== Verificar la base de datos ==&lt;br /&gt;
Una vez dentro del cliente MySQL, ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
SHOW DATABASES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver que existen la bases de datos uvlhubdb porque pedimos crearla a la hora de levantar el contenedor. ¿Qué tiene la base de datos dentro? Vamos a verlo...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
USE uvlhubdb;&lt;br /&gt;
SHOW TABLES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Un total de... 0 tablas... ¡Claro! Es que solo hemos creado la base en sí, pero no las tablas. ¿Con un script SQL tal vez? ¡Espera, espera, si tenemos las migraciones! Pero claro, para eso necesito el contenedor de la app levantado. Sal de la consola SQL y del contenedor de MariaDB. Bien, ahora en el host...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta vez con el flag &amp;quot;-d&amp;quot; para que corra en segundo plano. Ahora, accedamos al contenedor y ejecutemos las migraciones. Pero... ¿cómo se llama mi contenedor?&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate que Docker asigna un nombre aleatorio a tu contenedor web porque no definiste ninguno. Una vez identificado el nombre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it &amp;lt;nombre&amp;gt; bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
What? ¿Qué pasa? ¿Otra vez error de conexión con la base? Si tenemos el contenedor funcionando, esto es un jaleo...&lt;br /&gt;
&lt;br /&gt;
== Redes internas ==&lt;br /&gt;
&lt;br /&gt;
Ahora mismo tienes dos contenedores aislados, el de la app y el la base de datos. Por defecto, cada contenedor tiene su propia red interna y no puede ver a los demás por nombre. El hostname '''db''' que usa Flask no existe en su DNS interno.&lt;br /&gt;
&lt;br /&gt;
=== Conectar servicios ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una red interna que compartan ambos contenedores&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker network create uvlhub_network&lt;br /&gt;
docker network ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vemos que ya aparece nuestra red '''uvlhub_network'''. Ahora, conectemos el contenedor de MariaDB a la red:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Claro, si te fijas, ahora hay que añadir, además de toda esa configuración engorrosa, que se añada en la misma red y que además tenga de hostname &amp;quot;db&amp;quot; para que el contenedor web entienda qué es &amp;quot;db&amp;quot;. Tenemos que hacer lo mismo para el contenedor web:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop &amp;lt;nombre&amp;gt;&lt;br /&gt;
docker rm &amp;lt;nombre&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y lo levantamos de nuevo, pero ya en la nueva red. De paso, le ponemos un nombre para identificarlo mejor.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 --name web_app_container --network uvlhub_network -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y ahora ya podemos acceder al contenedor por su nombre y ejecutar las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Mejor?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 3: Persistencia =&lt;br /&gt;
&lt;br /&gt;
Vamos a simular que hemos desplegado justo esto en producción y el servidor se reinicia por un corte de luz. Reiniciemos a mano cada contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker restart web_app_container&lt;br /&gt;
docker restart mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. What? ¿Cómo? ¿Qué ha pasado? ¿Otra vez problema de conexión? Ve dándole a F5... F5... ¡bingo! Ahora sí sale, pero menuda aleatoriedad, ¿no? Claro, es que ahora caes en que una cosa es que el contenedor haya arrancado y otra que esté listo.&lt;br /&gt;
&lt;br /&gt;
Aparte, tenemos otro problema. ¿Qué pasa con la persistencia? Porque si borro el contenedor de MariaDB, todos los datos que estén en su interior también se pierden. Y si estamos en producción, lío asegurado.&lt;br /&gt;
&lt;br /&gt;
== Volúmenes ==&lt;br /&gt;
&lt;br /&gt;
Cada vez que eliminas el contenedor de MariaDB, todas las bases de datos se pierden. Esto ocurre porque los datos se guardan dentro del sistema de archivos del contenedor, y al borrarlo desaparecen con él.&lt;br /&gt;
&lt;br /&gt;
En este ejercicio creas un volumen de Docker para que los datos persistan entre ejecuciones.&lt;br /&gt;
&lt;br /&gt;
=== Crear un volumen ===&lt;br /&gt;
Un volumen es una carpeta gestionada por Docker que vive fuera del ciclo de vida de los contenedores.&lt;br /&gt;
&lt;br /&gt;
Ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume create mariadb_data&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba que existe:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
La salida muestra algo como:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
DRIVER    VOLUME NAME&lt;br /&gt;
local     mariadb_data&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Crear un contenedor de MariaDB con el volumen ===&lt;br /&gt;
Lanza el contenedor y conecta el volumen a la ruta donde MariaDB guarda los datos (/var/lib/mysql):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -v mariadb_data:/var/lib/mysql \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Qué significa &amp;quot;:&amp;quot; en los volúmenes ===&lt;br /&gt;
&lt;br /&gt;
Cuando usas la opción '''-v''' o '''--volume''' en Docker, el signo dos puntos (''':''') separa dos rutas:&lt;br /&gt;
&lt;br /&gt;
'''-v origen:destino'''&lt;br /&gt;
&lt;br /&gt;
origen → es la ruta o el volumen en tu máquina (el host).&lt;br /&gt;
&lt;br /&gt;
destino → es la ruta dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 4: Orquestación de servicios =&lt;br /&gt;
&lt;br /&gt;
Hasta ahora hemos creado y conectado los contenedores manualmente.&lt;br /&gt;
Ahora vas a usar Docker Compose para definir todo en un solo archivo.&lt;br /&gt;
&lt;br /&gt;
Lo primero es parar y eliminar los contenedores previos para que no interfieran:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop $(docker ps -a -q)&lt;br /&gt;
docker rm $(docker ps -a -q)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Orquestación básica ==&lt;br /&gt;
&lt;br /&gt;
Crea un archivo llamado docker-compose.yml en la carpeta '''docker''' con este contenido:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    container_name: web_app_container&lt;br /&gt;
    env_file:&lt;br /&gt;
      - ../.env&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;5000&amp;quot;&lt;br /&gt;
    depends_on:&lt;br /&gt;
      - db&lt;br /&gt;
    build:&lt;br /&gt;
      context: ../&lt;br /&gt;
      dockerfile: docker/images/Dockerfile&lt;br /&gt;
    networks:&lt;br /&gt;
      - uvlhub_network&lt;br /&gt;
&lt;br /&gt;
  db:&lt;br /&gt;
    container_name: mariadb_container&lt;br /&gt;
    image: mariadb:12.0.2&lt;br /&gt;
    env_file:&lt;br /&gt;
      - ../.env&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;3306:3306&amp;quot;&lt;br /&gt;
    volumes:&lt;br /&gt;
      - db_data:/var/lib/mysql&lt;br /&gt;
    networks:&lt;br /&gt;
      - uvlhub_network&lt;br /&gt;
&lt;br /&gt;
volumes:&lt;br /&gt;
  db_data:&lt;br /&gt;
&lt;br /&gt;
networks:&lt;br /&gt;
  uvlhub_network:&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate que esta orquestación de servicios me permite definir el nombre de cada servicio, su dependencia, sus puertos, sus volúmenes, redes... todo lo que hacíamos de forma manual y propenso a errores queda ahora más fácil de gestionar. Además, ahora ambos contenedores comparten el mismo archivo .env de variables de entorno.&lt;br /&gt;
&lt;br /&gt;
Bien, ahora vamos a levantar estos contenedores y acceder al contenedor web:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Inmediatamente después, ejecuta las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué ha ocurrido? Depende del tiempo que hayas tardado, puede que haya saltado un error o puede que haya funcionado. Puede que hayas intentado varias veces el comando anterior hasta que ha funcionado. ¿Por qué crees que ocurre esta aparente aleatoriedad?&lt;br /&gt;
&lt;br /&gt;
== Dependencias entre contenedores y entrypoints ==&lt;br /&gt;
&lt;br /&gt;
Fíjate que tenemos un &amp;quot;depends on&amp;quot; en el servicio web. Significa que el servicio &amp;quot;db&amp;quot; debe iniciarse antes que el servicio web. Pero, cuidado, ahí está la trampa, porque &amp;quot;servicio iniciado != servicio preparado&amp;quot;. El servicio de MariaDB tiene que arrancar, configurarse, etc...&lt;br /&gt;
&lt;br /&gt;
Cuando hicimos el comando COPY en nuestro Dockerfile, también copiamos la carpeta '''scripts'''. Uno de los scripts es '''wait-for-db.sh''', que realiza continuamente llamadas a MariaDB hasta que este servicio está listo para recibir peticiones. Ábrelo y estúdialo.&lt;br /&gt;
&lt;br /&gt;
Añade al servicio web de tu docker compose las siguientes líneas:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
entrypoint: [&amp;quot;/app/scripts/wait-for-db.sh&amp;quot;]&lt;br /&gt;
command: [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A su vez, del Dockerfile, hay que eliminar la línea de CMD que arrancaba nuestro servidor Flask.&lt;br /&gt;
&lt;br /&gt;
Luego, reinicializa todos los servicios:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. What? ¿No abre? Vamos a investigarlo, veamos el logs:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker logs web_app_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Nos debe salir un mensaje eterno de &amp;quot;/app/scripts/wait-for-db.sh: line 18: mariadb: command not found&amp;quot;. Esto ocurre porque en la imagen de nuestro servicio web NO TENEMOS el cliente de MariaDB. Debemos indicar entonces que lo instale. Añade justo debajo de FROM lo siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y --no-install-recommends mariadb-client &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Al ser un contenedor basado en Linux, y accediendo como root, es como si actualizáramos todos los paquetes y luego instalásemos el cliente de MariaDB. Intentémoslo otra vez:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. ¡Jopelines! ¿Tampoco? Si haces un &amp;quot;docker ps&amp;quot; verás el problema. El contenedor web no tiene expuesto ningún puerto.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    (...)&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;5000&amp;quot;&lt;br /&gt;
    (...)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;quot;Expose&amp;quot; hace visible el puerto '''entre contenedores''', pero '''NO entre tu contenedor y el host'''. Para eso usamos la directiva &amp;quot;ports&amp;quot;, que se encarga del mapping. En nuestro caso, será:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    (...)&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;5000:5000&amp;quot;&lt;br /&gt;
    (...)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
En ocasiones, si los dos puertos coinciden, se puede dejar uno solo con &amp;quot;5000&amp;quot; pero dejaremos los dos. El de la izquierda siempre es el puerto externo (host), el de la derecha es el puerto interno (contenedor). Así que cambia ese detalle y por última vez:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Recuerda ejecutar las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
exit&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que si falla, es porque la base aún no está lista, para eso tenemos el script... Y, ya que estamos, ¿se te ocurre alguna forma de automatizar este proceso? Es decir, que yo levante los contenedores y realice también las migraciones de la base de datos.&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. ¡Ahora sí!&lt;br /&gt;
&lt;br /&gt;
== Trabajando con CMD, entrypoints y commands ==&lt;br /&gt;
&lt;br /&gt;
Piensa que al arrancar un contenedor Docker, siempre se ejecuta un único comando final.&lt;br /&gt;
Lo que hacen ENTRYPOINT, CMD y command (de Compose) es decidir qué comando será ese y con qué argumentos.&lt;br /&gt;
&lt;br /&gt;
=== CMD → “qué hacer por defecto” ===&lt;br /&gt;
&lt;br /&gt;
* Está en el Dockerfile.&lt;br /&gt;
* Es el comando que Docker ejecuta si no se indica otra cosa. En nuestro caso:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== ENTRYPOINT → “qué se ejecuta siempre” ===&lt;br /&gt;
&lt;br /&gt;
* También va en el Dockerfile, o se puede sobreescribir en el docker-compose.yml.&lt;br /&gt;
* Define el programa o script principal que siempre se ejecuta.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
ENTRYPOINT [&amp;quot;/app/scripts/wait-for-db.sh&amp;quot;]&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Normalmente, el entrypoint se combina con CMD en modo &amp;quot;eh, ejecuta esto (entrypoint) y luego este comando final (cmd)&lt;br /&gt;
&lt;br /&gt;
Los entrypoints son muy útiles, sobre todo en los docker-compose's, porque puedo redefinir cómo quiero que inicie un contenedor en concreto '''SIN TENER QUE HACER UN BUILD DE NUEVO DE LA IMAGEN DE DOCKER'''. A diferencia de CMD, que viene predefinido, un entrypoint en mi docker-compose me da más versatilidad&lt;br /&gt;
&lt;br /&gt;
=== command (de docker-compose.yml) → “sobrescribe el CMD” ===&lt;br /&gt;
&lt;br /&gt;
En docker-compose.yml, el campo command: reemplaza al CMD del Dockerfile,pero no al ENTRYPOINT.&lt;br /&gt;
&lt;br /&gt;
== Combinación de elementos ==&lt;br /&gt;
&lt;br /&gt;
Es importante entender cómo Docker trabaja con la orquestación y con los contenedores:&lt;br /&gt;
&lt;br /&gt;
* Lee el YAML como un diccionario&lt;br /&gt;
* Ejecuta todo lo que haya en un Dockerfile EXCEPTUANDO directivas ENTRYPOINT y CMD&lt;br /&gt;
* Monta el volumen o los volúmenes de ese contenedor (si procede)&lt;br /&gt;
&lt;br /&gt;
Y ahora viene el detalle:&lt;br /&gt;
&lt;br /&gt;
* Si estamos levantando un contenedor a través de docker-compose, va a ejecutar los entrypoints y commands definidos en ese docker-compose.&lt;br /&gt;
* Un entrypoint definido en un docker-compose va a sustituir a un entrypoint definido en el Dockerfile&lt;br /&gt;
* Un command definido en un docker-compose va a sustituir a un CMD definido en el Dockerfile.&lt;br /&gt;
&lt;br /&gt;
Como consejo:&lt;br /&gt;
&lt;br /&gt;
* Definimos un entrypoint en el docker-compose. Esto es así porque está fuera de la imagen de Docker y nos permite redefinir el inicio del servicio sin modificar la imagen&lt;br /&gt;
* Eventualmente, definimos un CMD donde indicamos el comando final de ese contenedor al iniciar, que suele ser el arranque de Flask Server o de Gunicorn, dependiendo de si estamos en desarrollo o producción.&lt;br /&gt;
&lt;br /&gt;
No obstante, como nuestros entrypoints son todos scripts en bash, podemos definir en el mismo el arranque final (comando final) y nos olvidamos de CMD.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 5: Volumen de trabajo =&lt;br /&gt;
&lt;br /&gt;
== Problema de desarrollo ==&lt;br /&gt;
&lt;br /&gt;
Vale, ya parece que le hemos pillado el tranquillo a Docker y a Docker Compose. Ahora vamos a hacer algo como desarrolladores. Ve al archivo &amp;quot;app/templates/base_templates.html&amp;quot; y cambia la línea 22:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;title&amp;gt;{{ FLASK_APP_NAME }} - Repository of feature models in UVL &amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
por:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;title&amp;gt;Hello world! &amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. Fíjate en el nombre de la pestaña del navegador, ¿ha cambiado? ¿No? Tsss, esto de los contenedores... bueno, va, vamos a reiniciarlo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker restart web_app_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. ¿Tampoco? Anda que vaya idea la de usar Docker... Claro, es a la hora de construir la imagen, hicimos &amp;quot;COPY . .&amp;quot;, pero no hemos reconstruido nada, así que...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml build --no-cache web&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --force-recreate web&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Importante señalar que le pedimos que ignore la caché para que fuerce a copiar de nuevo todo el contenido (incluyendo el cambio de línea) al interior del contenedor.&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. ¡Ahora sí! Pero... ¿tengo que reconstruir TODO el contenedor en cada cambio línea? ¿Y eso es trabajar de forma ágil?&lt;br /&gt;
&lt;br /&gt;
== Crear volumen de trabajo (bind mount) ==&lt;br /&gt;
&lt;br /&gt;
Evidentemente, eso no tiene sentido, tenemos que usar un volumen de trabajo que nos ayude a sincronizar el código de nuestro host con el código interno del contenedor. Luego en nuestro docker-compose:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  web:&lt;br /&gt;
    (....)&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;5000:5000&amp;quot;&lt;br /&gt;
    volumes:&lt;br /&gt;
      - ../:/app&lt;br /&gt;
    (....)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
La línea&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
- ../:/app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Monta tu carpeta local del proyecto (../) dentro del contenedor, (/app). ¿Y por qué /app? Porque fue el espacio de trabajo que definimos en el Dockerfile. Como ves, la congruencia entre los Dockerfile y el docker-compose es importante.&lt;br /&gt;
&lt;br /&gt;
Recarguemos de nuevo nustra configuración:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ahora ya podemos trabajar con el código en host sin tener que reconstruir la imagen a cada cambio de línea.&lt;br /&gt;
&lt;br /&gt;
== Cuidado con COPY y los bind mounts ==&lt;br /&gt;
&lt;br /&gt;
Un '''bind mount''' (o “montaje de enlace”) es un tipo de volumen en el que Docker conecta directamente una carpeta de tu máquina (el host) con una carpeta dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
Cuando usas un bind mount (por ejemplo ../:/app), Docker monta tu carpeta local sobre la ruta /app del contenedor.&lt;br /&gt;
Eso significa que todo lo que estaba en /app dentro de la imagen desaparece al arrancar el contenedor, porque queda “oculto” por el volumen.&lt;br /&gt;
&lt;br /&gt;
Ahora bien, durante la construcción de la imagen usamos la directiva COPY. ¿Por qué? Porque necesitamos el archivo requirements.txt para instalar las dependencias de Python del proyecto. Pero... ¿realmente necesitamos copiar TODO? No, claro que no, podemos optar por copiar exclusivamente aquellos archivos que nos hagan falta durante '''LA CONSTRUCCIÓN''' de la imagen. El bind mount luego lo machacará.&lt;br /&gt;
&lt;br /&gt;
'''Muy importante:''' los bind mounts, al igual que cualquier otro volumen, están disponibles '''DESPUÉS''' de la construcción de la imagen. Es un error común creer que porque definamos un bind mount en un docker-compose, esos archivos aparecen ya para el contenedor. '''Esto es una fuente inagotable de problemas en la práctica'''.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 6: Distintas configuraciones de despliegue =&lt;br /&gt;
&lt;br /&gt;
Volvamos al punto de partida:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
rm -r docker&lt;br /&gt;
mv docker.bk docker&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta práctica ha sido engorrosa a propósito para quitar esa &amp;quot;magia&amp;quot; que existe cuando lanzas un comando simple de orquestación y todo se levanta y funciona perfectamente. Esa situación está muy bien para desarrollar, pero no para aprender.&lt;br /&gt;
&lt;br /&gt;
Es importante que analices que existen distintos escenarios:&lt;br /&gt;
&lt;br /&gt;
* Hay un docker-compose para desarrollo.&lt;br /&gt;
* Hay 3 docker-compose para producción.&lt;br /&gt;
* Hay 6 Dockerfiles distintos&lt;br /&gt;
* Hay 3 entrypoints distintos&lt;br /&gt;
* Hay una carpeta llamada letsencrypt y otra llamada nginx. Dentro de nginx hay dos archivos de configuración dependiendo de si estamos en dev o en prod.&lt;br /&gt;
&lt;br /&gt;
== Autoevaluación ==&lt;br /&gt;
&lt;br /&gt;
Estudia bien cada archivo. Tómate tu tiempo e intenta responder a estas preguntas:&lt;br /&gt;
&lt;br /&gt;
* ¿Por qué en el docker-compose de dev NO se usa &amp;quot;ports&amp;quot; para enlazar con el host? (pista: nginx)&lt;br /&gt;
* ¿Qué es nginx y por qué es útil aquí?&lt;br /&gt;
* ¿Cuántos bind mounts usa el contenedor web en desarrollo? ¿Para qué sirve el bind mount del propio Docker?&lt;br /&gt;
* ¿Qué diferencia hay entre el entrypoint de desarrollo y el entrypoint de producción?&lt;br /&gt;
* ¿Por qué el servicio web en producción está montando todos esos bind mounts por separado? ¿No sería más sencillo hacer un volumen de todo el código?&lt;br /&gt;
&lt;br /&gt;
== Comentarios finales ==&lt;br /&gt;
&lt;br /&gt;
=== Cuanto más se parezca el entorno de desarrollo al de producción, menos errores habrá ===&lt;br /&gt;
&lt;br /&gt;
En desarrollo es tentador usar configuraciones cómodas: servidor de Flask con --reload, bind mounts, base de datos vacía, logs en consola, etc.&lt;br /&gt;
Pero cuanto más difiere ese entorno de producción, más probable es que algo falle al desplegar.&lt;br /&gt;
&lt;br /&gt;
'''El código no falla por cambiar de servidor, sino porque los entornos se comportan distinto.'''&lt;br /&gt;
&lt;br /&gt;
Ejemplos clásicos:&lt;br /&gt;
&lt;br /&gt;
* En desarrollo usas SQLite, en producción MariaDB → aparecen errores de compatibilidad SQL.&lt;br /&gt;
* En desarrollo Flask sirve los archivos estáticos, en producción lo hace Nginx → las rutas cambian.&lt;br /&gt;
* En desarrollo ejecutas '''flask run''', en producción '''gunicorn''' → los hilos, workers o el reloader funcionan distinto.&lt;br /&gt;
&lt;br /&gt;
Por eso, el objetivo ideal es que tu entorno de desarrollo (dev), de preproducción (staging) y de producción (prod) sean idénticos salvo por las credenciales.&lt;br /&gt;
&lt;br /&gt;
Docker y docker-compose te permiten lograr eso: puedes usar la misma imagen base, cambiando solo el docker-compose (volúmenes, puertos, etc.).&lt;br /&gt;
&lt;br /&gt;
=== Diferencia entre el servidor de Flask y Gunicorn ===&lt;br /&gt;
&lt;br /&gt;
Flask trae incorporado un servidor de desarrollo.&lt;br /&gt;
Es muy útil para depurar porque:&lt;br /&gt;
&lt;br /&gt;
* recarga automáticamente el código (--reload),&lt;br /&gt;
* muestra errores detallados en el navegador,&lt;br /&gt;
* solo ejecuta un proceso.&lt;br /&gt;
&lt;br /&gt;
Sin embargo, no está pensado para producción:&lt;br /&gt;
&lt;br /&gt;
* no maneja múltiples conexiones concurrentes,&lt;br /&gt;
* no tiene control de procesos ni balanceo de carga,&lt;br /&gt;
* no implementa estándares de rendimiento o seguridad (como WSGI robusto).&lt;br /&gt;
&lt;br /&gt;
Ahí entra Gunicorn (Green Unicorn), que sí es un servidor WSGI  de producción (Web Server Gateway Interface, estándar de comunicación entre aplicaciones web Python y servidores web):&lt;br /&gt;
&lt;br /&gt;
* puede lanzar varios workers en paralelo,&lt;br /&gt;
* maneja múltiples peticiones concurrentes,&lt;br /&gt;
* se integra fácilmente con Nginx para servir contenido estático o HTTPS.&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10181</id>
		<title>Tutorial-docker</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10181"/>
				<updated>2025-10-26T16:14:47Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: /* Ejercicio 4: Orquestación de servicios */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Ejercicio 1: Empezamos desde cero =&lt;br /&gt;
&lt;br /&gt;
El objetivo es eliminar toda la configuración previa y crear desde cero la estructura de carpetas que contendrá nuestros futuros archivos Docker.&lt;br /&gt;
&lt;br /&gt;
== Eliminar la configuración existente ==&lt;br /&gt;
Antes de comenzar, asegúrate de borrar cualquier carpeta Docker existente en el proyecto. En este caso, le haremos una copia de seguridad para poder volver a ella.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mv docker docker.bk&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
También debes asegurarte que trabajas con las variables de entorno adecuadas:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
cp .env.docker.example .env&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
⚠️ Este paso es importante: partimos completamente desde cero para entender cada parte del sistema.&lt;br /&gt;
&lt;br /&gt;
== Instalar Docker y Docker Compose (Linux) ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo apt update&lt;br /&gt;
sudo apt install -y apt-transport-https ca-certificates curl software-properties-common&lt;br /&gt;
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg&lt;br /&gt;
echo &amp;quot;deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable&amp;quot; | sudo tee /etc/apt/sources.list.d/docker.list &amp;gt; /dev/null&lt;br /&gt;
sudo apt update&lt;br /&gt;
sudo apt install -y docker-ce&lt;br /&gt;
sudo usermod -aG docker ${USER}&lt;br /&gt;
mkdir -p ~/.docker/cli-plugins/&lt;br /&gt;
LATEST_VERSION=$(curl -s https://api.github.com/repos/docker/compose/releases/latest | grep '&amp;quot;tag_name&amp;quot;:' | sed -E 's/.*&amp;quot;([^&amp;quot;]+)&amp;quot;.*/\1/')&lt;br /&gt;
curl -SL &amp;quot;https://github.com/docker/compose/releases/download/${LATEST_VERSION}/docker-compose-$(uname -s)-$(uname -m)&amp;quot; -o ~/.docker/cli-plugins/docker-compose&lt;br /&gt;
chmod +x ~/.docker/cli-plugins/docker-compose&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Es posible que tengas que reiniciar la sesión de usuario para poder usar docker sin necesidad de sudo.&lt;br /&gt;
&lt;br /&gt;
== Crear un Dockerfile mínimo ==&lt;br /&gt;
&lt;br /&gt;
Ahora crea nuevamente la carpeta base donde almacenaremos los archivos relacionados con Docker:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt; mkdir -p docker/images &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Creamos un Dockerfile vacío&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
touch docker/images/Dockerfile&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vamos a explicar los distintos apartados:&lt;br /&gt;
&lt;br /&gt;
=== FROM ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Indica la imagen base sobre la que se construirá la tuya.&lt;br /&gt;
En este caso, parte de una imagen ligera que ya tiene Python 3.12 instalado.&lt;br /&gt;
&lt;br /&gt;
=== WORKDIR ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta línea define el directorio de trabajo dentro del contenedor.&lt;br /&gt;
A partir de aquí, todos los comandos que aparezcan después (COPY, RUN, CMD, etc.) se ejecutarán dentro de /app.&lt;br /&gt;
&lt;br /&gt;
'''Ojo: /app no es la carpeta app/ de tu proyecto local.'''. Son cosas distintas.&lt;br /&gt;
&lt;br /&gt;
En tu máquina puede existir algo como:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
uvlhub&lt;br /&gt;
   app&lt;br /&gt;
   core&lt;br /&gt;
   requirements.txt &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Pero dentro del contenedor la estructura será distinta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
/ &lt;br /&gt;
  app/ &lt;br /&gt;
    app/ ← aquí dentro se habrá copiado tu carpeta local &amp;quot;app&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Cuando más adelante uses:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Docker copiará todos los archivos de tu proyecto dentro de la carpeta /app del contenedor.&lt;br /&gt;
Por eso el código quedará en /app/app/.&lt;br /&gt;
&lt;br /&gt;
=== COPY ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Copia todos los archivos del proyecto (del host) dentro del contenedor, en /app.&lt;br /&gt;
El primer punto es el origen; el segundo, el destino.&lt;br /&gt;
&lt;br /&gt;
=== RUN ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ejecuta comandos durante la construcción de la imagen.&lt;br /&gt;
Aquí instalamos las dependencias necesarias desde requirements.txt.&lt;br /&gt;
El resultado de este paso quedará guardado en la imagen.&lt;br /&gt;
&lt;br /&gt;
=== EXPOSE ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Documenta el puerto que la aplicación usa dentro del contenedor.&lt;br /&gt;
Esto no lo abre al exterior; simplemente indica que la app escucha en el 5000.&lt;br /&gt;
&lt;br /&gt;
=== CMD ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Define el comando que se ejecutará cuando el contenedor se inicie.&lt;br /&gt;
En este caso, ejecutará la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
=== RUN vs CMD ===&lt;br /&gt;
&lt;br /&gt;
Estas dos instrucciones parecen similares, pero hacen cosas muy distintas.&lt;br /&gt;
&lt;br /&gt;
==== RUN ====&lt;br /&gt;
Se ejecuta durante la construcción de la imagen (en el momento del docker build).&lt;br /&gt;
Cada RUN crea una nueva capa dentro de la imagen.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando instala las dependencias una sola vez, cuando se construye la imagen.&lt;br /&gt;
El resultado (las librerías instaladas) queda grabado dentro de la imagen final.&lt;br /&gt;
&lt;br /&gt;
Si después lanzas un contenedor nuevo a partir de esa imagen, las dependencias ya estarán instaladas y no se volverán a ejecutar.&lt;br /&gt;
&lt;br /&gt;
==== CMD ====&lt;br /&gt;
Se ejecuta cuando se inicia el contenedor (en el docker run).&lt;br /&gt;
Define el proceso principal que se ejecutará dentro del contenedor mientras esté activo.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando no se ejecuta durante la construcción, sino cada vez que arrancas el contenedor.&lt;br /&gt;
Si detienes el contenedor, este proceso también se detiene.&lt;br /&gt;
&lt;br /&gt;
==== Diferencia resumida ====&lt;br /&gt;
&lt;br /&gt;
RUN → se ejecuta al construir la imagen (fase de docker build).&lt;br /&gt;
&lt;br /&gt;
CMD → se ejecuta al iniciar el contenedor (fase de docker run).&lt;br /&gt;
&lt;br /&gt;
Solo puede haber un CMD por Dockerfile.&lt;br /&gt;
Si hay más de uno, solo se usará el último.&lt;br /&gt;
&lt;br /&gt;
== Dockerfile completo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
COPY . .&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto tiene que ir en un Dockerfile en docker/images&lt;br /&gt;
&lt;br /&gt;
== Construir la imagen ==&lt;br /&gt;
Desde la raíz del proyecto (no dentro de docker/images):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker build -t uvlhub:dev -f docker/images/Dockerfile .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Ejecutar el contenedor ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Qué es lo que observas?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 2: Conectando servicios =&lt;br /&gt;
&lt;br /&gt;
¡Vaya! ¡No funciona! Can't connect to MySQL server on 'db'... ¡Ah, claro, intenta conectarse a la base de datos, que no lo tenemos! Pero claro, en un contenedor no es recomendable tener la app y la base de datos conviviendo...&lt;br /&gt;
&lt;br /&gt;
== Crear contenedor MariaDB ==&lt;br /&gt;
&lt;br /&gt;
Antes que nada, haz Ctrl + C para parar el contenedor web. En este ejercicio aprenderás a lanzar un contenedor de MariaDB 12.0.2 de forma manual, configurando todas las variables necesarias para tu aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
El objetivo es entender la complejidad de hacerlo “a mano”, antes de automatizarlo con docker-compose.&lt;br /&gt;
&lt;br /&gt;
== Descargar la imagen oficial ==&lt;br /&gt;
Primero descarga la versión exacta que usaremos:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker pull mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Crear el contenedor de MariaDB ==&lt;br /&gt;
Lanza un contenedor con todas las variables necesarias para que Flask pueda conectarse correctamente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d --name mariadb_container \&lt;br /&gt;
 -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
 -e FLASK_ENV=development \&lt;br /&gt;
 -e DOMAIN=localhost \&lt;br /&gt;
 -e MARIADB_HOSTNAME=db \&lt;br /&gt;
 -e MARIADB_PORT=3306 \&lt;br /&gt;
 -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
 -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
 -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
 -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
 -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
 -e WORKING_DIR=/app/ \&lt;br /&gt;
 -p 3306:3306 mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Explicación:&lt;br /&gt;
&lt;br /&gt;
* -d: ejecuta el contenedor en segundo plano (modo detached).&lt;br /&gt;
* --name: asigna un nombre identificable al contenedor.&lt;br /&gt;
* -e: define variables de entorno dentro del contenedor.&lt;br /&gt;
* -p 3306:3306: publica el puerto de MariaDB en tu máquina local.&lt;br /&gt;
* mariadb:12.0.2: imagen exacta que vamos a usar.&lt;br /&gt;
&lt;br /&gt;
Aquí ya, de entrada, vemos que esto es más feo que una nevera por detrás. Para empezar, estamos pasando a lo loco un montón de variables que difílmente será replicable en producción, por contener información sensible. Aparte, si metemos nuevas variables, ese comando queda inservible. Necesitaremos una solución más ágil para este problema&lt;br /&gt;
&lt;br /&gt;
== Comprobar que está corriendo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver algo así:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES&lt;br /&gt;
abcd1234efgh mariadb:12.0.2 &amp;quot;docker-entrypoint.s…&amp;quot; Up 5 seconds 0.0.0.0:3306-&amp;gt;3306/tcp mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Conectarse a la base de datos ==&lt;br /&gt;
Abre una consola dentro del contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it mariadb_container bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y dentro, accede al cliente de MariaDB:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mariadb -u root -p&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Contraseña: uvlhubdb_root_password&lt;br /&gt;
&lt;br /&gt;
== Verificar la base de datos ==&lt;br /&gt;
Una vez dentro del cliente MySQL, ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
SHOW DATABASES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver que existen la bases de datos uvlhubdb porque pedimos crearla a la hora de levantar el contenedor. ¿Qué tiene la base de datos dentro? Vamos a verlo...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
USE uvlhubdb;&lt;br /&gt;
SHOW TABLES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Un total de... 0 tablas... ¡Claro! Es que solo hemos creado la base en sí, pero no las tablas. ¿Con un script SQL tal vez? ¡Espera, espera, si tenemos las migraciones! Pero claro, para eso necesito el contenedor de la app levantado. Sal de la consola SQL y del contenedor de MariaDB. Bien, ahora en el host...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta vez con el flag &amp;quot;-d&amp;quot; para que corra en segundo plano. Ahora, accedamos al contenedor y ejecutemos las migraciones. Pero... ¿cómo se llama mi contenedor?&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate que Docker asigna un nombre aleatorio a tu contenedor web porque no definiste ninguno. Una vez identificado el nombre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it &amp;lt;nombre&amp;gt; bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
What? ¿Qué pasa? ¿Otra vez error de conexión con la base? Si tenemos el contenedor funcionando, esto es un jaleo...&lt;br /&gt;
&lt;br /&gt;
== Redes internas ==&lt;br /&gt;
&lt;br /&gt;
Ahora mismo tienes dos contenedores aislados, el de la app y el la base de datos. Por defecto, cada contenedor tiene su propia red interna y no puede ver a los demás por nombre. El hostname '''db''' que usa Flask no existe en su DNS interno.&lt;br /&gt;
&lt;br /&gt;
=== Conectar servicios ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una red interna que compartan ambos contenedores&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker network create uvlhub_network&lt;br /&gt;
docker network ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vemos que ya aparece nuestra red '''uvlhub_network'''. Ahora, conectemos el contenedor de MariaDB a la red:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Claro, si te fijas, ahora hay que añadir, además de toda esa configuración engorrosa, que se añada en la misma red y que además tenga de hostname &amp;quot;db&amp;quot; para que el contenedor web entienda qué es &amp;quot;db&amp;quot;. Tenemos que hacer lo mismo para el contenedor web:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop &amp;lt;nombre&amp;gt;&lt;br /&gt;
docker rm &amp;lt;nombre&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y lo levantamos de nuevo, pero ya en la nueva red. De paso, le ponemos un nombre para identificarlo mejor.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 --name web_app_container --network uvlhub_network -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y ahora ya podemos acceder al contenedor por su nombre y ejecutar las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Mejor?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 3: Persistencia =&lt;br /&gt;
&lt;br /&gt;
Vamos a simular que hemos desplegado justo esto en producción y el servidor se reinicia por un corte de luz. Reiniciemos a mano cada contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker restart web_app_container&lt;br /&gt;
docker restart mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. What? ¿Cómo? ¿Qué ha pasado? ¿Otra vez problema de conexión? Ve dándole a F5... F5... ¡bingo! Ahora sí sale, pero menuda aleatoriedad, ¿no? Claro, es que ahora caes en que una cosa es que el contenedor haya arrancado y otra que esté listo.&lt;br /&gt;
&lt;br /&gt;
Aparte, tenemos otro problema. ¿Qué pasa con la persistencia? Porque si borro el contenedor de MariaDB, todos los datos que estén en su interior también se pierden. Y si estamos en producción, lío asegurado.&lt;br /&gt;
&lt;br /&gt;
== Volúmenes ==&lt;br /&gt;
&lt;br /&gt;
Cada vez que eliminas el contenedor de MariaDB, todas las bases de datos se pierden. Esto ocurre porque los datos se guardan dentro del sistema de archivos del contenedor, y al borrarlo desaparecen con él.&lt;br /&gt;
&lt;br /&gt;
En este ejercicio creas un volumen de Docker para que los datos persistan entre ejecuciones.&lt;br /&gt;
&lt;br /&gt;
=== Crear un volumen ===&lt;br /&gt;
Un volumen es una carpeta gestionada por Docker que vive fuera del ciclo de vida de los contenedores.&lt;br /&gt;
&lt;br /&gt;
Ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume create mariadb_data&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba que existe:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
La salida muestra algo como:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
DRIVER    VOLUME NAME&lt;br /&gt;
local     mariadb_data&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Crear un contenedor de MariaDB con el volumen ===&lt;br /&gt;
Lanza el contenedor y conecta el volumen a la ruta donde MariaDB guarda los datos (/var/lib/mysql):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -v mariadb_data:/var/lib/mysql \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Qué significa &amp;quot;:&amp;quot; en los volúmenes ===&lt;br /&gt;
&lt;br /&gt;
Cuando usas la opción '''-v''' o '''--volume''' en Docker, el signo dos puntos (''':''') separa dos rutas:&lt;br /&gt;
&lt;br /&gt;
'''-v origen:destino'''&lt;br /&gt;
&lt;br /&gt;
origen → es la ruta o el volumen en tu máquina (el host).&lt;br /&gt;
&lt;br /&gt;
destino → es la ruta dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 4: Orquestación de servicios =&lt;br /&gt;
&lt;br /&gt;
Hasta ahora hemos creado y conectado los contenedores manualmente.&lt;br /&gt;
Ahora vas a usar Docker Compose para definir todo en un solo archivo.&lt;br /&gt;
&lt;br /&gt;
Lo primero es parar y eliminar los contenedores previos para que no interfieran:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop $(docker ps -a -q)&lt;br /&gt;
docker rm $(docker ps -a -q)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Orquestación básica ==&lt;br /&gt;
&lt;br /&gt;
Crea un archivo llamado docker-compose.yml en la carpeta '''docker''' con este contenido:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    container_name: web_app_container&lt;br /&gt;
    env_file:&lt;br /&gt;
      - ../.env&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;5000&amp;quot;&lt;br /&gt;
    depends_on:&lt;br /&gt;
      - db&lt;br /&gt;
    build:&lt;br /&gt;
      context: ../&lt;br /&gt;
      dockerfile: docker/images/Dockerfile&lt;br /&gt;
    networks:&lt;br /&gt;
      - uvlhub_network&lt;br /&gt;
&lt;br /&gt;
  db:&lt;br /&gt;
    container_name: mariadb_container&lt;br /&gt;
    image: mariadb:12.0.2&lt;br /&gt;
    env_file:&lt;br /&gt;
      - ../.env&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;3306:3306&amp;quot;&lt;br /&gt;
    volumes:&lt;br /&gt;
      - db_data:/var/lib/mysql&lt;br /&gt;
    networks:&lt;br /&gt;
      - uvlhub_network&lt;br /&gt;
&lt;br /&gt;
volumes:&lt;br /&gt;
  db_data:&lt;br /&gt;
&lt;br /&gt;
networks:&lt;br /&gt;
  uvlhub_network:&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate que esta orquestación de servicios me permite definir el nombre de cada servicio, su dependencia, sus puertos, sus volúmenes, redes... todo lo que hacíamos de forma manual y propenso a errores queda ahora más fácil de gestionar. Además, ahora ambos contenedores comparten el mismo archivo .env de variables de entorno.&lt;br /&gt;
&lt;br /&gt;
Bien, ahora vamos a levantar estos contenedores y acceder al contenedor web:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Inmediatamente después, ejecuta las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué ha ocurrido? Depende del tiempo que hayas tardado, puede que haya saltado un error o puede que haya funcionado. Puede que hayas intentado varias veces el comando anterior hasta que ha funcionado. ¿Por qué crees que ocurre esta aparente aleatoriedad?&lt;br /&gt;
&lt;br /&gt;
== Dependencias entre contenedores y entrypoints ==&lt;br /&gt;
&lt;br /&gt;
Fíjate que tenemos un &amp;quot;depends on&amp;quot; en el servicio web. Significa que el servicio &amp;quot;db&amp;quot; debe iniciarse antes que el servicio web. Pero, cuidado, ahí está la trampa, porque &amp;quot;servicio iniciado != servicio preparado&amp;quot;. El servicio de MariaDB tiene que arrancar, configurarse, etc...&lt;br /&gt;
&lt;br /&gt;
Cuando hicimos el comando COPY en nuestro Dockerfile, también copiamos la carpeta '''scripts'''. Uno de los scripts es '''wait-for-db.sh''', que realiza continuamente llamadas a MariaDB hasta que este servicio está listo para recibir peticiones. Ábrelo y estúdialo.&lt;br /&gt;
&lt;br /&gt;
Añade al servicio web de tu docker compose las siguientes líneas:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
entrypoint: [&amp;quot;/app/scripts/wait-for-db.sh&amp;quot;]&lt;br /&gt;
command: [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A su vez, del Dockerfile, hay que eliminar la línea de CMD que arrancaba nuestro servidor Flask.&lt;br /&gt;
&lt;br /&gt;
Luego, reinicializa todos los servicios:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. What? ¿No abre? Vamos a investigarlo, veamos el logs:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker logs web_app_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Nos debe salir un mensaje eterno de &amp;quot;/app/scripts/wait-for-db.sh: line 18: mariadb: command not found&amp;quot;. Esto ocurre porque en la imagen de nuestro servicio web NO TENEMOS el cliente de MariaDB. Debemos indicar entonces que lo instale. Añade justo debajo de FROM lo siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y --no-install-recommends mariadb-client &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Al ser un contenedor basado en Linux, y accediendo como root, es como si actualizáramos todos los paquetes y luego instalásemos el cliente de MariaDB. Intentémoslo otra vez:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. ¡Jopelines! ¿Tampoco? Si haces un &amp;quot;docker ps&amp;quot; verás el problema. El contenedor web no tiene expuesto ningún puerto.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    (...)&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;5000&amp;quot;&lt;br /&gt;
    (...)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;quot;Expose&amp;quot; hace visible el puerto '''entre contenedores''', pero '''NO entre tu contenedor y el host'''. Para eso usamos la directiva &amp;quot;ports&amp;quot;, que se encarga del mapping. En nuestro caso, será:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    (...)&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;5000:5000&amp;quot;&lt;br /&gt;
    (...)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
En ocasiones, si los dos puertos coinciden, se puede dejar uno solo con &amp;quot;5000&amp;quot; pero dejaremos los dos. El de la izquierda siempre es el puerto externo (host), el de la derecha es el puerto interno (contenedor). Así que cambia ese detalle y por última vez:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. ¡Ahora sí!&lt;br /&gt;
&lt;br /&gt;
== Trabajando con CMD, entrypoints y commands ==&lt;br /&gt;
&lt;br /&gt;
Piensa que al arrancar un contenedor Docker, siempre se ejecuta un único comando final.&lt;br /&gt;
Lo que hacen ENTRYPOINT, CMD y command (de Compose) es decidir qué comando será ese y con qué argumentos.&lt;br /&gt;
&lt;br /&gt;
=== CMD → “qué hacer por defecto” ===&lt;br /&gt;
&lt;br /&gt;
* Está en el Dockerfile.&lt;br /&gt;
* Es el comando que Docker ejecuta si no se indica otra cosa. En nuestro caso:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== ENTRYPOINT → “qué se ejecuta siempre” ===&lt;br /&gt;
&lt;br /&gt;
* También va en el Dockerfile, o se puede sobreescribir en el docker-compose.yml.&lt;br /&gt;
* Define el programa o script principal que siempre se ejecuta.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
ENTRYPOINT [&amp;quot;/app/scripts/wait-for-db.sh&amp;quot;]&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Normalmente, el entrypoint se combina con CMD en modo &amp;quot;eh, ejecuta esto (entrypoint) y luego este comando final (cmd)&lt;br /&gt;
&lt;br /&gt;
Los entrypoints son muy útiles, sobre todo en los docker-compose's, porque puedo redefinir cómo quiero que inicie un contenedor en concreto '''SIN TENER QUE HACER UN BUILD DE NUEVO DE LA IMAGEN DE DOCKER'''. A diferencia de CMD, que viene predefinido, un entrypoint en mi docker-compose me da más versatilidad&lt;br /&gt;
&lt;br /&gt;
=== command (de docker-compose.yml) → “sobrescribe el CMD” ===&lt;br /&gt;
&lt;br /&gt;
En docker-compose.yml, el campo command: reemplaza al CMD del Dockerfile,pero no al ENTRYPOINT.&lt;br /&gt;
&lt;br /&gt;
== Combinación de elementos ==&lt;br /&gt;
&lt;br /&gt;
Es importante entender cómo Docker trabaja con la orquestación y con los contenedores:&lt;br /&gt;
&lt;br /&gt;
* Lee el YAML como un diccionario&lt;br /&gt;
* Ejecuta todo lo que haya en un Dockerfile EXCEPTUANDO directivas ENTRYPOINT y CMD&lt;br /&gt;
* Monta el volumen o los volúmenes de ese contenedor (si procede)&lt;br /&gt;
&lt;br /&gt;
Y ahora viene el detalle:&lt;br /&gt;
&lt;br /&gt;
* Si estamos levantando un contenedor a través de docker-compose, va a ejecutar los entrypoints y commands definidos en ese docker-compose.&lt;br /&gt;
* Un entrypoint definido en un docker-compose va a sustituir a un entrypoint definido en el Dockerfile&lt;br /&gt;
* Un command definido en un docker-compose va a sustituir a un CMD definido en el Dockerfile.&lt;br /&gt;
&lt;br /&gt;
Como consejo:&lt;br /&gt;
&lt;br /&gt;
* Definimos un entrypoint en el docker-compose. Esto es así porque está fuera de la imagen de Docker y nos permite redefinir el inicio del servicio sin modificar la imagen&lt;br /&gt;
* Eventualmente, definimos un CMD donde indicamos el comando final de ese contenedor al iniciar, que suele ser el arranque de Flask Server o de Gunicorn, dependiendo de si estamos en desarrollo o producción.&lt;br /&gt;
&lt;br /&gt;
No obstante, como nuestros entrypoints son todos scripts en bash, podemos definir en el mismo el arranque final (comando final) y nos olvidamos de CMD.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 5: Volumen de trabajo =&lt;br /&gt;
&lt;br /&gt;
== Problema de desarrollo ==&lt;br /&gt;
&lt;br /&gt;
Vale, ya parece que le hemos pillado el tranquillo a Docker y a Docker Compose. Ahora vamos a hacer algo como desarrolladores. Ve al archivo &amp;quot;app/templates/base_templates.html&amp;quot; y cambia la línea 22:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;title&amp;gt;{{ FLASK_APP_NAME }} - Repository of feature models in UVL &amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
por:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;title&amp;gt;Hello world! &amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. Fíjate en el nombre de la pestaña del navegador, ¿ha cambiado? ¿No? Tsss, esto de los contenedores... bueno, va, vamos a reiniciarlo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker restart web_app_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. ¿Tampoco? Anda que vaya idea la de usar Docker... Claro, es a la hora de construir la imagen, hicimos &amp;quot;COPY . .&amp;quot;, pero no hemos reconstruido nada, así que...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml build --no-cache web&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --force-recreate web&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Importante señalar que le pedimos que ignore la caché para que fuerce a copiar de nuevo todo el contenido (incluyendo el cambio de línea) al interior del contenedor.&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. ¡Ahora sí! Pero... ¿tengo que reconstruir TODO el contenedor en cada cambio línea? ¿Y eso es trabajar de forma ágil?&lt;br /&gt;
&lt;br /&gt;
== Crear volumen de trabajo (bind mount) ==&lt;br /&gt;
&lt;br /&gt;
Evidentemente, eso no tiene sentido, tenemos que usar un volumen de trabajo que nos ayude a sincronizar el código de nuestro host con el código interno del contenedor. Luego en nuestro docker-compose:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  web:&lt;br /&gt;
    (....)&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;5000:5000&amp;quot;&lt;br /&gt;
    volumes:&lt;br /&gt;
      - ../:/app&lt;br /&gt;
    (....)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
La línea&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
- ../:/app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Monta tu carpeta local del proyecto (../) dentro del contenedor, (/app). ¿Y por qué /app? Porque fue el espacio de trabajo que definimos en el Dockerfile. Como ves, la congruencia entre los Dockerfile y el docker-compose es importante.&lt;br /&gt;
&lt;br /&gt;
Recarguemos de nuevo nustra configuración:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ahora ya podemos trabajar con el código en host sin tener que reconstruir la imagen a cada cambio de línea.&lt;br /&gt;
&lt;br /&gt;
== Cuidado con COPY y los bind mounts ==&lt;br /&gt;
&lt;br /&gt;
Un '''bind mount''' (o “montaje de enlace”) es un tipo de volumen en el que Docker conecta directamente una carpeta de tu máquina (el host) con una carpeta dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
Cuando usas un bind mount (por ejemplo ../:/app), Docker monta tu carpeta local sobre la ruta /app del contenedor.&lt;br /&gt;
Eso significa que todo lo que estaba en /app dentro de la imagen desaparece al arrancar el contenedor, porque queda “oculto” por el volumen.&lt;br /&gt;
&lt;br /&gt;
Ahora bien, durante la construcción de la imagen usamos la directiva COPY. ¿Por qué? Porque necesitamos el archivo requirements.txt para instalar las dependencias de Python del proyecto. Pero... ¿realmente necesitamos copiar TODO? No, claro que no, podemos optar por copiar exclusivamente aquellos archivos que nos hagan falta durante '''LA CONSTRUCCIÓN''' de la imagen. El bind mount luego lo machacará.&lt;br /&gt;
&lt;br /&gt;
'''Muy importante:''' los bind mounts, al igual que cualquier otro volumen, están disponibles '''DESPUÉS''' de la construcción de la imagen. Es un error común creer que porque definamos un bind mount en un docker-compose, esos archivos aparecen ya para el contenedor. '''Esto es una fuente inagotable de problemas en la práctica'''.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 6: Distintas configuraciones de despliegue =&lt;br /&gt;
&lt;br /&gt;
Volvamos al punto de partida:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
rm -r docker&lt;br /&gt;
mv docker.bk docker&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta práctica ha sido engorrosa a propósito para quitar esa &amp;quot;magia&amp;quot; que existe cuando lanzas un comando simple de orquestación y todo se levanta y funciona perfectamente. Esa situación está muy bien para desarrollar, pero no para aprender.&lt;br /&gt;
&lt;br /&gt;
Es importante que analices que existen distintos escenarios:&lt;br /&gt;
&lt;br /&gt;
* Hay un docker-compose para desarrollo.&lt;br /&gt;
* Hay 3 docker-compose para producción.&lt;br /&gt;
* Hay 6 Dockerfiles distintos&lt;br /&gt;
* Hay 3 entrypoints distintos&lt;br /&gt;
* Hay una carpeta llamada letsencrypt y otra llamada nginx. Dentro de nginx hay dos archivos de configuración dependiendo de si estamos en dev o en prod.&lt;br /&gt;
&lt;br /&gt;
== Autoevaluación ==&lt;br /&gt;
&lt;br /&gt;
Estudia bien cada archivo. Tómate tu tiempo e intenta responder a estas preguntas:&lt;br /&gt;
&lt;br /&gt;
* ¿Por qué en el docker-compose de dev NO se usa &amp;quot;ports&amp;quot; para enlazar con el host? (pista: nginx)&lt;br /&gt;
* ¿Qué es nginx y por qué es útil aquí?&lt;br /&gt;
* ¿Cuántos bind mounts usa el contenedor web en desarrollo? ¿Para qué sirve el bind mount del propio Docker?&lt;br /&gt;
* ¿Qué diferencia hay entre el entrypoint de desarrollo y el entrypoint de producción?&lt;br /&gt;
* ¿Por qué el servicio web en producción está montando todos esos bind mounts por separado? ¿No sería más sencillo hacer un volumen de todo el código?&lt;br /&gt;
&lt;br /&gt;
== Comentarios finales ==&lt;br /&gt;
&lt;br /&gt;
=== Cuanto más se parezca el entorno de desarrollo al de producción, menos errores habrá ===&lt;br /&gt;
&lt;br /&gt;
En desarrollo es tentador usar configuraciones cómodas: servidor de Flask con --reload, bind mounts, base de datos vacía, logs en consola, etc.&lt;br /&gt;
Pero cuanto más difiere ese entorno de producción, más probable es que algo falle al desplegar.&lt;br /&gt;
&lt;br /&gt;
'''El código no falla por cambiar de servidor, sino porque los entornos se comportan distinto.'''&lt;br /&gt;
&lt;br /&gt;
Ejemplos clásicos:&lt;br /&gt;
&lt;br /&gt;
* En desarrollo usas SQLite, en producción MariaDB → aparecen errores de compatibilidad SQL.&lt;br /&gt;
* En desarrollo Flask sirve los archivos estáticos, en producción lo hace Nginx → las rutas cambian.&lt;br /&gt;
* En desarrollo ejecutas '''flask run''', en producción '''gunicorn''' → los hilos, workers o el reloader funcionan distinto.&lt;br /&gt;
&lt;br /&gt;
Por eso, el objetivo ideal es que tu entorno de desarrollo (dev), de preproducción (staging) y de producción (prod) sean idénticos salvo por las credenciales.&lt;br /&gt;
&lt;br /&gt;
Docker y docker-compose te permiten lograr eso: puedes usar la misma imagen base, cambiando solo el docker-compose (volúmenes, puertos, etc.).&lt;br /&gt;
&lt;br /&gt;
=== Diferencia entre el servidor de Flask y Gunicorn ===&lt;br /&gt;
&lt;br /&gt;
Flask trae incorporado un servidor de desarrollo.&lt;br /&gt;
Es muy útil para depurar porque:&lt;br /&gt;
&lt;br /&gt;
* recarga automáticamente el código (--reload),&lt;br /&gt;
* muestra errores detallados en el navegador,&lt;br /&gt;
* solo ejecuta un proceso.&lt;br /&gt;
&lt;br /&gt;
Sin embargo, no está pensado para producción:&lt;br /&gt;
&lt;br /&gt;
* no maneja múltiples conexiones concurrentes,&lt;br /&gt;
* no tiene control de procesos ni balanceo de carga,&lt;br /&gt;
* no implementa estándares de rendimiento o seguridad (como WSGI robusto).&lt;br /&gt;
&lt;br /&gt;
Ahí entra Gunicorn (Green Unicorn), que sí es un servidor WSGI  de producción (Web Server Gateway Interface, estándar de comunicación entre aplicaciones web Python y servidores web):&lt;br /&gt;
&lt;br /&gt;
* puede lanzar varios workers en paralelo,&lt;br /&gt;
* maneja múltiples peticiones concurrentes,&lt;br /&gt;
* se integra fácilmente con Nginx para servir contenido estático o HTTPS.&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10180</id>
		<title>Tutorial-docker</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10180"/>
				<updated>2025-10-26T16:11:00Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: /* Ejercicio 1: Empezamos desde cero */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Ejercicio 1: Empezamos desde cero =&lt;br /&gt;
&lt;br /&gt;
El objetivo es eliminar toda la configuración previa y crear desde cero la estructura de carpetas que contendrá nuestros futuros archivos Docker.&lt;br /&gt;
&lt;br /&gt;
== Eliminar la configuración existente ==&lt;br /&gt;
Antes de comenzar, asegúrate de borrar cualquier carpeta Docker existente en el proyecto. En este caso, le haremos una copia de seguridad para poder volver a ella.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mv docker docker.bk&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
También debes asegurarte que trabajas con las variables de entorno adecuadas:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
cp .env.docker.example .env&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
⚠️ Este paso es importante: partimos completamente desde cero para entender cada parte del sistema.&lt;br /&gt;
&lt;br /&gt;
== Instalar Docker y Docker Compose (Linux) ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo apt update&lt;br /&gt;
sudo apt install -y apt-transport-https ca-certificates curl software-properties-common&lt;br /&gt;
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg&lt;br /&gt;
echo &amp;quot;deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable&amp;quot; | sudo tee /etc/apt/sources.list.d/docker.list &amp;gt; /dev/null&lt;br /&gt;
sudo apt update&lt;br /&gt;
sudo apt install -y docker-ce&lt;br /&gt;
sudo usermod -aG docker ${USER}&lt;br /&gt;
mkdir -p ~/.docker/cli-plugins/&lt;br /&gt;
LATEST_VERSION=$(curl -s https://api.github.com/repos/docker/compose/releases/latest | grep '&amp;quot;tag_name&amp;quot;:' | sed -E 's/.*&amp;quot;([^&amp;quot;]+)&amp;quot;.*/\1/')&lt;br /&gt;
curl -SL &amp;quot;https://github.com/docker/compose/releases/download/${LATEST_VERSION}/docker-compose-$(uname -s)-$(uname -m)&amp;quot; -o ~/.docker/cli-plugins/docker-compose&lt;br /&gt;
chmod +x ~/.docker/cli-plugins/docker-compose&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Es posible que tengas que reiniciar la sesión de usuario para poder usar docker sin necesidad de sudo.&lt;br /&gt;
&lt;br /&gt;
== Crear un Dockerfile mínimo ==&lt;br /&gt;
&lt;br /&gt;
Ahora crea nuevamente la carpeta base donde almacenaremos los archivos relacionados con Docker:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt; mkdir -p docker/images &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Creamos un Dockerfile vacío&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
touch docker/images/Dockerfile&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vamos a explicar los distintos apartados:&lt;br /&gt;
&lt;br /&gt;
=== FROM ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Indica la imagen base sobre la que se construirá la tuya.&lt;br /&gt;
En este caso, parte de una imagen ligera que ya tiene Python 3.12 instalado.&lt;br /&gt;
&lt;br /&gt;
=== WORKDIR ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta línea define el directorio de trabajo dentro del contenedor.&lt;br /&gt;
A partir de aquí, todos los comandos que aparezcan después (COPY, RUN, CMD, etc.) se ejecutarán dentro de /app.&lt;br /&gt;
&lt;br /&gt;
'''Ojo: /app no es la carpeta app/ de tu proyecto local.'''. Son cosas distintas.&lt;br /&gt;
&lt;br /&gt;
En tu máquina puede existir algo como:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
uvlhub&lt;br /&gt;
   app&lt;br /&gt;
   core&lt;br /&gt;
   requirements.txt &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Pero dentro del contenedor la estructura será distinta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
/ &lt;br /&gt;
  app/ &lt;br /&gt;
    app/ ← aquí dentro se habrá copiado tu carpeta local &amp;quot;app&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Cuando más adelante uses:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Docker copiará todos los archivos de tu proyecto dentro de la carpeta /app del contenedor.&lt;br /&gt;
Por eso el código quedará en /app/app/.&lt;br /&gt;
&lt;br /&gt;
=== COPY ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Copia todos los archivos del proyecto (del host) dentro del contenedor, en /app.&lt;br /&gt;
El primer punto es el origen; el segundo, el destino.&lt;br /&gt;
&lt;br /&gt;
=== RUN ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ejecuta comandos durante la construcción de la imagen.&lt;br /&gt;
Aquí instalamos las dependencias necesarias desde requirements.txt.&lt;br /&gt;
El resultado de este paso quedará guardado en la imagen.&lt;br /&gt;
&lt;br /&gt;
=== EXPOSE ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Documenta el puerto que la aplicación usa dentro del contenedor.&lt;br /&gt;
Esto no lo abre al exterior; simplemente indica que la app escucha en el 5000.&lt;br /&gt;
&lt;br /&gt;
=== CMD ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Define el comando que se ejecutará cuando el contenedor se inicie.&lt;br /&gt;
En este caso, ejecutará la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
=== RUN vs CMD ===&lt;br /&gt;
&lt;br /&gt;
Estas dos instrucciones parecen similares, pero hacen cosas muy distintas.&lt;br /&gt;
&lt;br /&gt;
==== RUN ====&lt;br /&gt;
Se ejecuta durante la construcción de la imagen (en el momento del docker build).&lt;br /&gt;
Cada RUN crea una nueva capa dentro de la imagen.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando instala las dependencias una sola vez, cuando se construye la imagen.&lt;br /&gt;
El resultado (las librerías instaladas) queda grabado dentro de la imagen final.&lt;br /&gt;
&lt;br /&gt;
Si después lanzas un contenedor nuevo a partir de esa imagen, las dependencias ya estarán instaladas y no se volverán a ejecutar.&lt;br /&gt;
&lt;br /&gt;
==== CMD ====&lt;br /&gt;
Se ejecuta cuando se inicia el contenedor (en el docker run).&lt;br /&gt;
Define el proceso principal que se ejecutará dentro del contenedor mientras esté activo.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando no se ejecuta durante la construcción, sino cada vez que arrancas el contenedor.&lt;br /&gt;
Si detienes el contenedor, este proceso también se detiene.&lt;br /&gt;
&lt;br /&gt;
==== Diferencia resumida ====&lt;br /&gt;
&lt;br /&gt;
RUN → se ejecuta al construir la imagen (fase de docker build).&lt;br /&gt;
&lt;br /&gt;
CMD → se ejecuta al iniciar el contenedor (fase de docker run).&lt;br /&gt;
&lt;br /&gt;
Solo puede haber un CMD por Dockerfile.&lt;br /&gt;
Si hay más de uno, solo se usará el último.&lt;br /&gt;
&lt;br /&gt;
== Dockerfile completo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
COPY . .&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto tiene que ir en un Dockerfile en docker/images&lt;br /&gt;
&lt;br /&gt;
== Construir la imagen ==&lt;br /&gt;
Desde la raíz del proyecto (no dentro de docker/images):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker build -t uvlhub:dev -f docker/images/Dockerfile .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Ejecutar el contenedor ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Qué es lo que observas?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 2: Conectando servicios =&lt;br /&gt;
&lt;br /&gt;
¡Vaya! ¡No funciona! Can't connect to MySQL server on 'db'... ¡Ah, claro, intenta conectarse a la base de datos, que no lo tenemos! Pero claro, en un contenedor no es recomendable tener la app y la base de datos conviviendo...&lt;br /&gt;
&lt;br /&gt;
== Crear contenedor MariaDB ==&lt;br /&gt;
&lt;br /&gt;
Antes que nada, haz Ctrl + C para parar el contenedor web. En este ejercicio aprenderás a lanzar un contenedor de MariaDB 12.0.2 de forma manual, configurando todas las variables necesarias para tu aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
El objetivo es entender la complejidad de hacerlo “a mano”, antes de automatizarlo con docker-compose.&lt;br /&gt;
&lt;br /&gt;
== Descargar la imagen oficial ==&lt;br /&gt;
Primero descarga la versión exacta que usaremos:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker pull mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Crear el contenedor de MariaDB ==&lt;br /&gt;
Lanza un contenedor con todas las variables necesarias para que Flask pueda conectarse correctamente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d --name mariadb_container \&lt;br /&gt;
 -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
 -e FLASK_ENV=development \&lt;br /&gt;
 -e DOMAIN=localhost \&lt;br /&gt;
 -e MARIADB_HOSTNAME=db \&lt;br /&gt;
 -e MARIADB_PORT=3306 \&lt;br /&gt;
 -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
 -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
 -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
 -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
 -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
 -e WORKING_DIR=/app/ \&lt;br /&gt;
 -p 3306:3306 mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Explicación:&lt;br /&gt;
&lt;br /&gt;
* -d: ejecuta el contenedor en segundo plano (modo detached).&lt;br /&gt;
* --name: asigna un nombre identificable al contenedor.&lt;br /&gt;
* -e: define variables de entorno dentro del contenedor.&lt;br /&gt;
* -p 3306:3306: publica el puerto de MariaDB en tu máquina local.&lt;br /&gt;
* mariadb:12.0.2: imagen exacta que vamos a usar.&lt;br /&gt;
&lt;br /&gt;
Aquí ya, de entrada, vemos que esto es más feo que una nevera por detrás. Para empezar, estamos pasando a lo loco un montón de variables que difílmente será replicable en producción, por contener información sensible. Aparte, si metemos nuevas variables, ese comando queda inservible. Necesitaremos una solución más ágil para este problema&lt;br /&gt;
&lt;br /&gt;
== Comprobar que está corriendo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver algo así:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES&lt;br /&gt;
abcd1234efgh mariadb:12.0.2 &amp;quot;docker-entrypoint.s…&amp;quot; Up 5 seconds 0.0.0.0:3306-&amp;gt;3306/tcp mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Conectarse a la base de datos ==&lt;br /&gt;
Abre una consola dentro del contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it mariadb_container bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y dentro, accede al cliente de MariaDB:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mariadb -u root -p&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Contraseña: uvlhubdb_root_password&lt;br /&gt;
&lt;br /&gt;
== Verificar la base de datos ==&lt;br /&gt;
Una vez dentro del cliente MySQL, ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
SHOW DATABASES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver que existen la bases de datos uvlhubdb porque pedimos crearla a la hora de levantar el contenedor. ¿Qué tiene la base de datos dentro? Vamos a verlo...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
USE uvlhubdb;&lt;br /&gt;
SHOW TABLES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Un total de... 0 tablas... ¡Claro! Es que solo hemos creado la base en sí, pero no las tablas. ¿Con un script SQL tal vez? ¡Espera, espera, si tenemos las migraciones! Pero claro, para eso necesito el contenedor de la app levantado. Sal de la consola SQL y del contenedor de MariaDB. Bien, ahora en el host...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta vez con el flag &amp;quot;-d&amp;quot; para que corra en segundo plano. Ahora, accedamos al contenedor y ejecutemos las migraciones. Pero... ¿cómo se llama mi contenedor?&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate que Docker asigna un nombre aleatorio a tu contenedor web porque no definiste ninguno. Una vez identificado el nombre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it &amp;lt;nombre&amp;gt; bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
What? ¿Qué pasa? ¿Otra vez error de conexión con la base? Si tenemos el contenedor funcionando, esto es un jaleo...&lt;br /&gt;
&lt;br /&gt;
== Redes internas ==&lt;br /&gt;
&lt;br /&gt;
Ahora mismo tienes dos contenedores aislados, el de la app y el la base de datos. Por defecto, cada contenedor tiene su propia red interna y no puede ver a los demás por nombre. El hostname '''db''' que usa Flask no existe en su DNS interno.&lt;br /&gt;
&lt;br /&gt;
=== Conectar servicios ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una red interna que compartan ambos contenedores&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker network create uvlhub_network&lt;br /&gt;
docker network ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vemos que ya aparece nuestra red '''uvlhub_network'''. Ahora, conectemos el contenedor de MariaDB a la red:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Claro, si te fijas, ahora hay que añadir, además de toda esa configuración engorrosa, que se añada en la misma red y que además tenga de hostname &amp;quot;db&amp;quot; para que el contenedor web entienda qué es &amp;quot;db&amp;quot;. Tenemos que hacer lo mismo para el contenedor web:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop &amp;lt;nombre&amp;gt;&lt;br /&gt;
docker rm &amp;lt;nombre&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y lo levantamos de nuevo, pero ya en la nueva red. De paso, le ponemos un nombre para identificarlo mejor.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 --name web_app_container --network uvlhub_network -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y ahora ya podemos acceder al contenedor por su nombre y ejecutar las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Mejor?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 3: Persistencia =&lt;br /&gt;
&lt;br /&gt;
Vamos a simular que hemos desplegado justo esto en producción y el servidor se reinicia por un corte de luz. Reiniciemos a mano cada contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker restart web_app_container&lt;br /&gt;
docker restart mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. What? ¿Cómo? ¿Qué ha pasado? ¿Otra vez problema de conexión? Ve dándole a F5... F5... ¡bingo! Ahora sí sale, pero menuda aleatoriedad, ¿no? Claro, es que ahora caes en que una cosa es que el contenedor haya arrancado y otra que esté listo.&lt;br /&gt;
&lt;br /&gt;
Aparte, tenemos otro problema. ¿Qué pasa con la persistencia? Porque si borro el contenedor de MariaDB, todos los datos que estén en su interior también se pierden. Y si estamos en producción, lío asegurado.&lt;br /&gt;
&lt;br /&gt;
== Volúmenes ==&lt;br /&gt;
&lt;br /&gt;
Cada vez que eliminas el contenedor de MariaDB, todas las bases de datos se pierden. Esto ocurre porque los datos se guardan dentro del sistema de archivos del contenedor, y al borrarlo desaparecen con él.&lt;br /&gt;
&lt;br /&gt;
En este ejercicio creas un volumen de Docker para que los datos persistan entre ejecuciones.&lt;br /&gt;
&lt;br /&gt;
=== Crear un volumen ===&lt;br /&gt;
Un volumen es una carpeta gestionada por Docker que vive fuera del ciclo de vida de los contenedores.&lt;br /&gt;
&lt;br /&gt;
Ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume create mariadb_data&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba que existe:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
La salida muestra algo como:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
DRIVER    VOLUME NAME&lt;br /&gt;
local     mariadb_data&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Crear un contenedor de MariaDB con el volumen ===&lt;br /&gt;
Lanza el contenedor y conecta el volumen a la ruta donde MariaDB guarda los datos (/var/lib/mysql):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -v mariadb_data:/var/lib/mysql \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Qué significa &amp;quot;:&amp;quot; en los volúmenes ===&lt;br /&gt;
&lt;br /&gt;
Cuando usas la opción '''-v''' o '''--volume''' en Docker, el signo dos puntos (''':''') separa dos rutas:&lt;br /&gt;
&lt;br /&gt;
'''-v origen:destino'''&lt;br /&gt;
&lt;br /&gt;
origen → es la ruta o el volumen en tu máquina (el host).&lt;br /&gt;
&lt;br /&gt;
destino → es la ruta dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 4: Orquestación de servicios =&lt;br /&gt;
&lt;br /&gt;
Hasta ahora hemos creado y conectado los contenedores manualmente.&lt;br /&gt;
Ahora vas a usar Docker Compose para definir todo en un solo archivo.&lt;br /&gt;
&lt;br /&gt;
== Orquestación básica ==&lt;br /&gt;
&lt;br /&gt;
Crea un archivo llamado docker-compose.yml en la carpeta '''docker''' con este contenido:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    container_name: web_app_container&lt;br /&gt;
    env_file:&lt;br /&gt;
      - ../.env&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;5000&amp;quot;&lt;br /&gt;
    depends_on:&lt;br /&gt;
      - db&lt;br /&gt;
    build:&lt;br /&gt;
      context: ../&lt;br /&gt;
      dockerfile: docker/images/Dockerfile&lt;br /&gt;
    networks:&lt;br /&gt;
      - uvlhub_network&lt;br /&gt;
&lt;br /&gt;
  db:&lt;br /&gt;
    container_name: mariadb_container&lt;br /&gt;
    image: mariadb:12.0.2&lt;br /&gt;
    env_file:&lt;br /&gt;
      - ../.env&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;3306:3306&amp;quot;&lt;br /&gt;
    volumes:&lt;br /&gt;
      - db_data:/var/lib/mysql&lt;br /&gt;
    networks:&lt;br /&gt;
      - uvlhub_network&lt;br /&gt;
&lt;br /&gt;
volumes:&lt;br /&gt;
  db_data:&lt;br /&gt;
&lt;br /&gt;
networks:&lt;br /&gt;
  uvlhub_network:&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate que esta orquestación de servicios me permite definir el nombre de cada servicio, su dependencia, sus puertos, sus volúmenes, redes... todo lo que hacíamos de forma manual y propenso a errores queda ahora más fácil de gestionar. Además, ahora ambos contenedores comparten el mismo archivo .env de variables de entorno.&lt;br /&gt;
&lt;br /&gt;
Bien, ahora vamos a levantar estos contenedores y acceder al contenedor web:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Inmediatamente después, ejecuta las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué ha ocurrido? Depende del tiempo que hayas tardado, puede que haya saltado un error o puede que haya funcionado. Puede que hayas intentado varias veces el comando anterior hasta que ha funcionado. ¿Por qué crees que ocurre esta aparente aleatoriedad?&lt;br /&gt;
&lt;br /&gt;
== Dependencias entre contenedores y entrypoints ==&lt;br /&gt;
&lt;br /&gt;
Fíjate que tenemos un &amp;quot;depends on&amp;quot; en el servicio web. Significa que el servicio &amp;quot;db&amp;quot; debe iniciarse antes que el servicio web. Pero, cuidado, ahí está la trampa, porque &amp;quot;servicio iniciado != servicio preparado&amp;quot;. El servicio de MariaDB tiene que arrancar, configurarse, etc...&lt;br /&gt;
&lt;br /&gt;
Cuando hicimos el comando COPY en nuestro Dockerfile, también copiamos la carpeta '''scripts'''. Uno de los scripts es '''wait-for-db.sh''', que realiza continuamente llamadas a MariaDB hasta que este servicio está listo para recibir peticiones. Ábrelo y estúdialo.&lt;br /&gt;
&lt;br /&gt;
Añade al servicio web de tu docker compose las siguientes líneas:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
entrypoint: [&amp;quot;/app/scripts/wait-for-db.sh&amp;quot;]&lt;br /&gt;
command: [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A su vez, del Dockerfile, hay que eliminar la línea de CMD que arrancaba nuestro servidor Flask.&lt;br /&gt;
&lt;br /&gt;
Luego, reinicializa todos los servicios:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. What? ¿No abre? Vamos a investigarlo, veamos el logs:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker logs web_app_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Nos debe salir un mensaje eterno de &amp;quot;/app/scripts/wait-for-db.sh: line 18: mariadb: command not found&amp;quot;. Esto ocurre porque en la imagen de nuestro servicio web NO TENEMOS el cliente de MariaDB. Debemos indicar entonces que lo instale. Añade justo debajo de FROM lo siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y --no-install-recommends mariadb-client &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Al ser un contenedor basado en Linux, y accediendo como root, es como si actualizáramos todos los paquetes y luego instalásemos el cliente de MariaDB. Intentémoslo otra vez:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. ¡Jopelines! ¿Tampoco? Si haces un &amp;quot;docker ps&amp;quot; verás el problema. El contenedor web no tiene expuesto ningún puerto.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    (...)&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;5000&amp;quot;&lt;br /&gt;
    (...)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;quot;Expose&amp;quot; hace visible el puerto '''entre contenedores''', pero '''NO entre tu contenedor y el host'''. Para eso usamos la directiva &amp;quot;ports&amp;quot;, que se encarga del mapping. En nuestro caso, será:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    (...)&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;5000:5000&amp;quot;&lt;br /&gt;
    (...)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
En ocasiones, si los dos puertos coinciden, se puede dejar uno solo con &amp;quot;5000&amp;quot; pero dejaremos los dos. El de la izquierda siempre es el puerto externo (host), el de la derecha es el puerto interno (contenedor). Así que cambia ese detalle y por última vez:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. ¡Ahora sí!&lt;br /&gt;
&lt;br /&gt;
== Trabajando con CMD, entrypoints y commands ==&lt;br /&gt;
&lt;br /&gt;
Piensa que al arrancar un contenedor Docker, siempre se ejecuta un único comando final.&lt;br /&gt;
Lo que hacen ENTRYPOINT, CMD y command (de Compose) es decidir qué comando será ese y con qué argumentos.&lt;br /&gt;
&lt;br /&gt;
=== CMD → “qué hacer por defecto” ===&lt;br /&gt;
&lt;br /&gt;
* Está en el Dockerfile.&lt;br /&gt;
* Es el comando que Docker ejecuta si no se indica otra cosa. En nuestro caso:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== ENTRYPOINT → “qué se ejecuta siempre” ===&lt;br /&gt;
&lt;br /&gt;
* También va en el Dockerfile, o se puede sobreescribir en el docker-compose.yml.&lt;br /&gt;
* Define el programa o script principal que siempre se ejecuta.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
ENTRYPOINT [&amp;quot;/app/scripts/wait-for-db.sh&amp;quot;]&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Normalmente, el entrypoint se combina con CMD en modo &amp;quot;eh, ejecuta esto (entrypoint) y luego este comando final (cmd)&lt;br /&gt;
&lt;br /&gt;
Los entrypoints son muy útiles, sobre todo en los docker-compose's, porque puedo redefinir cómo quiero que inicie un contenedor en concreto '''SIN TENER QUE HACER UN BUILD DE NUEVO DE LA IMAGEN DE DOCKER'''. A diferencia de CMD, que viene predefinido, un entrypoint en mi docker-compose me da más versatilidad&lt;br /&gt;
&lt;br /&gt;
=== command (de docker-compose.yml) → “sobrescribe el CMD” ===&lt;br /&gt;
&lt;br /&gt;
En docker-compose.yml, el campo command: reemplaza al CMD del Dockerfile,pero no al ENTRYPOINT.&lt;br /&gt;
&lt;br /&gt;
== Combinación de elementos ==&lt;br /&gt;
&lt;br /&gt;
Es importante entender cómo Docker trabaja con la orquestación y con los contenedores:&lt;br /&gt;
&lt;br /&gt;
* Lee el YAML como un diccionario&lt;br /&gt;
* Ejecuta todo lo que haya en un Dockerfile EXCEPTUANDO directivas ENTRYPOINT y CMD&lt;br /&gt;
* Monta el volumen o los volúmenes de ese contenedor (si procede)&lt;br /&gt;
&lt;br /&gt;
Y ahora viene el detalle:&lt;br /&gt;
&lt;br /&gt;
* Si estamos levantando un contenedor a través de docker-compose, va a ejecutar los entrypoints y commands definidos en ese docker-compose.&lt;br /&gt;
* Un entrypoint definido en un docker-compose va a sustituir a un entrypoint definido en el Dockerfile&lt;br /&gt;
* Un command definido en un docker-compose va a sustituir a un CMD definido en el Dockerfile.&lt;br /&gt;
&lt;br /&gt;
Como consejo:&lt;br /&gt;
&lt;br /&gt;
* Definimos un entrypoint en el docker-compose. Esto es así porque está fuera de la imagen de Docker y nos permite redefinir el inicio del servicio sin modificar la imagen&lt;br /&gt;
* Eventualmente, definimos un CMD donde indicamos el comando final de ese contenedor al iniciar, que suele ser el arranque de Flask Server o de Gunicorn, dependiendo de si estamos en desarrollo o producción.&lt;br /&gt;
&lt;br /&gt;
No obstante, como nuestros entrypoints son todos scripts en bash, podemos definir en el mismo el arranque final (comando final) y nos olvidamos de CMD.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 5: Volumen de trabajo =&lt;br /&gt;
&lt;br /&gt;
== Problema de desarrollo ==&lt;br /&gt;
&lt;br /&gt;
Vale, ya parece que le hemos pillado el tranquillo a Docker y a Docker Compose. Ahora vamos a hacer algo como desarrolladores. Ve al archivo &amp;quot;app/templates/base_templates.html&amp;quot; y cambia la línea 22:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;title&amp;gt;{{ FLASK_APP_NAME }} - Repository of feature models in UVL &amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
por:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;title&amp;gt;Hello world! &amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. Fíjate en el nombre de la pestaña del navegador, ¿ha cambiado? ¿No? Tsss, esto de los contenedores... bueno, va, vamos a reiniciarlo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker restart web_app_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. ¿Tampoco? Anda que vaya idea la de usar Docker... Claro, es a la hora de construir la imagen, hicimos &amp;quot;COPY . .&amp;quot;, pero no hemos reconstruido nada, así que...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml build --no-cache web&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --force-recreate web&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Importante señalar que le pedimos que ignore la caché para que fuerce a copiar de nuevo todo el contenido (incluyendo el cambio de línea) al interior del contenedor.&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. ¡Ahora sí! Pero... ¿tengo que reconstruir TODO el contenedor en cada cambio línea? ¿Y eso es trabajar de forma ágil?&lt;br /&gt;
&lt;br /&gt;
== Crear volumen de trabajo (bind mount) ==&lt;br /&gt;
&lt;br /&gt;
Evidentemente, eso no tiene sentido, tenemos que usar un volumen de trabajo que nos ayude a sincronizar el código de nuestro host con el código interno del contenedor. Luego en nuestro docker-compose:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  web:&lt;br /&gt;
    (....)&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;5000:5000&amp;quot;&lt;br /&gt;
    volumes:&lt;br /&gt;
      - ../:/app&lt;br /&gt;
    (....)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
La línea&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
- ../:/app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Monta tu carpeta local del proyecto (../) dentro del contenedor, (/app). ¿Y por qué /app? Porque fue el espacio de trabajo que definimos en el Dockerfile. Como ves, la congruencia entre los Dockerfile y el docker-compose es importante.&lt;br /&gt;
&lt;br /&gt;
Recarguemos de nuevo nustra configuración:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ahora ya podemos trabajar con el código en host sin tener que reconstruir la imagen a cada cambio de línea.&lt;br /&gt;
&lt;br /&gt;
== Cuidado con COPY y los bind mounts ==&lt;br /&gt;
&lt;br /&gt;
Un '''bind mount''' (o “montaje de enlace”) es un tipo de volumen en el que Docker conecta directamente una carpeta de tu máquina (el host) con una carpeta dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
Cuando usas un bind mount (por ejemplo ../:/app), Docker monta tu carpeta local sobre la ruta /app del contenedor.&lt;br /&gt;
Eso significa que todo lo que estaba en /app dentro de la imagen desaparece al arrancar el contenedor, porque queda “oculto” por el volumen.&lt;br /&gt;
&lt;br /&gt;
Ahora bien, durante la construcción de la imagen usamos la directiva COPY. ¿Por qué? Porque necesitamos el archivo requirements.txt para instalar las dependencias de Python del proyecto. Pero... ¿realmente necesitamos copiar TODO? No, claro que no, podemos optar por copiar exclusivamente aquellos archivos que nos hagan falta durante '''LA CONSTRUCCIÓN''' de la imagen. El bind mount luego lo machacará.&lt;br /&gt;
&lt;br /&gt;
'''Muy importante:''' los bind mounts, al igual que cualquier otro volumen, están disponibles '''DESPUÉS''' de la construcción de la imagen. Es un error común creer que porque definamos un bind mount en un docker-compose, esos archivos aparecen ya para el contenedor. '''Esto es una fuente inagotable de problemas en la práctica'''.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 6: Distintas configuraciones de despliegue =&lt;br /&gt;
&lt;br /&gt;
Volvamos al punto de partida:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
rm -r docker&lt;br /&gt;
mv docker.bk docker&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta práctica ha sido engorrosa a propósito para quitar esa &amp;quot;magia&amp;quot; que existe cuando lanzas un comando simple de orquestación y todo se levanta y funciona perfectamente. Esa situación está muy bien para desarrollar, pero no para aprender.&lt;br /&gt;
&lt;br /&gt;
Es importante que analices que existen distintos escenarios:&lt;br /&gt;
&lt;br /&gt;
* Hay un docker-compose para desarrollo.&lt;br /&gt;
* Hay 3 docker-compose para producción.&lt;br /&gt;
* Hay 6 Dockerfiles distintos&lt;br /&gt;
* Hay 3 entrypoints distintos&lt;br /&gt;
* Hay una carpeta llamada letsencrypt y otra llamada nginx. Dentro de nginx hay dos archivos de configuración dependiendo de si estamos en dev o en prod.&lt;br /&gt;
&lt;br /&gt;
== Autoevaluación ==&lt;br /&gt;
&lt;br /&gt;
Estudia bien cada archivo. Tómate tu tiempo e intenta responder a estas preguntas:&lt;br /&gt;
&lt;br /&gt;
* ¿Por qué en el docker-compose de dev NO se usa &amp;quot;ports&amp;quot; para enlazar con el host? (pista: nginx)&lt;br /&gt;
* ¿Qué es nginx y por qué es útil aquí?&lt;br /&gt;
* ¿Cuántos bind mounts usa el contenedor web en desarrollo? ¿Para qué sirve el bind mount del propio Docker?&lt;br /&gt;
* ¿Qué diferencia hay entre el entrypoint de desarrollo y el entrypoint de producción?&lt;br /&gt;
* ¿Por qué el servicio web en producción está montando todos esos bind mounts por separado? ¿No sería más sencillo hacer un volumen de todo el código?&lt;br /&gt;
&lt;br /&gt;
== Comentarios finales ==&lt;br /&gt;
&lt;br /&gt;
=== Cuanto más se parezca el entorno de desarrollo al de producción, menos errores habrá ===&lt;br /&gt;
&lt;br /&gt;
En desarrollo es tentador usar configuraciones cómodas: servidor de Flask con --reload, bind mounts, base de datos vacía, logs en consola, etc.&lt;br /&gt;
Pero cuanto más difiere ese entorno de producción, más probable es que algo falle al desplegar.&lt;br /&gt;
&lt;br /&gt;
'''El código no falla por cambiar de servidor, sino porque los entornos se comportan distinto.'''&lt;br /&gt;
&lt;br /&gt;
Ejemplos clásicos:&lt;br /&gt;
&lt;br /&gt;
* En desarrollo usas SQLite, en producción MariaDB → aparecen errores de compatibilidad SQL.&lt;br /&gt;
* En desarrollo Flask sirve los archivos estáticos, en producción lo hace Nginx → las rutas cambian.&lt;br /&gt;
* En desarrollo ejecutas '''flask run''', en producción '''gunicorn''' → los hilos, workers o el reloader funcionan distinto.&lt;br /&gt;
&lt;br /&gt;
Por eso, el objetivo ideal es que tu entorno de desarrollo (dev), de preproducción (staging) y de producción (prod) sean idénticos salvo por las credenciales.&lt;br /&gt;
&lt;br /&gt;
Docker y docker-compose te permiten lograr eso: puedes usar la misma imagen base, cambiando solo el docker-compose (volúmenes, puertos, etc.).&lt;br /&gt;
&lt;br /&gt;
=== Diferencia entre el servidor de Flask y Gunicorn ===&lt;br /&gt;
&lt;br /&gt;
Flask trae incorporado un servidor de desarrollo.&lt;br /&gt;
Es muy útil para depurar porque:&lt;br /&gt;
&lt;br /&gt;
* recarga automáticamente el código (--reload),&lt;br /&gt;
* muestra errores detallados en el navegador,&lt;br /&gt;
* solo ejecuta un proceso.&lt;br /&gt;
&lt;br /&gt;
Sin embargo, no está pensado para producción:&lt;br /&gt;
&lt;br /&gt;
* no maneja múltiples conexiones concurrentes,&lt;br /&gt;
* no tiene control de procesos ni balanceo de carga,&lt;br /&gt;
* no implementa estándares de rendimiento o seguridad (como WSGI robusto).&lt;br /&gt;
&lt;br /&gt;
Ahí entra Gunicorn (Green Unicorn), que sí es un servidor WSGI  de producción (Web Server Gateway Interface, estándar de comunicación entre aplicaciones web Python y servidores web):&lt;br /&gt;
&lt;br /&gt;
* puede lanzar varios workers en paralelo,&lt;br /&gt;
* maneja múltiples peticiones concurrentes,&lt;br /&gt;
* se integra fácilmente con Nginx para servir contenido estático o HTTPS.&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10179</id>
		<title>Tutorial-docker</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10179"/>
				<updated>2025-10-26T16:03:41Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: /* Crear un Dockerfile mínimo */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Ejercicio 1: Empezamos desde cero =&lt;br /&gt;
&lt;br /&gt;
El objetivo es eliminar toda la configuración previa y crear desde cero la estructura de carpetas que contendrá nuestros futuros archivos Docker.&lt;br /&gt;
&lt;br /&gt;
== Eliminar la configuración existente ==&lt;br /&gt;
Antes de comenzar, asegúrate de borrar cualquier carpeta Docker existente en el proyecto. En este caso, le haremos una copia de seguridad para poder volver a ella.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mv docker docker.bk&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
También debes asegurarte que trabajas con las variables de entorno adecuadas:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
cp .env.docker.example .env&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
⚠️ Este paso es importante: partimos completamente desde cero para entender cada parte del sistema.&lt;br /&gt;
&lt;br /&gt;
== Crear un Dockerfile mínimo ==&lt;br /&gt;
&lt;br /&gt;
Ahora crea nuevamente la carpeta base donde almacenaremos los archivos relacionados con Docker:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt; mkdir -p docker/images &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Creamos un Dockerfile vacío&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
touch docker/images/Dockerfile&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vamos a explicar los distintos apartados:&lt;br /&gt;
&lt;br /&gt;
=== FROM ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Indica la imagen base sobre la que se construirá la tuya.&lt;br /&gt;
En este caso, parte de una imagen ligera que ya tiene Python 3.12 instalado.&lt;br /&gt;
&lt;br /&gt;
=== WORKDIR ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta línea define el directorio de trabajo dentro del contenedor.&lt;br /&gt;
A partir de aquí, todos los comandos que aparezcan después (COPY, RUN, CMD, etc.) se ejecutarán dentro de /app.&lt;br /&gt;
&lt;br /&gt;
'''Ojo: /app no es la carpeta app/ de tu proyecto local.'''. Son cosas distintas.&lt;br /&gt;
&lt;br /&gt;
En tu máquina puede existir algo como:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
uvlhub&lt;br /&gt;
   app&lt;br /&gt;
   core&lt;br /&gt;
   requirements.txt &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Pero dentro del contenedor la estructura será distinta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
/ &lt;br /&gt;
  app/ &lt;br /&gt;
    app/ ← aquí dentro se habrá copiado tu carpeta local &amp;quot;app&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Cuando más adelante uses:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Docker copiará todos los archivos de tu proyecto dentro de la carpeta /app del contenedor.&lt;br /&gt;
Por eso el código quedará en /app/app/.&lt;br /&gt;
&lt;br /&gt;
=== COPY ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Copia todos los archivos del proyecto (del host) dentro del contenedor, en /app.&lt;br /&gt;
El primer punto es el origen; el segundo, el destino.&lt;br /&gt;
&lt;br /&gt;
=== RUN ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ejecuta comandos durante la construcción de la imagen.&lt;br /&gt;
Aquí instalamos las dependencias necesarias desde requirements.txt.&lt;br /&gt;
El resultado de este paso quedará guardado en la imagen.&lt;br /&gt;
&lt;br /&gt;
=== EXPOSE ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Documenta el puerto que la aplicación usa dentro del contenedor.&lt;br /&gt;
Esto no lo abre al exterior; simplemente indica que la app escucha en el 5000.&lt;br /&gt;
&lt;br /&gt;
=== CMD ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Define el comando que se ejecutará cuando el contenedor se inicie.&lt;br /&gt;
En este caso, ejecutará la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
=== RUN vs CMD ===&lt;br /&gt;
&lt;br /&gt;
Estas dos instrucciones parecen similares, pero hacen cosas muy distintas.&lt;br /&gt;
&lt;br /&gt;
==== RUN ====&lt;br /&gt;
Se ejecuta durante la construcción de la imagen (en el momento del docker build).&lt;br /&gt;
Cada RUN crea una nueva capa dentro de la imagen.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando instala las dependencias una sola vez, cuando se construye la imagen.&lt;br /&gt;
El resultado (las librerías instaladas) queda grabado dentro de la imagen final.&lt;br /&gt;
&lt;br /&gt;
Si después lanzas un contenedor nuevo a partir de esa imagen, las dependencias ya estarán instaladas y no se volverán a ejecutar.&lt;br /&gt;
&lt;br /&gt;
==== CMD ====&lt;br /&gt;
Se ejecuta cuando se inicia el contenedor (en el docker run).&lt;br /&gt;
Define el proceso principal que se ejecutará dentro del contenedor mientras esté activo.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando no se ejecuta durante la construcción, sino cada vez que arrancas el contenedor.&lt;br /&gt;
Si detienes el contenedor, este proceso también se detiene.&lt;br /&gt;
&lt;br /&gt;
==== Diferencia resumida ====&lt;br /&gt;
&lt;br /&gt;
RUN → se ejecuta al construir la imagen (fase de docker build).&lt;br /&gt;
&lt;br /&gt;
CMD → se ejecuta al iniciar el contenedor (fase de docker run).&lt;br /&gt;
&lt;br /&gt;
Solo puede haber un CMD por Dockerfile.&lt;br /&gt;
Si hay más de uno, solo se usará el último.&lt;br /&gt;
&lt;br /&gt;
== Dockerfile completo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
COPY . .&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto tiene que ir en un Dockerfile en docker/images&lt;br /&gt;
&lt;br /&gt;
== Construir la imagen ==&lt;br /&gt;
Desde la raíz del proyecto (no dentro de docker/images):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker build -t uvlhub:dev -f docker/images/Dockerfile .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Ejecutar el contenedor ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Qué es lo que observas?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 2: Conectando servicios =&lt;br /&gt;
&lt;br /&gt;
¡Vaya! ¡No funciona! Can't connect to MySQL server on 'db'... ¡Ah, claro, intenta conectarse a la base de datos, que no lo tenemos! Pero claro, en un contenedor no es recomendable tener la app y la base de datos conviviendo...&lt;br /&gt;
&lt;br /&gt;
== Crear contenedor MariaDB ==&lt;br /&gt;
&lt;br /&gt;
Antes que nada, haz Ctrl + C para parar el contenedor web. En este ejercicio aprenderás a lanzar un contenedor de MariaDB 12.0.2 de forma manual, configurando todas las variables necesarias para tu aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
El objetivo es entender la complejidad de hacerlo “a mano”, antes de automatizarlo con docker-compose.&lt;br /&gt;
&lt;br /&gt;
== Descargar la imagen oficial ==&lt;br /&gt;
Primero descarga la versión exacta que usaremos:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker pull mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Crear el contenedor de MariaDB ==&lt;br /&gt;
Lanza un contenedor con todas las variables necesarias para que Flask pueda conectarse correctamente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d --name mariadb_container \&lt;br /&gt;
 -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
 -e FLASK_ENV=development \&lt;br /&gt;
 -e DOMAIN=localhost \&lt;br /&gt;
 -e MARIADB_HOSTNAME=db \&lt;br /&gt;
 -e MARIADB_PORT=3306 \&lt;br /&gt;
 -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
 -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
 -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
 -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
 -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
 -e WORKING_DIR=/app/ \&lt;br /&gt;
 -p 3306:3306 mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Explicación:&lt;br /&gt;
&lt;br /&gt;
* -d: ejecuta el contenedor en segundo plano (modo detached).&lt;br /&gt;
* --name: asigna un nombre identificable al contenedor.&lt;br /&gt;
* -e: define variables de entorno dentro del contenedor.&lt;br /&gt;
* -p 3306:3306: publica el puerto de MariaDB en tu máquina local.&lt;br /&gt;
* mariadb:12.0.2: imagen exacta que vamos a usar.&lt;br /&gt;
&lt;br /&gt;
Aquí ya, de entrada, vemos que esto es más feo que una nevera por detrás. Para empezar, estamos pasando a lo loco un montón de variables que difílmente será replicable en producción, por contener información sensible. Aparte, si metemos nuevas variables, ese comando queda inservible. Necesitaremos una solución más ágil para este problema&lt;br /&gt;
&lt;br /&gt;
== Comprobar que está corriendo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver algo así:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES&lt;br /&gt;
abcd1234efgh mariadb:12.0.2 &amp;quot;docker-entrypoint.s…&amp;quot; Up 5 seconds 0.0.0.0:3306-&amp;gt;3306/tcp mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Conectarse a la base de datos ==&lt;br /&gt;
Abre una consola dentro del contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it mariadb_container bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y dentro, accede al cliente de MariaDB:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mariadb -u root -p&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Contraseña: uvlhubdb_root_password&lt;br /&gt;
&lt;br /&gt;
== Verificar la base de datos ==&lt;br /&gt;
Una vez dentro del cliente MySQL, ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
SHOW DATABASES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver que existen la bases de datos uvlhubdb porque pedimos crearla a la hora de levantar el contenedor. ¿Qué tiene la base de datos dentro? Vamos a verlo...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
USE uvlhubdb;&lt;br /&gt;
SHOW TABLES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Un total de... 0 tablas... ¡Claro! Es que solo hemos creado la base en sí, pero no las tablas. ¿Con un script SQL tal vez? ¡Espera, espera, si tenemos las migraciones! Pero claro, para eso necesito el contenedor de la app levantado. Sal de la consola SQL y del contenedor de MariaDB. Bien, ahora en el host...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta vez con el flag &amp;quot;-d&amp;quot; para que corra en segundo plano. Ahora, accedamos al contenedor y ejecutemos las migraciones. Pero... ¿cómo se llama mi contenedor?&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate que Docker asigna un nombre aleatorio a tu contenedor web porque no definiste ninguno. Una vez identificado el nombre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it &amp;lt;nombre&amp;gt; bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
What? ¿Qué pasa? ¿Otra vez error de conexión con la base? Si tenemos el contenedor funcionando, esto es un jaleo...&lt;br /&gt;
&lt;br /&gt;
== Redes internas ==&lt;br /&gt;
&lt;br /&gt;
Ahora mismo tienes dos contenedores aislados, el de la app y el la base de datos. Por defecto, cada contenedor tiene su propia red interna y no puede ver a los demás por nombre. El hostname '''db''' que usa Flask no existe en su DNS interno.&lt;br /&gt;
&lt;br /&gt;
=== Conectar servicios ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una red interna que compartan ambos contenedores&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker network create uvlhub_network&lt;br /&gt;
docker network ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vemos que ya aparece nuestra red '''uvlhub_network'''. Ahora, conectemos el contenedor de MariaDB a la red:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Claro, si te fijas, ahora hay que añadir, además de toda esa configuración engorrosa, que se añada en la misma red y que además tenga de hostname &amp;quot;db&amp;quot; para que el contenedor web entienda qué es &amp;quot;db&amp;quot;. Tenemos que hacer lo mismo para el contenedor web:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop &amp;lt;nombre&amp;gt;&lt;br /&gt;
docker rm &amp;lt;nombre&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y lo levantamos de nuevo, pero ya en la nueva red. De paso, le ponemos un nombre para identificarlo mejor.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 --name web_app_container --network uvlhub_network -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y ahora ya podemos acceder al contenedor por su nombre y ejecutar las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Mejor?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 3: Persistencia =&lt;br /&gt;
&lt;br /&gt;
Vamos a simular que hemos desplegado justo esto en producción y el servidor se reinicia por un corte de luz. Reiniciemos a mano cada contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker restart web_app_container&lt;br /&gt;
docker restart mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. What? ¿Cómo? ¿Qué ha pasado? ¿Otra vez problema de conexión? Ve dándole a F5... F5... ¡bingo! Ahora sí sale, pero menuda aleatoriedad, ¿no? Claro, es que ahora caes en que una cosa es que el contenedor haya arrancado y otra que esté listo.&lt;br /&gt;
&lt;br /&gt;
Aparte, tenemos otro problema. ¿Qué pasa con la persistencia? Porque si borro el contenedor de MariaDB, todos los datos que estén en su interior también se pierden. Y si estamos en producción, lío asegurado.&lt;br /&gt;
&lt;br /&gt;
== Volúmenes ==&lt;br /&gt;
&lt;br /&gt;
Cada vez que eliminas el contenedor de MariaDB, todas las bases de datos se pierden. Esto ocurre porque los datos se guardan dentro del sistema de archivos del contenedor, y al borrarlo desaparecen con él.&lt;br /&gt;
&lt;br /&gt;
En este ejercicio creas un volumen de Docker para que los datos persistan entre ejecuciones.&lt;br /&gt;
&lt;br /&gt;
=== Crear un volumen ===&lt;br /&gt;
Un volumen es una carpeta gestionada por Docker que vive fuera del ciclo de vida de los contenedores.&lt;br /&gt;
&lt;br /&gt;
Ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume create mariadb_data&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba que existe:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
La salida muestra algo como:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
DRIVER    VOLUME NAME&lt;br /&gt;
local     mariadb_data&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Crear un contenedor de MariaDB con el volumen ===&lt;br /&gt;
Lanza el contenedor y conecta el volumen a la ruta donde MariaDB guarda los datos (/var/lib/mysql):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -v mariadb_data:/var/lib/mysql \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Qué significa &amp;quot;:&amp;quot; en los volúmenes ===&lt;br /&gt;
&lt;br /&gt;
Cuando usas la opción '''-v''' o '''--volume''' en Docker, el signo dos puntos (''':''') separa dos rutas:&lt;br /&gt;
&lt;br /&gt;
'''-v origen:destino'''&lt;br /&gt;
&lt;br /&gt;
origen → es la ruta o el volumen en tu máquina (el host).&lt;br /&gt;
&lt;br /&gt;
destino → es la ruta dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 4: Orquestación de servicios =&lt;br /&gt;
&lt;br /&gt;
Hasta ahora hemos creado y conectado los contenedores manualmente.&lt;br /&gt;
Ahora vas a usar Docker Compose para definir todo en un solo archivo.&lt;br /&gt;
&lt;br /&gt;
== Orquestación básica ==&lt;br /&gt;
&lt;br /&gt;
Crea un archivo llamado docker-compose.yml en la carpeta '''docker''' con este contenido:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    container_name: web_app_container&lt;br /&gt;
    env_file:&lt;br /&gt;
      - ../.env&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;5000&amp;quot;&lt;br /&gt;
    depends_on:&lt;br /&gt;
      - db&lt;br /&gt;
    build:&lt;br /&gt;
      context: ../&lt;br /&gt;
      dockerfile: docker/images/Dockerfile&lt;br /&gt;
    networks:&lt;br /&gt;
      - uvlhub_network&lt;br /&gt;
&lt;br /&gt;
  db:&lt;br /&gt;
    container_name: mariadb_container&lt;br /&gt;
    image: mariadb:12.0.2&lt;br /&gt;
    env_file:&lt;br /&gt;
      - ../.env&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;3306:3306&amp;quot;&lt;br /&gt;
    volumes:&lt;br /&gt;
      - db_data:/var/lib/mysql&lt;br /&gt;
    networks:&lt;br /&gt;
      - uvlhub_network&lt;br /&gt;
&lt;br /&gt;
volumes:&lt;br /&gt;
  db_data:&lt;br /&gt;
&lt;br /&gt;
networks:&lt;br /&gt;
  uvlhub_network:&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate que esta orquestación de servicios me permite definir el nombre de cada servicio, su dependencia, sus puertos, sus volúmenes, redes... todo lo que hacíamos de forma manual y propenso a errores queda ahora más fácil de gestionar. Además, ahora ambos contenedores comparten el mismo archivo .env de variables de entorno.&lt;br /&gt;
&lt;br /&gt;
Bien, ahora vamos a levantar estos contenedores y acceder al contenedor web:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Inmediatamente después, ejecuta las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué ha ocurrido? Depende del tiempo que hayas tardado, puede que haya saltado un error o puede que haya funcionado. Puede que hayas intentado varias veces el comando anterior hasta que ha funcionado. ¿Por qué crees que ocurre esta aparente aleatoriedad?&lt;br /&gt;
&lt;br /&gt;
== Dependencias entre contenedores y entrypoints ==&lt;br /&gt;
&lt;br /&gt;
Fíjate que tenemos un &amp;quot;depends on&amp;quot; en el servicio web. Significa que el servicio &amp;quot;db&amp;quot; debe iniciarse antes que el servicio web. Pero, cuidado, ahí está la trampa, porque &amp;quot;servicio iniciado != servicio preparado&amp;quot;. El servicio de MariaDB tiene que arrancar, configurarse, etc...&lt;br /&gt;
&lt;br /&gt;
Cuando hicimos el comando COPY en nuestro Dockerfile, también copiamos la carpeta '''scripts'''. Uno de los scripts es '''wait-for-db.sh''', que realiza continuamente llamadas a MariaDB hasta que este servicio está listo para recibir peticiones. Ábrelo y estúdialo.&lt;br /&gt;
&lt;br /&gt;
Añade al servicio web de tu docker compose las siguientes líneas:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
entrypoint: [&amp;quot;/app/scripts/wait-for-db.sh&amp;quot;]&lt;br /&gt;
command: [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A su vez, del Dockerfile, hay que eliminar la línea de CMD que arrancaba nuestro servidor Flask.&lt;br /&gt;
&lt;br /&gt;
Luego, reinicializa todos los servicios:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. What? ¿No abre? Vamos a investigarlo, veamos el logs:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker logs web_app_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Nos debe salir un mensaje eterno de &amp;quot;/app/scripts/wait-for-db.sh: line 18: mariadb: command not found&amp;quot;. Esto ocurre porque en la imagen de nuestro servicio web NO TENEMOS el cliente de MariaDB. Debemos indicar entonces que lo instale. Añade justo debajo de FROM lo siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y --no-install-recommends mariadb-client &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Al ser un contenedor basado en Linux, y accediendo como root, es como si actualizáramos todos los paquetes y luego instalásemos el cliente de MariaDB. Intentémoslo otra vez:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. ¡Jopelines! ¿Tampoco? Si haces un &amp;quot;docker ps&amp;quot; verás el problema. El contenedor web no tiene expuesto ningún puerto.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    (...)&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;5000&amp;quot;&lt;br /&gt;
    (...)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;quot;Expose&amp;quot; hace visible el puerto '''entre contenedores''', pero '''NO entre tu contenedor y el host'''. Para eso usamos la directiva &amp;quot;ports&amp;quot;, que se encarga del mapping. En nuestro caso, será:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    (...)&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;5000:5000&amp;quot;&lt;br /&gt;
    (...)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
En ocasiones, si los dos puertos coinciden, se puede dejar uno solo con &amp;quot;5000&amp;quot; pero dejaremos los dos. El de la izquierda siempre es el puerto externo (host), el de la derecha es el puerto interno (contenedor). Así que cambia ese detalle y por última vez:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. ¡Ahora sí!&lt;br /&gt;
&lt;br /&gt;
== Trabajando con CMD, entrypoints y commands ==&lt;br /&gt;
&lt;br /&gt;
Piensa que al arrancar un contenedor Docker, siempre se ejecuta un único comando final.&lt;br /&gt;
Lo que hacen ENTRYPOINT, CMD y command (de Compose) es decidir qué comando será ese y con qué argumentos.&lt;br /&gt;
&lt;br /&gt;
=== CMD → “qué hacer por defecto” ===&lt;br /&gt;
&lt;br /&gt;
* Está en el Dockerfile.&lt;br /&gt;
* Es el comando que Docker ejecuta si no se indica otra cosa. En nuestro caso:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== ENTRYPOINT → “qué se ejecuta siempre” ===&lt;br /&gt;
&lt;br /&gt;
* También va en el Dockerfile, o se puede sobreescribir en el docker-compose.yml.&lt;br /&gt;
* Define el programa o script principal que siempre se ejecuta.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
ENTRYPOINT [&amp;quot;/app/scripts/wait-for-db.sh&amp;quot;]&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Normalmente, el entrypoint se combina con CMD en modo &amp;quot;eh, ejecuta esto (entrypoint) y luego este comando final (cmd)&lt;br /&gt;
&lt;br /&gt;
Los entrypoints son muy útiles, sobre todo en los docker-compose's, porque puedo redefinir cómo quiero que inicie un contenedor en concreto '''SIN TENER QUE HACER UN BUILD DE NUEVO DE LA IMAGEN DE DOCKER'''. A diferencia de CMD, que viene predefinido, un entrypoint en mi docker-compose me da más versatilidad&lt;br /&gt;
&lt;br /&gt;
=== command (de docker-compose.yml) → “sobrescribe el CMD” ===&lt;br /&gt;
&lt;br /&gt;
En docker-compose.yml, el campo command: reemplaza al CMD del Dockerfile,pero no al ENTRYPOINT.&lt;br /&gt;
&lt;br /&gt;
== Combinación de elementos ==&lt;br /&gt;
&lt;br /&gt;
Es importante entender cómo Docker trabaja con la orquestación y con los contenedores:&lt;br /&gt;
&lt;br /&gt;
* Lee el YAML como un diccionario&lt;br /&gt;
* Ejecuta todo lo que haya en un Dockerfile EXCEPTUANDO directivas ENTRYPOINT y CMD&lt;br /&gt;
* Monta el volumen o los volúmenes de ese contenedor (si procede)&lt;br /&gt;
&lt;br /&gt;
Y ahora viene el detalle:&lt;br /&gt;
&lt;br /&gt;
* Si estamos levantando un contenedor a través de docker-compose, va a ejecutar los entrypoints y commands definidos en ese docker-compose.&lt;br /&gt;
* Un entrypoint definido en un docker-compose va a sustituir a un entrypoint definido en el Dockerfile&lt;br /&gt;
* Un command definido en un docker-compose va a sustituir a un CMD definido en el Dockerfile.&lt;br /&gt;
&lt;br /&gt;
Como consejo:&lt;br /&gt;
&lt;br /&gt;
* Definimos un entrypoint en el docker-compose. Esto es así porque está fuera de la imagen de Docker y nos permite redefinir el inicio del servicio sin modificar la imagen&lt;br /&gt;
* Eventualmente, definimos un CMD donde indicamos el comando final de ese contenedor al iniciar, que suele ser el arranque de Flask Server o de Gunicorn, dependiendo de si estamos en desarrollo o producción.&lt;br /&gt;
&lt;br /&gt;
No obstante, como nuestros entrypoints son todos scripts en bash, podemos definir en el mismo el arranque final (comando final) y nos olvidamos de CMD.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 5: Volumen de trabajo =&lt;br /&gt;
&lt;br /&gt;
== Problema de desarrollo ==&lt;br /&gt;
&lt;br /&gt;
Vale, ya parece que le hemos pillado el tranquillo a Docker y a Docker Compose. Ahora vamos a hacer algo como desarrolladores. Ve al archivo &amp;quot;app/templates/base_templates.html&amp;quot; y cambia la línea 22:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;title&amp;gt;{{ FLASK_APP_NAME }} - Repository of feature models in UVL &amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
por:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;title&amp;gt;Hello world! &amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. Fíjate en el nombre de la pestaña del navegador, ¿ha cambiado? ¿No? Tsss, esto de los contenedores... bueno, va, vamos a reiniciarlo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker restart web_app_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. ¿Tampoco? Anda que vaya idea la de usar Docker... Claro, es a la hora de construir la imagen, hicimos &amp;quot;COPY . .&amp;quot;, pero no hemos reconstruido nada, así que...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml build --no-cache web&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --force-recreate web&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Importante señalar que le pedimos que ignore la caché para que fuerce a copiar de nuevo todo el contenido (incluyendo el cambio de línea) al interior del contenedor.&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. ¡Ahora sí! Pero... ¿tengo que reconstruir TODO el contenedor en cada cambio línea? ¿Y eso es trabajar de forma ágil?&lt;br /&gt;
&lt;br /&gt;
== Crear volumen de trabajo (bind mount) ==&lt;br /&gt;
&lt;br /&gt;
Evidentemente, eso no tiene sentido, tenemos que usar un volumen de trabajo que nos ayude a sincronizar el código de nuestro host con el código interno del contenedor. Luego en nuestro docker-compose:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  web:&lt;br /&gt;
    (....)&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;5000:5000&amp;quot;&lt;br /&gt;
    volumes:&lt;br /&gt;
      - ../:/app&lt;br /&gt;
    (....)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
La línea&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
- ../:/app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Monta tu carpeta local del proyecto (../) dentro del contenedor, (/app). ¿Y por qué /app? Porque fue el espacio de trabajo que definimos en el Dockerfile. Como ves, la congruencia entre los Dockerfile y el docker-compose es importante.&lt;br /&gt;
&lt;br /&gt;
Recarguemos de nuevo nustra configuración:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ahora ya podemos trabajar con el código en host sin tener que reconstruir la imagen a cada cambio de línea.&lt;br /&gt;
&lt;br /&gt;
== Cuidado con COPY y los bind mounts ==&lt;br /&gt;
&lt;br /&gt;
Un '''bind mount''' (o “montaje de enlace”) es un tipo de volumen en el que Docker conecta directamente una carpeta de tu máquina (el host) con una carpeta dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
Cuando usas un bind mount (por ejemplo ../:/app), Docker monta tu carpeta local sobre la ruta /app del contenedor.&lt;br /&gt;
Eso significa que todo lo que estaba en /app dentro de la imagen desaparece al arrancar el contenedor, porque queda “oculto” por el volumen.&lt;br /&gt;
&lt;br /&gt;
Ahora bien, durante la construcción de la imagen usamos la directiva COPY. ¿Por qué? Porque necesitamos el archivo requirements.txt para instalar las dependencias de Python del proyecto. Pero... ¿realmente necesitamos copiar TODO? No, claro que no, podemos optar por copiar exclusivamente aquellos archivos que nos hagan falta durante '''LA CONSTRUCCIÓN''' de la imagen. El bind mount luego lo machacará.&lt;br /&gt;
&lt;br /&gt;
'''Muy importante:''' los bind mounts, al igual que cualquier otro volumen, están disponibles '''DESPUÉS''' de la construcción de la imagen. Es un error común creer que porque definamos un bind mount en un docker-compose, esos archivos aparecen ya para el contenedor. '''Esto es una fuente inagotable de problemas en la práctica'''.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 6: Distintas configuraciones de despliegue =&lt;br /&gt;
&lt;br /&gt;
Volvamos al punto de partida:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
rm -r docker&lt;br /&gt;
mv docker.bk docker&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta práctica ha sido engorrosa a propósito para quitar esa &amp;quot;magia&amp;quot; que existe cuando lanzas un comando simple de orquestación y todo se levanta y funciona perfectamente. Esa situación está muy bien para desarrollar, pero no para aprender.&lt;br /&gt;
&lt;br /&gt;
Es importante que analices que existen distintos escenarios:&lt;br /&gt;
&lt;br /&gt;
* Hay un docker-compose para desarrollo.&lt;br /&gt;
* Hay 3 docker-compose para producción.&lt;br /&gt;
* Hay 6 Dockerfiles distintos&lt;br /&gt;
* Hay 3 entrypoints distintos&lt;br /&gt;
* Hay una carpeta llamada letsencrypt y otra llamada nginx. Dentro de nginx hay dos archivos de configuración dependiendo de si estamos en dev o en prod.&lt;br /&gt;
&lt;br /&gt;
== Autoevaluación ==&lt;br /&gt;
&lt;br /&gt;
Estudia bien cada archivo. Tómate tu tiempo e intenta responder a estas preguntas:&lt;br /&gt;
&lt;br /&gt;
* ¿Por qué en el docker-compose de dev NO se usa &amp;quot;ports&amp;quot; para enlazar con el host? (pista: nginx)&lt;br /&gt;
* ¿Qué es nginx y por qué es útil aquí?&lt;br /&gt;
* ¿Cuántos bind mounts usa el contenedor web en desarrollo? ¿Para qué sirve el bind mount del propio Docker?&lt;br /&gt;
* ¿Qué diferencia hay entre el entrypoint de desarrollo y el entrypoint de producción?&lt;br /&gt;
* ¿Por qué el servicio web en producción está montando todos esos bind mounts por separado? ¿No sería más sencillo hacer un volumen de todo el código?&lt;br /&gt;
&lt;br /&gt;
== Comentarios finales ==&lt;br /&gt;
&lt;br /&gt;
=== Cuanto más se parezca el entorno de desarrollo al de producción, menos errores habrá ===&lt;br /&gt;
&lt;br /&gt;
En desarrollo es tentador usar configuraciones cómodas: servidor de Flask con --reload, bind mounts, base de datos vacía, logs en consola, etc.&lt;br /&gt;
Pero cuanto más difiere ese entorno de producción, más probable es que algo falle al desplegar.&lt;br /&gt;
&lt;br /&gt;
'''El código no falla por cambiar de servidor, sino porque los entornos se comportan distinto.'''&lt;br /&gt;
&lt;br /&gt;
Ejemplos clásicos:&lt;br /&gt;
&lt;br /&gt;
* En desarrollo usas SQLite, en producción MariaDB → aparecen errores de compatibilidad SQL.&lt;br /&gt;
* En desarrollo Flask sirve los archivos estáticos, en producción lo hace Nginx → las rutas cambian.&lt;br /&gt;
* En desarrollo ejecutas '''flask run''', en producción '''gunicorn''' → los hilos, workers o el reloader funcionan distinto.&lt;br /&gt;
&lt;br /&gt;
Por eso, el objetivo ideal es que tu entorno de desarrollo (dev), de preproducción (staging) y de producción (prod) sean idénticos salvo por las credenciales.&lt;br /&gt;
&lt;br /&gt;
Docker y docker-compose te permiten lograr eso: puedes usar la misma imagen base, cambiando solo el docker-compose (volúmenes, puertos, etc.).&lt;br /&gt;
&lt;br /&gt;
=== Diferencia entre el servidor de Flask y Gunicorn ===&lt;br /&gt;
&lt;br /&gt;
Flask trae incorporado un servidor de desarrollo.&lt;br /&gt;
Es muy útil para depurar porque:&lt;br /&gt;
&lt;br /&gt;
* recarga automáticamente el código (--reload),&lt;br /&gt;
* muestra errores detallados en el navegador,&lt;br /&gt;
* solo ejecuta un proceso.&lt;br /&gt;
&lt;br /&gt;
Sin embargo, no está pensado para producción:&lt;br /&gt;
&lt;br /&gt;
* no maneja múltiples conexiones concurrentes,&lt;br /&gt;
* no tiene control de procesos ni balanceo de carga,&lt;br /&gt;
* no implementa estándares de rendimiento o seguridad (como WSGI robusto).&lt;br /&gt;
&lt;br /&gt;
Ahí entra Gunicorn (Green Unicorn), que sí es un servidor WSGI  de producción (Web Server Gateway Interface, estándar de comunicación entre aplicaciones web Python y servidores web):&lt;br /&gt;
&lt;br /&gt;
* puede lanzar varios workers en paralelo,&lt;br /&gt;
* maneja múltiples peticiones concurrentes,&lt;br /&gt;
* se integra fácilmente con Nginx para servir contenido estático o HTTPS.&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10178</id>
		<title>Tutorial-docker</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10178"/>
				<updated>2025-10-25T17:07:42Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Ejercicio 1: Empezamos desde cero =&lt;br /&gt;
&lt;br /&gt;
El objetivo es eliminar toda la configuración previa y crear desde cero la estructura de carpetas que contendrá nuestros futuros archivos Docker.&lt;br /&gt;
&lt;br /&gt;
== Eliminar la configuración existente ==&lt;br /&gt;
Antes de comenzar, asegúrate de borrar cualquier carpeta Docker existente en el proyecto. En este caso, le haremos una copia de seguridad para poder volver a ella.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mv docker docker.bk&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
También debes asegurarte que trabajas con las variables de entorno adecuadas:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
cp .env.docker.example .env&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
⚠️ Este paso es importante: partimos completamente desde cero para entender cada parte del sistema.&lt;br /&gt;
&lt;br /&gt;
== Crear un Dockerfile mínimo ==&lt;br /&gt;
&lt;br /&gt;
Ahora crea nuevamente la carpeta base donde almacenaremos los archivos relacionados con Docker:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt; mkdir -p docker/images &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vamos a explicar los distintos apartados:&lt;br /&gt;
&lt;br /&gt;
=== FROM ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Indica la imagen base sobre la que se construirá la tuya.&lt;br /&gt;
En este caso, parte de una imagen ligera que ya tiene Python 3.12 instalado.&lt;br /&gt;
&lt;br /&gt;
=== WORKDIR ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta línea define el directorio de trabajo dentro del contenedor.&lt;br /&gt;
A partir de aquí, todos los comandos que aparezcan después (COPY, RUN, CMD, etc.) se ejecutarán dentro de /app.&lt;br /&gt;
&lt;br /&gt;
'''Ojo: /app no es la carpeta app/ de tu proyecto local.'''. Son cosas distintas.&lt;br /&gt;
&lt;br /&gt;
En tu máquina puede existir algo como:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
uvlhub&lt;br /&gt;
   app&lt;br /&gt;
   core&lt;br /&gt;
   requirements.txt &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Pero dentro del contenedor la estructura será distinta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
/ &lt;br /&gt;
  app/ &lt;br /&gt;
    app/ ← aquí dentro se habrá copiado tu carpeta local &amp;quot;app&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Cuando más adelante uses:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Docker copiará todos los archivos de tu proyecto dentro de la carpeta /app del contenedor.&lt;br /&gt;
Por eso el código quedará en /app/app/.&lt;br /&gt;
&lt;br /&gt;
=== COPY ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Copia todos los archivos del proyecto (del host) dentro del contenedor, en /app.&lt;br /&gt;
El primer punto es el origen; el segundo, el destino.&lt;br /&gt;
&lt;br /&gt;
=== RUN ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ejecuta comandos durante la construcción de la imagen.&lt;br /&gt;
Aquí instalamos las dependencias necesarias desde requirements.txt.&lt;br /&gt;
El resultado de este paso quedará guardado en la imagen.&lt;br /&gt;
&lt;br /&gt;
=== EXPOSE ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Documenta el puerto que la aplicación usa dentro del contenedor.&lt;br /&gt;
Esto no lo abre al exterior; simplemente indica que la app escucha en el 5000.&lt;br /&gt;
&lt;br /&gt;
=== CMD ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Define el comando que se ejecutará cuando el contenedor se inicie.&lt;br /&gt;
En este caso, ejecutará la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
=== RUN vs CMD ===&lt;br /&gt;
&lt;br /&gt;
Estas dos instrucciones parecen similares, pero hacen cosas muy distintas.&lt;br /&gt;
&lt;br /&gt;
==== RUN ====&lt;br /&gt;
Se ejecuta durante la construcción de la imagen (en el momento del docker build).&lt;br /&gt;
Cada RUN crea una nueva capa dentro de la imagen.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando instala las dependencias una sola vez, cuando se construye la imagen.&lt;br /&gt;
El resultado (las librerías instaladas) queda grabado dentro de la imagen final.&lt;br /&gt;
&lt;br /&gt;
Si después lanzas un contenedor nuevo a partir de esa imagen, las dependencias ya estarán instaladas y no se volverán a ejecutar.&lt;br /&gt;
&lt;br /&gt;
==== CMD ====&lt;br /&gt;
Se ejecuta cuando se inicia el contenedor (en el docker run).&lt;br /&gt;
Define el proceso principal que se ejecutará dentro del contenedor mientras esté activo.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando no se ejecuta durante la construcción, sino cada vez que arrancas el contenedor.&lt;br /&gt;
Si detienes el contenedor, este proceso también se detiene.&lt;br /&gt;
&lt;br /&gt;
==== Diferencia resumida ====&lt;br /&gt;
&lt;br /&gt;
RUN → se ejecuta al construir la imagen (fase de docker build).&lt;br /&gt;
&lt;br /&gt;
CMD → se ejecuta al iniciar el contenedor (fase de docker run).&lt;br /&gt;
&lt;br /&gt;
Solo puede haber un CMD por Dockerfile.&lt;br /&gt;
Si hay más de uno, solo se usará el último.&lt;br /&gt;
&lt;br /&gt;
== Dockerfile completo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
COPY . .&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto tiene que ir en un Dockerfile en docker/images&lt;br /&gt;
&lt;br /&gt;
== Construir la imagen ==&lt;br /&gt;
Desde la raíz del proyecto (no dentro de docker/images):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker build -t uvlhub:dev -f docker/images/Dockerfile .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Ejecutar el contenedor ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Qué es lo que observas?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 2: Conectando servicios =&lt;br /&gt;
&lt;br /&gt;
¡Vaya! ¡No funciona! Can't connect to MySQL server on 'db'... ¡Ah, claro, intenta conectarse a la base de datos, que no lo tenemos! Pero claro, en un contenedor no es recomendable tener la app y la base de datos conviviendo...&lt;br /&gt;
&lt;br /&gt;
== Crear contenedor MariaDB ==&lt;br /&gt;
&lt;br /&gt;
Antes que nada, haz Ctrl + C para parar el contenedor web. En este ejercicio aprenderás a lanzar un contenedor de MariaDB 12.0.2 de forma manual, configurando todas las variables necesarias para tu aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
El objetivo es entender la complejidad de hacerlo “a mano”, antes de automatizarlo con docker-compose.&lt;br /&gt;
&lt;br /&gt;
== Descargar la imagen oficial ==&lt;br /&gt;
Primero descarga la versión exacta que usaremos:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker pull mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Crear el contenedor de MariaDB ==&lt;br /&gt;
Lanza un contenedor con todas las variables necesarias para que Flask pueda conectarse correctamente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d --name mariadb_container \&lt;br /&gt;
 -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
 -e FLASK_ENV=development \&lt;br /&gt;
 -e DOMAIN=localhost \&lt;br /&gt;
 -e MARIADB_HOSTNAME=db \&lt;br /&gt;
 -e MARIADB_PORT=3306 \&lt;br /&gt;
 -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
 -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
 -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
 -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
 -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
 -e WORKING_DIR=/app/ \&lt;br /&gt;
 -p 3306:3306 mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Explicación:&lt;br /&gt;
&lt;br /&gt;
* -d: ejecuta el contenedor en segundo plano (modo detached).&lt;br /&gt;
* --name: asigna un nombre identificable al contenedor.&lt;br /&gt;
* -e: define variables de entorno dentro del contenedor.&lt;br /&gt;
* -p 3306:3306: publica el puerto de MariaDB en tu máquina local.&lt;br /&gt;
* mariadb:12.0.2: imagen exacta que vamos a usar.&lt;br /&gt;
&lt;br /&gt;
Aquí ya, de entrada, vemos que esto es más feo que una nevera por detrás. Para empezar, estamos pasando a lo loco un montón de variables que difílmente será replicable en producción, por contener información sensible. Aparte, si metemos nuevas variables, ese comando queda inservible. Necesitaremos una solución más ágil para este problema&lt;br /&gt;
&lt;br /&gt;
== Comprobar que está corriendo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver algo así:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES&lt;br /&gt;
abcd1234efgh mariadb:12.0.2 &amp;quot;docker-entrypoint.s…&amp;quot; Up 5 seconds 0.0.0.0:3306-&amp;gt;3306/tcp mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Conectarse a la base de datos ==&lt;br /&gt;
Abre una consola dentro del contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it mariadb_container bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y dentro, accede al cliente de MariaDB:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mariadb -u root -p&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Contraseña: uvlhubdb_root_password&lt;br /&gt;
&lt;br /&gt;
== Verificar la base de datos ==&lt;br /&gt;
Una vez dentro del cliente MySQL, ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
SHOW DATABASES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver que existen la bases de datos uvlhubdb porque pedimos crearla a la hora de levantar el contenedor. ¿Qué tiene la base de datos dentro? Vamos a verlo...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
USE uvlhubdb;&lt;br /&gt;
SHOW TABLES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Un total de... 0 tablas... ¡Claro! Es que solo hemos creado la base en sí, pero no las tablas. ¿Con un script SQL tal vez? ¡Espera, espera, si tenemos las migraciones! Pero claro, para eso necesito el contenedor de la app levantado. Sal de la consola SQL y del contenedor de MariaDB. Bien, ahora en el host...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta vez con el flag &amp;quot;-d&amp;quot; para que corra en segundo plano. Ahora, accedamos al contenedor y ejecutemos las migraciones. Pero... ¿cómo se llama mi contenedor?&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate que Docker asigna un nombre aleatorio a tu contenedor web porque no definiste ninguno. Una vez identificado el nombre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it &amp;lt;nombre&amp;gt; bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
What? ¿Qué pasa? ¿Otra vez error de conexión con la base? Si tenemos el contenedor funcionando, esto es un jaleo...&lt;br /&gt;
&lt;br /&gt;
== Redes internas ==&lt;br /&gt;
&lt;br /&gt;
Ahora mismo tienes dos contenedores aislados, el de la app y el la base de datos. Por defecto, cada contenedor tiene su propia red interna y no puede ver a los demás por nombre. El hostname '''db''' que usa Flask no existe en su DNS interno.&lt;br /&gt;
&lt;br /&gt;
=== Conectar servicios ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una red interna que compartan ambos contenedores&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker network create uvlhub_network&lt;br /&gt;
docker network ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vemos que ya aparece nuestra red '''uvlhub_network'''. Ahora, conectemos el contenedor de MariaDB a la red:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Claro, si te fijas, ahora hay que añadir, además de toda esa configuración engorrosa, que se añada en la misma red y que además tenga de hostname &amp;quot;db&amp;quot; para que el contenedor web entienda qué es &amp;quot;db&amp;quot;. Tenemos que hacer lo mismo para el contenedor web:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop &amp;lt;nombre&amp;gt;&lt;br /&gt;
docker rm &amp;lt;nombre&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y lo levantamos de nuevo, pero ya en la nueva red. De paso, le ponemos un nombre para identificarlo mejor.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 --name web_app_container --network uvlhub_network -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y ahora ya podemos acceder al contenedor por su nombre y ejecutar las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Mejor?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 3: Persistencia =&lt;br /&gt;
&lt;br /&gt;
Vamos a simular que hemos desplegado justo esto en producción y el servidor se reinicia por un corte de luz. Reiniciemos a mano cada contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker restart web_app_container&lt;br /&gt;
docker restart mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. What? ¿Cómo? ¿Qué ha pasado? ¿Otra vez problema de conexión? Ve dándole a F5... F5... ¡bingo! Ahora sí sale, pero menuda aleatoriedad, ¿no? Claro, es que ahora caes en que una cosa es que el contenedor haya arrancado y otra que esté listo.&lt;br /&gt;
&lt;br /&gt;
Aparte, tenemos otro problema. ¿Qué pasa con la persistencia? Porque si borro el contenedor de MariaDB, todos los datos que estén en su interior también se pierden. Y si estamos en producción, lío asegurado.&lt;br /&gt;
&lt;br /&gt;
== Volúmenes ==&lt;br /&gt;
&lt;br /&gt;
Cada vez que eliminas el contenedor de MariaDB, todas las bases de datos se pierden. Esto ocurre porque los datos se guardan dentro del sistema de archivos del contenedor, y al borrarlo desaparecen con él.&lt;br /&gt;
&lt;br /&gt;
En este ejercicio creas un volumen de Docker para que los datos persistan entre ejecuciones.&lt;br /&gt;
&lt;br /&gt;
=== Crear un volumen ===&lt;br /&gt;
Un volumen es una carpeta gestionada por Docker que vive fuera del ciclo de vida de los contenedores.&lt;br /&gt;
&lt;br /&gt;
Ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume create mariadb_data&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba que existe:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
La salida muestra algo como:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
DRIVER    VOLUME NAME&lt;br /&gt;
local     mariadb_data&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Crear un contenedor de MariaDB con el volumen ===&lt;br /&gt;
Lanza el contenedor y conecta el volumen a la ruta donde MariaDB guarda los datos (/var/lib/mysql):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -v mariadb_data:/var/lib/mysql \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Qué significa &amp;quot;:&amp;quot; en los volúmenes ===&lt;br /&gt;
&lt;br /&gt;
Cuando usas la opción '''-v''' o '''--volume''' en Docker, el signo dos puntos (''':''') separa dos rutas:&lt;br /&gt;
&lt;br /&gt;
'''-v origen:destino'''&lt;br /&gt;
&lt;br /&gt;
origen → es la ruta o el volumen en tu máquina (el host).&lt;br /&gt;
&lt;br /&gt;
destino → es la ruta dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 4: Orquestación de servicios =&lt;br /&gt;
&lt;br /&gt;
Hasta ahora hemos creado y conectado los contenedores manualmente.&lt;br /&gt;
Ahora vas a usar Docker Compose para definir todo en un solo archivo.&lt;br /&gt;
&lt;br /&gt;
== Orquestación básica ==&lt;br /&gt;
&lt;br /&gt;
Crea un archivo llamado docker-compose.yml en la carpeta '''docker''' con este contenido:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    container_name: web_app_container&lt;br /&gt;
    env_file:&lt;br /&gt;
      - ../.env&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;5000&amp;quot;&lt;br /&gt;
    depends_on:&lt;br /&gt;
      - db&lt;br /&gt;
    build:&lt;br /&gt;
      context: ../&lt;br /&gt;
      dockerfile: docker/images/Dockerfile&lt;br /&gt;
    networks:&lt;br /&gt;
      - uvlhub_network&lt;br /&gt;
&lt;br /&gt;
  db:&lt;br /&gt;
    container_name: mariadb_container&lt;br /&gt;
    image: mariadb:12.0.2&lt;br /&gt;
    env_file:&lt;br /&gt;
      - ../.env&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;3306:3306&amp;quot;&lt;br /&gt;
    volumes:&lt;br /&gt;
      - db_data:/var/lib/mysql&lt;br /&gt;
    networks:&lt;br /&gt;
      - uvlhub_network&lt;br /&gt;
&lt;br /&gt;
volumes:&lt;br /&gt;
  db_data:&lt;br /&gt;
&lt;br /&gt;
networks:&lt;br /&gt;
  uvlhub_network:&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate que esta orquestación de servicios me permite definir el nombre de cada servicio, su dependencia, sus puertos, sus volúmenes, redes... todo lo que hacíamos de forma manual y propenso a errores queda ahora más fácil de gestionar. Además, ahora ambos contenedores comparten el mismo archivo .env de variables de entorno.&lt;br /&gt;
&lt;br /&gt;
Bien, ahora vamos a levantar estos contenedores y acceder al contenedor web:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Inmediatamente después, ejecuta las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué ha ocurrido? Depende del tiempo que hayas tardado, puede que haya saltado un error o puede que haya funcionado. Puede que hayas intentado varias veces el comando anterior hasta que ha funcionado. ¿Por qué crees que ocurre esta aparente aleatoriedad?&lt;br /&gt;
&lt;br /&gt;
== Dependencias entre contenedores y entrypoints ==&lt;br /&gt;
&lt;br /&gt;
Fíjate que tenemos un &amp;quot;depends on&amp;quot; en el servicio web. Significa que el servicio &amp;quot;db&amp;quot; debe iniciarse antes que el servicio web. Pero, cuidado, ahí está la trampa, porque &amp;quot;servicio iniciado != servicio preparado&amp;quot;. El servicio de MariaDB tiene que arrancar, configurarse, etc...&lt;br /&gt;
&lt;br /&gt;
Cuando hicimos el comando COPY en nuestro Dockerfile, también copiamos la carpeta '''scripts'''. Uno de los scripts es '''wait-for-db.sh''', que realiza continuamente llamadas a MariaDB hasta que este servicio está listo para recibir peticiones. Ábrelo y estúdialo.&lt;br /&gt;
&lt;br /&gt;
Añade al servicio web de tu docker compose las siguientes líneas:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
entrypoint: [&amp;quot;/app/scripts/wait-for-db.sh&amp;quot;]&lt;br /&gt;
command: [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A su vez, del Dockerfile, hay que eliminar la línea de CMD que arrancaba nuestro servidor Flask.&lt;br /&gt;
&lt;br /&gt;
Luego, reinicializa todos los servicios:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. What? ¿No abre? Vamos a investigarlo, veamos el logs:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker logs web_app_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Nos debe salir un mensaje eterno de &amp;quot;/app/scripts/wait-for-db.sh: line 18: mariadb: command not found&amp;quot;. Esto ocurre porque en la imagen de nuestro servicio web NO TENEMOS el cliente de MariaDB. Debemos indicar entonces que lo instale. Añade justo debajo de FROM lo siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y --no-install-recommends mariadb-client &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Al ser un contenedor basado en Linux, y accediendo como root, es como si actualizáramos todos los paquetes y luego instalásemos el cliente de MariaDB. Intentémoslo otra vez:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. ¡Jopelines! ¿Tampoco? Si haces un &amp;quot;docker ps&amp;quot; verás el problema. El contenedor web no tiene expuesto ningún puerto.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    (...)&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;5000&amp;quot;&lt;br /&gt;
    (...)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;quot;Expose&amp;quot; hace visible el puerto '''entre contenedores''', pero '''NO entre tu contenedor y el host'''. Para eso usamos la directiva &amp;quot;ports&amp;quot;, que se encarga del mapping. En nuestro caso, será:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    (...)&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;5000:5000&amp;quot;&lt;br /&gt;
    (...)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
En ocasiones, si los dos puertos coinciden, se puede dejar uno solo con &amp;quot;5000&amp;quot; pero dejaremos los dos. El de la izquierda siempre es el puerto externo (host), el de la derecha es el puerto interno (contenedor). Así que cambia ese detalle y por última vez:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. ¡Ahora sí!&lt;br /&gt;
&lt;br /&gt;
== Trabajando con CMD, entrypoints y commands ==&lt;br /&gt;
&lt;br /&gt;
Piensa que al arrancar un contenedor Docker, siempre se ejecuta un único comando final.&lt;br /&gt;
Lo que hacen ENTRYPOINT, CMD y command (de Compose) es decidir qué comando será ese y con qué argumentos.&lt;br /&gt;
&lt;br /&gt;
=== CMD → “qué hacer por defecto” ===&lt;br /&gt;
&lt;br /&gt;
* Está en el Dockerfile.&lt;br /&gt;
* Es el comando que Docker ejecuta si no se indica otra cosa. En nuestro caso:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== ENTRYPOINT → “qué se ejecuta siempre” ===&lt;br /&gt;
&lt;br /&gt;
* También va en el Dockerfile, o se puede sobreescribir en el docker-compose.yml.&lt;br /&gt;
* Define el programa o script principal que siempre se ejecuta.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
ENTRYPOINT [&amp;quot;/app/scripts/wait-for-db.sh&amp;quot;]&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Normalmente, el entrypoint se combina con CMD en modo &amp;quot;eh, ejecuta esto (entrypoint) y luego este comando final (cmd)&lt;br /&gt;
&lt;br /&gt;
Los entrypoints son muy útiles, sobre todo en los docker-compose's, porque puedo redefinir cómo quiero que inicie un contenedor en concreto '''SIN TENER QUE HACER UN BUILD DE NUEVO DE LA IMAGEN DE DOCKER'''. A diferencia de CMD, que viene predefinido, un entrypoint en mi docker-compose me da más versatilidad&lt;br /&gt;
&lt;br /&gt;
=== command (de docker-compose.yml) → “sobrescribe el CMD” ===&lt;br /&gt;
&lt;br /&gt;
En docker-compose.yml, el campo command: reemplaza al CMD del Dockerfile,pero no al ENTRYPOINT.&lt;br /&gt;
&lt;br /&gt;
== Combinación de elementos ==&lt;br /&gt;
&lt;br /&gt;
Es importante entender cómo Docker trabaja con la orquestación y con los contenedores:&lt;br /&gt;
&lt;br /&gt;
* Lee el YAML como un diccionario&lt;br /&gt;
* Ejecuta todo lo que haya en un Dockerfile EXCEPTUANDO directivas ENTRYPOINT y CMD&lt;br /&gt;
* Monta el volumen o los volúmenes de ese contenedor (si procede)&lt;br /&gt;
&lt;br /&gt;
Y ahora viene el detalle:&lt;br /&gt;
&lt;br /&gt;
* Si estamos levantando un contenedor a través de docker-compose, va a ejecutar los entrypoints y commands definidos en ese docker-compose.&lt;br /&gt;
* Un entrypoint definido en un docker-compose va a sustituir a un entrypoint definido en el Dockerfile&lt;br /&gt;
* Un command definido en un docker-compose va a sustituir a un CMD definido en el Dockerfile.&lt;br /&gt;
&lt;br /&gt;
Como consejo:&lt;br /&gt;
&lt;br /&gt;
* Definimos un entrypoint en el docker-compose. Esto es así porque está fuera de la imagen de Docker y nos permite redefinir el inicio del servicio sin modificar la imagen&lt;br /&gt;
* Eventualmente, definimos un CMD donde indicamos el comando final de ese contenedor al iniciar, que suele ser el arranque de Flask Server o de Gunicorn, dependiendo de si estamos en desarrollo o producción.&lt;br /&gt;
&lt;br /&gt;
No obstante, como nuestros entrypoints son todos scripts en bash, podemos definir en el mismo el arranque final (comando final) y nos olvidamos de CMD.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 5: Volumen de trabajo =&lt;br /&gt;
&lt;br /&gt;
== Problema de desarrollo ==&lt;br /&gt;
&lt;br /&gt;
Vale, ya parece que le hemos pillado el tranquillo a Docker y a Docker Compose. Ahora vamos a hacer algo como desarrolladores. Ve al archivo &amp;quot;app/templates/base_templates.html&amp;quot; y cambia la línea 22:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;title&amp;gt;{{ FLASK_APP_NAME }} - Repository of feature models in UVL &amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
por:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;title&amp;gt;Hello world! &amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. Fíjate en el nombre de la pestaña del navegador, ¿ha cambiado? ¿No? Tsss, esto de los contenedores... bueno, va, vamos a reiniciarlo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker restart web_app_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. ¿Tampoco? Anda que vaya idea la de usar Docker... Claro, es a la hora de construir la imagen, hicimos &amp;quot;COPY . .&amp;quot;, pero no hemos reconstruido nada, así que...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml build --no-cache web&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --force-recreate web&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Importante señalar que le pedimos que ignore la caché para que fuerce a copiar de nuevo todo el contenido (incluyendo el cambio de línea) al interior del contenedor.&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. ¡Ahora sí! Pero... ¿tengo que reconstruir TODO el contenedor en cada cambio línea? ¿Y eso es trabajar de forma ágil?&lt;br /&gt;
&lt;br /&gt;
== Crear volumen de trabajo (bind mount) ==&lt;br /&gt;
&lt;br /&gt;
Evidentemente, eso no tiene sentido, tenemos que usar un volumen de trabajo que nos ayude a sincronizar el código de nuestro host con el código interno del contenedor. Luego en nuestro docker-compose:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  web:&lt;br /&gt;
    (....)&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;5000:5000&amp;quot;&lt;br /&gt;
    volumes:&lt;br /&gt;
      - ../:/app&lt;br /&gt;
    (....)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
La línea&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
- ../:/app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Monta tu carpeta local del proyecto (../) dentro del contenedor, (/app). ¿Y por qué /app? Porque fue el espacio de trabajo que definimos en el Dockerfile. Como ves, la congruencia entre los Dockerfile y el docker-compose es importante.&lt;br /&gt;
&lt;br /&gt;
Recarguemos de nuevo nustra configuración:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ahora ya podemos trabajar con el código en host sin tener que reconstruir la imagen a cada cambio de línea.&lt;br /&gt;
&lt;br /&gt;
== Cuidado con COPY y los bind mounts ==&lt;br /&gt;
&lt;br /&gt;
Un '''bind mount''' (o “montaje de enlace”) es un tipo de volumen en el que Docker conecta directamente una carpeta de tu máquina (el host) con una carpeta dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
Cuando usas un bind mount (por ejemplo ../:/app), Docker monta tu carpeta local sobre la ruta /app del contenedor.&lt;br /&gt;
Eso significa que todo lo que estaba en /app dentro de la imagen desaparece al arrancar el contenedor, porque queda “oculto” por el volumen.&lt;br /&gt;
&lt;br /&gt;
Ahora bien, durante la construcción de la imagen usamos la directiva COPY. ¿Por qué? Porque necesitamos el archivo requirements.txt para instalar las dependencias de Python del proyecto. Pero... ¿realmente necesitamos copiar TODO? No, claro que no, podemos optar por copiar exclusivamente aquellos archivos que nos hagan falta durante '''LA CONSTRUCCIÓN''' de la imagen. El bind mount luego lo machacará.&lt;br /&gt;
&lt;br /&gt;
'''Muy importante:''' los bind mounts, al igual que cualquier otro volumen, están disponibles '''DESPUÉS''' de la construcción de la imagen. Es un error común creer que porque definamos un bind mount en un docker-compose, esos archivos aparecen ya para el contenedor. '''Esto es una fuente inagotable de problemas en la práctica'''.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 6: Distintas configuraciones de despliegue =&lt;br /&gt;
&lt;br /&gt;
Volvamos al punto de partida:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
rm -r docker&lt;br /&gt;
mv docker.bk docker&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta práctica ha sido engorrosa a propósito para quitar esa &amp;quot;magia&amp;quot; que existe cuando lanzas un comando simple de orquestación y todo se levanta y funciona perfectamente. Esa situación está muy bien para desarrollar, pero no para aprender.&lt;br /&gt;
&lt;br /&gt;
Es importante que analices que existen distintos escenarios:&lt;br /&gt;
&lt;br /&gt;
* Hay un docker-compose para desarrollo.&lt;br /&gt;
* Hay 3 docker-compose para producción.&lt;br /&gt;
* Hay 6 Dockerfiles distintos&lt;br /&gt;
* Hay 3 entrypoints distintos&lt;br /&gt;
* Hay una carpeta llamada letsencrypt y otra llamada nginx. Dentro de nginx hay dos archivos de configuración dependiendo de si estamos en dev o en prod.&lt;br /&gt;
&lt;br /&gt;
== Autoevaluación ==&lt;br /&gt;
&lt;br /&gt;
Estudia bien cada archivo. Tómate tu tiempo e intenta responder a estas preguntas:&lt;br /&gt;
&lt;br /&gt;
* ¿Por qué en el docker-compose de dev NO se usa &amp;quot;ports&amp;quot; para enlazar con el host? (pista: nginx)&lt;br /&gt;
* ¿Qué es nginx y por qué es útil aquí?&lt;br /&gt;
* ¿Cuántos bind mounts usa el contenedor web en desarrollo? ¿Para qué sirve el bind mount del propio Docker?&lt;br /&gt;
* ¿Qué diferencia hay entre el entrypoint de desarrollo y el entrypoint de producción?&lt;br /&gt;
* ¿Por qué el servicio web en producción está montando todos esos bind mounts por separado? ¿No sería más sencillo hacer un volumen de todo el código?&lt;br /&gt;
&lt;br /&gt;
== Comentarios finales ==&lt;br /&gt;
&lt;br /&gt;
=== Cuanto más se parezca el entorno de desarrollo al de producción, menos errores habrá ===&lt;br /&gt;
&lt;br /&gt;
En desarrollo es tentador usar configuraciones cómodas: servidor de Flask con --reload, bind mounts, base de datos vacía, logs en consola, etc.&lt;br /&gt;
Pero cuanto más difiere ese entorno de producción, más probable es que algo falle al desplegar.&lt;br /&gt;
&lt;br /&gt;
'''El código no falla por cambiar de servidor, sino porque los entornos se comportan distinto.'''&lt;br /&gt;
&lt;br /&gt;
Ejemplos clásicos:&lt;br /&gt;
&lt;br /&gt;
* En desarrollo usas SQLite, en producción MariaDB → aparecen errores de compatibilidad SQL.&lt;br /&gt;
* En desarrollo Flask sirve los archivos estáticos, en producción lo hace Nginx → las rutas cambian.&lt;br /&gt;
* En desarrollo ejecutas '''flask run''', en producción '''gunicorn''' → los hilos, workers o el reloader funcionan distinto.&lt;br /&gt;
&lt;br /&gt;
Por eso, el objetivo ideal es que tu entorno de desarrollo (dev), de preproducción (staging) y de producción (prod) sean idénticos salvo por las credenciales.&lt;br /&gt;
&lt;br /&gt;
Docker y docker-compose te permiten lograr eso: puedes usar la misma imagen base, cambiando solo el docker-compose (volúmenes, puertos, etc.).&lt;br /&gt;
&lt;br /&gt;
=== Diferencia entre el servidor de Flask y Gunicorn ===&lt;br /&gt;
&lt;br /&gt;
Flask trae incorporado un servidor de desarrollo.&lt;br /&gt;
Es muy útil para depurar porque:&lt;br /&gt;
&lt;br /&gt;
* recarga automáticamente el código (--reload),&lt;br /&gt;
* muestra errores detallados en el navegador,&lt;br /&gt;
* solo ejecuta un proceso.&lt;br /&gt;
&lt;br /&gt;
Sin embargo, no está pensado para producción:&lt;br /&gt;
&lt;br /&gt;
* no maneja múltiples conexiones concurrentes,&lt;br /&gt;
* no tiene control de procesos ni balanceo de carga,&lt;br /&gt;
* no implementa estándares de rendimiento o seguridad (como WSGI robusto).&lt;br /&gt;
&lt;br /&gt;
Ahí entra Gunicorn (Green Unicorn), que sí es un servidor WSGI  de producción (Web Server Gateway Interface, estándar de comunicación entre aplicaciones web Python y servidores web):&lt;br /&gt;
&lt;br /&gt;
* puede lanzar varios workers en paralelo,&lt;br /&gt;
* maneja múltiples peticiones concurrentes,&lt;br /&gt;
* se integra fácilmente con Nginx para servir contenido estático o HTTPS.&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=P%C3%A1gina_Principal&amp;diff=10177</id>
		<title>Página Principal</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=P%C3%A1gina_Principal&amp;diff=10177"/>
				<updated>2025-10-24T10:00:03Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: /* Enlaces */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;'''Wiki de Evaluación y Gestión de la Configuración (EGC)'''&lt;br /&gt;
'''Grado de Ingeniería del Software'''&lt;br /&gt;
&lt;br /&gt;
= Cursos =&lt;br /&gt;
&lt;br /&gt;
=== Presente curso ===&lt;br /&gt;
&lt;br /&gt;
* [[2025/2026]] &lt;br /&gt;
&lt;br /&gt;
==== Cursos Pasados ====&lt;br /&gt;
* [[2013/2014]]&lt;br /&gt;
* [[2014/2015]]&lt;br /&gt;
* [[2015/2016]]&lt;br /&gt;
* [[2016/2017]]&lt;br /&gt;
* [[2017/2018]]&lt;br /&gt;
* [[2018/2019]]&lt;br /&gt;
* [[2019/2020]]&lt;br /&gt;
* [[2020/2021]]&lt;br /&gt;
* [[2021/2022]]&lt;br /&gt;
* [[2022/2023]]&lt;br /&gt;
* [[2023/2024]]&lt;br /&gt;
* [[2024/2025]]&lt;br /&gt;
&lt;br /&gt;
= Enlaces =&lt;br /&gt;
* [https://telegram.me/egcetsii Canal de Telegram]&lt;br /&gt;
* [https://github.com/EGCETSII GitHub de la asignatura]&lt;br /&gt;
* [[Salón de la fama de EGC]]&lt;br /&gt;
* [https://hdvirtual.us.es/discovirt/index.php/s/P6s8aoFQqe8NHtA Carpeta con algunos ejercicios y exámenes de otros cursos]&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=M1-25_26&amp;diff=10174</id>
		<title>M1-25 26</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=M1-25_26&amp;diff=10174"/>
				<updated>2025-10-16T11:33:39Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: /* M1: Sistema funcionando */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[Página_Principal]] -&amp;gt; [[2025/2026]] -&amp;gt; [[Proyecto - 25/26]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!--&lt;br /&gt;
== Listado de turnos para el M1 ==&lt;br /&gt;
&lt;br /&gt;
🆕 &lt;br /&gt;
&lt;br /&gt;
* [https://hdvirtual.us.es/discovirt/index.php/s/MS5aoRbMD9gMHAH Listado de turnos de defensas para el M1]. Recuerde leer atentamente las instrucciones para la defensa y llevar todo el material que se requiere. &lt;br /&gt;
--&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== M1: Sistema funcionando ==&lt;br /&gt;
&lt;br /&gt;
M1: 🔊,🔊 🔊 🔊 🔊 🔊&lt;br /&gt;
&lt;br /&gt;
primer  milestone (21 de octubre)&lt;br /&gt;
&lt;br /&gt;
[https://uses0.sharepoint.com/:x:/s/EGCETSII/EbGu4WsT6YdEp_gOZ8v2k9YByLHed_tkAu69klayrOI-IQ?e=2q3tNY Reservas aquí siguiendo las instrucciones]&lt;br /&gt;
&lt;br /&gt;
plazo para coger turno de defensa: '''viernes 17 de octubre de 2025 a las 14.00''' &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
➖➖➖➖➖➖➖➖➖&lt;br /&gt;
 👉🏾 M1: Seguimiento de proyectos&lt;br /&gt;
&lt;br /&gt;
* '''Dirigido a''': todos los equipos, de todos los proyectos y todos los alumnos del equipo&lt;br /&gt;
* '''Formato''': &lt;br /&gt;
&lt;br /&gt;
Se reservará espacio para la defensa siguiendo [https://uses0.sharepoint.com/:x:/s/EGCETSII/EbGu4WsT6YdEp_gOZ8v2k9YByLHed_tkAu69klayrOI-IQ?e=2q3tNY estas instrucciones (use su UVUS para acceder)] &lt;br /&gt;
&lt;br /&gt;
* '''¿Qué debe tener preparado cada equipo?''': &lt;br /&gt;
&lt;br /&gt;
** Debe tener impreso y relleno el [https://hdvirtual.us.es/discovirt/index.php/s/TN3SsantHGiB7o8 formulario de seguimiento]&lt;br /&gt;
** Debe tener decidido el proyecto que va a abordar&lt;br /&gt;
** Debe tener elaborada y presentar impresa, en el momento de la defensa, el &amp;quot;Acta Fundacional&amp;quot;, conforme a las indicaciones establecidas en las instrucciones del proyecto.&lt;br /&gt;
** Debe tener el sistema instalado y funcionando (no es obligatorio tener implementado ningún cambio aunque se valorará positivamente tenerlo hecho)&lt;br /&gt;
** Debe tener el entorno de pruebas instalado y funcionando. &lt;br /&gt;
** Deben entregar una presentación en la que describan los WIs que va a realizar y cómo los pretende abordar y para ello llevarán una propuesta de &amp;quot;prototipo realista&amp;quot; siguiendo las instrucciones vistas en clases. **Para entregar la presentación use el siguiente [https://hdvirtual.us.es/discovirt/index.php/s/i8RZMoJoERYZndw enlace] usando para el nombre del archivo lo siguiente: tutor-nombreproyecto.pdf (siendo tutor= benavides|moreno|ramos|romero|galindo)&lt;br /&gt;
** En la presentación también indicará cuál va a ser su ciclo de CI/CD y un esbozo del script que podría darle soporte. &lt;br /&gt;
** Proyectos UVLHub: debe mostrar que todos los miembros del equipo han hecho las prácticas de instalación del sistema y la de pruebas y por lo tanto el proyecto UVLHub está desplegado en local y pasa las distintas pruebas que se han visto en prácticas. &lt;br /&gt;
&lt;br /&gt;
La defensa terminará con una breve explicación de cuáles son los WIs que se van a hacer al proyecto y otros detalles que quieran comentar los miembros del equipo. La defensa no pude durar más de 25 minutos (no hace falta tampoco que sea de 25 minutos si puede hacerlo en menos tiempo)&lt;br /&gt;
&lt;br /&gt;
* '''Impacto en la nota''': Si no se supera esta entrega, la nota parcial del proyecto se puede ver afectada con una reducción de hasta 1 punto. Ver más detalles en el apartado de Notas de la página del [[Proyecto - 25/26 | proyecto]]. &lt;br /&gt;
&lt;br /&gt;
* '''Dinámica de la sesión''': Las defensas se harán en presencia de los tutores en los turnos asignados y constará de las siguientes partes: &lt;br /&gt;
&lt;br /&gt;
** Todos los miembros del equipo deben tener o bien un ordenador personal o bien un ordenador del aula de prácticas en los que mostrarán que son capaces de hacer las prácticas mencionadas. Deben prepararlo previamente para no tardar más de 10 minutos en hacer dicha instalación. &lt;br /&gt;
** El profesor empleará 5 minutos en verificar (sí/no) si la práctica 1 (instalación) y 4 (pruebas) está hecha por cada uno de los miembros.&lt;br /&gt;
** El resto del tiempo se empleará en explicar cuáles son los WIs que se quieren abordar y solventar dudas. &lt;br /&gt;
** La sesión no podrá durar más de 25 minutos. &lt;br /&gt;
** Se ruega máxima puntualidad.&lt;br /&gt;
&lt;br /&gt;
Lo que se espera en general de este primer milestone es verificar que todos los equipos y todos los miembros de los equipos han sido capaces de hacer las prácticas mencionadas y tener funcionando el entorno de desarrollo y despliegue de la herramienta UVLHub. También se mirará que se presente de manera coherente el proyecto en el que va a participar el equipo y que se tenga pensado cómo se van a abordar los WIs.&lt;br /&gt;
&lt;br /&gt;
* [[Dudas-M1]]&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=M1-25_26&amp;diff=10168</id>
		<title>M1-25 26</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=M1-25_26&amp;diff=10168"/>
				<updated>2025-10-15T10:00:16Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: /* M1: Sistema funcionando */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[Página_Principal]] -&amp;gt; [[2025/2026]] -&amp;gt; [[Proyecto - 25/26]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!--&lt;br /&gt;
== Listado de turnos para el M1 ==&lt;br /&gt;
&lt;br /&gt;
🆕 &lt;br /&gt;
&lt;br /&gt;
* [https://hdvirtual.us.es/discovirt/index.php/s/MS5aoRbMD9gMHAH Listado de turnos de defensas para el M1]. Recuerde leer atentamente las instrucciones para la defensa y llevar todo el material que se requiere. &lt;br /&gt;
--&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== M1: Sistema funcionando ==&lt;br /&gt;
&lt;br /&gt;
M1: 🔊,🔊 🔊 🔊 🔊 🔊&lt;br /&gt;
&lt;br /&gt;
primer  milestone (21 de octubre)&lt;br /&gt;
&lt;br /&gt;
plazo para coger turno de defensa: '''viernes 17 de octubre de 2025 a las 14.00''' [Reservas aquí siguiendo las instrucciones]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
➖➖➖➖➖➖➖➖➖&lt;br /&gt;
 👉🏾 M1: Seguimiento de proyectos&lt;br /&gt;
&lt;br /&gt;
* '''Dirigido a''': todos los equipos, de todos los proyectos y todos los alumnos del equipo&lt;br /&gt;
* '''Formato''': &lt;br /&gt;
&lt;br /&gt;
Se reservará espacio para la defensa siguiendo [ estas instrucciones (use su UVUS para acceder)] &lt;br /&gt;
&lt;br /&gt;
* '''¿Qué debe tener preparado cada equipo?''': &lt;br /&gt;
&lt;br /&gt;
** Debe tener impreso y relleno el [ formulario de seguimiento]&lt;br /&gt;
** Debe tener decidido el proyecto que va a abordar&lt;br /&gt;
** Debe tener elaborada y presentar impresa, en el momento de la defensa, el &amp;quot;Acta Fundacional&amp;quot;, conforme a las indicaciones establecidas en las instrucciones del proyecto.&lt;br /&gt;
** Debe tener el sistema instalado y funcionando (no es obligatorio tener implementado ningún cambio aunque se valorará positivamente tenerlo hecho)&lt;br /&gt;
** Debe tener el entorno de pruebas instalado y funcionando. &lt;br /&gt;
** Deben entregar una presentación en la que describan los WIs que va a realizar y cómo los pretende abordar y para ello llevarán una propuesta de &amp;quot;prototipo realista&amp;quot; siguiendo las instrucciones vistas en clases. **Para entregar la presentación use el siguiente [ enlace] usando para el nombre del archivo lo siguiente: tutor-nombreproyecto.pdf (siendo tutor= benavides|moreno|ramos|romero|galindo)&lt;br /&gt;
** En la presentación también indicará cuál va a ser su ciclo de CI/CD y un esbozo del script que podría darle soporte. &lt;br /&gt;
** Proyectos UVLHub: debe mostrar que todos los miembros del equipo han hecho las prácticas de instalación del sistema y la de pruebas y por lo tanto el proyecto UVLHub está desplegado en local y pasa las distintas pruebas que se han visto en prácticas. &lt;br /&gt;
&lt;br /&gt;
La defensa terminará con una breve explicación de cuáles son los WIs que se van a hacer al proyecto y otros detalles que quieran comentar los miembros del equipo. La defensa no pude durar más de 25 minutos (no hace falta tampoco que sea de 25 minutos si puede hacerlo en menos tiempo)&lt;br /&gt;
&lt;br /&gt;
* '''Impacto en la nota''': Si no se supera esta entrega, la nota parcial del proyecto se puede ver afectada con una reducción de hasta 1 punto. Ver más detalles en el apartado de Notas de la página del [[Proyecto - 25/26 | proyecto]]. &lt;br /&gt;
&lt;br /&gt;
* '''Dinámica de la sesión''': Las defensas se harán en presencia de los tutores en los turnos asignados y constará de las siguientes partes: &lt;br /&gt;
&lt;br /&gt;
** Todos los miembros del equipo deben tener o bien un ordenador personal o bien un ordenador del aula de prácticas en los que mostrarán que son capaces de hacer las prácticas mencionadas. Deben prepararlo previamente para no tardar más de 10 minutos en hacer dicha instalación. &lt;br /&gt;
** El profesor empleará 5 minutos en verificar (sí/no) si la práctica 1 (instalación) y 4 (pruebas) está hecha por cada uno de los miembros.&lt;br /&gt;
** El resto del tiempo se empleará en explicar cuáles son los WIs que se quieren abordar y solventar dudas. &lt;br /&gt;
** La sesión no podrá durar más de 25 minutos. &lt;br /&gt;
** Se ruega máxima puntualidad.&lt;br /&gt;
&lt;br /&gt;
Lo que se espera en general de este primer milestone es verificar que todos los equipos y todos los miembros de los equipos han sido capaces de hacer las prácticas mencionadas y tener funcionando el entorno de desarrollo y despliegue de la herramienta UVLHub. También se mirará que se presente de manera coherente el proyecto en el que va a participar el equipo y que tenga pensados al menos algunos WIs.&lt;br /&gt;
&lt;br /&gt;
* [[Dudas-M1]]&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Dudas-M1&amp;diff=10167</id>
		<title>Dudas-M1</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Dudas-M1&amp;diff=10167"/>
				<updated>2025-10-15T09:53:03Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: /* Sobre lo que tener hecho en el M1 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[Página_Principal]]&lt;br /&gt;
&lt;br /&gt;
''Estos son preguntas frecuentes basadas en las dudas históricas de los alumnos y que se han ido contestando. No obstante, puede que alguna cosa no esté del todo bien explicada por lo que, si hubiera alguna duda, por favor, preguntad a vuestros tutores.'' &lt;br /&gt;
&lt;br /&gt;
==De organización de los equipos ==&lt;br /&gt;
'''* PREGUNTA:'''  &lt;br /&gt;
Por cuestiones laborales un miembro de nuestro equipo ha tenido que abandonar la asignatura, lo que ha resultado que el grupo se quede con 5 personas. ¿Nos puede afectar esto de cara a las entregas o al trabajo en grupo? &lt;br /&gt;
&lt;br /&gt;
RESPUESTA: &lt;br /&gt;
&lt;br /&gt;
No, apuntadlo en el diario del equipo pero no debe ser problema.  &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''* PREGUNTA:'''  &lt;br /&gt;
¿Que ocurre si todavía no tenemos otro equipo con el que colaborar? Cuando hemos tratado de buscar uno nadie nos ha contactado. &lt;br /&gt;
&lt;br /&gt;
RESPUESTA: &lt;br /&gt;
En la lista que se publica con los equipos se podrán ver claramente cuáles son los equipos disponibles, en todo caso, contacta con tu tutor para solventar el problema y ver qué solución se puede dar.  &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''* PREGUNTA:'''  &lt;br /&gt;
Se puede establecer un criterio en el acta fundacional para que el miembro que incumpla un plazo o caiga en un conflicto pierda puntos de cara a un entregable? &lt;br /&gt;
&lt;br /&gt;
RESPUESTA:&lt;br /&gt;
Sí, pero más que &amp;quot;que pierda puntos&amp;quot;, estableced niveles de implicación. Recordad que en la entrega final, vais a tener que indicar el grado de implicación de cada miembro del equipo. En el acta fundacional podéis establecer cómo vais a gestionar esos valores. &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Sobre lo que tener hecho en el M1 ==&lt;br /&gt;
'''* PREGUNTA:'''  &lt;br /&gt;
¿Para el milestone 1 hace falta tener hecho todos los ejercicios propuestos en las prácticas? &lt;br /&gt;
 &lt;br /&gt;
RESPUESTA: &lt;br /&gt;
Hay que tener hechas las prácticas de instalación (práctica 1) y la de pruebas (práctica 4) como viene indicado en la wiki. Si tienes hechos además ejercicios extras, mejor, pero no es obligatorio.  &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''* PREGUNTA:'''  &lt;br /&gt;
En la defensa,  ¿se debe mostrarse las prácticas hechas de todos los miembros del equipo? ¿O vale con que se muestre las de un miembro? &lt;br /&gt;
&lt;br /&gt;
RESPUESTA:&lt;br /&gt;
Mientras más evidencias dejéis de que todos habéis hecho el trabajo, mejor. Por lo tanto, todos debéis tener hechas las prácticas.  &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''* PREGUNTA:'''  &lt;br /&gt;
¿Hay que llevar implementado todo el pipeline de CI/CD?&lt;br /&gt;
&lt;br /&gt;
RESPUESTA:&lt;br /&gt;
Lo ideal es llevar un esbozo de qué pipeline se quiere llevar y cómo se relacionan los diferentes workflows, qué aporta cada uno etc. Si además lo tenéis ya implementado, mejor, pero no es obligatorio&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''* PREGUNTA:'''  &lt;br /&gt;
¿Cuantos cambios o mejoras hace falta apuntar para nuestro sistema o no hace falta indicar ningún cambio sobre UVLHub para este milestone? &lt;br /&gt;
&lt;br /&gt;
RESPUESTA:&lt;br /&gt;
Lo ideal sería comentar los cambios que queréis hacer aunque no los tengáis implementados. Mirad en lo que está publicado acerca del M1.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''* PREGUNTA:'''  &lt;br /&gt;
Una duda, en la última práctica existen las pruebas de carga que se hacen con locust. Al inicializar el test, la url localhost:8089 no está disponible. Mi entorno de trabajo es con VirtualBox. ¿Cómo se solucionaría? &lt;br /&gt;
&lt;br /&gt;
RESPUESTA:&lt;br /&gt;
Esto no se puede contestar por aquí. Debes consultar en una tutoría con tu profesor de prácticas. &lt;br /&gt;
&lt;br /&gt;
'''* PREGUNTA:'''  &lt;br /&gt;
Sobre el repositorio en el que se alojará el proyecto del equipo, ¿Podemos hacer ya un fork de UVLHub (El que está en la organización EGCETSII)? ¿O se explicará más tarde?&lt;br /&gt;
&lt;br /&gt;
RESPUESTA:&lt;br /&gt;
Sí, podéis ya hacer fork y hacedlo del repo oficial de la asignatura que está enlazado en la wiki. Recordar también que tenéis que limpiar las ramas del repositorio original para así facilitar la gestión y mejorar la entrega. Igualmente los commits.&lt;br /&gt;
&lt;br /&gt;
== Sobre plazos de entrega ==&lt;br /&gt;
&lt;br /&gt;
'''* PREGUNTA:'''  &lt;br /&gt;
¿El día que pone que es la entrega, se podrá entregar durante todo el día o tiene una hora límite? &lt;br /&gt;
&lt;br /&gt;
RESPUESTA:&lt;br /&gt;
Todo el día&lt;br /&gt;
&lt;br /&gt;
'''* PREGUNTA:'''  &lt;br /&gt;
¿Qué pasa si no puedo asistir el día de la entrega?&lt;br /&gt;
&lt;br /&gt;
RESPUESTA:&lt;br /&gt;
La entrega es un elemento evaluable de la asignatura. No presentarse a la evaluación del M1 sin ser algunas de las causas justificadas recogidas en la normativa de la universidad, implicaría una penalización señalada en el apartado de notas. &lt;br /&gt;
&lt;br /&gt;
== Sobre Evidentia o proyectos InnoSoft ==&lt;br /&gt;
'''* PREGUNTA:'''  &lt;br /&gt;
Los que hacemos Evidentia aunque tengamos UVLHub configurado por las prácticas no hace falta que lo mostremos en la defensa, ¿no? &lt;br /&gt;
 &lt;br /&gt;
RESPUESTA:  &lt;br /&gt;
No, no hace falta &lt;br /&gt;
&lt;br /&gt;
'''* PREGUNTA:'''  &lt;br /&gt;
¿En el caso de los trabajos de InnoSoft, tenemos que presentar las prácticas de UVLHub resueltas?  &lt;br /&gt;
&lt;br /&gt;
RESPUESTA: &lt;br /&gt;
&lt;br /&gt;
No, pero sí hacer la defensa con lo que hayáis hecho del trabajo como viene indicado en la wiki.&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10164</id>
		<title>Tutorial-docker</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10164"/>
				<updated>2025-10-12T20:29:32Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: /* Orquestación básica */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Ejercicio 1: Empezamos desde cero =&lt;br /&gt;
&lt;br /&gt;
El objetivo es eliminar toda la configuración previa y crear desde cero la estructura de carpetas que contendrá nuestros futuros archivos Docker.&lt;br /&gt;
&lt;br /&gt;
== Eliminar la configuración existente ==&lt;br /&gt;
Antes de comenzar, asegúrate de borrar cualquier carpeta Docker existente en el proyecto. En este caso, le haremos una copia de seguridad para poder volver a ella.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mv docker docker.bk&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
También debes asegurarte que trabajas con las variables de entorno adecuadas:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
cp .env.docker.example .env&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
⚠️ Este paso es importante: partimos completamente desde cero para entender cada parte del sistema.&lt;br /&gt;
&lt;br /&gt;
== Crear un Dockerfile mínimo ==&lt;br /&gt;
&lt;br /&gt;
Ahora crea nuevamente la carpeta base donde almacenaremos los archivos relacionados con Docker:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt; mkdir -p docker/images &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vamos a explicar los distintos apartados:&lt;br /&gt;
&lt;br /&gt;
=== FROM ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Indica la imagen base sobre la que se construirá la tuya.&lt;br /&gt;
En este caso, parte de una imagen ligera que ya tiene Python 3.12 instalado.&lt;br /&gt;
&lt;br /&gt;
=== WORKDIR ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta línea define el directorio de trabajo dentro del contenedor.&lt;br /&gt;
A partir de aquí, todos los comandos que aparezcan después (COPY, RUN, CMD, etc.) se ejecutarán dentro de /app.&lt;br /&gt;
&lt;br /&gt;
'''Ojo: /app no es la carpeta app/ de tu proyecto local.'''. Son cosas distintas.&lt;br /&gt;
&lt;br /&gt;
En tu máquina puede existir algo como:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
uvlhub&lt;br /&gt;
   app&lt;br /&gt;
   core&lt;br /&gt;
   requirements.txt &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Pero dentro del contenedor la estructura será distinta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
/ &lt;br /&gt;
  app/ &lt;br /&gt;
    app/ ← aquí dentro se habrá copiado tu carpeta local &amp;quot;app&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Cuando más adelante uses:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Docker copiará todos los archivos de tu proyecto dentro de la carpeta /app del contenedor.&lt;br /&gt;
Por eso el código quedará en /app/app/.&lt;br /&gt;
&lt;br /&gt;
=== COPY ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Copia todos los archivos del proyecto (del host) dentro del contenedor, en /app.&lt;br /&gt;
El primer punto es el origen; el segundo, el destino.&lt;br /&gt;
&lt;br /&gt;
=== RUN ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ejecuta comandos durante la construcción de la imagen.&lt;br /&gt;
Aquí instalamos las dependencias necesarias desde requirements.txt.&lt;br /&gt;
El resultado de este paso quedará guardado en la imagen.&lt;br /&gt;
&lt;br /&gt;
=== EXPOSE ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Documenta el puerto que la aplicación usa dentro del contenedor.&lt;br /&gt;
Esto no lo abre al exterior; simplemente indica que la app escucha en el 5000.&lt;br /&gt;
&lt;br /&gt;
=== CMD ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Define el comando que se ejecutará cuando el contenedor se inicie.&lt;br /&gt;
En este caso, ejecutará la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
=== RUN vs CMD ===&lt;br /&gt;
&lt;br /&gt;
Estas dos instrucciones parecen similares, pero hacen cosas muy distintas.&lt;br /&gt;
&lt;br /&gt;
==== RUN ====&lt;br /&gt;
Se ejecuta durante la construcción de la imagen (en el momento del docker build).&lt;br /&gt;
Cada RUN crea una nueva capa dentro de la imagen.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando instala las dependencias una sola vez, cuando se construye la imagen.&lt;br /&gt;
El resultado (las librerías instaladas) queda grabado dentro de la imagen final.&lt;br /&gt;
&lt;br /&gt;
Si después lanzas un contenedor nuevo a partir de esa imagen, las dependencias ya estarán instaladas y no se volverán a ejecutar.&lt;br /&gt;
&lt;br /&gt;
==== CMD ====&lt;br /&gt;
Se ejecuta cuando se inicia el contenedor (en el docker run).&lt;br /&gt;
Define el proceso principal que se ejecutará dentro del contenedor mientras esté activo.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando no se ejecuta durante la construcción, sino cada vez que arrancas el contenedor.&lt;br /&gt;
Si detienes el contenedor, este proceso también se detiene.&lt;br /&gt;
&lt;br /&gt;
==== Diferencia resumida ====&lt;br /&gt;
&lt;br /&gt;
RUN → se ejecuta al construir la imagen (fase de docker build).&lt;br /&gt;
&lt;br /&gt;
CMD → se ejecuta al iniciar el contenedor (fase de docker run).&lt;br /&gt;
&lt;br /&gt;
Solo puede haber un CMD por Dockerfile.&lt;br /&gt;
Si hay más de uno, solo se usará el último.&lt;br /&gt;
&lt;br /&gt;
== Dockerfile completo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
COPY . .&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto tiene que ir en un Dockerfile en docker/images&lt;br /&gt;
&lt;br /&gt;
== Construir la imagen ==&lt;br /&gt;
Desde la raíz del proyecto (no dentro de docker/images):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker build -t uvlhub:dev -f docker/images/Dockerfile .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Ejecutar el contenedor ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Qué es lo que observas?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 2: Conectando servicios =&lt;br /&gt;
&lt;br /&gt;
¡Vaya! ¡No funciona! Can't connect to MySQL server on 'db'... ¡Ah, claro, intenta conectarse a la base de datos, que no lo tenemos! Pero claro, en un contenedor no es recomendable tener la app y la base de datos conviviendo...&lt;br /&gt;
&lt;br /&gt;
== Crear contenedor MariaDB ==&lt;br /&gt;
&lt;br /&gt;
Antes que nada, haz Ctrl + C para parar el contenedor web. En este ejercicio aprenderás a lanzar un contenedor de MariaDB 12.0.2 de forma manual, configurando todas las variables necesarias para tu aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
El objetivo es entender la complejidad de hacerlo “a mano”, antes de automatizarlo con docker-compose.&lt;br /&gt;
&lt;br /&gt;
== Descargar la imagen oficial ==&lt;br /&gt;
Primero descarga la versión exacta que usaremos:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker pull mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Crear el contenedor de MariaDB ==&lt;br /&gt;
Lanza un contenedor con todas las variables necesarias para que Flask pueda conectarse correctamente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d --name mariadb_container \&lt;br /&gt;
 -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
 -e FLASK_ENV=development \&lt;br /&gt;
 -e DOMAIN=localhost \&lt;br /&gt;
 -e MARIADB_HOSTNAME=db \&lt;br /&gt;
 -e MARIADB_PORT=3306 \&lt;br /&gt;
 -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
 -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
 -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
 -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
 -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
 -e WORKING_DIR=/app/ \&lt;br /&gt;
 -p 3306:3306 mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Explicación:&lt;br /&gt;
&lt;br /&gt;
* -d: ejecuta el contenedor en segundo plano (modo detached).&lt;br /&gt;
* --name: asigna un nombre identificable al contenedor.&lt;br /&gt;
* -e: define variables de entorno dentro del contenedor.&lt;br /&gt;
* -p 3306:3306: publica el puerto de MariaDB en tu máquina local.&lt;br /&gt;
* mariadb:12.0.2: imagen exacta que vamos a usar.&lt;br /&gt;
&lt;br /&gt;
Aquí ya, de entrada, vemos que esto es más feo que una nevera por detrás. Para empezar, estamos pasando a lo loco un montón de variables que difílmente será replicable en producción, por contener información sensible. Aparte, si metemos nuevas variables, ese comando queda inservible. Necesitaremos una solución más ágil para este problema&lt;br /&gt;
&lt;br /&gt;
== Comprobar que está corriendo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver algo así:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES&lt;br /&gt;
abcd1234efgh mariadb:12.0.2 &amp;quot;docker-entrypoint.s…&amp;quot; Up 5 seconds 0.0.0.0:3306-&amp;gt;3306/tcp mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Conectarse a la base de datos ==&lt;br /&gt;
Abre una consola dentro del contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it mariadb_container bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y dentro, accede al cliente de MariaDB:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mariadb -u root -p&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Contraseña: uvlhubdb_root_password&lt;br /&gt;
&lt;br /&gt;
== Verificar la base de datos ==&lt;br /&gt;
Una vez dentro del cliente MySQL, ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
SHOW DATABASES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver que existen la bases de datos uvlhubdb porque pedimos crearla a la hora de levantar el contenedor. ¿Qué tiene la base de datos dentro? Vamos a verlo...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
USE uvlhubdb;&lt;br /&gt;
SHOW TABLES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Un total de... 0 tablas... ¡Claro! Es que solo hemos creado la base en sí, pero no las tablas. ¿Con un script SQL tal vez? ¡Espera, espera, si tenemos las migraciones! Pero claro, para eso necesito el contenedor de la app levantado. Sal de la consola SQL y del contenedor de MariaDB. Bien, ahora en el host...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta vez con el flag &amp;quot;-d&amp;quot; para que corra en segundo plano. Ahora, accedamos al contenedor y ejecutemos las migraciones. Pero... ¿cómo se llama mi contenedor?&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate que Docker asigna un nombre aleatorio a tu contenedor web porque no definiste ninguno. Una vez identificado el nombre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it &amp;lt;nombre&amp;gt; bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
What? ¿Qué pasa? ¿Otra vez error de conexión con la base? Si tenemos el contenedor funcionando, esto es un jaleo...&lt;br /&gt;
&lt;br /&gt;
== Redes internas ==&lt;br /&gt;
&lt;br /&gt;
Ahora mismo tienes dos contenedores aislados, el de la app y el la base de datos. Por defecto, cada contenedor tiene su propia red interna y no puede ver a los demás por nombre. El hostname '''db''' que usa Flask no existe en su DNS interno.&lt;br /&gt;
&lt;br /&gt;
=== Conectar servicios ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una red interna que compartan ambos contenedores&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker network create uvlhub_network&lt;br /&gt;
docker network ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vemos que ya aparece nuestra red '''uvlhub_network'''. Ahora, conectemos el contenedor de MariaDB a la red:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Claro, si te fijas, ahora hay que añadir, además de toda esa configuración engorrosa, que se añada en la misma red y que además tenga de hostname &amp;quot;db&amp;quot; para que el contenedor web entienda qué es &amp;quot;db&amp;quot;. Tenemos que hacer lo mismo para el contenedor web:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop &amp;lt;nombre&amp;gt;&lt;br /&gt;
docker rm &amp;lt;nombre&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y lo levantamos de nuevo, pero ya en la nueva red. De paso, le ponemos un nombre para identificarlo mejor.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 --name web_app_container --network uvlhub_network -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y ahora ya podemos acceder al contenedor por su nombre y ejecutar las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Mejor?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 3: Persistencia =&lt;br /&gt;
&lt;br /&gt;
Vamos a simular que hemos desplegado justo esto en producción y el servidor se reinicia por un corte de luz. Reiniciemos a mano cada contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker restart web_app_container&lt;br /&gt;
docker restart mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. What? ¿Cómo? ¿Qué ha pasado? ¿Otra vez problema de conexión? Ve dándole a F5... F5... ¡bingo! Ahora sí sale, pero menuda aleatoriedad, ¿no? Claro, es que ahora caes en que una cosa es que el contenedor haya arrancado y otra que esté listo.&lt;br /&gt;
&lt;br /&gt;
Aparte, tenemos otro problema. ¿Qué pasa con la persistencia? Porque si borro el contenedor de MariaDB, todos los datos que estén en su interior también se pierden. Y si estamos en producción, lío asegurado.&lt;br /&gt;
&lt;br /&gt;
== Volúmenes ==&lt;br /&gt;
&lt;br /&gt;
Cada vez que eliminas el contenedor de MariaDB, todas las bases de datos se pierden. Esto ocurre porque los datos se guardan dentro del sistema de archivos del contenedor, y al borrarlo desaparecen con él.&lt;br /&gt;
&lt;br /&gt;
En este ejercicio creas un volumen de Docker para que los datos persistan entre ejecuciones.&lt;br /&gt;
&lt;br /&gt;
=== Crear un volumen ===&lt;br /&gt;
Un volumen es una carpeta gestionada por Docker que vive fuera del ciclo de vida de los contenedores.&lt;br /&gt;
&lt;br /&gt;
Ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume create mariadb_data&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba que existe:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
La salida muestra algo como:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
DRIVER    VOLUME NAME&lt;br /&gt;
local     mariadb_data&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Crear un contenedor de MariaDB con el volumen ===&lt;br /&gt;
Lanza el contenedor y conecta el volumen a la ruta donde MariaDB guarda los datos (/var/lib/mysql):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -v mariadb_data:/var/lib/mysql \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Qué significa &amp;quot;:&amp;quot; en los volúmenes ===&lt;br /&gt;
&lt;br /&gt;
Cuando usas la opción '''-v''' o '''--volume''' en Docker, el signo dos puntos (''':''') separa dos rutas:&lt;br /&gt;
&lt;br /&gt;
'''-v origen:destino'''&lt;br /&gt;
&lt;br /&gt;
origen → es la ruta o el volumen en tu máquina (el host).&lt;br /&gt;
&lt;br /&gt;
destino → es la ruta dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 4: Orquestación de servicios =&lt;br /&gt;
&lt;br /&gt;
Hasta ahora hemos creado y conectado los contenedores manualmente.&lt;br /&gt;
Ahora vas a usar Docker Compose para definir todo en un solo archivo.&lt;br /&gt;
&lt;br /&gt;
== Orquestación básica ==&lt;br /&gt;
&lt;br /&gt;
Crea un archivo llamado docker-compose.yml en la carpeta '''docker''' con este contenido:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    container_name: web_app_container&lt;br /&gt;
    env_file:&lt;br /&gt;
      - ../.env&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;5000&amp;quot;&lt;br /&gt;
    depends_on:&lt;br /&gt;
      - db&lt;br /&gt;
    build:&lt;br /&gt;
      context: ../&lt;br /&gt;
      dockerfile: docker/images/Dockerfile&lt;br /&gt;
    networks:&lt;br /&gt;
      - uvlhub_network&lt;br /&gt;
&lt;br /&gt;
  db:&lt;br /&gt;
    container_name: mariadb_container&lt;br /&gt;
    image: mariadb:12.0.2&lt;br /&gt;
    env_file:&lt;br /&gt;
      - ../.env&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;3306:3306&amp;quot;&lt;br /&gt;
    volumes:&lt;br /&gt;
      - db_data:/var/lib/mysql&lt;br /&gt;
    networks:&lt;br /&gt;
      - uvlhub_network&lt;br /&gt;
&lt;br /&gt;
volumes:&lt;br /&gt;
  db_data:&lt;br /&gt;
&lt;br /&gt;
networks:&lt;br /&gt;
  uvlhub_network:&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate que esta orquestación de servicios me permite definir el nombre de cada servicio, su dependencia, sus puertos, sus volúmenes, redes... todo lo que hacíamos de forma manual y propenso a errores queda ahora más fácil de gestionar. Además, ahora ambos contenedores comparten el mismo archivo .env de variables de entorno.&lt;br /&gt;
&lt;br /&gt;
Bien, ahora vamos a levantar estos contenedores y acceder al contenedor web:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Inmediatamente después, ejecuta las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué ha ocurrido? Depende del tiempo que hayas tardado, puede que haya saltado un error o puede que haya funcionado. Puede que hayas intentado varias veces el comando anterior hasta que ha funcionado. ¿Por qué crees que ocurre esta aparente aleatoriedad?&lt;br /&gt;
&lt;br /&gt;
== Dependencias entre contenedores y entrypoints ==&lt;br /&gt;
&lt;br /&gt;
Fíjate que tenemos un &amp;quot;depends on&amp;quot; en el servicio web. Significa que el servicio &amp;quot;db&amp;quot; debe iniciarse antes que el servicio web. Pero, cuidado, ahí está la trampa, porque &amp;quot;servicio iniciado != servicio preparado&amp;quot;. El servicio de MariaDB tiene que arrancar, configurarse, etc...&lt;br /&gt;
&lt;br /&gt;
Cuando hicimos el comando COPY en nuestro Dockerfile, también copiamos la carpeta '''scripts'''. Uno de los scripts es '''wait-for-db.sh''', que realiza continuamente llamadas a MariaDB hasta que este servicio está listo para recibir peticiones. Ábrelo y estúdialo.&lt;br /&gt;
&lt;br /&gt;
Añade al servicio web de tu docker compose las siguientes líneas:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
entrypoint: [&amp;quot;/app/scripts/wait-for-db.sh&amp;quot;]&lt;br /&gt;
command: [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A su vez, del Dockerfile, hay que eliminar la línea de CMD que arrancaba nuestro servidor Flask.&lt;br /&gt;
&lt;br /&gt;
Luego, reinicializa todos los servicios:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. What? ¿No abre? Vamos a investigarlo, veamos el logs:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker logs web_app_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Nos debe salir un mensaje eterno de &amp;quot;/app/scripts/wait-for-db.sh: line 18: mariadb: command not found&amp;quot;. Esto ocurre porque en la imagen de nuestro servicio web NO TENEMOS el cliente de MariaDB. Debemos indicar entonces que lo instale. Añade justo debajo de FROM lo siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y --no-install-recommends mariadb-client &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Al ser un contenedor basado en Linux, y accediendo como root, es como si actualizáramos todos los paquetes y luego instalásemos el cliente de MariaDB. Intentémoslo otra vez:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. ¡Jopelines! ¿Tampoco? Si haces un &amp;quot;docker ps&amp;quot; verás el problema. El contenedor web no tiene expuesto ningún puerto.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    (...)&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;5000&amp;quot;&lt;br /&gt;
    (...)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;quot;Expose&amp;quot; hace visible el puerto '''entre contenedores''', pero '''NO entre tu contenedor y el host'''. Para eso usamos la directiva &amp;quot;ports&amp;quot;, que se encarga del mapping. En nuestro caso, será:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    (...)&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;5000:5000&amp;quot;&lt;br /&gt;
    (...)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
En ocasiones, si los dos puertos coinciden, se puede dejar uno solo con &amp;quot;5000&amp;quot; pero dejaremos los dos. El de la izquierda siempre es el puerto externo (host), el de la derecha es el puerto interno (contenedor). Así que cambia ese detalle y por última vez:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. ¡Ahora sí!&lt;br /&gt;
&lt;br /&gt;
== Trabajando con CMD, entrypoints y commands ==&lt;br /&gt;
&lt;br /&gt;
Piensa que al arrancar un contenedor Docker, siempre se ejecuta un único comando final.&lt;br /&gt;
Lo que hacen ENTRYPOINT, CMD y command (de Compose) es decidir qué comando será ese y con qué argumentos.&lt;br /&gt;
&lt;br /&gt;
=== CMD → “qué hacer por defecto” ===&lt;br /&gt;
&lt;br /&gt;
* Está en el Dockerfile.&lt;br /&gt;
* Es el comando que Docker ejecuta si no se indica otra cosa. En nuestro caso:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== ENTRYPOINT → “qué se ejecuta siempre” ===&lt;br /&gt;
&lt;br /&gt;
* También va en el Dockerfile, o se puede sobreescribir en el docker-compose.yml.&lt;br /&gt;
* Define el programa o script principal que siempre se ejecuta.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
ENTRYPOINT [&amp;quot;/app/scripts/wait-for-db.sh&amp;quot;]&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Normalmente, el entrypoint se combina con CMD en modo &amp;quot;eh, ejecuta esto (entrypoint) y luego este comando final (cmd)&lt;br /&gt;
&lt;br /&gt;
Los entrypoints son muy útiles, sobre todo en los docker-compose's, porque puedo redefinir cómo quiero que inicie un contenedor en concreto '''SIN TENER QUE HACER UN BUILD DE NUEVO DE LA IMAGEN DE DOCKER'''. A diferencia de CMD, que viene predefinido, un entrypoint en mi docker-compose me da más versatilidad&lt;br /&gt;
&lt;br /&gt;
=== command (de docker-compose.yml) → “sobrescribe el CMD” ===&lt;br /&gt;
&lt;br /&gt;
En docker-compose.yml, el campo command: reemplaza al CMD del Dockerfile,pero no al ENTRYPOINT.&lt;br /&gt;
&lt;br /&gt;
== Combinación de elementos ==&lt;br /&gt;
&lt;br /&gt;
Es importante entender cómo Docker trabaja con la orquestación y con los contenedores:&lt;br /&gt;
&lt;br /&gt;
* Lee el YAML como un diccionario&lt;br /&gt;
* Ejecuta todo lo que haya en un Dockerfile EXCEPTUANDO directivas ENTRYPOINT y CMD&lt;br /&gt;
* Monta el volumen o los volúmenes de ese contenedor (si procede)&lt;br /&gt;
&lt;br /&gt;
Y ahora viene el detalle:&lt;br /&gt;
&lt;br /&gt;
* Si estamos levantando un contenedor a través de docker-compose, va a ejecutar los entrypoints y commands definidos en ese docker-compose.&lt;br /&gt;
* Un entrypoint definido en un docker-compose va a sustituir a un entrypoint definido en el Dockerfile&lt;br /&gt;
* Un command definido en un docker-compose va a sustituir a un CMD definido en el Dockerfile.&lt;br /&gt;
&lt;br /&gt;
Como consejo:&lt;br /&gt;
&lt;br /&gt;
* Definimos un entrypoint en el docker-compose. Esto es así porque está fuera de la imagen de Docker y nos permite redefinir el inicio del servicio sin modificar la imagen&lt;br /&gt;
* Eventualmente, definimos un CMD donde indicamos el comando final de ese contenedor al iniciar, que suele ser el arranque de Flask Server o de Gunicorn, dependiendo de si estamos en desarrollo o producción.&lt;br /&gt;
&lt;br /&gt;
No obstante, como nuestros entrypoints son todos scripts en bash, podemos definir en el mismo el arranque final (comando final) y nos olvidamos de CMD.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 5: Volumen de trabajo =&lt;br /&gt;
&lt;br /&gt;
== Problema de desarrollo ==&lt;br /&gt;
&lt;br /&gt;
Vale, ya parece que le hemos pillado el tranquillo a Docker y a Docker Compose. Ahora vamos a hacer algo como desarrolladores. Ve al archivo &amp;quot;app/templates/base_templates.html&amp;quot; y cambia la línea 22:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;title&amp;gt;{{ FLASK_APP_NAME }} - Repository of feature models in UVL &amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
por:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;title&amp;gt;Hello world! &amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. Fíjate en el nombre de la pestaña del navegador, ¿ha cambiado? ¿No? Tsss, esto de los contenedores... bueno, va, vamos a reiniciarlo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker restart web_app_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. ¿Tampoco? Anda que vaya idea la de usar Docker... Claro, es a la hora de construir la imagen, hicimos &amp;quot;COPY . .&amp;quot;, pero no hemos reconstruido nada, así que...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml build --no-cache web&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --force-recreate web&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Importante señalar que le pedimos que ignore la caché para que fuerce a copiar de nuevo todo el contenido (incluyendo el cambio de línea) al interior del contenedor.&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. ¡Ahora sí! Pero... ¿tengo que reconstruir TODO el contenedor en cada cambio línea? ¿Y eso es trabajar de forma ágil?&lt;br /&gt;
&lt;br /&gt;
== Crear volumen de trabajo (bind mount) ==&lt;br /&gt;
&lt;br /&gt;
Evidentemente, eso no tiene sentido, tenemos que usar un volumen de trabajo que nos ayude a sincronizar el código de nuestro host con el código interno del contenedor. Luego en nuestro docker-compose:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  web:&lt;br /&gt;
    (....)&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;5000:5000&amp;quot;&lt;br /&gt;
    volumes:&lt;br /&gt;
      - ../:/app&lt;br /&gt;
    (....)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
La línea&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
- ../:/app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Monta tu carpeta local del proyecto (../) dentro del contenedor, (/app). ¿Y por qué /app? Porque fue el espacio de trabajo que definimos en el Dockerfile. Como ves, la congruencia entre los Dockerfile y el docker-compose es importante.&lt;br /&gt;
&lt;br /&gt;
Recargemos de nuevo nustra configuración:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ahora ya podemos trabajar con el código en host sin tener que reconstruir la imagen a cada cambio de línea.&lt;br /&gt;
&lt;br /&gt;
== Cuidado con COPY y los bind mounts ==&lt;br /&gt;
&lt;br /&gt;
Un '''bind mount''' (o “montaje de enlace”) es un tipo de volumen en el que Docker conecta directamente una carpeta de tu máquina (el host) con una carpeta dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
Cuando usas un bind mount (por ejemplo ../:/app), Docker monta tu carpeta local sobre la ruta /app del contenedor.&lt;br /&gt;
Eso significa que todo lo que estaba en /app dentro de la imagen desaparece al arrancar el contenedor, porque queda “oculto” por el volumen.&lt;br /&gt;
&lt;br /&gt;
Ahora bien, durante la construcción de la imagen usamos la directiva COPY. ¿Por qué? Porque necesitamos el archivo requirements.txt para instalar las dependencias de Python del proyecto. Pero... ¿realmente necesitamos copiar TODO? No, claro que no, podemos optar por copiar exclusivamente aquellos archivos que nos hagan falta durante '''LA CONSTRUCCIÓN''' de la imagen. El bind mount luego lo machacará.&lt;br /&gt;
&lt;br /&gt;
'''Muy importante:''' los bind mounts, al igual que cualquier otro volumen, están disponibles '''DESPUÉS''' de la construcción de la imagen. Es un error común creer que porque definamos un bind mount en un docker-compose, esos archivos aparecen ya para el contenedor. '''Esto es una fuente inagotable de problemas en la práctica'''.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 6: Distintas configuraciones de despliegue =&lt;br /&gt;
&lt;br /&gt;
Volvamos al punto de partida:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
rm -r docker&lt;br /&gt;
mv docker.bk docker&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta práctica ha sido engorrosa a propósito para quitar esa &amp;quot;magia&amp;quot; que existe cuando lanzas un comando simple de orquestación y todo se levanta y funciona perfectamente. Esa situación está muy bien para desarrollar, pero no para aprender.&lt;br /&gt;
&lt;br /&gt;
Es importante que analices que existen distintos escenarios:&lt;br /&gt;
&lt;br /&gt;
* Hay un docker-compose para desarrollo.&lt;br /&gt;
* Hay 3 docker-compose para producción.&lt;br /&gt;
* Hay 6 Dockerfiles distintos&lt;br /&gt;
* Hay 3 entrypoints distintos&lt;br /&gt;
* Hay una carpeta llamada letsencrypt y otra llamada nginx. Dentro de nginx hay dos archivos de configuración dependiendo de si estamos en dev o en prod.&lt;br /&gt;
&lt;br /&gt;
== Autoevaluación ==&lt;br /&gt;
&lt;br /&gt;
Estudia bien cada archivo. Tómate tu tiempo e intenta responder a estas preguntas:&lt;br /&gt;
&lt;br /&gt;
* ¿Por qué en el docker-compose de dev NO se usa &amp;quot;ports&amp;quot; para enlazar con el host? (pista: nginx)&lt;br /&gt;
* ¿Qué es nginx y por qué es útil aquí?&lt;br /&gt;
* ¿Cuántos bind mounts usa el contenedor web en desarrollo? ¿Para qué sirve el bind mount del propio Docker?&lt;br /&gt;
* ¿Qué diferencia hay entre el entrypoint de desarrollo y el entrypoint de producción?&lt;br /&gt;
* ¿Por qué el servicio web en producción está montando todos esos bind mounts por separado? ¿No sería más sencillo hacer un volumen de todo el código?&lt;br /&gt;
&lt;br /&gt;
== Comentarios finales ==&lt;br /&gt;
&lt;br /&gt;
=== Cuanto más se parezca el entorno de desarrollo al de producción, menos errores habrá ===&lt;br /&gt;
&lt;br /&gt;
En desarrollo es tentador usar configuraciones cómodas: servidor de Flask con --reload, bind mounts, base de datos vacía, logs en consola, etc.&lt;br /&gt;
Pero cuanto más difiere ese entorno de producción, más probable es que algo falle al desplegar.&lt;br /&gt;
&lt;br /&gt;
'''El código no falla por cambiar de servidor, sino porque los entornos se comportan distinto.'''&lt;br /&gt;
&lt;br /&gt;
Ejemplos clásicos:&lt;br /&gt;
&lt;br /&gt;
* En desarrollo usas SQLite, en producción MariaDB → aparecen errores de compatibilidad SQL.&lt;br /&gt;
* En desarrollo Flask sirve los archivos estáticos, en producción lo hace Nginx → las rutas cambian.&lt;br /&gt;
* En desarrollo ejecutas '''flask run''', en producción '''gunicorn''' → los hilos, workers o el reloader funcionan distinto.&lt;br /&gt;
&lt;br /&gt;
Por eso, el objetivo ideal es que tu entorno de desarrollo (dev), de preproducción (staging) y de producción (prod) sean idénticos salvo por las credenciales.&lt;br /&gt;
&lt;br /&gt;
Docker y docker-compose te permiten lograr eso: puedes usar la misma imagen base, cambiando solo el docker-compose (volúmenes, puertos, etc.).&lt;br /&gt;
&lt;br /&gt;
=== Diferencia entre el servidor de Flask y Gunicorn ===&lt;br /&gt;
&lt;br /&gt;
Flask trae incorporado un servidor de desarrollo.&lt;br /&gt;
Es muy útil para depurar porque:&lt;br /&gt;
&lt;br /&gt;
* recarga automáticamente el código (--reload),&lt;br /&gt;
* muestra errores detallados en el navegador,&lt;br /&gt;
* solo ejecuta un proceso.&lt;br /&gt;
&lt;br /&gt;
Sin embargo, no está pensado para producción:&lt;br /&gt;
&lt;br /&gt;
* no maneja múltiples conexiones concurrentes,&lt;br /&gt;
* no tiene control de procesos ni balanceo de carga,&lt;br /&gt;
* no implementa estándares de rendimiento o seguridad (como WSGI robusto).&lt;br /&gt;
&lt;br /&gt;
Ahí entra Gunicorn (Green Unicorn), que sí es un servidor WSGI  de producción (Web Server Gateway Interface, estándar de comunicación entre aplicaciones web Python y servidores web):&lt;br /&gt;
&lt;br /&gt;
* puede lanzar varios workers en paralelo,&lt;br /&gt;
* maneja múltiples peticiones concurrentes,&lt;br /&gt;
* se integra fácilmente con Nginx para servir contenido estático o HTTPS.&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10163</id>
		<title>Tutorial-docker</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10163"/>
				<updated>2025-10-12T20:24:05Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: /* Autoevaluación */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Ejercicio 1: Empezamos desde cero =&lt;br /&gt;
&lt;br /&gt;
El objetivo es eliminar toda la configuración previa y crear desde cero la estructura de carpetas que contendrá nuestros futuros archivos Docker.&lt;br /&gt;
&lt;br /&gt;
== Eliminar la configuración existente ==&lt;br /&gt;
Antes de comenzar, asegúrate de borrar cualquier carpeta Docker existente en el proyecto. En este caso, le haremos una copia de seguridad para poder volver a ella.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mv docker docker.bk&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
También debes asegurarte que trabajas con las variables de entorno adecuadas:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
cp .env.docker.example .env&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
⚠️ Este paso es importante: partimos completamente desde cero para entender cada parte del sistema.&lt;br /&gt;
&lt;br /&gt;
== Crear un Dockerfile mínimo ==&lt;br /&gt;
&lt;br /&gt;
Ahora crea nuevamente la carpeta base donde almacenaremos los archivos relacionados con Docker:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt; mkdir -p docker/images &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vamos a explicar los distintos apartados:&lt;br /&gt;
&lt;br /&gt;
=== FROM ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Indica la imagen base sobre la que se construirá la tuya.&lt;br /&gt;
En este caso, parte de una imagen ligera que ya tiene Python 3.12 instalado.&lt;br /&gt;
&lt;br /&gt;
=== WORKDIR ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta línea define el directorio de trabajo dentro del contenedor.&lt;br /&gt;
A partir de aquí, todos los comandos que aparezcan después (COPY, RUN, CMD, etc.) se ejecutarán dentro de /app.&lt;br /&gt;
&lt;br /&gt;
'''Ojo: /app no es la carpeta app/ de tu proyecto local.'''. Son cosas distintas.&lt;br /&gt;
&lt;br /&gt;
En tu máquina puede existir algo como:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
uvlhub&lt;br /&gt;
   app&lt;br /&gt;
   core&lt;br /&gt;
   requirements.txt &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Pero dentro del contenedor la estructura será distinta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
/ &lt;br /&gt;
  app/ &lt;br /&gt;
    app/ ← aquí dentro se habrá copiado tu carpeta local &amp;quot;app&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Cuando más adelante uses:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Docker copiará todos los archivos de tu proyecto dentro de la carpeta /app del contenedor.&lt;br /&gt;
Por eso el código quedará en /app/app/.&lt;br /&gt;
&lt;br /&gt;
=== COPY ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Copia todos los archivos del proyecto (del host) dentro del contenedor, en /app.&lt;br /&gt;
El primer punto es el origen; el segundo, el destino.&lt;br /&gt;
&lt;br /&gt;
=== RUN ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ejecuta comandos durante la construcción de la imagen.&lt;br /&gt;
Aquí instalamos las dependencias necesarias desde requirements.txt.&lt;br /&gt;
El resultado de este paso quedará guardado en la imagen.&lt;br /&gt;
&lt;br /&gt;
=== EXPOSE ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Documenta el puerto que la aplicación usa dentro del contenedor.&lt;br /&gt;
Esto no lo abre al exterior; simplemente indica que la app escucha en el 5000.&lt;br /&gt;
&lt;br /&gt;
=== CMD ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Define el comando que se ejecutará cuando el contenedor se inicie.&lt;br /&gt;
En este caso, ejecutará la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
=== RUN vs CMD ===&lt;br /&gt;
&lt;br /&gt;
Estas dos instrucciones parecen similares, pero hacen cosas muy distintas.&lt;br /&gt;
&lt;br /&gt;
==== RUN ====&lt;br /&gt;
Se ejecuta durante la construcción de la imagen (en el momento del docker build).&lt;br /&gt;
Cada RUN crea una nueva capa dentro de la imagen.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando instala las dependencias una sola vez, cuando se construye la imagen.&lt;br /&gt;
El resultado (las librerías instaladas) queda grabado dentro de la imagen final.&lt;br /&gt;
&lt;br /&gt;
Si después lanzas un contenedor nuevo a partir de esa imagen, las dependencias ya estarán instaladas y no se volverán a ejecutar.&lt;br /&gt;
&lt;br /&gt;
==== CMD ====&lt;br /&gt;
Se ejecuta cuando se inicia el contenedor (en el docker run).&lt;br /&gt;
Define el proceso principal que se ejecutará dentro del contenedor mientras esté activo.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando no se ejecuta durante la construcción, sino cada vez que arrancas el contenedor.&lt;br /&gt;
Si detienes el contenedor, este proceso también se detiene.&lt;br /&gt;
&lt;br /&gt;
==== Diferencia resumida ====&lt;br /&gt;
&lt;br /&gt;
RUN → se ejecuta al construir la imagen (fase de docker build).&lt;br /&gt;
&lt;br /&gt;
CMD → se ejecuta al iniciar el contenedor (fase de docker run).&lt;br /&gt;
&lt;br /&gt;
Solo puede haber un CMD por Dockerfile.&lt;br /&gt;
Si hay más de uno, solo se usará el último.&lt;br /&gt;
&lt;br /&gt;
== Dockerfile completo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
COPY . .&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto tiene que ir en un Dockerfile en docker/images&lt;br /&gt;
&lt;br /&gt;
== Construir la imagen ==&lt;br /&gt;
Desde la raíz del proyecto (no dentro de docker/images):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker build -t uvlhub:dev -f docker/images/Dockerfile .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Ejecutar el contenedor ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Qué es lo que observas?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 2: Conectando servicios =&lt;br /&gt;
&lt;br /&gt;
¡Vaya! ¡No funciona! Can't connect to MySQL server on 'db'... ¡Ah, claro, intenta conectarse a la base de datos, que no lo tenemos! Pero claro, en un contenedor no es recomendable tener la app y la base de datos conviviendo...&lt;br /&gt;
&lt;br /&gt;
== Crear contenedor MariaDB ==&lt;br /&gt;
&lt;br /&gt;
Antes que nada, haz Ctrl + C para parar el contenedor web. En este ejercicio aprenderás a lanzar un contenedor de MariaDB 12.0.2 de forma manual, configurando todas las variables necesarias para tu aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
El objetivo es entender la complejidad de hacerlo “a mano”, antes de automatizarlo con docker-compose.&lt;br /&gt;
&lt;br /&gt;
== Descargar la imagen oficial ==&lt;br /&gt;
Primero descarga la versión exacta que usaremos:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker pull mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Crear el contenedor de MariaDB ==&lt;br /&gt;
Lanza un contenedor con todas las variables necesarias para que Flask pueda conectarse correctamente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d --name mariadb_container \&lt;br /&gt;
 -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
 -e FLASK_ENV=development \&lt;br /&gt;
 -e DOMAIN=localhost \&lt;br /&gt;
 -e MARIADB_HOSTNAME=db \&lt;br /&gt;
 -e MARIADB_PORT=3306 \&lt;br /&gt;
 -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
 -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
 -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
 -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
 -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
 -e WORKING_DIR=/app/ \&lt;br /&gt;
 -p 3306:3306 mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Explicación:&lt;br /&gt;
&lt;br /&gt;
* -d: ejecuta el contenedor en segundo plano (modo detached).&lt;br /&gt;
* --name: asigna un nombre identificable al contenedor.&lt;br /&gt;
* -e: define variables de entorno dentro del contenedor.&lt;br /&gt;
* -p 3306:3306: publica el puerto de MariaDB en tu máquina local.&lt;br /&gt;
* mariadb:12.0.2: imagen exacta que vamos a usar.&lt;br /&gt;
&lt;br /&gt;
Aquí ya, de entrada, vemos que esto es más feo que una nevera por detrás. Para empezar, estamos pasando a lo loco un montón de variables que difílmente será replicable en producción, por contener información sensible. Aparte, si metemos nuevas variables, ese comando queda inservible. Necesitaremos una solución más ágil para este problema&lt;br /&gt;
&lt;br /&gt;
== Comprobar que está corriendo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver algo así:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES&lt;br /&gt;
abcd1234efgh mariadb:12.0.2 &amp;quot;docker-entrypoint.s…&amp;quot; Up 5 seconds 0.0.0.0:3306-&amp;gt;3306/tcp mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Conectarse a la base de datos ==&lt;br /&gt;
Abre una consola dentro del contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it mariadb_container bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y dentro, accede al cliente de MariaDB:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mariadb -u root -p&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Contraseña: uvlhubdb_root_password&lt;br /&gt;
&lt;br /&gt;
== Verificar la base de datos ==&lt;br /&gt;
Una vez dentro del cliente MySQL, ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
SHOW DATABASES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver que existen la bases de datos uvlhubdb porque pedimos crearla a la hora de levantar el contenedor. ¿Qué tiene la base de datos dentro? Vamos a verlo...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
USE uvlhubdb;&lt;br /&gt;
SHOW TABLES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Un total de... 0 tablas... ¡Claro! Es que solo hemos creado la base en sí, pero no las tablas. ¿Con un script SQL tal vez? ¡Espera, espera, si tenemos las migraciones! Pero claro, para eso necesito el contenedor de la app levantado. Sal de la consola SQL y del contenedor de MariaDB. Bien, ahora en el host...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta vez con el flag &amp;quot;-d&amp;quot; para que corra en segundo plano. Ahora, accedamos al contenedor y ejecutemos las migraciones. Pero... ¿cómo se llama mi contenedor?&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate que Docker asigna un nombre aleatorio a tu contenedor web porque no definiste ninguno. Una vez identificado el nombre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it &amp;lt;nombre&amp;gt; bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
What? ¿Qué pasa? ¿Otra vez error de conexión con la base? Si tenemos el contenedor funcionando, esto es un jaleo...&lt;br /&gt;
&lt;br /&gt;
== Redes internas ==&lt;br /&gt;
&lt;br /&gt;
Ahora mismo tienes dos contenedores aislados, el de la app y el la base de datos. Por defecto, cada contenedor tiene su propia red interna y no puede ver a los demás por nombre. El hostname '''db''' que usa Flask no existe en su DNS interno.&lt;br /&gt;
&lt;br /&gt;
=== Conectar servicios ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una red interna que compartan ambos contenedores&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker network create uvlhub_network&lt;br /&gt;
docker network ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vemos que ya aparece nuestra red '''uvlhub_network'''. Ahora, conectemos el contenedor de MariaDB a la red:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Claro, si te fijas, ahora hay que añadir, además de toda esa configuración engorrosa, que se añada en la misma red y que además tenga de hostname &amp;quot;db&amp;quot; para que el contenedor web entienda qué es &amp;quot;db&amp;quot;. Tenemos que hacer lo mismo para el contenedor web:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop &amp;lt;nombre&amp;gt;&lt;br /&gt;
docker rm &amp;lt;nombre&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y lo levantamos de nuevo, pero ya en la nueva red. De paso, le ponemos un nombre para identificarlo mejor.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 --name web_app_container --network uvlhub_network -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y ahora ya podemos acceder al contenedor por su nombre y ejecutar las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Mejor?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 3: Persistencia =&lt;br /&gt;
&lt;br /&gt;
Vamos a simular que hemos desplegado justo esto en producción y el servidor se reinicia por un corte de luz. Reiniciemos a mano cada contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker restart web_app_container&lt;br /&gt;
docker restart mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. What? ¿Cómo? ¿Qué ha pasado? ¿Otra vez problema de conexión? Ve dándole a F5... F5... ¡bingo! Ahora sí sale, pero menuda aleatoriedad, ¿no? Claro, es que ahora caes en que una cosa es que el contenedor haya arrancado y otra que esté listo.&lt;br /&gt;
&lt;br /&gt;
Aparte, tenemos otro problema. ¿Qué pasa con la persistencia? Porque si borro el contenedor de MariaDB, todos los datos que estén en su interior también se pierden. Y si estamos en producción, lío asegurado.&lt;br /&gt;
&lt;br /&gt;
== Volúmenes ==&lt;br /&gt;
&lt;br /&gt;
Cada vez que eliminas el contenedor de MariaDB, todas las bases de datos se pierden. Esto ocurre porque los datos se guardan dentro del sistema de archivos del contenedor, y al borrarlo desaparecen con él.&lt;br /&gt;
&lt;br /&gt;
En este ejercicio creas un volumen de Docker para que los datos persistan entre ejecuciones.&lt;br /&gt;
&lt;br /&gt;
=== Crear un volumen ===&lt;br /&gt;
Un volumen es una carpeta gestionada por Docker que vive fuera del ciclo de vida de los contenedores.&lt;br /&gt;
&lt;br /&gt;
Ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume create mariadb_data&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba que existe:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
La salida muestra algo como:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
DRIVER    VOLUME NAME&lt;br /&gt;
local     mariadb_data&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Crear un contenedor de MariaDB con el volumen ===&lt;br /&gt;
Lanza el contenedor y conecta el volumen a la ruta donde MariaDB guarda los datos (/var/lib/mysql):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -v mariadb_data:/var/lib/mysql \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Qué significa &amp;quot;:&amp;quot; en los volúmenes ===&lt;br /&gt;
&lt;br /&gt;
Cuando usas la opción '''-v''' o '''--volume''' en Docker, el signo dos puntos (''':''') separa dos rutas:&lt;br /&gt;
&lt;br /&gt;
'''-v origen:destino'''&lt;br /&gt;
&lt;br /&gt;
origen → es la ruta o el volumen en tu máquina (el host).&lt;br /&gt;
&lt;br /&gt;
destino → es la ruta dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 4: Orquestación de servicios =&lt;br /&gt;
&lt;br /&gt;
Hasta ahora hemos creado y conectado los contenedores manualmente.&lt;br /&gt;
Ahora vas a usar Docker Compose para definir todo en un solo archivo.&lt;br /&gt;
&lt;br /&gt;
== Orquestación básica ==&lt;br /&gt;
&lt;br /&gt;
Crea un archivo llamado docker-compose.yml en la carpeta '''docker''' con este contenido:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    container_name: web_app_container&lt;br /&gt;
    env_file:&lt;br /&gt;
      - ../.env&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;5000&amp;quot;&lt;br /&gt;
    depends_on:&lt;br /&gt;
      - db&lt;br /&gt;
    build:&lt;br /&gt;
      context: ../&lt;br /&gt;
      dockerfile: docker/images/Dockerfile&lt;br /&gt;
    networks:&lt;br /&gt;
      - uvlhub_network&lt;br /&gt;
&lt;br /&gt;
  db:&lt;br /&gt;
    container_name: mariadb_container&lt;br /&gt;
    image: mariadb:12.0.2&lt;br /&gt;
    env_file:&lt;br /&gt;
      - ../.env&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;3306:3306&amp;quot;&lt;br /&gt;
    volumes:&lt;br /&gt;
      - db_data:/var/lib/mysql&lt;br /&gt;
    networks:&lt;br /&gt;
      - uvlhub_network&lt;br /&gt;
&lt;br /&gt;
volumes:&lt;br /&gt;
  db_data:&lt;br /&gt;
&lt;br /&gt;
networks:&lt;br /&gt;
  uvlhub_network:&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate que esta orquestación de servicios me permite definir el nombre de cada servicio, su dependencia, sus puertos, sus volúmenes, redes... todo lo que hacíamos de forma manual y propenso a errores queda ahora más fácil de gestionar. Además, ahora ambos contenedores comparten el mismo archivo .env de variables de entorno.&lt;br /&gt;
&lt;br /&gt;
Bien, ahora vamos a levantar estos contenedores y acceder al contenedor web:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Inmediatamente después, ejecuta las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué ha ocurrido? Depende del tiempo que hayas tardado, puede que haya saltado un error o puede que haya funcionado. Puede que hayas intentado varias veces el comando anterior hasta que ha funcionado. ¿Por qué crees que ocurre esta aparete aleatoriedad?&lt;br /&gt;
&lt;br /&gt;
== Dependencias entre contenedores y entrypoints ==&lt;br /&gt;
&lt;br /&gt;
Fíjate que tenemos un &amp;quot;depends on&amp;quot; en el servicio web. Significa que el servicio &amp;quot;db&amp;quot; debe iniciarse antes que el servicio web. Pero, cuidado, ahí está la trampa, porque &amp;quot;servicio iniciado != servicio preparado&amp;quot;. El servicio de MariaDB tiene que arrancar, configurarse, etc...&lt;br /&gt;
&lt;br /&gt;
Cuando hicimos el comando COPY en nuestro Dockerfile, también copiamos la carpeta '''scripts'''. Uno de los scripts es '''wait-for-db.sh''', que realiza continuamente llamadas a MariaDB hasta que este servicio está listo para recibir peticiones. Ábrelo y estúdialo.&lt;br /&gt;
&lt;br /&gt;
Añade al servicio web de tu docker compose las siguientes líneas:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
entrypoint: [&amp;quot;/app/scripts/wait-for-db.sh&amp;quot;]&lt;br /&gt;
command: [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A su vez, del Dockerfile, hay que eliminar la línea de CMD que arrancaba nuestro servidor Flask.&lt;br /&gt;
&lt;br /&gt;
Luego, reinicializa todos los servicios:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. What? ¿No abre? Vamos a investigarlo, veamos el logs:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker logs web_app_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Nos debe salir un mensaje eterno de &amp;quot;/app/scripts/wait-for-db.sh: line 18: mariadb: command not found&amp;quot;. Esto ocurre porque en la imagen de nuestro servicio web NO TENEMOS el cliente de MariaDB. Debemos indicar entonces que lo instale. Añade justo debajo de FROM lo siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y --no-install-recommends mariadb-client &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Al ser un contenedor basado en Linux, y accediendo como root, es como si actualizáramos todos los paquetes y luego instalásemos el cliente de MariaDB. Intentémoslo otra vez:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. ¡Jopelines! ¿Tampoco? Si haces un &amp;quot;docker ps&amp;quot; verás el problema. El contenedor web no tiene expuesto ningún puerto.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    (...)&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;5000&amp;quot;&lt;br /&gt;
    (...)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;quot;Expose&amp;quot; hace visible el puerto '''entre contenedores''', pero '''NO entre tu contenedor y el host'''. Para eso usamos la directiva &amp;quot;ports&amp;quot;, que se encarga del mapping. En nuestro caso, será:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    (...)&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;5000:5000&amp;quot;&lt;br /&gt;
    (...)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
En ocasiones, si los dos puertos coinciden, se puede dejar uno solo con &amp;quot;5000&amp;quot; pero dejaremos los dos. El de la izquierda siempre es el puerto externo (host), el de la derecha es el puerto interno (contenedor). Así que cambia ese detalle y por última vez:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. ¡Ahora sí!&lt;br /&gt;
&lt;br /&gt;
== Trabajando con CMD, entrypoints y commands ==&lt;br /&gt;
&lt;br /&gt;
Piensa que al arrancar un contenedor Docker, siempre se ejecuta un único comando final.&lt;br /&gt;
Lo que hacen ENTRYPOINT, CMD y command (de Compose) es decidir qué comando será ese y con qué argumentos.&lt;br /&gt;
&lt;br /&gt;
=== CMD → “qué hacer por defecto” ===&lt;br /&gt;
&lt;br /&gt;
* Está en el Dockerfile.&lt;br /&gt;
* Es el comando que Docker ejecuta si no se indica otra cosa. En nuestro caso:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== ENTRYPOINT → “qué se ejecuta siempre” ===&lt;br /&gt;
&lt;br /&gt;
* También va en el Dockerfile, o se puede sobreescribir en el docker-compose.yml.&lt;br /&gt;
* Define el programa o script principal que siempre se ejecuta.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
ENTRYPOINT [&amp;quot;/app/scripts/wait-for-db.sh&amp;quot;]&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Normalmente, el entrypoint se combina con CMD en modo &amp;quot;eh, ejecuta esto (entrypoint) y luego este comando final (cmd)&lt;br /&gt;
&lt;br /&gt;
Los entrypoints son muy útiles, sobre todo en los docker-compose's, porque puedo redefinir cómo quiero que inicie un contenedor en concreto '''SIN TENER QUE HACER UN BUILD DE NUEVO DE LA IMAGEN DE DOCKER'''. A diferencia de CMD, que viene predefinido, un entrypoint en mi docker-compose me da más versatilidad&lt;br /&gt;
&lt;br /&gt;
=== command (de docker-compose.yml) → “sobrescribe el CMD” ===&lt;br /&gt;
&lt;br /&gt;
En docker-compose.yml, el campo command: reemplaza al CMD del Dockerfile,pero no al ENTRYPOINT.&lt;br /&gt;
&lt;br /&gt;
== Combinación de elementos ==&lt;br /&gt;
&lt;br /&gt;
Es importante entender cómo Docker trabaja con la orquestación y con los contenedores:&lt;br /&gt;
&lt;br /&gt;
* Lee el YAML como un diccionario&lt;br /&gt;
* Ejecuta todo lo que haya en un Dockerfile EXCEPTUANDO directivas ENTRYPOINT y CMD&lt;br /&gt;
* Monta el volumen o los volúmenes de ese contenedor (si procede)&lt;br /&gt;
&lt;br /&gt;
Y ahora viene el detalle:&lt;br /&gt;
&lt;br /&gt;
* Si estamos levantando un contenedor a través de docker-compose, va a ejecutar los entrypoints y commands definidos en ese docker-compose.&lt;br /&gt;
* Un entrypoint definido en un docker-compose va a sustituir a un entrypoint definido en el Dockerfile&lt;br /&gt;
* Un command definido en un docker-compose va a sustituir a un CMD definido en el Dockerfile.&lt;br /&gt;
&lt;br /&gt;
Como consejo:&lt;br /&gt;
&lt;br /&gt;
* Definimos un entrypoint en el docker-compose. Esto es así porque está fuera de la imagen de Docker y nos permite redefinir el inicio del servicio sin modificar la imagen&lt;br /&gt;
* Eventualmente, definimos un CMD donde indicamos el comando final de ese contenedor al iniciar, que suele ser el arranque de Flask Server o de Gunicorn, dependiendo de si estamos en desarrollo o producción.&lt;br /&gt;
&lt;br /&gt;
No obstante, como nuestros entrypoints son todos scripts en bash, podemos definir en el mismo el arranque final (comando final) y nos olvidamos de CMD.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 5: Volumen de trabajo =&lt;br /&gt;
&lt;br /&gt;
== Problema de desarrollo ==&lt;br /&gt;
&lt;br /&gt;
Vale, ya parece que le hemos pillado el tranquillo a Docker y a Docker Compose. Ahora vamos a hacer algo como desarrolladores. Ve al archivo &amp;quot;app/templates/base_templates.html&amp;quot; y cambia la línea 22:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;title&amp;gt;{{ FLASK_APP_NAME }} - Repository of feature models in UVL &amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
por:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;title&amp;gt;Hello world! &amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. Fíjate en el nombre de la pestaña del navegador, ¿ha cambiado? ¿No? Tsss, esto de los contenedores... bueno, va, vamos a reiniciarlo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker restart web_app_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. ¿Tampoco? Anda que vaya idea la de usar Docker... Claro, es a la hora de construir la imagen, hicimos &amp;quot;COPY . .&amp;quot;, pero no hemos reconstruido nada, así que...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml build --no-cache web&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --force-recreate web&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Importante señalar que le pedimos que ignore la caché para que fuerce a copiar de nuevo todo el contenido (incluyendo el cambio de línea) al interior del contenedor.&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. ¡Ahora sí! Pero... ¿tengo que reconstruir TODO el contenedor en cada cambio línea? ¿Y eso es trabajar de forma ágil?&lt;br /&gt;
&lt;br /&gt;
== Crear volumen de trabajo (bind mount) ==&lt;br /&gt;
&lt;br /&gt;
Evidentemente, eso no tiene sentido, tenemos que usar un volumen de trabajo que nos ayude a sincronizar el código de nuestro host con el código interno del contenedor. Luego en nuestro docker-compose:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  web:&lt;br /&gt;
    (....)&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;5000:5000&amp;quot;&lt;br /&gt;
    volumes:&lt;br /&gt;
      - ../:/app&lt;br /&gt;
    (....)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
La línea&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
- ../:/app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Monta tu carpeta local del proyecto (../) dentro del contenedor, (/app). ¿Y por qué /app? Porque fue el espacio de trabajo que definimos en el Dockerfile. Como ves, la congruencia entre los Dockerfile y el docker-compose es importante.&lt;br /&gt;
&lt;br /&gt;
Recargemos de nuevo nustra configuración:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ahora ya podemos trabajar con el código en host sin tener que reconstruir la imagen a cada cambio de línea.&lt;br /&gt;
&lt;br /&gt;
== Cuidado con COPY y los bind mounts ==&lt;br /&gt;
&lt;br /&gt;
Un '''bind mount''' (o “montaje de enlace”) es un tipo de volumen en el que Docker conecta directamente una carpeta de tu máquina (el host) con una carpeta dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
Cuando usas un bind mount (por ejemplo ../:/app), Docker monta tu carpeta local sobre la ruta /app del contenedor.&lt;br /&gt;
Eso significa que todo lo que estaba en /app dentro de la imagen desaparece al arrancar el contenedor, porque queda “oculto” por el volumen.&lt;br /&gt;
&lt;br /&gt;
Ahora bien, durante la construcción de la imagen usamos la directiva COPY. ¿Por qué? Porque necesitamos el archivo requirements.txt para instalar las dependencias de Python del proyecto. Pero... ¿realmente necesitamos copiar TODO? No, claro que no, podemos optar por copiar exclusivamente aquellos archivos que nos hagan falta durante '''LA CONSTRUCCIÓN''' de la imagen. El bind mount luego lo machacará.&lt;br /&gt;
&lt;br /&gt;
'''Muy importante:''' los bind mounts, al igual que cualquier otro volumen, están disponibles '''DESPUÉS''' de la construcción de la imagen. Es un error común creer que porque definamos un bind mount en un docker-compose, esos archivos aparecen ya para el contenedor. '''Esto es una fuente inagotable de problemas en la práctica'''.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 6: Distintas configuraciones de despliegue =&lt;br /&gt;
&lt;br /&gt;
Volvamos al punto de partida:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
rm -r docker&lt;br /&gt;
mv docker.bk docker&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta práctica ha sido engorrosa a propósito para quitar esa &amp;quot;magia&amp;quot; que existe cuando lanzas un comando simple de orquestación y todo se levanta y funciona perfectamente. Esa situación está muy bien para desarrollar, pero no para aprender.&lt;br /&gt;
&lt;br /&gt;
Es importante que analices que existen distintos escenarios:&lt;br /&gt;
&lt;br /&gt;
* Hay un docker-compose para desarrollo.&lt;br /&gt;
* Hay 3 docker-compose para producción.&lt;br /&gt;
* Hay 6 Dockerfiles distintos&lt;br /&gt;
* Hay 3 entrypoints distintos&lt;br /&gt;
* Hay una carpeta llamada letsencrypt y otra llamada nginx. Dentro de nginx hay dos archivos de configuración dependiendo de si estamos en dev o en prod.&lt;br /&gt;
&lt;br /&gt;
== Autoevaluación ==&lt;br /&gt;
&lt;br /&gt;
Estudia bien cada archivo. Tómate tu tiempo e intenta responder a estas preguntas:&lt;br /&gt;
&lt;br /&gt;
* ¿Por qué en el docker-compose de dev NO se usa &amp;quot;ports&amp;quot; para enlazar con el host? (pista: nginx)&lt;br /&gt;
* ¿Qué es nginx y por qué es útil aquí?&lt;br /&gt;
* ¿Cuántos bind mounts usa el contenedor web en desarrollo? ¿Para qué sirve el bind mount del propio Docker?&lt;br /&gt;
* ¿Qué diferencia hay entre el entrypoint de desarrollo y el entrypoint de producción?&lt;br /&gt;
* ¿Por qué el servicio web en producción está montando todos esos bind mounts por separado? ¿No sería más sencillo hacer un volumen de todo el código?&lt;br /&gt;
&lt;br /&gt;
== Comentarios finales ==&lt;br /&gt;
&lt;br /&gt;
=== Cuanto más se parezca el entorno de desarrollo al de producción, menos errores habrá ===&lt;br /&gt;
&lt;br /&gt;
En desarrollo es tentador usar configuraciones cómodas: servidor de Flask con --reload, bind mounts, base de datos vacía, logs en consola, etc.&lt;br /&gt;
Pero cuanto más difiere ese entorno de producción, más probable es que algo falle al desplegar.&lt;br /&gt;
&lt;br /&gt;
'''El código no falla por cambiar de servidor, sino porque los entornos se comportan distinto.'''&lt;br /&gt;
&lt;br /&gt;
Ejemplos clásicos:&lt;br /&gt;
&lt;br /&gt;
* En desarrollo usas SQLite, en producción MariaDB → aparecen errores de compatibilidad SQL.&lt;br /&gt;
* En desarrollo Flask sirve los archivos estáticos, en producción lo hace Nginx → las rutas cambian.&lt;br /&gt;
* En desarrollo ejecutas '''flask run''', en producción '''gunicorn''' → los hilos, workers o el reloader funcionan distinto.&lt;br /&gt;
&lt;br /&gt;
Por eso, el objetivo ideal es que tu entorno de desarrollo (dev), de preproducción (staging) y de producción (prod) sean idénticos salvo por las credenciales.&lt;br /&gt;
&lt;br /&gt;
Docker y docker-compose te permiten lograr eso: puedes usar la misma imagen base, cambiando solo el docker-compose (volúmenes, puertos, etc.).&lt;br /&gt;
&lt;br /&gt;
=== Diferencia entre el servidor de Flask y Gunicorn ===&lt;br /&gt;
&lt;br /&gt;
Flask trae incorporado un servidor de desarrollo.&lt;br /&gt;
Es muy útil para depurar porque:&lt;br /&gt;
&lt;br /&gt;
* recarga automáticamente el código (--reload),&lt;br /&gt;
* muestra errores detallados en el navegador,&lt;br /&gt;
* solo ejecuta un proceso.&lt;br /&gt;
&lt;br /&gt;
Sin embargo, no está pensado para producción:&lt;br /&gt;
&lt;br /&gt;
* no maneja múltiples conexiones concurrentes,&lt;br /&gt;
* no tiene control de procesos ni balanceo de carga,&lt;br /&gt;
* no implementa estándares de rendimiento o seguridad (como WSGI robusto).&lt;br /&gt;
&lt;br /&gt;
Ahí entra Gunicorn (Green Unicorn), que sí es un servidor WSGI  de producción (Web Server Gateway Interface, estándar de comunicación entre aplicaciones web Python y servidores web):&lt;br /&gt;
&lt;br /&gt;
* puede lanzar varios workers en paralelo,&lt;br /&gt;
* maneja múltiples peticiones concurrentes,&lt;br /&gt;
* se integra fácilmente con Nginx para servir contenido estático o HTTPS.&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10162</id>
		<title>Tutorial-docker</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10162"/>
				<updated>2025-10-12T20:07:35Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Ejercicio 1: Empezamos desde cero =&lt;br /&gt;
&lt;br /&gt;
El objetivo es eliminar toda la configuración previa y crear desde cero la estructura de carpetas que contendrá nuestros futuros archivos Docker.&lt;br /&gt;
&lt;br /&gt;
== Eliminar la configuración existente ==&lt;br /&gt;
Antes de comenzar, asegúrate de borrar cualquier carpeta Docker existente en el proyecto. En este caso, le haremos una copia de seguridad para poder volver a ella.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mv docker docker.bk&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
También debes asegurarte que trabajas con las variables de entorno adecuadas:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
cp .env.docker.example .env&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
⚠️ Este paso es importante: partimos completamente desde cero para entender cada parte del sistema.&lt;br /&gt;
&lt;br /&gt;
== Crear un Dockerfile mínimo ==&lt;br /&gt;
&lt;br /&gt;
Ahora crea nuevamente la carpeta base donde almacenaremos los archivos relacionados con Docker:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt; mkdir -p docker/images &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vamos a explicar los distintos apartados:&lt;br /&gt;
&lt;br /&gt;
=== FROM ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Indica la imagen base sobre la que se construirá la tuya.&lt;br /&gt;
En este caso, parte de una imagen ligera que ya tiene Python 3.12 instalado.&lt;br /&gt;
&lt;br /&gt;
=== WORKDIR ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta línea define el directorio de trabajo dentro del contenedor.&lt;br /&gt;
A partir de aquí, todos los comandos que aparezcan después (COPY, RUN, CMD, etc.) se ejecutarán dentro de /app.&lt;br /&gt;
&lt;br /&gt;
'''Ojo: /app no es la carpeta app/ de tu proyecto local.'''. Son cosas distintas.&lt;br /&gt;
&lt;br /&gt;
En tu máquina puede existir algo como:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
uvlhub&lt;br /&gt;
   app&lt;br /&gt;
   core&lt;br /&gt;
   requirements.txt &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Pero dentro del contenedor la estructura será distinta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
/ &lt;br /&gt;
  app/ &lt;br /&gt;
    app/ ← aquí dentro se habrá copiado tu carpeta local &amp;quot;app&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Cuando más adelante uses:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Docker copiará todos los archivos de tu proyecto dentro de la carpeta /app del contenedor.&lt;br /&gt;
Por eso el código quedará en /app/app/.&lt;br /&gt;
&lt;br /&gt;
=== COPY ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Copia todos los archivos del proyecto (del host) dentro del contenedor, en /app.&lt;br /&gt;
El primer punto es el origen; el segundo, el destino.&lt;br /&gt;
&lt;br /&gt;
=== RUN ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ejecuta comandos durante la construcción de la imagen.&lt;br /&gt;
Aquí instalamos las dependencias necesarias desde requirements.txt.&lt;br /&gt;
El resultado de este paso quedará guardado en la imagen.&lt;br /&gt;
&lt;br /&gt;
=== EXPOSE ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Documenta el puerto que la aplicación usa dentro del contenedor.&lt;br /&gt;
Esto no lo abre al exterior; simplemente indica que la app escucha en el 5000.&lt;br /&gt;
&lt;br /&gt;
=== CMD ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Define el comando que se ejecutará cuando el contenedor se inicie.&lt;br /&gt;
En este caso, ejecutará la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
=== RUN vs CMD ===&lt;br /&gt;
&lt;br /&gt;
Estas dos instrucciones parecen similares, pero hacen cosas muy distintas.&lt;br /&gt;
&lt;br /&gt;
==== RUN ====&lt;br /&gt;
Se ejecuta durante la construcción de la imagen (en el momento del docker build).&lt;br /&gt;
Cada RUN crea una nueva capa dentro de la imagen.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando instala las dependencias una sola vez, cuando se construye la imagen.&lt;br /&gt;
El resultado (las librerías instaladas) queda grabado dentro de la imagen final.&lt;br /&gt;
&lt;br /&gt;
Si después lanzas un contenedor nuevo a partir de esa imagen, las dependencias ya estarán instaladas y no se volverán a ejecutar.&lt;br /&gt;
&lt;br /&gt;
==== CMD ====&lt;br /&gt;
Se ejecuta cuando se inicia el contenedor (en el docker run).&lt;br /&gt;
Define el proceso principal que se ejecutará dentro del contenedor mientras esté activo.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando no se ejecuta durante la construcción, sino cada vez que arrancas el contenedor.&lt;br /&gt;
Si detienes el contenedor, este proceso también se detiene.&lt;br /&gt;
&lt;br /&gt;
==== Diferencia resumida ====&lt;br /&gt;
&lt;br /&gt;
RUN → se ejecuta al construir la imagen (fase de docker build).&lt;br /&gt;
&lt;br /&gt;
CMD → se ejecuta al iniciar el contenedor (fase de docker run).&lt;br /&gt;
&lt;br /&gt;
Solo puede haber un CMD por Dockerfile.&lt;br /&gt;
Si hay más de uno, solo se usará el último.&lt;br /&gt;
&lt;br /&gt;
== Dockerfile completo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
COPY . .&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto tiene que ir en un Dockerfile en docker/images&lt;br /&gt;
&lt;br /&gt;
== Construir la imagen ==&lt;br /&gt;
Desde la raíz del proyecto (no dentro de docker/images):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker build -t uvlhub:dev -f docker/images/Dockerfile .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Ejecutar el contenedor ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Qué es lo que observas?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 2: Conectando servicios =&lt;br /&gt;
&lt;br /&gt;
¡Vaya! ¡No funciona! Can't connect to MySQL server on 'db'... ¡Ah, claro, intenta conectarse a la base de datos, que no lo tenemos! Pero claro, en un contenedor no es recomendable tener la app y la base de datos conviviendo...&lt;br /&gt;
&lt;br /&gt;
== Crear contenedor MariaDB ==&lt;br /&gt;
&lt;br /&gt;
Antes que nada, haz Ctrl + C para parar el contenedor web. En este ejercicio aprenderás a lanzar un contenedor de MariaDB 12.0.2 de forma manual, configurando todas las variables necesarias para tu aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
El objetivo es entender la complejidad de hacerlo “a mano”, antes de automatizarlo con docker-compose.&lt;br /&gt;
&lt;br /&gt;
== Descargar la imagen oficial ==&lt;br /&gt;
Primero descarga la versión exacta que usaremos:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker pull mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Crear el contenedor de MariaDB ==&lt;br /&gt;
Lanza un contenedor con todas las variables necesarias para que Flask pueda conectarse correctamente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d --name mariadb_container \&lt;br /&gt;
 -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
 -e FLASK_ENV=development \&lt;br /&gt;
 -e DOMAIN=localhost \&lt;br /&gt;
 -e MARIADB_HOSTNAME=db \&lt;br /&gt;
 -e MARIADB_PORT=3306 \&lt;br /&gt;
 -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
 -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
 -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
 -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
 -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
 -e WORKING_DIR=/app/ \&lt;br /&gt;
 -p 3306:3306 mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Explicación:&lt;br /&gt;
&lt;br /&gt;
* -d: ejecuta el contenedor en segundo plano (modo detached).&lt;br /&gt;
* --name: asigna un nombre identificable al contenedor.&lt;br /&gt;
* -e: define variables de entorno dentro del contenedor.&lt;br /&gt;
* -p 3306:3306: publica el puerto de MariaDB en tu máquina local.&lt;br /&gt;
* mariadb:12.0.2: imagen exacta que vamos a usar.&lt;br /&gt;
&lt;br /&gt;
Aquí ya, de entrada, vemos que esto es más feo que una nevera por detrás. Para empezar, estamos pasando a lo loco un montón de variables que difílmente será replicable en producción, por contener información sensible. Aparte, si metemos nuevas variables, ese comando queda inservible. Necesitaremos una solución más ágil para este problema&lt;br /&gt;
&lt;br /&gt;
== Comprobar que está corriendo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver algo así:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES&lt;br /&gt;
abcd1234efgh mariadb:12.0.2 &amp;quot;docker-entrypoint.s…&amp;quot; Up 5 seconds 0.0.0.0:3306-&amp;gt;3306/tcp mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Conectarse a la base de datos ==&lt;br /&gt;
Abre una consola dentro del contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it mariadb_container bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y dentro, accede al cliente de MariaDB:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mariadb -u root -p&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Contraseña: uvlhubdb_root_password&lt;br /&gt;
&lt;br /&gt;
== Verificar la base de datos ==&lt;br /&gt;
Una vez dentro del cliente MySQL, ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
SHOW DATABASES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver que existen la bases de datos uvlhubdb porque pedimos crearla a la hora de levantar el contenedor. ¿Qué tiene la base de datos dentro? Vamos a verlo...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
USE uvlhubdb;&lt;br /&gt;
SHOW TABLES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Un total de... 0 tablas... ¡Claro! Es que solo hemos creado la base en sí, pero no las tablas. ¿Con un script SQL tal vez? ¡Espera, espera, si tenemos las migraciones! Pero claro, para eso necesito el contenedor de la app levantado. Sal de la consola SQL y del contenedor de MariaDB. Bien, ahora en el host...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta vez con el flag &amp;quot;-d&amp;quot; para que corra en segundo plano. Ahora, accedamos al contenedor y ejecutemos las migraciones. Pero... ¿cómo se llama mi contenedor?&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate que Docker asigna un nombre aleatorio a tu contenedor web porque no definiste ninguno. Una vez identificado el nombre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it &amp;lt;nombre&amp;gt; bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
What? ¿Qué pasa? ¿Otra vez error de conexión con la base? Si tenemos el contenedor funcionando, esto es un jaleo...&lt;br /&gt;
&lt;br /&gt;
== Redes internas ==&lt;br /&gt;
&lt;br /&gt;
Ahora mismo tienes dos contenedores aislados, el de la app y el la base de datos. Por defecto, cada contenedor tiene su propia red interna y no puede ver a los demás por nombre. El hostname '''db''' que usa Flask no existe en su DNS interno.&lt;br /&gt;
&lt;br /&gt;
=== Conectar servicios ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una red interna que compartan ambos contenedores&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker network create uvlhub_network&lt;br /&gt;
docker network ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vemos que ya aparece nuestra red '''uvlhub_network'''. Ahora, conectemos el contenedor de MariaDB a la red:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Claro, si te fijas, ahora hay que añadir, además de toda esa configuración engorrosa, que se añada en la misma red y que además tenga de hostname &amp;quot;db&amp;quot; para que el contenedor web entienda qué es &amp;quot;db&amp;quot;. Tenemos que hacer lo mismo para el contenedor web:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop &amp;lt;nombre&amp;gt;&lt;br /&gt;
docker rm &amp;lt;nombre&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y lo levantamos de nuevo, pero ya en la nueva red. De paso, le ponemos un nombre para identificarlo mejor.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 --name web_app_container --network uvlhub_network -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y ahora ya podemos acceder al contenedor por su nombre y ejecutar las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Mejor?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 3: Persistencia =&lt;br /&gt;
&lt;br /&gt;
Vamos a simular que hemos desplegado justo esto en producción y el servidor se reinicia por un corte de luz. Reiniciemos a mano cada contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker restart web_app_container&lt;br /&gt;
docker restart mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. What? ¿Cómo? ¿Qué ha pasado? ¿Otra vez problema de conexión? Ve dándole a F5... F5... ¡bingo! Ahora sí sale, pero menuda aleatoriedad, ¿no? Claro, es que ahora caes en que una cosa es que el contenedor haya arrancado y otra que esté listo.&lt;br /&gt;
&lt;br /&gt;
Aparte, tenemos otro problema. ¿Qué pasa con la persistencia? Porque si borro el contenedor de MariaDB, todos los datos que estén en su interior también se pierden. Y si estamos en producción, lío asegurado.&lt;br /&gt;
&lt;br /&gt;
== Volúmenes ==&lt;br /&gt;
&lt;br /&gt;
Cada vez que eliminas el contenedor de MariaDB, todas las bases de datos se pierden. Esto ocurre porque los datos se guardan dentro del sistema de archivos del contenedor, y al borrarlo desaparecen con él.&lt;br /&gt;
&lt;br /&gt;
En este ejercicio creas un volumen de Docker para que los datos persistan entre ejecuciones.&lt;br /&gt;
&lt;br /&gt;
=== Crear un volumen ===&lt;br /&gt;
Un volumen es una carpeta gestionada por Docker que vive fuera del ciclo de vida de los contenedores.&lt;br /&gt;
&lt;br /&gt;
Ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume create mariadb_data&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba que existe:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
La salida muestra algo como:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
DRIVER    VOLUME NAME&lt;br /&gt;
local     mariadb_data&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Crear un contenedor de MariaDB con el volumen ===&lt;br /&gt;
Lanza el contenedor y conecta el volumen a la ruta donde MariaDB guarda los datos (/var/lib/mysql):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -v mariadb_data:/var/lib/mysql \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Qué significa &amp;quot;:&amp;quot; en los volúmenes ===&lt;br /&gt;
&lt;br /&gt;
Cuando usas la opción '''-v''' o '''--volume''' en Docker, el signo dos puntos (''':''') separa dos rutas:&lt;br /&gt;
&lt;br /&gt;
'''-v origen:destino'''&lt;br /&gt;
&lt;br /&gt;
origen → es la ruta o el volumen en tu máquina (el host).&lt;br /&gt;
&lt;br /&gt;
destino → es la ruta dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 4: Orquestación de servicios =&lt;br /&gt;
&lt;br /&gt;
Hasta ahora hemos creado y conectado los contenedores manualmente.&lt;br /&gt;
Ahora vas a usar Docker Compose para definir todo en un solo archivo.&lt;br /&gt;
&lt;br /&gt;
== Orquestación básica ==&lt;br /&gt;
&lt;br /&gt;
Crea un archivo llamado docker-compose.yml en la carpeta '''docker''' con este contenido:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    container_name: web_app_container&lt;br /&gt;
    env_file:&lt;br /&gt;
      - ../.env&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;5000&amp;quot;&lt;br /&gt;
    depends_on:&lt;br /&gt;
      - db&lt;br /&gt;
    build:&lt;br /&gt;
      context: ../&lt;br /&gt;
      dockerfile: docker/images/Dockerfile&lt;br /&gt;
    networks:&lt;br /&gt;
      - uvlhub_network&lt;br /&gt;
&lt;br /&gt;
  db:&lt;br /&gt;
    container_name: mariadb_container&lt;br /&gt;
    image: mariadb:12.0.2&lt;br /&gt;
    env_file:&lt;br /&gt;
      - ../.env&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;3306:3306&amp;quot;&lt;br /&gt;
    volumes:&lt;br /&gt;
      - db_data:/var/lib/mysql&lt;br /&gt;
    networks:&lt;br /&gt;
      - uvlhub_network&lt;br /&gt;
&lt;br /&gt;
volumes:&lt;br /&gt;
  db_data:&lt;br /&gt;
&lt;br /&gt;
networks:&lt;br /&gt;
  uvlhub_network:&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate que esta orquestación de servicios me permite definir el nombre de cada servicio, su dependencia, sus puertos, sus volúmenes, redes... todo lo que hacíamos de forma manual y propenso a errores queda ahora más fácil de gestionar. Además, ahora ambos contenedores comparten el mismo archivo .env de variables de entorno.&lt;br /&gt;
&lt;br /&gt;
Bien, ahora vamos a levantar estos contenedores y acceder al contenedor web:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Inmediatamente después, ejecuta las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué ha ocurrido? Depende del tiempo que hayas tardado, puede que haya saltado un error o puede que haya funcionado. Puede que hayas intentado varias veces el comando anterior hasta que ha funcionado. ¿Por qué crees que ocurre esta aparete aleatoriedad?&lt;br /&gt;
&lt;br /&gt;
== Dependencias entre contenedores y entrypoints ==&lt;br /&gt;
&lt;br /&gt;
Fíjate que tenemos un &amp;quot;depends on&amp;quot; en el servicio web. Significa que el servicio &amp;quot;db&amp;quot; debe iniciarse antes que el servicio web. Pero, cuidado, ahí está la trampa, porque &amp;quot;servicio iniciado != servicio preparado&amp;quot;. El servicio de MariaDB tiene que arrancar, configurarse, etc...&lt;br /&gt;
&lt;br /&gt;
Cuando hicimos el comando COPY en nuestro Dockerfile, también copiamos la carpeta '''scripts'''. Uno de los scripts es '''wait-for-db.sh''', que realiza continuamente llamadas a MariaDB hasta que este servicio está listo para recibir peticiones. Ábrelo y estúdialo.&lt;br /&gt;
&lt;br /&gt;
Añade al servicio web de tu docker compose las siguientes líneas:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
entrypoint: [&amp;quot;/app/scripts/wait-for-db.sh&amp;quot;]&lt;br /&gt;
command: [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A su vez, del Dockerfile, hay que eliminar la línea de CMD que arrancaba nuestro servidor Flask.&lt;br /&gt;
&lt;br /&gt;
Luego, reinicializa todos los servicios:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. What? ¿No abre? Vamos a investigarlo, veamos el logs:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker logs web_app_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Nos debe salir un mensaje eterno de &amp;quot;/app/scripts/wait-for-db.sh: line 18: mariadb: command not found&amp;quot;. Esto ocurre porque en la imagen de nuestro servicio web NO TENEMOS el cliente de MariaDB. Debemos indicar entonces que lo instale. Añade justo debajo de FROM lo siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y --no-install-recommends mariadb-client &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Al ser un contenedor basado en Linux, y accediendo como root, es como si actualizáramos todos los paquetes y luego instalásemos el cliente de MariaDB. Intentémoslo otra vez:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. ¡Jopelines! ¿Tampoco? Si haces un &amp;quot;docker ps&amp;quot; verás el problema. El contenedor web no tiene expuesto ningún puerto.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    (...)&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;5000&amp;quot;&lt;br /&gt;
    (...)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;quot;Expose&amp;quot; hace visible el puerto '''entre contenedores''', pero '''NO entre tu contenedor y el host'''. Para eso usamos la directiva &amp;quot;ports&amp;quot;, que se encarga del mapping. En nuestro caso, será:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    (...)&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;5000:5000&amp;quot;&lt;br /&gt;
    (...)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
En ocasiones, si los dos puertos coinciden, se puede dejar uno solo con &amp;quot;5000&amp;quot; pero dejaremos los dos. El de la izquierda siempre es el puerto externo (host), el de la derecha es el puerto interno (contenedor). Así que cambia ese detalle y por última vez:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. ¡Ahora sí!&lt;br /&gt;
&lt;br /&gt;
== Trabajando con CMD, entrypoints y commands ==&lt;br /&gt;
&lt;br /&gt;
Piensa que al arrancar un contenedor Docker, siempre se ejecuta un único comando final.&lt;br /&gt;
Lo que hacen ENTRYPOINT, CMD y command (de Compose) es decidir qué comando será ese y con qué argumentos.&lt;br /&gt;
&lt;br /&gt;
=== CMD → “qué hacer por defecto” ===&lt;br /&gt;
&lt;br /&gt;
* Está en el Dockerfile.&lt;br /&gt;
* Es el comando que Docker ejecuta si no se indica otra cosa. En nuestro caso:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== ENTRYPOINT → “qué se ejecuta siempre” ===&lt;br /&gt;
&lt;br /&gt;
* También va en el Dockerfile, o se puede sobreescribir en el docker-compose.yml.&lt;br /&gt;
* Define el programa o script principal que siempre se ejecuta.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
ENTRYPOINT [&amp;quot;/app/scripts/wait-for-db.sh&amp;quot;]&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Normalmente, el entrypoint se combina con CMD en modo &amp;quot;eh, ejecuta esto (entrypoint) y luego este comando final (cmd)&lt;br /&gt;
&lt;br /&gt;
Los entrypoints son muy útiles, sobre todo en los docker-compose's, porque puedo redefinir cómo quiero que inicie un contenedor en concreto '''SIN TENER QUE HACER UN BUILD DE NUEVO DE LA IMAGEN DE DOCKER'''. A diferencia de CMD, que viene predefinido, un entrypoint en mi docker-compose me da más versatilidad&lt;br /&gt;
&lt;br /&gt;
=== command (de docker-compose.yml) → “sobrescribe el CMD” ===&lt;br /&gt;
&lt;br /&gt;
En docker-compose.yml, el campo command: reemplaza al CMD del Dockerfile,pero no al ENTRYPOINT.&lt;br /&gt;
&lt;br /&gt;
== Combinación de elementos ==&lt;br /&gt;
&lt;br /&gt;
Es importante entender cómo Docker trabaja con la orquestación y con los contenedores:&lt;br /&gt;
&lt;br /&gt;
* Lee el YAML como un diccionario&lt;br /&gt;
* Ejecuta todo lo que haya en un Dockerfile EXCEPTUANDO directivas ENTRYPOINT y CMD&lt;br /&gt;
* Monta el volumen o los volúmenes de ese contenedor (si procede)&lt;br /&gt;
&lt;br /&gt;
Y ahora viene el detalle:&lt;br /&gt;
&lt;br /&gt;
* Si estamos levantando un contenedor a través de docker-compose, va a ejecutar los entrypoints y commands definidos en ese docker-compose.&lt;br /&gt;
* Un entrypoint definido en un docker-compose va a sustituir a un entrypoint definido en el Dockerfile&lt;br /&gt;
* Un command definido en un docker-compose va a sustituir a un CMD definido en el Dockerfile.&lt;br /&gt;
&lt;br /&gt;
Como consejo:&lt;br /&gt;
&lt;br /&gt;
* Definimos un entrypoint en el docker-compose. Esto es así porque está fuera de la imagen de Docker y nos permite redefinir el inicio del servicio sin modificar la imagen&lt;br /&gt;
* Eventualmente, definimos un CMD donde indicamos el comando final de ese contenedor al iniciar, que suele ser el arranque de Flask Server o de Gunicorn, dependiendo de si estamos en desarrollo o producción.&lt;br /&gt;
&lt;br /&gt;
No obstante, como nuestros entrypoints son todos scripts en bash, podemos definir en el mismo el arranque final (comando final) y nos olvidamos de CMD.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 5: Volumen de trabajo =&lt;br /&gt;
&lt;br /&gt;
== Problema de desarrollo ==&lt;br /&gt;
&lt;br /&gt;
Vale, ya parece que le hemos pillado el tranquillo a Docker y a Docker Compose. Ahora vamos a hacer algo como desarrolladores. Ve al archivo &amp;quot;app/templates/base_templates.html&amp;quot; y cambia la línea 22:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;title&amp;gt;{{ FLASK_APP_NAME }} - Repository of feature models in UVL &amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
por:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;title&amp;gt;Hello world! &amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. Fíjate en el nombre de la pestaña del navegador, ¿ha cambiado? ¿No? Tsss, esto de los contenedores... bueno, va, vamos a reiniciarlo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker restart web_app_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. ¿Tampoco? Anda que vaya idea la de usar Docker... Claro, es a la hora de construir la imagen, hicimos &amp;quot;COPY . .&amp;quot;, pero no hemos reconstruido nada, así que...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml build --no-cache web&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --force-recreate web&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Importante señalar que le pedimos que ignore la caché para que fuerce a copiar de nuevo todo el contenido (incluyendo el cambio de línea) al interior del contenedor.&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. ¡Ahora sí! Pero... ¿tengo que reconstruir TODO el contenedor en cada cambio línea? ¿Y eso es trabajar de forma ágil?&lt;br /&gt;
&lt;br /&gt;
== Crear volumen de trabajo (bind mount) ==&lt;br /&gt;
&lt;br /&gt;
Evidentemente, eso no tiene sentido, tenemos que usar un volumen de trabajo que nos ayude a sincronizar el código de nuestro host con el código interno del contenedor. Luego en nuestro docker-compose:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  web:&lt;br /&gt;
    (....)&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;5000:5000&amp;quot;&lt;br /&gt;
    volumes:&lt;br /&gt;
      - ../:/app&lt;br /&gt;
    (....)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
La línea&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
- ../:/app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Monta tu carpeta local del proyecto (../) dentro del contenedor, (/app). ¿Y por qué /app? Porque fue el espacio de trabajo que definimos en el Dockerfile. Como ves, la congruencia entre los Dockerfile y el docker-compose es importante.&lt;br /&gt;
&lt;br /&gt;
Recargemos de nuevo nustra configuración:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ahora ya podemos trabajar con el código en host sin tener que reconstruir la imagen a cada cambio de línea.&lt;br /&gt;
&lt;br /&gt;
== Cuidado con COPY y los bind mounts ==&lt;br /&gt;
&lt;br /&gt;
Un '''bind mount''' (o “montaje de enlace”) es un tipo de volumen en el que Docker conecta directamente una carpeta de tu máquina (el host) con una carpeta dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
Cuando usas un bind mount (por ejemplo ../:/app), Docker monta tu carpeta local sobre la ruta /app del contenedor.&lt;br /&gt;
Eso significa que todo lo que estaba en /app dentro de la imagen desaparece al arrancar el contenedor, porque queda “oculto” por el volumen.&lt;br /&gt;
&lt;br /&gt;
Ahora bien, durante la construcción de la imagen usamos la directiva COPY. ¿Por qué? Porque necesitamos el archivo requirements.txt para instalar las dependencias de Python del proyecto. Pero... ¿realmente necesitamos copiar TODO? No, claro que no, podemos optar por copiar exclusivamente aquellos archivos que nos hagan falta durante '''LA CONSTRUCCIÓN''' de la imagen. El bind mount luego lo machacará.&lt;br /&gt;
&lt;br /&gt;
'''Muy importante:''' los bind mounts, al igual que cualquier otro volumen, están disponibles '''DESPUÉS''' de la construcción de la imagen. Es un error común creer que porque definamos un bind mount en un docker-compose, esos archivos aparecen ya para el contenedor. '''Esto es una fuente inagotable de problemas en la práctica'''.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 6: Distintas configuraciones de despliegue =&lt;br /&gt;
&lt;br /&gt;
Volvamos al punto de partida:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
rm -r docker&lt;br /&gt;
mv docker.bk docker&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta práctica ha sido engorrosa a propósito para quitar esa &amp;quot;magia&amp;quot; que existe cuando lanzas un comando simple de orquestación y todo se levanta y funciona perfectamente. Esa situación está muy bien para desarrollar, pero no para aprender.&lt;br /&gt;
&lt;br /&gt;
Es importante que analices que existen distintos escenarios:&lt;br /&gt;
&lt;br /&gt;
* Hay un docker-compose para desarrollo.&lt;br /&gt;
* Hay 3 docker-compose para producción.&lt;br /&gt;
* Hay 6 Dockerfiles distintos&lt;br /&gt;
* Hay 3 entrypoints distintos&lt;br /&gt;
* Hay una carpeta llamada letsencrypt y otra llamada nginx. Dentro de nginx hay dos archivos de configuración dependiendo de si estamos en dev o en prod.&lt;br /&gt;
&lt;br /&gt;
== Autoevaluación ==&lt;br /&gt;
&lt;br /&gt;
Estudia bien cada archivo. Tómate tu tiempo e intenta responder a estas preguntas:&lt;br /&gt;
&lt;br /&gt;
* ¿Por qué en el docker-compose de dev NO se usa &amp;quot;ports&amp;quot; para enlazar con el host? (pista: nginx)&lt;br /&gt;
* ¿Qué es nginx y por qué es útil aquí?&lt;br /&gt;
* ¿Cuántos bind mounts usa el contenedor web? ¿Para qué sirve el bind mount del propio Docker?&lt;br /&gt;
* ¿Qué diferencia hay entre el entrypoint de desarrollo y el entrypoint de producción?&lt;br /&gt;
* ¿Por qué el servicio web en producción está montando todos esos bind mounts por separado? ¿No sería más sencillo hacer un volumen de todo el código?&lt;br /&gt;
&lt;br /&gt;
== Comentarios finales ==&lt;br /&gt;
&lt;br /&gt;
=== Cuanto más se parezca el entorno de desarrollo al de producción, menos errores habrá ===&lt;br /&gt;
&lt;br /&gt;
En desarrollo es tentador usar configuraciones cómodas: servidor de Flask con --reload, bind mounts, base de datos vacía, logs en consola, etc.&lt;br /&gt;
Pero cuanto más difiere ese entorno de producción, más probable es que algo falle al desplegar.&lt;br /&gt;
&lt;br /&gt;
'''El código no falla por cambiar de servidor, sino porque los entornos se comportan distinto.'''&lt;br /&gt;
&lt;br /&gt;
Ejemplos clásicos:&lt;br /&gt;
&lt;br /&gt;
* En desarrollo usas SQLite, en producción MariaDB → aparecen errores de compatibilidad SQL.&lt;br /&gt;
* En desarrollo Flask sirve los archivos estáticos, en producción lo hace Nginx → las rutas cambian.&lt;br /&gt;
* En desarrollo ejecutas '''flask run''', en producción '''gunicorn''' → los hilos, workers o el reloader funcionan distinto.&lt;br /&gt;
&lt;br /&gt;
Por eso, el objetivo ideal es que tu entorno de desarrollo (dev), de preproducción (staging) y de producción (prod) sean idénticos salvo por las credenciales.&lt;br /&gt;
&lt;br /&gt;
Docker y docker-compose te permiten lograr eso: puedes usar la misma imagen base, cambiando solo el docker-compose (volúmenes, puertos, etc.).&lt;br /&gt;
&lt;br /&gt;
=== Diferencia entre el servidor de Flask y Gunicorn ===&lt;br /&gt;
&lt;br /&gt;
Flask trae incorporado un servidor de desarrollo.&lt;br /&gt;
Es muy útil para depurar porque:&lt;br /&gt;
&lt;br /&gt;
* recarga automáticamente el código (--reload),&lt;br /&gt;
* muestra errores detallados en el navegador,&lt;br /&gt;
* solo ejecuta un proceso.&lt;br /&gt;
&lt;br /&gt;
Sin embargo, no está pensado para producción:&lt;br /&gt;
&lt;br /&gt;
* no maneja múltiples conexiones concurrentes,&lt;br /&gt;
* no tiene control de procesos ni balanceo de carga,&lt;br /&gt;
* no implementa estándares de rendimiento o seguridad (como WSGI robusto).&lt;br /&gt;
&lt;br /&gt;
Ahí entra Gunicorn (Green Unicorn), que sí es un servidor WSGI  de producción (Web Server Gateway Interface, estándar de comunicación entre aplicaciones web Python y servidores web):&lt;br /&gt;
&lt;br /&gt;
* puede lanzar varios workers en paralelo,&lt;br /&gt;
* maneja múltiples peticiones concurrentes,&lt;br /&gt;
* se integra fácilmente con Nginx para servir contenido estático o HTTPS.&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10161</id>
		<title>Tutorial-docker</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10161"/>
				<updated>2025-10-12T19:38:06Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Ejercicio 1: Empezamos desde cero =&lt;br /&gt;
&lt;br /&gt;
El objetivo es eliminar toda la configuración previa y crear desde cero la estructura de carpetas que contendrá nuestros futuros archivos Docker.&lt;br /&gt;
&lt;br /&gt;
== Eliminar la configuración existente ==&lt;br /&gt;
Antes de comenzar, asegúrate de borrar cualquier carpeta Docker existente en el proyecto. En este caso, le haremos una copia de seguridad para poder volver a ella.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mv docker docker.bk&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
También debes asegurarte que trabajas con las variables de entorno adecuadas:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
cp .env.docker.example .env&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
⚠️ Este paso es importante: partimos completamente desde cero para entender cada parte del sistema.&lt;br /&gt;
&lt;br /&gt;
== Crear un Dockerfile mínimo ==&lt;br /&gt;
&lt;br /&gt;
Ahora crea nuevamente la carpeta base donde almacenaremos los archivos relacionados con Docker:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt; mkdir -p docker/images &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vamos a explicar los distintos apartados:&lt;br /&gt;
&lt;br /&gt;
=== FROM ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Indica la imagen base sobre la que se construirá la tuya.&lt;br /&gt;
En este caso, parte de una imagen ligera que ya tiene Python 3.12 instalado.&lt;br /&gt;
&lt;br /&gt;
=== WORKDIR ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta línea define el directorio de trabajo dentro del contenedor.&lt;br /&gt;
A partir de aquí, todos los comandos que aparezcan después (COPY, RUN, CMD, etc.) se ejecutarán dentro de /app.&lt;br /&gt;
&lt;br /&gt;
'''Ojo: /app no es la carpeta app/ de tu proyecto local.'''. Son cosas distintas.&lt;br /&gt;
&lt;br /&gt;
En tu máquina puede existir algo como:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
uvlhub&lt;br /&gt;
   app&lt;br /&gt;
   core&lt;br /&gt;
   requirements.txt &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Pero dentro del contenedor la estructura será distinta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
/ &lt;br /&gt;
  app/ &lt;br /&gt;
    app/ ← aquí dentro se habrá copiado tu carpeta local &amp;quot;app&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Cuando más adelante uses:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Docker copiará todos los archivos de tu proyecto dentro de la carpeta /app del contenedor.&lt;br /&gt;
Por eso el código quedará en /app/app/.&lt;br /&gt;
&lt;br /&gt;
=== COPY ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Copia todos los archivos del proyecto (del host) dentro del contenedor, en /app.&lt;br /&gt;
El primer punto es el origen; el segundo, el destino.&lt;br /&gt;
&lt;br /&gt;
=== RUN ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ejecuta comandos durante la construcción de la imagen.&lt;br /&gt;
Aquí instalamos las dependencias necesarias desde requirements.txt.&lt;br /&gt;
El resultado de este paso quedará guardado en la imagen.&lt;br /&gt;
&lt;br /&gt;
=== EXPOSE ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Documenta el puerto que la aplicación usa dentro del contenedor.&lt;br /&gt;
Esto no lo abre al exterior; simplemente indica que la app escucha en el 5000.&lt;br /&gt;
&lt;br /&gt;
=== CMD ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Define el comando que se ejecutará cuando el contenedor se inicie.&lt;br /&gt;
En este caso, ejecutará la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
=== RUN vs CMD ===&lt;br /&gt;
&lt;br /&gt;
Estas dos instrucciones parecen similares, pero hacen cosas muy distintas.&lt;br /&gt;
&lt;br /&gt;
==== RUN ====&lt;br /&gt;
Se ejecuta durante la construcción de la imagen (en el momento del docker build).&lt;br /&gt;
Cada RUN crea una nueva capa dentro de la imagen.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando instala las dependencias una sola vez, cuando se construye la imagen.&lt;br /&gt;
El resultado (las librerías instaladas) queda grabado dentro de la imagen final.&lt;br /&gt;
&lt;br /&gt;
Si después lanzas un contenedor nuevo a partir de esa imagen, las dependencias ya estarán instaladas y no se volverán a ejecutar.&lt;br /&gt;
&lt;br /&gt;
==== CMD ====&lt;br /&gt;
Se ejecuta cuando se inicia el contenedor (en el docker run).&lt;br /&gt;
Define el proceso principal que se ejecutará dentro del contenedor mientras esté activo.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando no se ejecuta durante la construcción, sino cada vez que arrancas el contenedor.&lt;br /&gt;
Si detienes el contenedor, este proceso también se detiene.&lt;br /&gt;
&lt;br /&gt;
==== Diferencia resumida ====&lt;br /&gt;
&lt;br /&gt;
RUN → se ejecuta al construir la imagen (fase de docker build).&lt;br /&gt;
&lt;br /&gt;
CMD → se ejecuta al iniciar el contenedor (fase de docker run).&lt;br /&gt;
&lt;br /&gt;
Solo puede haber un CMD por Dockerfile.&lt;br /&gt;
Si hay más de uno, solo se usará el último.&lt;br /&gt;
&lt;br /&gt;
== Dockerfile completo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
COPY . .&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto tiene que ir en un Dockerfile en docker/images&lt;br /&gt;
&lt;br /&gt;
== Construir la imagen ==&lt;br /&gt;
Desde la raíz del proyecto (no dentro de docker/images):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker build -t uvlhub:dev -f docker/images/Dockerfile.dev .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Ejecutar el contenedor ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Qué es lo que observas?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 2: Conectando servicios =&lt;br /&gt;
&lt;br /&gt;
¡Vaya! ¡No funciona! Can't connect to MySQL server on 'db'... ¡Ah, claro, intenta conectarse a la base de datos, que no lo tenemos! Pero claro, en un contenedor no es recomendable tener la app y la base de datos conviviendo...&lt;br /&gt;
&lt;br /&gt;
== Crear contenedor MariaDB ==&lt;br /&gt;
&lt;br /&gt;
Antes que nada, haz Ctrl + C para parar el contenedor web. En este ejercicio aprenderás a lanzar un contenedor de MariaDB 12.0.2 de forma manual, configurando todas las variables necesarias para tu aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
El objetivo es entender la complejidad de hacerlo “a mano”, antes de automatizarlo con docker-compose.&lt;br /&gt;
&lt;br /&gt;
== Descargar la imagen oficial ==&lt;br /&gt;
Primero descarga la versión exacta que usaremos:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker pull mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Crear el contenedor de MariaDB ==&lt;br /&gt;
Lanza un contenedor con todas las variables necesarias para que Flask pueda conectarse correctamente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d --name mariadb_container \&lt;br /&gt;
 -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
 -e FLASK_ENV=development \&lt;br /&gt;
 -e DOMAIN=localhost \&lt;br /&gt;
 -e MARIADB_HOSTNAME=db \&lt;br /&gt;
 -e MARIADB_PORT=3306 \&lt;br /&gt;
 -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
 -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
 -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
 -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
 -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
 -e WORKING_DIR=/app/ \&lt;br /&gt;
 -p 3306:3306 mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Explicación:&lt;br /&gt;
&lt;br /&gt;
* -d: ejecuta el contenedor en segundo plano (modo detached).&lt;br /&gt;
* --name: asigna un nombre identificable al contenedor.&lt;br /&gt;
* -e: define variables de entorno dentro del contenedor.&lt;br /&gt;
* -p 3306:3306: publica el puerto de MariaDB en tu máquina local.&lt;br /&gt;
* mariadb:12.0.2: imagen exacta que vamos a usar.&lt;br /&gt;
&lt;br /&gt;
Aquí ya, de entrada, vemos que esto es más feo que una nevera por detrás. Para empezar, estamos pasando a lo loco un montón de variables que difílmente será replicable en producción, por contener información sensible. Aparte, si metemos nuevas variables, ese comando queda inservible. Necesitaremos una solución más ágil para este problema&lt;br /&gt;
&lt;br /&gt;
== Comprobar que está corriendo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver algo así:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES&lt;br /&gt;
abcd1234efgh mariadb:12.0.2 &amp;quot;docker-entrypoint.s…&amp;quot; Up 5 seconds 0.0.0.0:3306-&amp;gt;3306/tcp mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Conectarse a la base de datos ==&lt;br /&gt;
Abre una consola dentro del contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it mariadb_container bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y dentro, accede al cliente de MariaDB:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mariadb -u root -p&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Contraseña: uvlhubdb_root_password&lt;br /&gt;
&lt;br /&gt;
== Verificar la base de datos ==&lt;br /&gt;
Una vez dentro del cliente MySQL, ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
SHOW DATABASES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver que existen la bases de datos uvlhubdb porque pedimos crearla a la hora de levantar el contenedor. ¿Qué tiene la base de datos dentro? Vamos a verlo...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
USE uvlhubdb;&lt;br /&gt;
SHOW TABLES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Un total de... 0 tablas... ¡Claro! Es que solo hemos creado la base en sí, pero no las tablas. ¿Con un script SQL tal vez? ¡Espera, espera, si tenemos las migraciones! Pero claro, para eso necesito el contenedor de la app levantado. Sal de la consola SQL y del contenedor de MariaDB. Bien, ahora en el host...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta vez con el flag &amp;quot;-d&amp;quot; para que corra en segundo plano. Ahora, accedamos al contenedor y ejecutemos las migraciones. Pero... ¿cómo se llama mi contenedor?&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate que Docker asigna un nombre aleatorio a tu contenedor web porque no definiste ninguno. Una vez identificado el nombre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it &amp;lt;nombre&amp;gt; bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
What? ¿Qué pasa? ¿Otra vez error de conexión con la base? Si tenemos el contenedor funcionando, esto es un jaleo...&lt;br /&gt;
&lt;br /&gt;
== Redes internas ==&lt;br /&gt;
&lt;br /&gt;
Ahora mismo tienes dos contenedores aislados, el de la app y el la base de datos. Por defecto, cada contenedor tiene su propia red interna y no puede ver a los demás por nombre. El hostname '''db''' que usa Flask no existe en su DNS interno.&lt;br /&gt;
&lt;br /&gt;
=== Conectar servicios ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una red interna que compartan ambos contenedores&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker network create uvlhub_network&lt;br /&gt;
docker network ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vemos que ya aparece nuestra red '''uvlhub_network'''. Ahora, conectemos el contenedor de MariaDB a la red:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Claro, si te fijas, ahora hay que añadir, además de toda esa configuración engorrosa, que se añada en la misma red y que además tenga de hostname &amp;quot;db&amp;quot; para que el contenedor web entienda qué es &amp;quot;db&amp;quot;. Tenemos que hacer lo mismo para el contenedor web:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop &amp;lt;nombre&amp;gt;&lt;br /&gt;
docker rm &amp;lt;nombre&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y lo levantamos de nuevo, pero ya en la nueva red. De paso, le ponemos un nombre para identificarlo mejor.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 --name web_app_container --network uvlhub_network -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y ahora ya podemos acceder al contenedor por su nombre y ejecutar las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Mejor?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 3: Persistencia =&lt;br /&gt;
&lt;br /&gt;
Vamos a simular que hemos desplegado justo esto en producción y el servidor se reinicia por un corte de luz. Reiniciemos a mano cada contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker restart web_app_container&lt;br /&gt;
docker restart mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. What? ¿Cómo? ¿Qué ha pasado? ¿Otra vez problema de conexión? Ve dándole a F5... F5... ¡bingo! Ahora sí sale, pero menuda aleatoriedad, ¿no? Claro, es que ahora caes en que una cosa es que el contenedor haya arrancado y otra que esté listo.&lt;br /&gt;
&lt;br /&gt;
Aparte, tenemos otro problema. ¿Qué pasa con la persistencia? Porque si borro el contenedor de MariaDB, todos los datos que estén en su interior también se pierden. Y si estamos en producción, lío asegurado.&lt;br /&gt;
&lt;br /&gt;
== Volúmenes ==&lt;br /&gt;
&lt;br /&gt;
Cada vez que eliminas el contenedor de MariaDB, todas las bases de datos se pierden. Esto ocurre porque los datos se guardan dentro del sistema de archivos del contenedor, y al borrarlo desaparecen con él.&lt;br /&gt;
&lt;br /&gt;
En este ejercicio creas un volumen de Docker para que los datos persistan entre ejecuciones.&lt;br /&gt;
&lt;br /&gt;
=== Crear un volumen ===&lt;br /&gt;
Un volumen es una carpeta gestionada por Docker que vive fuera del ciclo de vida de los contenedores.&lt;br /&gt;
&lt;br /&gt;
Ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume create mariadb_data&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba que existe:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
La salida muestra algo como:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
DRIVER    VOLUME NAME&lt;br /&gt;
local     mariadb_data&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Crear un contenedor de MariaDB con el volumen ===&lt;br /&gt;
Lanza el contenedor y conecta el volumen a la ruta donde MariaDB guarda los datos (/var/lib/mysql):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -v mariadb_data:/var/lib/mysql \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Qué significa &amp;quot;:&amp;quot; en los volúmenes ===&lt;br /&gt;
&lt;br /&gt;
Cuando usas la opción '''-v''' o '''--volume''' en Docker, el signo dos puntos (''':''') separa dos rutas:&lt;br /&gt;
&lt;br /&gt;
'''-v origen:destino'''&lt;br /&gt;
&lt;br /&gt;
origen → es la ruta o el volumen en tu máquina (el host).&lt;br /&gt;
&lt;br /&gt;
destino → es la ruta dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 4: Orquestación de servicios =&lt;br /&gt;
&lt;br /&gt;
Hasta ahora hemos creado y conectado los contenedores manualmente.&lt;br /&gt;
Ahora vas a usar Docker Compose para definir todo en un solo archivo.&lt;br /&gt;
&lt;br /&gt;
== Orquestación básica ==&lt;br /&gt;
&lt;br /&gt;
Crea un archivo llamado docker-compose.yml en la carpeta '''docker''' con este contenido:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    container_name: web_app_container&lt;br /&gt;
    env_file:&lt;br /&gt;
      - ../.env&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;5000&amp;quot;&lt;br /&gt;
    depends_on:&lt;br /&gt;
      - db&lt;br /&gt;
    build:&lt;br /&gt;
      context: ../&lt;br /&gt;
      dockerfile: docker/images/Dockerfile&lt;br /&gt;
    networks:&lt;br /&gt;
      - uvlhub_network&lt;br /&gt;
&lt;br /&gt;
  db:&lt;br /&gt;
    container_name: mariadb_container&lt;br /&gt;
    image: mariadb:12.0.2&lt;br /&gt;
    env_file:&lt;br /&gt;
      - ../.env&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;3306:3306&amp;quot;&lt;br /&gt;
    volumes:&lt;br /&gt;
      - db_data:/var/lib/mysql&lt;br /&gt;
    networks:&lt;br /&gt;
      - uvlhub_network&lt;br /&gt;
&lt;br /&gt;
volumes:&lt;br /&gt;
  db_data:&lt;br /&gt;
&lt;br /&gt;
networks:&lt;br /&gt;
  uvlhub_network:&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate que esta orquestación de servicios me permite definir el nombre de cada servicio, su dependencia, sus puertos, sus volúmenes, redes... todo lo que hacíamos de forma manual y propenso a errores queda ahora más fácil de gestionar. Además, ahora ambos contenedores comparten el mismo archivo .env de variables de entorno.&lt;br /&gt;
&lt;br /&gt;
Bien, ahora vamos a levantar estos contenedores y acceder al contenedor web:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Inmediatamente después, ejecuta las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué ha ocurrido? Depende del tiempo que hayas tardado, puede que haya saltado un error o puede que haya funcionado. Puede que hayas intentado varias veces el comando anterior hasta que ha funcionado. ¿Por qué crees que ocurre esta aparete aleatoriedad?&lt;br /&gt;
&lt;br /&gt;
== Dependencias entre contenedores y entrypoints ==&lt;br /&gt;
&lt;br /&gt;
Fíjate que tenemos un &amp;quot;depends on&amp;quot; en el servicio web. Significa que el servicio &amp;quot;db&amp;quot; debe iniciarse antes que el servicio web. Pero, cuidado, ahí está la trampa, porque &amp;quot;servicio iniciado != servicio preparado&amp;quot;. El servicio de MariaDB tiene que arrancar, configurarse, etc...&lt;br /&gt;
&lt;br /&gt;
Cuando hicimos el comando COPY en nuestro Dockerfile, también copiamos la carpeta '''scripts'''. Uno de los scripts es '''wait-for-db.sh''', que realiza continuamente llamadas a MariaDB hasta que este servicio está listo para recibir peticiones. Ábrelo y estúdialo.&lt;br /&gt;
&lt;br /&gt;
Añade al servicio web de tu docker compose las siguientes líneas:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
entrypoint: [&amp;quot;/app/scripts/wait-for-db.sh&amp;quot;]&lt;br /&gt;
command: [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A su vez, del Dockerfile, hay que eliminar la línea de CMD que arrancaba nuestro servidor Flask.&lt;br /&gt;
&lt;br /&gt;
Luego, reinicializa todos los servicios:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. What? ¿No abre? Vamos a investigarlo, veamos el logs:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker logs web_app_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Nos debe salir un mensaje eterno de &amp;quot;/app/scripts/wait-for-db.sh: line 18: mariadb: command not found&amp;quot;. Esto ocurre porque en la imagen de nuestro servicio web NO TENEMOS el cliente de MariaDB. Debemos indicar entonces que lo instale. Añade justo debajo de FROM lo siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y --no-install-recommends mariadb-client &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Al ser un contenedor basado en Linux, y accediendo como root, es como si actualizáramos todos los paquetes y luego instalásemos el cliente de MariaDB. Intentémoslo otra vez:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. ¡Jopelines! ¿Tampoco? Si haces un &amp;quot;docker ps&amp;quot; verás el problema. El contenedor web no tiene expuesto ningún puerto.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    (...)&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;5000&amp;quot;&lt;br /&gt;
    (...)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;quot;Expose&amp;quot; hace visible el puerto '''entre contenedores''', pero '''NO entre tu contenedor y el host'''. Para eso usamos la directiva &amp;quot;ports&amp;quot;, que se encarga del mapping. En nuestro caso, será:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    (...)&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;5000:5000&amp;quot;&lt;br /&gt;
    (...)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
En ocasiones, si los dos puertos coinciden, se puede dejar uno solo con &amp;quot;5000&amp;quot; pero dejaremos los dos. El de la izquierda siempre es el puerto externo (host), el de la derecha es el puerto interno (contenedor). Así que cambia ese detalle y por última vez:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. ¡Ahora sí!&lt;br /&gt;
&lt;br /&gt;
== Trabajando con CMD, entrypoints y commands ==&lt;br /&gt;
&lt;br /&gt;
Piensa que al arrancar un contenedor Docker, siempre se ejecuta un único comando final.&lt;br /&gt;
Lo que hacen ENTRYPOINT, CMD y command (de Compose) es decidir qué comando será ese y con qué argumentos.&lt;br /&gt;
&lt;br /&gt;
=== CMD → “qué hacer por defecto” ===&lt;br /&gt;
&lt;br /&gt;
* Está en el Dockerfile.&lt;br /&gt;
* Es el comando que Docker ejecuta si no se indica otra cosa. En nuestro caso:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== ENTRYPOINT → “qué se ejecuta siempre” ===&lt;br /&gt;
&lt;br /&gt;
* También va en el Dockerfile, o se puede sobreescribir en el docker-compose.yml.&lt;br /&gt;
* Define el programa o script principal que siempre se ejecuta.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
ENTRYPOINT [&amp;quot;/app/scripts/wait-for-db.sh&amp;quot;]&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Normalmente, el entrypoint se combina con CMD en modo &amp;quot;eh, ejecuta esto (entrypoint) y luego este comando final (cmd)&lt;br /&gt;
&lt;br /&gt;
Los entrypoints son muy útiles, sobre todo en los docker-compose's, porque puedo redefinir cómo quiero que inicie un contenedor en concreto '''SIN TENER QUE HACER UN BUILD DE NUEVO DE LA IMAGEN DE DOCKER'''. A diferencia de CMD, que viene predefinido, un entrypoint en mi docker-compose me da más versatilidad&lt;br /&gt;
&lt;br /&gt;
=== command (de docker-compose.yml) → “sobrescribe el CMD” ===&lt;br /&gt;
&lt;br /&gt;
En docker-compose.yml, el campo command: reemplaza al CMD del Dockerfile,pero no al ENTRYPOINT.&lt;br /&gt;
&lt;br /&gt;
== Combinación de elementos ==&lt;br /&gt;
&lt;br /&gt;
Es importante entender cómo Docker trabaja con la orquestación y con los contenedores:&lt;br /&gt;
&lt;br /&gt;
* Lee el YAML como un diccionario&lt;br /&gt;
* Ejecuta todo lo que haya en un Dockerfile EXCEPTUANDO directivas ENTRYPOINT y CMD&lt;br /&gt;
* Monta el volumen o los volúmenes de ese contenedor (si procede)&lt;br /&gt;
&lt;br /&gt;
Y ahora viene el detalle:&lt;br /&gt;
&lt;br /&gt;
* Si estamos levantando un contenedor a través de docker-compose, va a ejecutar los entrypoints y commands definidos en ese docker-compose.&lt;br /&gt;
* Un entrypoint definido en un docker-compose va a sustituir a un entrypoint definido en el Dockerfile&lt;br /&gt;
* Un command definido en un docker-compose va a sustituir a un CMD definido en el Dockerfile.&lt;br /&gt;
&lt;br /&gt;
Como consejo:&lt;br /&gt;
&lt;br /&gt;
* Definimos un entrypoint en el docker-compose. Esto es así porque está fuera de la imagen de Docker y nos permite redefinir el inicio del servicio sin modificar la imagen&lt;br /&gt;
* Eventualmente, definimos un CMD donde indicamos el comando final de ese contenedor al iniciar, que suele ser el arranque de Flask Server o de Gunicorn, dependiendo de si estamos en desarrollo o producción.&lt;br /&gt;
&lt;br /&gt;
No obstante, como nuestros entrypoints son todos scripts en bash, podemos definir en el mismo el arranque final (comando final) y nos olvidamos de CMD.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 5: Volumen de trabajo =&lt;br /&gt;
&lt;br /&gt;
== Problema de desarrollo ==&lt;br /&gt;
&lt;br /&gt;
Vale, ya parece que le hemos pillado el tranquillo a Docker y a Docker Compose. Ahora vamos a hacer algo como desarrolladores. Ve al archivo &amp;quot;app/templates/base_templates.html&amp;quot; y cambia la línea 22:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;title&amp;gt;{{ FLASK_APP_NAME }} - Repository of feature models in UVL &amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
por:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;title&amp;gt;Hello world! &amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. Fíjate en el nombre de la pestaña del navegador, ¿ha cambiado? ¿No? Tsss, esto de los contenedores... bueno, va, vamos a reiniciarlo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker restart web_app_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. ¿Tampoco? Anda que vaya idea la de usar Docker... Claro, es a la hora de construir la imagen, hicimos &amp;quot;COPY . .&amp;quot;, pero no hemos reconstruido nada, así que...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml build --no-cache web&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --force-recreate web&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Importante señalar que le pedimos que ignore la caché para que fuerce a copiar de nuevo todo el contenido (incluyendo el cambio de línea) al interior del contenedor.&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. ¡Ahora sí! Pero... ¿tengo que reconstruir TODO el contenedor en cada cambio línea? ¿Y eso es trabajar de forma ágil?&lt;br /&gt;
&lt;br /&gt;
== Crear volumen de trabajo (bind mount) ==&lt;br /&gt;
&lt;br /&gt;
Evidentemente, eso no tiene sentido, tenemos que usar un volumen de trabajo que nos ayude a sincronizar el código de nuestro host con el código interno del contenedor. Luego en nuestro docker-compose:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  web:&lt;br /&gt;
    (....)&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;5000:5000&amp;quot;&lt;br /&gt;
    volumes:&lt;br /&gt;
      - ../:/app&lt;br /&gt;
    (....)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
La línea&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
- ../:/app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Monta tu carpeta local del proyecto (../) dentro del contenedor, (/app). ¿Y por qué /app? Porque fue el espacio de trabajo que definimos en el Dockerfile. Como ves, la congruencia entre los Dockerfile y el docker-compose es importante.&lt;br /&gt;
&lt;br /&gt;
Recargemos de nuevo nustra configuración:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ahora ya podemos trabajar con el código en host sin tener que reconstruir la imagen a cada cambio de línea.&lt;br /&gt;
&lt;br /&gt;
== Cuidado con COPY y los bind mounts ==&lt;br /&gt;
&lt;br /&gt;
Un '''bind mount''' (o “montaje de enlace”) es un tipo de volumen en el que Docker conecta directamente una carpeta de tu máquina (el host) con una carpeta dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
Cuando usas un bind mount (por ejemplo ../:/app), Docker monta tu carpeta local sobre la ruta /app del contenedor.&lt;br /&gt;
Eso significa que todo lo que estaba en /app dentro de la imagen desaparece al arrancar el contenedor, porque queda “oculto” por el volumen.&lt;br /&gt;
&lt;br /&gt;
Ahora bien, durante la construcción de la imagen usamos la directiva COPY. ¿Por qué? Porque necesitamos el archivo requirements.txt para instalar las dependencias de Python del proyecto. Pero... ¿realmente necesitamos copiar TODO? No, claro que no, podemos optar por copiar exclusivamente aquellos archivos que nos hagan falta durante '''LA CONSTRUCCIÓN''' de la imagen. El bind mount luego lo machacará.&lt;br /&gt;
&lt;br /&gt;
'''Muy importante:''' los bind mounts, al igual que cualquier otro volumen, están disponibles '''DESPUÉS''' de la construcción de la imagen. Es un error común creer que porque definamos un bind mount en un docker-compose, esos archivos aparecen ya para el contenedor. '''Esto es una fuente inagotable de problemas en la práctica'''.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 6: Distintas configuraciones de despliegue =&lt;br /&gt;
&lt;br /&gt;
Volvamos al punto de partida:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
rm -r docker&lt;br /&gt;
mv docker.bk docker&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta práctica ha sido engorrosa a propósito para quitar esa &amp;quot;magia&amp;quot; que existe cuando lanzas un comando simple de orquestación y todo se levanta y funciona perfectamente. Esa situación está muy bien para desarrollar, pero no para aprender.&lt;br /&gt;
&lt;br /&gt;
Es importante que analices que existen distintos escenarios:&lt;br /&gt;
&lt;br /&gt;
* Hay un docker-compose para desarrollo.&lt;br /&gt;
* Hay 3 docker-compose para producción.&lt;br /&gt;
* Hay 6 Dockerfiles distintos&lt;br /&gt;
* Hay 3 entrypoints distintos&lt;br /&gt;
* Hay una carpeta llamada letsencrypt y otra llamada nginx. Dentro de nginx hay dos archivos de configuración dependiendo de si estamos en dev o en prod.&lt;br /&gt;
&lt;br /&gt;
== Autoevaluación ==&lt;br /&gt;
&lt;br /&gt;
Estudia bien cada archivo. Tómate tu tiempo e intenta responder a estas preguntas:&lt;br /&gt;
&lt;br /&gt;
* ¿Por qué en el docker-compose de dev se usa &amp;quot;ports&amp;quot; para enlazar con el host? (pista: nginx)&lt;br /&gt;
* ¿Qué es nginx y por qué es útil aquí?&lt;br /&gt;
* ¿Cuántos bind mounts usa el contenedor web? ¿Para qué sirve el bind mount del propio Docker?&lt;br /&gt;
* ¿Qué diferencia hay entre el entrypoint de desarrollo y el entrypoint de producción?&lt;br /&gt;
* ¿Por qué el servicio web en producción está montando todos esos bind mounts por separado? ¿No sería más sencillo hacer un volumen de todo el código?&lt;br /&gt;
&lt;br /&gt;
== Comentarios finales ==&lt;br /&gt;
&lt;br /&gt;
=== Cuanto más se parezca el entorno de desarrollo al de producción, menos errores habrá ===&lt;br /&gt;
&lt;br /&gt;
En desarrollo es tentador usar configuraciones cómodas: servidor de Flask con --reload, bind mounts, base de datos vacía, logs en consola, etc.&lt;br /&gt;
Pero cuanto más difiere ese entorno de producción, más probable es que algo falle al desplegar.&lt;br /&gt;
&lt;br /&gt;
'''El código no falla por cambiar de servidor, sino porque los entornos se comportan distinto.'''&lt;br /&gt;
&lt;br /&gt;
Ejemplos clásicos:&lt;br /&gt;
&lt;br /&gt;
* En desarrollo usas SQLite, en producción MariaDB → aparecen errores de compatibilidad SQL.&lt;br /&gt;
* En desarrollo Flask sirve los archivos estáticos, en producción lo hace Nginx → las rutas cambian.&lt;br /&gt;
* En desarrollo ejecutas '''flask run''', en producción '''gunicorn''' → los hilos, workers o el reloader funcionan distinto.&lt;br /&gt;
&lt;br /&gt;
Por eso, el objetivo ideal es que tu entorno de desarrollo (dev), de preproducción (staging) y de producción (prod) sean idénticos salvo por las credenciales.&lt;br /&gt;
&lt;br /&gt;
Docker y docker-compose te permiten lograr eso: puedes usar la misma imagen base, cambiando solo el docker-compose.dev.yml (volúmenes, puertos, etc.).&lt;br /&gt;
&lt;br /&gt;
=== Diferencia entre el servidor de Flask y Gunicorn ===&lt;br /&gt;
&lt;br /&gt;
Flask trae incorporado un servidor de desarrollo.&lt;br /&gt;
Es muy útil para depurar porque:&lt;br /&gt;
&lt;br /&gt;
* recarga automáticamente el código (--reload),&lt;br /&gt;
* muestra errores detallados en el navegador,&lt;br /&gt;
* solo ejecuta un proceso.&lt;br /&gt;
&lt;br /&gt;
Sin embargo, no está pensado para producción:&lt;br /&gt;
&lt;br /&gt;
* no maneja múltiples conexiones concurrentes,&lt;br /&gt;
* no tiene control de procesos ni balanceo de carga,&lt;br /&gt;
* no implementa estándares de rendimiento o seguridad (como WSGI robusto).&lt;br /&gt;
&lt;br /&gt;
Ahí entra Gunicorn (Green Unicorn), que sí es un servidor WSGI  de producción (Web Server Gateway Interface, estándar de comunicación entre aplicaciones web Python y servidores web):&lt;br /&gt;
&lt;br /&gt;
* puede lanzar varios workers en paralelo,&lt;br /&gt;
* maneja múltiples peticiones concurrentes,&lt;br /&gt;
* se integra fácilmente con Nginx para servir contenido estático o HTTPS.&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10160</id>
		<title>Tutorial-docker</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10160"/>
				<updated>2025-10-12T19:16:00Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Ejercicio 1: Empezamos desde cero =&lt;br /&gt;
&lt;br /&gt;
El objetivo es eliminar toda la configuración previa y crear desde cero la estructura de carpetas que contendrá nuestros futuros archivos Docker.&lt;br /&gt;
&lt;br /&gt;
== Eliminar la configuración existente ==&lt;br /&gt;
Antes de comenzar, asegúrate de borrar cualquier carpeta Docker existente en el proyecto. En este caso, le haremos una copia de seguridad para poder volver a ella.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mv docker docker.bk&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
También debes asegurarte que trabajas con las variables de entorno adecuadas:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
cp .env.docker.example .env&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
⚠️ Este paso es importante: partimos completamente desde cero para entender cada parte del sistema.&lt;br /&gt;
&lt;br /&gt;
== Crear un Dockerfile mínimo ==&lt;br /&gt;
&lt;br /&gt;
Ahora crea nuevamente la carpeta base donde almacenaremos los archivos relacionados con Docker:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt; mkdir -p docker/images &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vamos a explicar los distintos apartados:&lt;br /&gt;
&lt;br /&gt;
=== FROM ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Indica la imagen base sobre la que se construirá la tuya.&lt;br /&gt;
En este caso, parte de una imagen ligera que ya tiene Python 3.12 instalado.&lt;br /&gt;
&lt;br /&gt;
=== WORKDIR ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta línea define el directorio de trabajo dentro del contenedor.&lt;br /&gt;
A partir de aquí, todos los comandos que aparezcan después (COPY, RUN, CMD, etc.) se ejecutarán dentro de /app.&lt;br /&gt;
&lt;br /&gt;
'''Ojo: /app no es la carpeta app/ de tu proyecto local.'''. Son cosas distintas.&lt;br /&gt;
&lt;br /&gt;
En tu máquina puede existir algo como:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
uvlhub&lt;br /&gt;
   app&lt;br /&gt;
   core&lt;br /&gt;
   requirements.txt &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Pero dentro del contenedor la estructura será distinta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
/ &lt;br /&gt;
  app/ &lt;br /&gt;
    app/ ← aquí dentro se habrá copiado tu carpeta local &amp;quot;app&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Cuando más adelante uses:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Docker copiará todos los archivos de tu proyecto dentro de la carpeta /app del contenedor.&lt;br /&gt;
Por eso el código quedará en /app/app/.&lt;br /&gt;
&lt;br /&gt;
=== COPY ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Copia todos los archivos del proyecto (del host) dentro del contenedor, en /app.&lt;br /&gt;
El primer punto es el origen; el segundo, el destino.&lt;br /&gt;
&lt;br /&gt;
=== RUN ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ejecuta comandos durante la construcción de la imagen.&lt;br /&gt;
Aquí instalamos las dependencias necesarias desde requirements.txt.&lt;br /&gt;
El resultado de este paso quedará guardado en la imagen.&lt;br /&gt;
&lt;br /&gt;
=== EXPOSE ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Documenta el puerto que la aplicación usa dentro del contenedor.&lt;br /&gt;
Esto no lo abre al exterior; simplemente indica que la app escucha en el 5000.&lt;br /&gt;
&lt;br /&gt;
=== CMD ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Define el comando que se ejecutará cuando el contenedor se inicie.&lt;br /&gt;
En este caso, ejecutará la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
=== RUN vs CMD ===&lt;br /&gt;
&lt;br /&gt;
Estas dos instrucciones parecen similares, pero hacen cosas muy distintas.&lt;br /&gt;
&lt;br /&gt;
==== RUN ====&lt;br /&gt;
Se ejecuta durante la construcción de la imagen (en el momento del docker build).&lt;br /&gt;
Cada RUN crea una nueva capa dentro de la imagen.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando instala las dependencias una sola vez, cuando se construye la imagen.&lt;br /&gt;
El resultado (las librerías instaladas) queda grabado dentro de la imagen final.&lt;br /&gt;
&lt;br /&gt;
Si después lanzas un contenedor nuevo a partir de esa imagen, las dependencias ya estarán instaladas y no se volverán a ejecutar.&lt;br /&gt;
&lt;br /&gt;
==== CMD ====&lt;br /&gt;
Se ejecuta cuando se inicia el contenedor (en el docker run).&lt;br /&gt;
Define el proceso principal que se ejecutará dentro del contenedor mientras esté activo.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando no se ejecuta durante la construcción, sino cada vez que arrancas el contenedor.&lt;br /&gt;
Si detienes el contenedor, este proceso también se detiene.&lt;br /&gt;
&lt;br /&gt;
==== Diferencia resumida ====&lt;br /&gt;
&lt;br /&gt;
RUN → se ejecuta al construir la imagen (fase de docker build).&lt;br /&gt;
&lt;br /&gt;
CMD → se ejecuta al iniciar el contenedor (fase de docker run).&lt;br /&gt;
&lt;br /&gt;
Solo puede haber un CMD por Dockerfile.&lt;br /&gt;
Si hay más de uno, solo se usará el último.&lt;br /&gt;
&lt;br /&gt;
== Dockerfile completo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
COPY . .&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto tiene que ir en un Dockerfile en docker/images&lt;br /&gt;
&lt;br /&gt;
== Construir la imagen ==&lt;br /&gt;
Desde la raíz del proyecto (no dentro de docker/images):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker build -t uvlhub:dev -f docker/images/Dockerfile.dev .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Ejecutar el contenedor ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Qué es lo que observas?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 2: Conectando servicios =&lt;br /&gt;
&lt;br /&gt;
¡Vaya! ¡No funciona! Can't connect to MySQL server on 'db'... ¡Ah, claro, intenta conectarse a la base de datos, que no lo tenemos! Pero claro, en un contenedor no es recomendable tener la app y la base de datos conviviendo...&lt;br /&gt;
&lt;br /&gt;
== Crear contenedor MariaDB ==&lt;br /&gt;
&lt;br /&gt;
Antes que nada, haz Ctrl + C para parar el contenedor web. En este ejercicio aprenderás a lanzar un contenedor de MariaDB 12.0.2 de forma manual, configurando todas las variables necesarias para tu aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
El objetivo es entender la complejidad de hacerlo “a mano”, antes de automatizarlo con docker-compose.&lt;br /&gt;
&lt;br /&gt;
== Descargar la imagen oficial ==&lt;br /&gt;
Primero descarga la versión exacta que usaremos:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker pull mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Crear el contenedor de MariaDB ==&lt;br /&gt;
Lanza un contenedor con todas las variables necesarias para que Flask pueda conectarse correctamente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d --name mariadb_container \&lt;br /&gt;
 -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
 -e FLASK_ENV=development \&lt;br /&gt;
 -e DOMAIN=localhost \&lt;br /&gt;
 -e MARIADB_HOSTNAME=db \&lt;br /&gt;
 -e MARIADB_PORT=3306 \&lt;br /&gt;
 -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
 -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
 -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
 -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
 -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
 -e WORKING_DIR=/app/ \&lt;br /&gt;
 -p 3306:3306 mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Explicación:&lt;br /&gt;
&lt;br /&gt;
* -d: ejecuta el contenedor en segundo plano (modo detached).&lt;br /&gt;
* --name: asigna un nombre identificable al contenedor.&lt;br /&gt;
* -e: define variables de entorno dentro del contenedor.&lt;br /&gt;
* -p 3306:3306: publica el puerto de MariaDB en tu máquina local.&lt;br /&gt;
* mariadb:12.0.2: imagen exacta que vamos a usar.&lt;br /&gt;
&lt;br /&gt;
Aquí ya, de entrada, vemos que esto es más feo que una nevera por detrás. Para empezar, estamos pasando a lo loco un montón de variables que difílmente será replicable en producción, por contener información sensible. Aparte, si metemos nuevas variables, ese comando queda inservible. Necesitaremos una solución más ágil para este problema&lt;br /&gt;
&lt;br /&gt;
== Comprobar que está corriendo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver algo así:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES&lt;br /&gt;
abcd1234efgh mariadb:12.0.2 &amp;quot;docker-entrypoint.s…&amp;quot; Up 5 seconds 0.0.0.0:3306-&amp;gt;3306/tcp mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Conectarse a la base de datos ==&lt;br /&gt;
Abre una consola dentro del contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it mariadb_container bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y dentro, accede al cliente de MariaDB:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mariadb -u root -p&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Contraseña: uvlhubdb_root_password&lt;br /&gt;
&lt;br /&gt;
== Verificar la base de datos ==&lt;br /&gt;
Una vez dentro del cliente MySQL, ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
SHOW DATABASES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver que existen la bases de datos uvlhubdb porque pedimos crearla a la hora de levantar el contenedor. ¿Qué tiene la base de datos dentro? Vamos a verlo...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
USE uvlhubdb;&lt;br /&gt;
SHOW TABLES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Un total de... 0 tablas... ¡Claro! Es que solo hemos creado la base en sí, pero no las tablas. ¿Con un script SQL tal vez? ¡Espera, espera, si tenemos las migraciones! Pero claro, para eso necesito el contenedor de la app levantado. Sal de la consola SQL y del contenedor de MariaDB. Bien, ahora en el host...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta vez con el flag &amp;quot;-d&amp;quot; para que corra en segundo plano. Ahora, accedamos al contenedor y ejecutemos las migraciones. Pero... ¿cómo se llama mi contenedor?&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate que Docker asigna un nombre aleatorio a tu contenedor web porque no definiste ninguno. Una vez identificado el nombre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it &amp;lt;nombre&amp;gt; bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
What? ¿Qué pasa? ¿Otra vez error de conexión con la base? Si tenemos el contenedor funcionando, esto es un jaleo...&lt;br /&gt;
&lt;br /&gt;
== Redes internas ==&lt;br /&gt;
&lt;br /&gt;
Ahora mismo tienes dos contenedores aislados, el de la app y el la base de datos. Por defecto, cada contenedor tiene su propia red interna y no puede ver a los demás por nombre. El hostname '''db''' que usa Flask no existe en su DNS interno.&lt;br /&gt;
&lt;br /&gt;
=== Conectar servicios ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una red interna que compartan ambos contenedores&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker network create uvlhub_network&lt;br /&gt;
docker network ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vemos que ya aparece nuestra red '''uvlhub_network'''. Ahora, conectemos el contenedor de MariaDB a la red:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Claro, si te fijas, ahora hay que añadir, además de toda esa configuración engorrosa, que se añada en la misma red y que además tenga de hostname &amp;quot;db&amp;quot; para que el contenedor web entienda qué es &amp;quot;db&amp;quot;. Tenemos que hacer lo mismo para el contenedor web:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop &amp;lt;nombre&amp;gt;&lt;br /&gt;
docker rm &amp;lt;nombre&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y lo levantamos de nuevo, pero ya en la nueva red. De paso, le ponemos un nombre para identificarlo mejor.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 --name web_app_container --network uvlhub_network -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y ahora ya podemos acceder al contenedor por su nombre y ejecutar las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Mejor?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 3: Persistencia =&lt;br /&gt;
&lt;br /&gt;
Vamos a simular que hemos desplegado justo esto en producción y el servidor se reinicia por un corte de luz. Reiniciemos a mano cada contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker restart web_app_container&lt;br /&gt;
docker restart mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. What? ¿Cómo? ¿Qué ha pasado? ¿Otra vez problema de conexión? Ve dándole a F5... F5... ¡bingo! Ahora sí sale, pero menuda aleatoriedad, ¿no? Claro, es que ahora caes en que una cosa es que el contenedor haya arrancado y otra que esté listo.&lt;br /&gt;
&lt;br /&gt;
Aparte, tenemos otro problema. ¿Qué pasa con la persistencia? Porque si borro el contenedor de MariaDB, todos los datos que estén en su interior también se pierden. Y si estamos en producción, lío asegurado.&lt;br /&gt;
&lt;br /&gt;
== Volúmenes ==&lt;br /&gt;
&lt;br /&gt;
Cada vez que eliminas el contenedor de MariaDB, todas las bases de datos se pierden. Esto ocurre porque los datos se guardan dentro del sistema de archivos del contenedor, y al borrarlo desaparecen con él.&lt;br /&gt;
&lt;br /&gt;
En este ejercicio creas un volumen de Docker para que los datos persistan entre ejecuciones.&lt;br /&gt;
&lt;br /&gt;
=== Crear un volumen ===&lt;br /&gt;
Un volumen es una carpeta gestionada por Docker que vive fuera del ciclo de vida de los contenedores.&lt;br /&gt;
&lt;br /&gt;
Ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume create mariadb_data&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba que existe:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
La salida muestra algo como:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
DRIVER    VOLUME NAME&lt;br /&gt;
local     mariadb_data&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Crear un contenedor de MariaDB con el volumen ===&lt;br /&gt;
Lanza el contenedor y conecta el volumen a la ruta donde MariaDB guarda los datos (/var/lib/mysql):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -v mariadb_data:/var/lib/mysql \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Qué significa &amp;quot;:&amp;quot; en los volúmenes ===&lt;br /&gt;
&lt;br /&gt;
Cuando usas la opción '''-v''' o '''--volume''' en Docker, el signo dos puntos (''':''') separa dos rutas:&lt;br /&gt;
&lt;br /&gt;
'''-v origen:destino'''&lt;br /&gt;
&lt;br /&gt;
origen → es la ruta o el volumen en tu máquina (el host).&lt;br /&gt;
&lt;br /&gt;
destino → es la ruta dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 4: Orquestación de servicios =&lt;br /&gt;
&lt;br /&gt;
Hasta ahora hemos creado y conectado los contenedores manualmente.&lt;br /&gt;
Ahora vas a usar Docker Compose para definir todo en un solo archivo.&lt;br /&gt;
&lt;br /&gt;
== Orquestación básica ==&lt;br /&gt;
&lt;br /&gt;
Crea un archivo llamado docker-compose.yml en la carpeta '''docker''' con este contenido:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    container_name: web_app_container&lt;br /&gt;
    env_file:&lt;br /&gt;
      - ../.env&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;5000&amp;quot;&lt;br /&gt;
    depends_on:&lt;br /&gt;
      - db&lt;br /&gt;
    build:&lt;br /&gt;
      context: ../&lt;br /&gt;
      dockerfile: docker/images/Dockerfile&lt;br /&gt;
    networks:&lt;br /&gt;
      - uvlhub_network&lt;br /&gt;
&lt;br /&gt;
  db:&lt;br /&gt;
    container_name: mariadb_container&lt;br /&gt;
    image: mariadb:12.0.2&lt;br /&gt;
    env_file:&lt;br /&gt;
      - ../.env&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;3306:3306&amp;quot;&lt;br /&gt;
    volumes:&lt;br /&gt;
      - db_data:/var/lib/mysql&lt;br /&gt;
    networks:&lt;br /&gt;
      - uvlhub_network&lt;br /&gt;
&lt;br /&gt;
volumes:&lt;br /&gt;
  db_data:&lt;br /&gt;
&lt;br /&gt;
networks:&lt;br /&gt;
  uvlhub_network:&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate que esta orquestación de servicios me permite definir el nombre de cada servicio, su dependencia, sus puertos, sus volúmenes, redes... todo lo que hacíamos de forma manual y propenso a errores queda ahora más fácil de gestionar. Además, ahora ambos contenedores comparten el mismo archivo .env de variables de entorno.&lt;br /&gt;
&lt;br /&gt;
Bien, ahora vamos a levantar estos contenedores y acceder al contenedor web:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Inmediatamente después, ejecuta las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué ha ocurrido? Depende del tiempo que hayas tardado, puede que haya saltado un error o puede que haya funcionado. Puede que hayas intentado varias veces el comando anterior hasta que ha funcionado. ¿Por qué crees que ocurre esta aparete aleatoriedad?&lt;br /&gt;
&lt;br /&gt;
== Dependencias entre contenedores y entrypoints ==&lt;br /&gt;
&lt;br /&gt;
Fíjate que tenemos un &amp;quot;depends on&amp;quot; en el servicio web. Significa que el servicio &amp;quot;db&amp;quot; debe iniciarse antes que el servicio web. Pero, cuidado, ahí está la trampa, porque &amp;quot;servicio iniciado != servicio preparado&amp;quot;. El servicio de MariaDB tiene que arrancar, configurarse, etc...&lt;br /&gt;
&lt;br /&gt;
Cuando hicimos el comando COPY en nuestro Dockerfile, también copiamos la carpeta '''scripts'''. Uno de los scripts es '''wait-for-db.sh''', que realiza continuamente llamadas a MariaDB hasta que este servicio está listo para recibir peticiones. Ábrelo y estúdialo.&lt;br /&gt;
&lt;br /&gt;
Añade al servicio web de tu docker compose las siguientes líneas:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
entrypoint: [&amp;quot;/app/scripts/wait-for-db.sh&amp;quot;]&lt;br /&gt;
command: [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A su vez, del Dockerfile, hay que eliminar la línea de CMD que arrancaba nuestro servidor Flask.&lt;br /&gt;
&lt;br /&gt;
Luego, reinicializa todos los servicios:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. What? ¿No abre? Vamos a investigarlo, veamos el logs:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker logs web_app_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Nos debe salir un mensaje eterno de &amp;quot;/app/scripts/wait-for-db.sh: line 18: mariadb: command not found&amp;quot;. Esto ocurre porque en la imagen de nuestro servicio web NO TENEMOS el cliente de MariaDB. Debemos indicar entonces que lo instale. Añade justo debajo de FROM lo siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y --no-install-recommends mariadb-client &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Al ser un contenedor basado en Linux, y accediendo como root, es como si actualizáramos todos los paquetes y luego instalásemos el cliente de MariaDB. Intentémoslo otra vez:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. ¡Jopelines! ¿Tampoco? Si haces un &amp;quot;docker ps&amp;quot; verás el problema. El contenedor web no tiene expuesto ningún puerto.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    (...)&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;5000&amp;quot;&lt;br /&gt;
    (...)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;quot;Expose&amp;quot; hace visible el puerto '''entre contenedores''', pero '''NO entre tu contenedor y el host'''. Para eso usamos la directiva &amp;quot;ports&amp;quot;, que se encarga del mapping. En nuestro caso, será:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    (...)&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;5000:5000&amp;quot;&lt;br /&gt;
    (...)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
En ocasiones, si los dos puertos coinciden, se puede dejar uno solo con &amp;quot;5000&amp;quot; pero dejaremos los dos. El de la izquierda siempre es el puerto externo (host), el de la derecha es el puerto interno (contenedor). Así que cambia ese detalle y por última vez:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. ¡Ahora sí!&lt;br /&gt;
&lt;br /&gt;
== Trabajando con CMD, entrypoints y commands ==&lt;br /&gt;
&lt;br /&gt;
Piensa que al arrancar un contenedor Docker, siempre se ejecuta un único comando final.&lt;br /&gt;
Lo que hacen ENTRYPOINT, CMD y command (de Compose) es decidir qué comando será ese y con qué argumentos.&lt;br /&gt;
&lt;br /&gt;
=== CMD → “qué hacer por defecto” ===&lt;br /&gt;
&lt;br /&gt;
* Está en el Dockerfile.&lt;br /&gt;
* Es el comando que Docker ejecuta si no se indica otra cosa. En nuestro caso:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== ENTRYPOINT → “qué se ejecuta siempre” ===&lt;br /&gt;
&lt;br /&gt;
* También va en el Dockerfile, o se puede sobreescribir en el docker-compose.yml.&lt;br /&gt;
* Define el programa o script principal que siempre se ejecuta.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
ENTRYPOINT [&amp;quot;/app/scripts/wait-for-db.sh&amp;quot;]&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Normalmente, el entrypoint se combina con CMD en modo &amp;quot;eh, ejecuta esto (entrypoint) y luego este comando final (cmd)&lt;br /&gt;
&lt;br /&gt;
Los entrypoints son muy útiles, sobre todo en los docker-compose's, porque puedo redefinir cómo quiero que inicie un contenedor en concreto '''SIN TENER QUE HACER UN BUILD DE NUEVO DE LA IMAGEN DE DOCKER'''. A diferencia de CMD, que viene predefinido, un entrypoint en mi docker-compose me da más versatilidad&lt;br /&gt;
&lt;br /&gt;
=== command (de docker-compose.yml) → “sobrescribe el CMD” ===&lt;br /&gt;
&lt;br /&gt;
En docker-compose.yml, el campo command: reemplaza al CMD del Dockerfile,pero no al ENTRYPOINT.&lt;br /&gt;
&lt;br /&gt;
== Combinación de elementos ==&lt;br /&gt;
&lt;br /&gt;
Es importante entender cómo Docker trabaja con la orquestación y con los contenedores:&lt;br /&gt;
&lt;br /&gt;
* Lee el YAML como un diccionario&lt;br /&gt;
* Ejecuta todo lo que haya en un Dockerfile EXCEPTUANDO directivas ENTRYPOINT y CMD&lt;br /&gt;
* Monta el volumen o los volúmenes de ese contenedor (si procede)&lt;br /&gt;
&lt;br /&gt;
Y ahora viene el detalle:&lt;br /&gt;
&lt;br /&gt;
* Si estamos levantando un contenedor a través de docker-compose, va a ejecutar los entrypoints y commands definidos en ese docker-compose.&lt;br /&gt;
* Un entrypoint definido en un docker-compose va a sustituir a un entrypoint definido en el Dockerfile&lt;br /&gt;
* Un command definido en un docker-compose va a sustituir a un CMD definido en el Dockerfile.&lt;br /&gt;
&lt;br /&gt;
Como consejo:&lt;br /&gt;
&lt;br /&gt;
* Definimos un entrypoint en el docker-compose. Esto es así porque está fuera de la imagen de Docker y nos permite redefinir el inicio del servicio sin modificar la imagen&lt;br /&gt;
* Eventualmente, definimos un CMD donde indicamos el comando final de ese contenedor al iniciar, que suele ser el arranque de Flask Server o de Gunicorn, dependiendo de si estamos en desarrollo o producción.&lt;br /&gt;
&lt;br /&gt;
No obstante, como nuestros entrypoints son todos scripts en bash, podemos definir en el mismo el arranque final (comando final) y nos olvidamos de CMD.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 5: Volumen de trabajo =&lt;br /&gt;
&lt;br /&gt;
== Problema de desarrollo ==&lt;br /&gt;
&lt;br /&gt;
Vale, ya parece que le hemos pillado el tranquillo a Docker y a Docker Compose. Ahora vamos a hacer algo como desarrolladores. Ve al archivo &amp;quot;app/templates/base_templates.html&amp;quot; y cambia la línea 22:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;title&amp;gt;{{ FLASK_APP_NAME }} - Repository of feature models in UVL &amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
por:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;title&amp;gt;Hello world! &amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. Fíjate en el nombre de la pestaña del navegador, ¿ha cambiado? ¿No? Tsss, esto de los contenedores... bueno, va, vamos a reiniciarlo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker restart web_app_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. ¿Tampoco? Anda que vaya idea la de usar Docker... Claro, es a la hora de construir la imagen, hicimos &amp;quot;COPY . .&amp;quot;, pero no hemos reconstruido nada, así que...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml build --no-cache web&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --force-recreate web&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Importante señalar que le pedimos que ignore la caché para que fuerce a copiar de nuevo todo el contenido (incluyendo el cambio de línea) al interior del contenedor.&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. ¡Ahora sí! Pero... ¿tengo que reconstruir TODO el contenedor en cada cambio línea? ¿Y eso es trabajar de forma ágil?&lt;br /&gt;
&lt;br /&gt;
== Crear volumen de trabajo (bind mount) ==&lt;br /&gt;
&lt;br /&gt;
Evidentemente, eso no tiene sentido, tenemos que usar un volumen de trabajo que nos ayude a sincronizar el código de nuestro host con el código interno del contenedor. Luego en nuestro docker-compose:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  web:&lt;br /&gt;
    (....)&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;5000:5000&amp;quot;&lt;br /&gt;
    volumes:&lt;br /&gt;
      - ../:/app&lt;br /&gt;
    (....)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
La línea&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
- ../:/app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Monta tu carpeta local del proyecto (../) dentro del contenedor, (/app). ¿Y por qué /app? Porque fue el espacio de trabajo que definimos en el Dockerfile. Como ves, la congruencia entre los Dockerfile y el docker-compose es importante.&lt;br /&gt;
&lt;br /&gt;
Recargemos de nuevo nustra configuración:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ahora ya podemos trabajar con el código en host sin tener que reconstruir la imagen a cada cambio de línea.&lt;br /&gt;
&lt;br /&gt;
== Cuidado con COPY y los bind mounts ==&lt;br /&gt;
&lt;br /&gt;
Un '''bind mount''' (o “montaje de enlace”) es un tipo de volumen en el que Docker conecta directamente una carpeta de tu máquina (el host) con una carpeta dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
Cuando usas un bind mount (por ejemplo ../:/app), Docker monta tu carpeta local sobre la ruta /app del contenedor.&lt;br /&gt;
Eso significa que todo lo que estaba en /app dentro de la imagen desaparece al arrancar el contenedor, porque queda “oculto” por el volumen.&lt;br /&gt;
&lt;br /&gt;
Ahora bien, durante la construcción de la imagen usamos la directiva COPY. ¿Por qué? Porque necesitamos el archivo requirements.txt para instalar las dependencias de Python del proyecto. Pero... ¿realmente necesitamos copiar TODO? No, claro que no, podemos optar por copiar exclusivamente aquellos archivos que nos hagan falta durante '''LA CONSTRUCCIÓN''' de la imagen. El bind mount luego lo machacará.&lt;br /&gt;
&lt;br /&gt;
'''Muy importante:''' los bind mounts, al igual que cualquier otro volumen, están disponibles '''DESPUÉS''' de la construcción de la imagen. Es un error común creer que porque definamos un bind mount en un docker-compose, esos archivos aparecen ya para el contenedor. '''Esto es una fuente inagotable de problemas en la práctica'''.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 6: Distintas configuraciones de despliegue =&lt;br /&gt;
&lt;br /&gt;
1: Explicar las distintas imágenes, entrypoints y docker-compose.ymls&lt;br /&gt;
2: Explicar el nginx&lt;br /&gt;
3: Explicar que cuanto más se parezca el despliegue en dev y en prod, menos propenso a errores seremos&lt;br /&gt;
4: Explicar diferencia entre flask server y gunicorn. Explicar que, dado que es una configuración distinta en dev y en prod, necesitamos siempre una máquina de preproducción&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10159</id>
		<title>Tutorial-docker</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10159"/>
				<updated>2025-10-12T19:10:41Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Ejercicio 1: Empezamos desde cero =&lt;br /&gt;
&lt;br /&gt;
El objetivo es eliminar toda la configuración previa y crear desde cero la estructura de carpetas que contendrá nuestros futuros archivos Docker.&lt;br /&gt;
&lt;br /&gt;
== Eliminar la configuración existente ==&lt;br /&gt;
Antes de comenzar, asegúrate de borrar cualquier carpeta Docker existente en el proyecto. En este caso, le haremos una copia de seguridad para poder volver a ella.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mv docker docker.bk&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
También debes asegurarte que trabajas con las variables de entorno adecuadas:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
cp .env.docker.example .env&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
⚠️ Este paso es importante: partimos completamente desde cero para entender cada parte del sistema.&lt;br /&gt;
&lt;br /&gt;
== Crear un Dockerfile mínimo ==&lt;br /&gt;
&lt;br /&gt;
Ahora crea nuevamente la carpeta base donde almacenaremos los archivos relacionados con Docker:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt; mkdir -p docker/images &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vamos a explicar los distintos apartados:&lt;br /&gt;
&lt;br /&gt;
=== FROM ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Indica la imagen base sobre la que se construirá la tuya.&lt;br /&gt;
En este caso, parte de una imagen ligera que ya tiene Python 3.12 instalado.&lt;br /&gt;
&lt;br /&gt;
=== WORKDIR ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta línea define el directorio de trabajo dentro del contenedor.&lt;br /&gt;
A partir de aquí, todos los comandos que aparezcan después (COPY, RUN, CMD, etc.) se ejecutarán dentro de /app.&lt;br /&gt;
&lt;br /&gt;
'''Ojo: /app no es la carpeta app/ de tu proyecto local.'''. Son cosas distintas.&lt;br /&gt;
&lt;br /&gt;
En tu máquina puede existir algo como:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
uvlhub&lt;br /&gt;
   app&lt;br /&gt;
   core&lt;br /&gt;
   requirements.txt &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Pero dentro del contenedor la estructura será distinta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
/ &lt;br /&gt;
  app/ &lt;br /&gt;
    app/ ← aquí dentro se habrá copiado tu carpeta local &amp;quot;app&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Cuando más adelante uses:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Docker copiará todos los archivos de tu proyecto dentro de la carpeta /app del contenedor.&lt;br /&gt;
Por eso el código quedará en /app/app/.&lt;br /&gt;
&lt;br /&gt;
=== COPY ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Copia todos los archivos del proyecto (del host) dentro del contenedor, en /app.&lt;br /&gt;
El primer punto es el origen; el segundo, el destino.&lt;br /&gt;
&lt;br /&gt;
=== RUN ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ejecuta comandos durante la construcción de la imagen.&lt;br /&gt;
Aquí instalamos las dependencias necesarias desde requirements.txt.&lt;br /&gt;
El resultado de este paso quedará guardado en la imagen.&lt;br /&gt;
&lt;br /&gt;
=== EXPOSE ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Documenta el puerto que la aplicación usa dentro del contenedor.&lt;br /&gt;
Esto no lo abre al exterior; simplemente indica que la app escucha en el 5000.&lt;br /&gt;
&lt;br /&gt;
=== CMD ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Define el comando que se ejecutará cuando el contenedor se inicie.&lt;br /&gt;
En este caso, ejecutará la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
=== RUN vs CMD ===&lt;br /&gt;
&lt;br /&gt;
Estas dos instrucciones parecen similares, pero hacen cosas muy distintas.&lt;br /&gt;
&lt;br /&gt;
==== RUN ====&lt;br /&gt;
Se ejecuta durante la construcción de la imagen (en el momento del docker build).&lt;br /&gt;
Cada RUN crea una nueva capa dentro de la imagen.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando instala las dependencias una sola vez, cuando se construye la imagen.&lt;br /&gt;
El resultado (las librerías instaladas) queda grabado dentro de la imagen final.&lt;br /&gt;
&lt;br /&gt;
Si después lanzas un contenedor nuevo a partir de esa imagen, las dependencias ya estarán instaladas y no se volverán a ejecutar.&lt;br /&gt;
&lt;br /&gt;
==== CMD ====&lt;br /&gt;
Se ejecuta cuando se inicia el contenedor (en el docker run).&lt;br /&gt;
Define el proceso principal que se ejecutará dentro del contenedor mientras esté activo.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando no se ejecuta durante la construcción, sino cada vez que arrancas el contenedor.&lt;br /&gt;
Si detienes el contenedor, este proceso también se detiene.&lt;br /&gt;
&lt;br /&gt;
==== Diferencia resumida ====&lt;br /&gt;
&lt;br /&gt;
RUN → se ejecuta al construir la imagen (fase de docker build).&lt;br /&gt;
&lt;br /&gt;
CMD → se ejecuta al iniciar el contenedor (fase de docker run).&lt;br /&gt;
&lt;br /&gt;
Solo puede haber un CMD por Dockerfile.&lt;br /&gt;
Si hay más de uno, solo se usará el último.&lt;br /&gt;
&lt;br /&gt;
== Dockerfile completo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
COPY . .&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto tiene que ir en un Dockerfile en docker/images&lt;br /&gt;
&lt;br /&gt;
== Construir la imagen ==&lt;br /&gt;
Desde la raíz del proyecto (no dentro de docker/images):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker build -t uvlhub:dev -f docker/images/Dockerfile.dev .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Ejecutar el contenedor ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Qué es lo que observas?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 2: Conectando servicios =&lt;br /&gt;
&lt;br /&gt;
¡Vaya! ¡No funciona! Can't connect to MySQL server on 'db'... ¡Ah, claro, intenta conectarse a la base de datos, que no lo tenemos! Pero claro, en un contenedor no es recomendable tener la app y la base de datos conviviendo...&lt;br /&gt;
&lt;br /&gt;
== Crear contenedor MariaDB ==&lt;br /&gt;
&lt;br /&gt;
Antes que nada, haz Ctrl + C para parar el contenedor web. En este ejercicio aprenderás a lanzar un contenedor de MariaDB 12.0.2 de forma manual, configurando todas las variables necesarias para tu aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
El objetivo es entender la complejidad de hacerlo “a mano”, antes de automatizarlo con docker-compose.&lt;br /&gt;
&lt;br /&gt;
== Descargar la imagen oficial ==&lt;br /&gt;
Primero descarga la versión exacta que usaremos:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker pull mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Crear el contenedor de MariaDB ==&lt;br /&gt;
Lanza un contenedor con todas las variables necesarias para que Flask pueda conectarse correctamente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d --name mariadb_container \&lt;br /&gt;
 -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
 -e FLASK_ENV=development \&lt;br /&gt;
 -e DOMAIN=localhost \&lt;br /&gt;
 -e MARIADB_HOSTNAME=db \&lt;br /&gt;
 -e MARIADB_PORT=3306 \&lt;br /&gt;
 -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
 -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
 -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
 -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
 -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
 -e WORKING_DIR=/app/ \&lt;br /&gt;
 -p 3306:3306 mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Explicación:&lt;br /&gt;
&lt;br /&gt;
-d: ejecuta el contenedor en segundo plano (modo detached).&lt;br /&gt;
&lt;br /&gt;
--name: asigna un nombre identificable al contenedor.&lt;br /&gt;
&lt;br /&gt;
-e: define variables de entorno dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
-p 3306:3306: publica el puerto de MariaDB en tu máquina local.&lt;br /&gt;
&lt;br /&gt;
mariadb:12.0.2: imagen exacta que vamos a usar.&lt;br /&gt;
&lt;br /&gt;
Aquí ya, de entrada, vemos que esto es más feo que una nevera por detrás. Para empezar, estamos pasando a lo loco un montón de variables que difílmente será replicable en producción, por contener información sensible. Aparte, si metemos nuevas variables, ese comando queda inservible. Necesitaremos una solución más ágil para este problema&lt;br /&gt;
&lt;br /&gt;
== Comprobar que está corriendo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver algo así:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES&lt;br /&gt;
abcd1234efgh mariadb:12.0.2 &amp;quot;docker-entrypoint.s…&amp;quot; Up 5 seconds 0.0.0.0:3306-&amp;gt;3306/tcp mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Conectarse a la base de datos ==&lt;br /&gt;
Abre una consola dentro del contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it mariadb_container bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y dentro, accede al cliente de MariaDB:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mariadb -u root -p&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Contraseña: uvlhubdb_root_password&lt;br /&gt;
&lt;br /&gt;
== Verificar la base de datos ==&lt;br /&gt;
Una vez dentro del cliente MySQL, ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
SHOW DATABASES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver que existen la bases de datos uvlhubdb porque pedimos crearla a la hora de levantar el contenedor. ¿Qué tiene la base de datos dentro? Vamos a verlo...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
USE uvlhubdb;&lt;br /&gt;
SHOW TABLES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Un total de... 0 tablas... ¡Claro! Es que solo hemos creado la base en sí, pero no las tablas. ¿Con un script SQL tal vez? ¡Espera, espera, si tenemos las migraciones! Pero claro, para eso necesito el contenedor de la app levantado. Sal de la consola SQL y del contenedor de MariaDB. Bien, ahora en el host...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta vez con el flag &amp;quot;-d&amp;quot; para que corra en segundo plano. Ahora, accedamos al contenedor y ejecutemos las migraciones. Pero... ¿cómo se llama mi contenedor?&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate que Docker asigna un nombre aleatorio a tu contenedor web porque no definiste ninguno. Una vez identificado el nombre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it &amp;lt;nombre&amp;gt; bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
What? ¿Qué pasa? ¿Otra vez error de conexión con la base? Si tenemos el contenedor funcionando, esto es un jaleo...&lt;br /&gt;
&lt;br /&gt;
== Redes internas ==&lt;br /&gt;
&lt;br /&gt;
Ahora mismo tienes dos contenedores aislados, el de la app y el la base de datos. Por defecto, cada contenedor tiene su propia red interna y no puede ver a los demás por nombre. El hostname '''db''' que usa Flask no existe en su DNS interno.&lt;br /&gt;
&lt;br /&gt;
=== Conectar servicios ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una red interna que compartan ambos contenedores&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker network create uvlhub_network&lt;br /&gt;
docker network ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vemos que ya aparece nuestra red '''uvlhub_network'''. Ahora, conectemos el contenedor de MariaDB a la red:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Claro, si te fijas, ahora hay que añadir, además de toda esa configuración engorrosa, que se añada en la misma red y que además tenga de hostname &amp;quot;db&amp;quot; para que el contenedor web entienda qué es &amp;quot;db&amp;quot;. Tenemos que hacer lo mismo para el contenedor web:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop &amp;lt;nombre&amp;gt;&lt;br /&gt;
docker rm &amp;lt;nombre&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y lo levantamos de nuevo, pero ya en la nueva red. De paso, le ponemos un nombre para identificarlo mejor.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 --name web_app_container --network uvlhub_network -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y ahora ya podemos acceder al contenedor por su nombre y ejecutar las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Mejor?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 3: Persistencia =&lt;br /&gt;
&lt;br /&gt;
Vamos a simular que hemos desplegado justo esto en producción y el servidor se reinicia por un corte de luz. Reiniciemos a mano cada contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker restart web_app_container&lt;br /&gt;
docker restart mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. What? ¿Cómo? ¿Qué ha pasado? ¿Otra vez problema de conexión? Ve dándole a F5... F5... ¡bingo! Ahora sí sale, pero menuda aleatoriedad, ¿no? Claro, es que ahora caes en que una cosa es que el contenedor haya arrancado y otra que esté listo.&lt;br /&gt;
&lt;br /&gt;
Aparte, tenemos otro problema. ¿Qué pasa con la persistencia? Porque si borro el contenedor de MariaDB, todos los datos que estén en su interior también se pierden. Y si estamos en producción, lío asegurado.&lt;br /&gt;
&lt;br /&gt;
== Volúmenes ==&lt;br /&gt;
&lt;br /&gt;
Cada vez que eliminas el contenedor de MariaDB, todas las bases de datos se pierden. Esto ocurre porque los datos se guardan dentro del sistema de archivos del contenedor, y al borrarlo desaparecen con él.&lt;br /&gt;
&lt;br /&gt;
En este ejercicio creas un volumen de Docker para que los datos persistan entre ejecuciones.&lt;br /&gt;
&lt;br /&gt;
=== Crear un volumen ===&lt;br /&gt;
Un volumen es una carpeta gestionada por Docker que vive fuera del ciclo de vida de los contenedores.&lt;br /&gt;
&lt;br /&gt;
Ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume create mariadb_data&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba que existe:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
La salida muestra algo como:&lt;br /&gt;
&lt;br /&gt;
DRIVER    VOLUME NAME&lt;br /&gt;
local     mariadb_data&lt;br /&gt;
&lt;br /&gt;
=== Crear un contenedor de MariaDB con el volumen ===&lt;br /&gt;
Lanza el contenedor y conecta el volumen a la ruta donde MariaDB guarda los datos (/var/lib/mysql):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -v mariadb_data:/var/lib/mysql \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Qué significa &amp;quot;:&amp;quot; en los volúmenes ===&lt;br /&gt;
&lt;br /&gt;
Cuando usas la opción '''-v''' o '''--volume''' en Docker, el signo dos puntos (''':''') separa dos rutas:&lt;br /&gt;
&lt;br /&gt;
'''-v origen:destino'''&lt;br /&gt;
&lt;br /&gt;
origen → es la ruta o el volumen en tu máquina (el host).&lt;br /&gt;
&lt;br /&gt;
destino → es la ruta dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 4: Orquestación de servicios =&lt;br /&gt;
&lt;br /&gt;
Hasta ahora hemos creado y conectado los contenedores manualmente.&lt;br /&gt;
Ahora vas a usar Docker Compose para definir todo en un solo archivo.&lt;br /&gt;
&lt;br /&gt;
== Orquestación básica ==&lt;br /&gt;
&lt;br /&gt;
Crea un archivo llamado docker-compose.yml en la carpeta '''docker''' con este contenido:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    container_name: web_app_container&lt;br /&gt;
    env_file:&lt;br /&gt;
      - ../.env&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;5000&amp;quot;&lt;br /&gt;
    depends_on:&lt;br /&gt;
      - db&lt;br /&gt;
    build:&lt;br /&gt;
      context: ../&lt;br /&gt;
      dockerfile: docker/images/Dockerfile&lt;br /&gt;
    networks:&lt;br /&gt;
      - uvlhub_network&lt;br /&gt;
&lt;br /&gt;
  db:&lt;br /&gt;
    container_name: mariadb_container&lt;br /&gt;
    image: mariadb:12.0.2&lt;br /&gt;
    env_file:&lt;br /&gt;
      - ../.env&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;3306:3306&amp;quot;&lt;br /&gt;
    volumes:&lt;br /&gt;
      - db_data:/var/lib/mysql&lt;br /&gt;
    networks:&lt;br /&gt;
      - uvlhub_network&lt;br /&gt;
&lt;br /&gt;
volumes:&lt;br /&gt;
  db_data:&lt;br /&gt;
&lt;br /&gt;
networks:&lt;br /&gt;
  uvlhub_network:&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate que esta orquestación de servicios me permite definir el nombre de cada servicio, su dependencia, sus puertos, sus volúmenes, redes... todo lo que hacíamos de forma manual y propenso a errores queda ahora más fácil de gestionar. Además, ahora ambos contenedores comparten el mismo archivo .env de variables de entorno.&lt;br /&gt;
&lt;br /&gt;
Bien, ahora vamos a levantar estos contenedores y acceder al contenedor web:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Inmediatamente después, ejecuta las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué ha ocurrido? Depende del tiempo que hayas tardado, puede que haya saltado un error o puede que haya funcionado. Puede que hayas intentado varias veces el comando anterior hasta que ha funcionado. ¿Por qué crees que ocurre esta aparete aleatoriedad?&lt;br /&gt;
&lt;br /&gt;
== Dependencias entre contenedores y entrypoints ==&lt;br /&gt;
&lt;br /&gt;
Fíjate que tenemos un &amp;quot;depends on&amp;quot; en el servicio web. Significa que el servicio &amp;quot;db&amp;quot; debe iniciarse antes que el servicio web. Pero, cuidado, ahí está la trampa, porque &amp;quot;servicio iniciado != servicio preparado&amp;quot;. El servicio de MariaDB tiene que arrancar, configurarse, etc...&lt;br /&gt;
&lt;br /&gt;
Cuando hicimos el comando COPY en nuestro Dockerfile, también copiamos la carpeta '''scripts'''. Uno de los scripts es '''wait-for-db.sh''', que realiza continuamente llamadas a MariaDB hasta que este servicio está listo para recibir peticiones. Ábrelo y estúdialo.&lt;br /&gt;
&lt;br /&gt;
Añade al servicio web de tu docker compose las siguientes líneas:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
entrypoint: [&amp;quot;/app/scripts/wait-for-db.sh&amp;quot;]&lt;br /&gt;
command: [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A su vez, del Dockerfile, hay que eliminar la línea de CMD que arrancaba nuestro servidor Flask.&lt;br /&gt;
&lt;br /&gt;
Luego, reinicializa todos los servicios:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. What? ¿No abre? Vamos a investigarlo, veamos el logs:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker logs web_app_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Nos debe salir un mensaje eterno de &amp;quot;/app/scripts/wait-for-db.sh: line 18: mariadb: command not found&amp;quot;. Esto ocurre porque en la imagen de nuestro servicio web NO TENEMOS el cliente de MariaDB. Debemos indicar entonces que lo instale. Añade justo debajo de FROM lo siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y --no-install-recommends mariadb-client &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Al ser un contenedor basado en Linux, y accediendo como root, es como si actualizáramos todos los paquetes y luego instalásemos el cliente de MariaDB. Intentémoslo otra vez:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. ¡Jopelines! ¿Tampoco? Si haces un &amp;quot;docker ps&amp;quot; verás el problema. El contenedor web no tiene expuesto ningún puerto.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    (...)&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;5000&amp;quot;&lt;br /&gt;
    (...)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;quot;Expose&amp;quot; hace visible el puerto '''entre contenedores''', pero '''NO entre tu contenedor y el host'''. Para eso usamos la directiva &amp;quot;ports&amp;quot;, que se encarga del mapping. En nuestro caso, será:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    (...)&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;5000:5000&amp;quot;&lt;br /&gt;
    (...)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
En ocasiones, si los dos puertos coinciden, se puede dejar uno solo con &amp;quot;5000&amp;quot; pero dejaremos los dos. El de la izquierda siempre es el puerto externo (host), el de la derecha es el puerto interno (contenedor). Así que cambia ese detalle y por última vez:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. ¡Ahora sí!&lt;br /&gt;
&lt;br /&gt;
== Trabajando con CMD, entrypoints y commands ==&lt;br /&gt;
&lt;br /&gt;
Piensa que al arrancar un contenedor Docker, siempre se ejecuta un único comando final.&lt;br /&gt;
Lo que hacen ENTRYPOINT, CMD y command (de Compose) es decidir qué comando será ese y con qué argumentos.&lt;br /&gt;
&lt;br /&gt;
=== CMD → “qué hacer por defecto” ===&lt;br /&gt;
&lt;br /&gt;
* Está en el Dockerfile.&lt;br /&gt;
* Es el comando que Docker ejecuta si no se indica otra cosa. En nuestro caso:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== ENTRYPOINT → “qué se ejecuta siempre” ===&lt;br /&gt;
&lt;br /&gt;
* También va en el Dockerfile, o se puede sobreescribir en el docker-compose.yml.&lt;br /&gt;
* Define el programa o script principal que siempre se ejecuta.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
ENTRYPOINT [&amp;quot;/app/scripts/wait-for-db.sh&amp;quot;]&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Normalmente, el entrypoint se combina con CMD en modo &amp;quot;eh, ejecuta esto (entrypoint) y luego este comando final (cmd)&lt;br /&gt;
&lt;br /&gt;
Los entrypoints son muy útiles, sobre todo en los docker-compose's, porque puedo redefinir cómo quiero que inicie un contenedor en concreto '''SIN TENER QUE HACER UN BUILD DE NUEVO DE LA IMAGEN DE DOCKER'''. A diferencia de CMD, que viene predefinido, un entrypoint en mi docker-compose me da más versatilidad&lt;br /&gt;
&lt;br /&gt;
=== command (de docker-compose.yml) → “sobrescribe el CMD” ===&lt;br /&gt;
&lt;br /&gt;
En docker-compose.yml, el campo command: reemplaza al CMD del Dockerfile,pero no al ENTRYPOINT.&lt;br /&gt;
&lt;br /&gt;
== Combinación de elementos ==&lt;br /&gt;
&lt;br /&gt;
Es importante entender cómo Docker trabaja con la orquestación y con los contenedores:&lt;br /&gt;
&lt;br /&gt;
* Lee el YAML como un diccionario&lt;br /&gt;
* Ejecuta todo lo que haya en un Dockerfile EXCEPTUANDO directivas ENTRYPOINT y CMD&lt;br /&gt;
* Monta el volumen o los volúmenes de ese contenedor (si procede)&lt;br /&gt;
&lt;br /&gt;
Y ahora viene el detalle:&lt;br /&gt;
&lt;br /&gt;
* Si estamos levantando un contenedor a través de docker-compose, va a ejecutar los entrypoints y commands definidos en ese docker-compose.&lt;br /&gt;
* Un entrypoint definido en un docker-compose va a sustituir a un entrypoint definido en el Dockerfile&lt;br /&gt;
* Un command definido en un docker-compose va a sustituir a un CMD definido en el Dockerfile.&lt;br /&gt;
&lt;br /&gt;
Como consejo:&lt;br /&gt;
&lt;br /&gt;
* Definimos un entrypoint en el docker-compose. Esto es así porque está fuera de la imagen de Docker y nos permite redefinir el inicio del servicio sin modificar la imagen&lt;br /&gt;
* Eventualmente, definimos un CMD donde indicamos el comando final de ese contenedor al iniciar, que suele ser el arranque de Flask Server o de Gunicorn, dependiendo de si estamos en desarrollo o producción.&lt;br /&gt;
&lt;br /&gt;
No obstante, como nuestros entrypoints son todos scripts en bash, podemos definir en el mismo el arranque final (comando final) y nos olvidamos de CMD.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 5: Volumen de trabajo =&lt;br /&gt;
&lt;br /&gt;
== Problema de desarrollo ==&lt;br /&gt;
&lt;br /&gt;
Vale, ya parece que le hemos pillado el tranquillo a Docker y a Docker Compose. Ahora vamos a hacer algo como desarrolladores. Ve al archivo &amp;quot;app/templates/base_templates.html&amp;quot; y cambia la línea 22:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;title&amp;gt;{{ FLASK_APP_NAME }} - Repository of feature models in UVL &amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
por:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;title&amp;gt;Hello world! &amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. Fíjate en el nombre de la pestaña del navegador, ¿ha cambiado? ¿No? Tsss, esto de los contenedores... bueno, va, vamos a reiniciarlo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker restart web_app_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. ¿Tampoco? Anda que vaya idea la de usar Docker... Claro, es a la hora de construir la imagen, hicimos &amp;quot;COPY . .&amp;quot;, pero no hemos reconstruido nada, así que...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml build --no-cache web&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --force-recreate web&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Importante señalar que le pedimos que ignore la caché para que fuerce a copiar de nuevo todo el contenido (incluyendo el cambio de línea) al interior del contenedor.&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. ¡Ahora sí! Pero... ¿tengo que reconstruir TODO el contenedor en cada cambio línea? ¿Y eso es trabajar de forma ágil?&lt;br /&gt;
&lt;br /&gt;
== Crear volumen de trabajo (bind mount) ==&lt;br /&gt;
&lt;br /&gt;
Evidentemente, eso no tiene sentido, tenemos que usar un volumen de trabajo que nos ayude a sincronizar el código de nuestro host con el código interno del contenedor. Luego en nuestro docker-compose:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  web:&lt;br /&gt;
    (....)&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;5000:5000&amp;quot;&lt;br /&gt;
    volumes:&lt;br /&gt;
      - ../:/app&lt;br /&gt;
    (....)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
La línea&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
- ../:/app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Monta tu carpeta local del proyecto (../) dentro del contenedor, (/app). ¿Y por qué /app? Porque fue el espacio de trabajo que definimos en el Dockerfile. Como ves, la congruencia entre los Dockerfile y el docker-compose es importante.&lt;br /&gt;
&lt;br /&gt;
Recargemos de nuevo nustra configuración:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ahora ya podemos trabajar con el código en host sin tener que reconstruir la imagen a cada cambio de línea.&lt;br /&gt;
&lt;br /&gt;
== Cuidado con COPY y los bind mounts ==&lt;br /&gt;
&lt;br /&gt;
Un '''bind mount''' (o “montaje de enlace”) es un tipo de volumen en el que Docker conecta directamente una carpeta de tu máquina (el host) con una carpeta dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
Cuando usas un bind mount (por ejemplo ../:/app), Docker monta tu carpeta local sobre la ruta /app del contenedor.&lt;br /&gt;
Eso significa que todo lo que estaba en /app dentro de la imagen desaparece al arrancar el contenedor, porque queda “oculto” por el volumen.&lt;br /&gt;
&lt;br /&gt;
Ahora bien, durante la construcción de la imagen usamos la directiva COPY. ¿Por qué? Porque necesitamos el archivo requirements.txt para instalar las dependencias de Python del proyecto. Pero... ¿realmente necesitamos copiar TODO? No, claro que no, podemos optar por copiar exclusivamente aquellos archivos que nos hagan falta durante '''LA CONSTRUCCIÓN''' de la imagen. El bind mount luego lo machacará.&lt;br /&gt;
&lt;br /&gt;
'''Muy importante:''' los bind mounts, al igual que cualquier otro volumen, están disponibles '''DESPUÉS''' de la construcción de la imagen. Es un error común creer que porque definamos un bind mount en un docker-compose, esos archivos aparecen ya para el contenedor. '''Esto es una fuente inagotable de problemas en la práctica'''.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 6: Distintas configuraciones de despliegue =&lt;br /&gt;
&lt;br /&gt;
1: Explicar las distintas imágenes, entrypoints y docker-compose.ymls&lt;br /&gt;
2: Explicar el nginx&lt;br /&gt;
3: Explicar que cuanto más se parezca el despliegue en dev y en prod, menos propenso a errores seremos&lt;br /&gt;
4: Explicar diferencia entre flask server y gunicorn. Explicar que, dado que es una configuración distinta en dev y en prod, necesitamos siempre una máquina de preproducción&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10158</id>
		<title>Tutorial-docker</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10158"/>
				<updated>2025-10-12T19:08:57Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Ejercicio 1: Empezamos desde cero =&lt;br /&gt;
&lt;br /&gt;
El objetivo es eliminar toda la configuración previa y crear desde cero la estructura de carpetas que contendrá nuestros futuros archivos Docker.&lt;br /&gt;
&lt;br /&gt;
== Eliminar la configuración existente ==&lt;br /&gt;
Antes de comenzar, asegúrate de borrar cualquier carpeta Docker existente en el proyecto. En este caso, le haremos una copia de seguridad para poder volver a ella.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mv docker docker.bk&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
También debes asegurarte que trabajas con las variables de entorno adecuadas:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
cp .env.docker.example .env&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
⚠️ Este paso es importante: partimos completamente desde cero para entender cada parte del sistema.&lt;br /&gt;
&lt;br /&gt;
== Crear un Dockerfile mínimo ==&lt;br /&gt;
&lt;br /&gt;
Ahora crea nuevamente la carpeta base donde almacenaremos los archivos relacionados con Docker:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt; mkdir -p docker/images &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vamos a explicar los distintos apartados:&lt;br /&gt;
&lt;br /&gt;
=== FROM ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Indica la imagen base sobre la que se construirá la tuya.&lt;br /&gt;
En este caso, parte de una imagen ligera que ya tiene Python 3.12 instalado.&lt;br /&gt;
&lt;br /&gt;
=== WORKDIR ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta línea define el directorio de trabajo dentro del contenedor.&lt;br /&gt;
A partir de aquí, todos los comandos que aparezcan después (COPY, RUN, CMD, etc.) se ejecutarán dentro de /app.&lt;br /&gt;
&lt;br /&gt;
'''Ojo: /app no es la carpeta app/ de tu proyecto local.'''. Son cosas distintas.&lt;br /&gt;
&lt;br /&gt;
En tu máquina puede existir algo como:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
uvlhub&lt;br /&gt;
   app&lt;br /&gt;
   core&lt;br /&gt;
   requirements.txt &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Pero dentro del contenedor la estructura será distinta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
/ &lt;br /&gt;
  app/ &lt;br /&gt;
    app/ ← aquí dentro se habrá copiado tu carpeta local &amp;quot;app&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Cuando más adelante uses:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Docker copiará todos los archivos de tu proyecto dentro de la carpeta /app del contenedor.&lt;br /&gt;
Por eso el código quedará en /app/app/.&lt;br /&gt;
&lt;br /&gt;
=== COPY ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Copia todos los archivos del proyecto (del host) dentro del contenedor, en /app.&lt;br /&gt;
El primer punto es el origen; el segundo, el destino.&lt;br /&gt;
&lt;br /&gt;
=== RUN ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ejecuta comandos durante la construcción de la imagen.&lt;br /&gt;
Aquí instalamos las dependencias necesarias desde requirements.txt.&lt;br /&gt;
El resultado de este paso quedará guardado en la imagen.&lt;br /&gt;
&lt;br /&gt;
=== EXPOSE ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Documenta el puerto que la aplicación usa dentro del contenedor.&lt;br /&gt;
Esto no lo abre al exterior; simplemente indica que la app escucha en el 5000.&lt;br /&gt;
&lt;br /&gt;
=== CMD ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Define el comando que se ejecutará cuando el contenedor se inicie.&lt;br /&gt;
En este caso, ejecutará la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
=== RUN vs CMD ===&lt;br /&gt;
&lt;br /&gt;
Estas dos instrucciones parecen similares, pero hacen cosas muy distintas.&lt;br /&gt;
&lt;br /&gt;
==== RUN ====&lt;br /&gt;
Se ejecuta durante la construcción de la imagen (en el momento del docker build).&lt;br /&gt;
Cada RUN crea una nueva capa dentro de la imagen.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando instala las dependencias una sola vez, cuando se construye la imagen.&lt;br /&gt;
El resultado (las librerías instaladas) queda grabado dentro de la imagen final.&lt;br /&gt;
&lt;br /&gt;
Si después lanzas un contenedor nuevo a partir de esa imagen, las dependencias ya estarán instaladas y no se volverán a ejecutar.&lt;br /&gt;
&lt;br /&gt;
==== CMD ====&lt;br /&gt;
Se ejecuta cuando se inicia el contenedor (en el docker run).&lt;br /&gt;
Define el proceso principal que se ejecutará dentro del contenedor mientras esté activo.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando no se ejecuta durante la construcción, sino cada vez que arrancas el contenedor.&lt;br /&gt;
Si detienes el contenedor, este proceso también se detiene.&lt;br /&gt;
&lt;br /&gt;
==== Diferencia resumida ====&lt;br /&gt;
&lt;br /&gt;
RUN → se ejecuta al construir la imagen (fase de docker build).&lt;br /&gt;
&lt;br /&gt;
CMD → se ejecuta al iniciar el contenedor (fase de docker run).&lt;br /&gt;
&lt;br /&gt;
Solo puede haber un CMD por Dockerfile.&lt;br /&gt;
Si hay más de uno, solo se usará el último.&lt;br /&gt;
&lt;br /&gt;
== Dockerfile completo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
COPY . .&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto tiene que ir en un Dockerfile en docker/images&lt;br /&gt;
&lt;br /&gt;
== Construir la imagen ==&lt;br /&gt;
Desde la raíz del proyecto (no dentro de docker/images):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker build -t uvlhub:dev -f docker/images/Dockerfile.dev .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Ejecutar el contenedor ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Qué es lo que observas?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 2: Conectando servicios =&lt;br /&gt;
&lt;br /&gt;
¡Vaya! ¡No funciona! Can't connect to MySQL server on 'db'... ¡Ah, claro, intenta conectarse a la base de datos, que no lo tenemos! Pero claro, en un contenedor no es recomendable tener la app y la base de datos conviviendo...&lt;br /&gt;
&lt;br /&gt;
== Crear contenedor MariaDB ==&lt;br /&gt;
&lt;br /&gt;
Antes que nada, haz Ctrl + C para parar el contenedor web. En este ejercicio aprenderás a lanzar un contenedor de MariaDB 12.0.2 de forma manual, configurando todas las variables necesarias para tu aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
El objetivo es entender la complejidad de hacerlo “a mano”, antes de automatizarlo con docker-compose.&lt;br /&gt;
&lt;br /&gt;
== Descargar la imagen oficial ==&lt;br /&gt;
Primero descarga la versión exacta que usaremos:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker pull mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Crear el contenedor de MariaDB ==&lt;br /&gt;
Lanza un contenedor con todas las variables necesarias para que Flask pueda conectarse correctamente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d --name mariadb_container \&lt;br /&gt;
 -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
 -e FLASK_ENV=development \&lt;br /&gt;
 -e DOMAIN=localhost \&lt;br /&gt;
 -e MARIADB_HOSTNAME=db \&lt;br /&gt;
 -e MARIADB_PORT=3306 \&lt;br /&gt;
 -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
 -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
 -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
 -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
 -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
 -e WORKING_DIR=/app/ \&lt;br /&gt;
 -p 3306:3306 mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Explicación:&lt;br /&gt;
&lt;br /&gt;
-d: ejecuta el contenedor en segundo plano (modo detached).&lt;br /&gt;
&lt;br /&gt;
--name: asigna un nombre identificable al contenedor.&lt;br /&gt;
&lt;br /&gt;
-e: define variables de entorno dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
-p 3306:3306: publica el puerto de MariaDB en tu máquina local.&lt;br /&gt;
&lt;br /&gt;
mariadb:12.0.2: imagen exacta que vamos a usar.&lt;br /&gt;
&lt;br /&gt;
Aquí ya, de entrada, vemos que esto es más feo que una nevera por detrás. Para empezar, estamos pasando a lo loco un montón de variables que difílmente será replicable en producción, por contener información sensible. Aparte, si metemos nuevas variables, ese comando queda inservible. Necesitaremos una solución más ágil para este problema&lt;br /&gt;
&lt;br /&gt;
== Comprobar que está corriendo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver algo así:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES&lt;br /&gt;
abcd1234efgh mariadb:12.0.2 &amp;quot;docker-entrypoint.s…&amp;quot; Up 5 seconds 0.0.0.0:3306-&amp;gt;3306/tcp mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Conectarse a la base de datos ==&lt;br /&gt;
Abre una consola dentro del contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it mariadb_container bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y dentro, accede al cliente de MariaDB:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mariadb -u root -p&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Contraseña: uvlhubdb_root_password&lt;br /&gt;
&lt;br /&gt;
== Verificar la base de datos ==&lt;br /&gt;
Una vez dentro del cliente MySQL, ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
SHOW DATABASES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver que existen la bases de datos uvlhubdb porque pedimos crearla a la hora de levantar el contenedor. ¿Qué tiene la base de datos dentro? Vamos a verlo...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
USE uvlhubdb;&lt;br /&gt;
SHOW TABLES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Un total de... 0 tablas... ¡Claro! Es que solo hemos creado la base en sí, pero no las tablas. ¿Con un script SQL tal vez? ¡Espera, espera, si tenemos las migraciones! Pero claro, para eso necesito el contenedor de la app levantado. Sal de la consola SQL y del contenedor de MariaDB. Bien, ahora en el host...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta vez con el flag &amp;quot;-d&amp;quot; para que corra en segundo plano. Ahora, accedamos al contenedor y ejecutemos las migraciones. Pero... ¿cómo se llama mi contenedor?&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate que Docker asigna un nombre aleatorio a tu contenedor web porque no definiste ninguno. Una vez identificado el nombre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it &amp;lt;nombre&amp;gt; bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
What? ¿Qué pasa? ¿Otra vez error de conexión con la base? Si tenemos el contenedor funcionando, esto es un jaleo...&lt;br /&gt;
&lt;br /&gt;
== Redes internas ==&lt;br /&gt;
&lt;br /&gt;
Ahora mismo tienes dos contenedores aislados, el de la app y el la base de datos. Por defecto, cada contenedor tiene su propia red interna y no puede ver a los demás por nombre. El hostname '''db''' que usa Flask no existe en su DNS interno.&lt;br /&gt;
&lt;br /&gt;
=== Conectar servicios ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una red interna que compartan ambos contenedores&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker network create uvlhub_network&lt;br /&gt;
docker network ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vemos que ya aparece nuestra red '''uvlhub_network'''. Ahora, conectemos el contenedor de MariaDB a la red:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Claro, si te fijas, ahora hay que añadir, además de toda esa configuración engorrosa, que se añada en la misma red y que además tenga de hostname &amp;quot;db&amp;quot; para que el contenedor web entienda qué es &amp;quot;db&amp;quot;. Tenemos que hacer lo mismo para el contenedor web:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop &amp;lt;nombre&amp;gt;&lt;br /&gt;
docker rm &amp;lt;nombre&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y lo levantamos de nuevo, pero ya en la nueva red. De paso, le ponemos un nombre para identificarlo mejor.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 --name web_app_container --network uvlhub_network -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y ahora ya podemos acceder al contenedor por su nombre y ejecutar las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Mejor?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 3: Persistencia =&lt;br /&gt;
&lt;br /&gt;
Vamos a simular que hemos desplegado justo esto en producción y el servidor se reinicia por un corte de luz. Reiniciemos a mano cada contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker restart web_app_container&lt;br /&gt;
docker restart mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. What? ¿Cómo? ¿Qué ha pasado? ¿Otra vez problema de conexión? Ve dándole a F5... F5... ¡bingo! Ahora sí sale, pero menuda aleatoriedad, ¿no? Claro, es que ahora caes en que una cosa es que el contenedor haya arrancado y otra que esté listo.&lt;br /&gt;
&lt;br /&gt;
Aparte, tenemos otro problema. ¿Qué pasa con la persistencia? Porque si borro el contenedor de MariaDB, todos los datos que estén en su interior también se pierden. Y si estamos en producción, lío asegurado.&lt;br /&gt;
&lt;br /&gt;
== Volúmenes ==&lt;br /&gt;
&lt;br /&gt;
Cada vez que eliminas el contenedor de MariaDB, todas las bases de datos se pierden. Esto ocurre porque los datos se guardan dentro del sistema de archivos del contenedor, y al borrarlo desaparecen con él.&lt;br /&gt;
&lt;br /&gt;
En este ejercicio creas un volumen de Docker para que los datos persistan entre ejecuciones.&lt;br /&gt;
&lt;br /&gt;
=== Crear un volumen ===&lt;br /&gt;
Un volumen es una carpeta gestionada por Docker que vive fuera del ciclo de vida de los contenedores.&lt;br /&gt;
&lt;br /&gt;
Ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume create mariadb_data&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba que existe:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
La salida muestra algo como:&lt;br /&gt;
&lt;br /&gt;
DRIVER    VOLUME NAME&lt;br /&gt;
local     mariadb_data&lt;br /&gt;
&lt;br /&gt;
=== Crear un contenedor de MariaDB con el volumen ===&lt;br /&gt;
Lanza el contenedor y conecta el volumen a la ruta donde MariaDB guarda los datos (/var/lib/mysql):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -v mariadb_data:/var/lib/mysql \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Qué significa &amp;quot;:&amp;quot; en los volúmenes ===&lt;br /&gt;
&lt;br /&gt;
Cuando usas la opción '''-v''' o '''--volume''' en Docker, el signo dos puntos (''':''') separa dos rutas:&lt;br /&gt;
&lt;br /&gt;
'''-v origen:destino'''&lt;br /&gt;
&lt;br /&gt;
origen → es la ruta o el volumen en tu máquina (el host).&lt;br /&gt;
&lt;br /&gt;
destino → es la ruta dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 4: Orquestación de servicios =&lt;br /&gt;
&lt;br /&gt;
Hasta ahora hemos creado y conectado los contenedores manualmente.&lt;br /&gt;
Ahora vas a usar Docker Compose para definir todo en un solo archivo.&lt;br /&gt;
&lt;br /&gt;
== Orquestación básica ==&lt;br /&gt;
&lt;br /&gt;
Crea un archivo llamado docker-compose.yml en la carpeta '''docker''' con este contenido:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    container_name: web_app_container&lt;br /&gt;
    env_file:&lt;br /&gt;
      - ../.env&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;5000&amp;quot;&lt;br /&gt;
    depends_on:&lt;br /&gt;
      - db&lt;br /&gt;
    build:&lt;br /&gt;
      context: ../&lt;br /&gt;
      dockerfile: docker/images/Dockerfile&lt;br /&gt;
    networks:&lt;br /&gt;
      - uvlhub_network&lt;br /&gt;
&lt;br /&gt;
  db:&lt;br /&gt;
    container_name: mariadb_container&lt;br /&gt;
    image: mariadb:12.0.2&lt;br /&gt;
    env_file:&lt;br /&gt;
      - ../.env&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;3306:3306&amp;quot;&lt;br /&gt;
    volumes:&lt;br /&gt;
      - db_data:/var/lib/mysql&lt;br /&gt;
    networks:&lt;br /&gt;
      - uvlhub_network&lt;br /&gt;
&lt;br /&gt;
volumes:&lt;br /&gt;
  db_data:&lt;br /&gt;
&lt;br /&gt;
networks:&lt;br /&gt;
  uvlhub_network:&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate que esta orquestación de servicios me permite definir el nombre de cada servicio, su dependencia, sus puertos, sus volúmenes, redes... todo lo que hacíamos de forma manual y propenso a errores queda ahora más fácil de gestionar. Además, ahora ambos contenedores comparten el mismo archivo .env de variables de entorno.&lt;br /&gt;
&lt;br /&gt;
Bien, ahora vamos a levantar estos contenedores y acceder al contenedor web:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Inmediatamente después, ejecuta las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué ha ocurrido? Depende del tiempo que hayas tardado, puede que haya saltado un error o puede que haya funcionado. Puede que hayas intentado varias veces el comando anterior hasta que ha funcionado. ¿Por qué crees que ocurre esta aparete aleatoriedad?&lt;br /&gt;
&lt;br /&gt;
== Dependencias entre contenedores y entrypoints ==&lt;br /&gt;
&lt;br /&gt;
Fíjate que tenemos un &amp;quot;depends on&amp;quot; en el servicio web. Significa que el servicio &amp;quot;db&amp;quot; debe iniciarse antes que el servicio web. Pero, cuidado, ahí está la trampa, porque &amp;quot;servicio iniciado != servicio preparado&amp;quot;. El servicio de MariaDB tiene que arrancar, configurarse, etc...&lt;br /&gt;
&lt;br /&gt;
Cuando hicimos el comando COPY en nuestro Dockerfile, también copiamos la carpeta '''scripts'''. Uno de los scripts es '''wait-for-db.sh''', que realiza continuamente llamadas a MariaDB hasta que este servicio está listo para recibir peticiones. Ábrelo y estúdialo.&lt;br /&gt;
&lt;br /&gt;
Añade al servicio web de tu docker compose las siguientes líneas:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
entrypoint: [&amp;quot;/app/scripts/wait-for-db.sh&amp;quot;]&lt;br /&gt;
command: [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A su vez, del Dockerfile, hay que eliminar la línea de CMD que arrancaba nuestro servidor Flask.&lt;br /&gt;
&lt;br /&gt;
Luego, reinicializa todos los servicios:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. What? ¿No abre? Vamos a investigarlo, veamos el logs:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker logs web_app_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Nos debe salir un mensaje eterno de &amp;quot;/app/scripts/wait-for-db.sh: line 18: mariadb: command not found&amp;quot;. Esto ocurre porque en la imagen de nuestro servicio web NO TENEMOS el cliente de MariaDB. Debemos indicar entonces que lo instale. Añade justo debajo de FROM lo siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y --no-install-recommends mariadb-client &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Al ser un contenedor basado en Linux, y accediendo como root, es como si actualizáramos todos los paquetes y luego instalásemos el cliente de MariaDB. Intentémoslo otra vez:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. ¡Jopelines! ¿Tampoco? Si haces un &amp;quot;docker ps&amp;quot; verás el problema. El contenedor web no tiene expuesto ningún puerto.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    (...)&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;5000&amp;quot;&lt;br /&gt;
    (...)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;quot;Expose&amp;quot; hace visible el puerto '''entre contenedores''', pero '''NO entre tu contenedor y el host'''. Para eso usamos la directiva &amp;quot;ports&amp;quot;, que se encarga del mapping. En nuestro caso, será:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    (...)&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;5000:5000&amp;quot;&lt;br /&gt;
    (...)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
En ocasiones, si los dos puertos coinciden, se puede dejar uno solo con &amp;quot;5000&amp;quot; pero dejaremos los dos. El de la izquierda siempre es el puerto externo (host), el de la derecha es el puerto interno (contenedor). Así que cambia ese detalle y por última vez:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. ¡Ahora sí!&lt;br /&gt;
&lt;br /&gt;
== Trabajando con CMD, entrypoints y commands ==&lt;br /&gt;
&lt;br /&gt;
Piensa que al arrancar un contenedor Docker, siempre se ejecuta un único comando final.&lt;br /&gt;
Lo que hacen ENTRYPOINT, CMD y command (de Compose) es decidir qué comando será ese y con qué argumentos.&lt;br /&gt;
&lt;br /&gt;
=== CMD → “qué hacer por defecto” ===&lt;br /&gt;
&lt;br /&gt;
* Está en el Dockerfile.&lt;br /&gt;
* Es el comando que Docker ejecuta si no se indica otra cosa. En nuestro caso:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== ENTRYPOINT → “qué se ejecuta siempre” ===&lt;br /&gt;
&lt;br /&gt;
* También va en el Dockerfile, o se puede sobreescribir en el docker-compose.yml.&lt;br /&gt;
* Define el programa o script principal que siempre se ejecuta.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
ENTRYPOINT [&amp;quot;/app/scripts/wait-for-db.sh&amp;quot;]&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Normalmente, el entrypoint se combina con CMD en modo &amp;quot;eh, ejecuta esto (entrypoint) y luego este comando final (cmd)&lt;br /&gt;
&lt;br /&gt;
Los entrypoints son muy útiles, sobre todo en los docker-compose's, porque puedo redefinir cómo quiero que inicie un contenedor en concreto '''SIN TENER QUE HACER UN BUILD DE NUEVO DE LA IMAGEN DE DOCKER'''. A diferencia de CMD, que viene predefinido, un entrypoint en mi docker-compose me da más versatilidad&lt;br /&gt;
&lt;br /&gt;
=== command (de docker-compose.yml) → “sobrescribe el CMD” ===&lt;br /&gt;
&lt;br /&gt;
En docker-compose.yml, el campo command: reemplaza al CMD del Dockerfile,pero no al ENTRYPOINT.&lt;br /&gt;
&lt;br /&gt;
== Combinación de elementos ==&lt;br /&gt;
&lt;br /&gt;
Es importante entender cómo Docker trabaja con la orquestación y con los contenedores:&lt;br /&gt;
&lt;br /&gt;
* Lee el YAML como un diccionario&lt;br /&gt;
* Ejecuta todo lo que haya en un Dockerfile EXCEPTUANDO directivas ENTRYPOINT y CMD&lt;br /&gt;
* Monta el volumen o los volúmenes de ese contenedor (si procede)&lt;br /&gt;
&lt;br /&gt;
Y ahora viene el detalle:&lt;br /&gt;
&lt;br /&gt;
* Si estamos levantando un contenedor a través de docker-compose, va a ejecutar los entrypoints y commands definidos en ese docker-compose.&lt;br /&gt;
* Un entrypoint definido en un docker-compose va a sustituir a un entrypoint definido en el Dockerfile&lt;br /&gt;
* Un command definido en un docker-compose va a sustituir a un CMD definido en el Dockerfile.&lt;br /&gt;
&lt;br /&gt;
Como consejo:&lt;br /&gt;
&lt;br /&gt;
* Definimos un entrypoint en el docker-compose. Esto es así porque está fuera de la imagen de Docker y nos permite redefinir el inicio del servicio sin modificar la imagen&lt;br /&gt;
* Eventualmente, definimos un CMD donde indicamos el comando final de ese contenedor al iniciar, que suele ser el arranque de Flask Server o de Gunicorn, dependiendo de si estamos en desarrollo o producción.&lt;br /&gt;
&lt;br /&gt;
No obstante, como nuestros entrypoints son todos scripts en bash, podemos definir en el mismo el arranque final (comando final) y nos olvidamos de CMD.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 5: Volumen de trabajo =&lt;br /&gt;
&lt;br /&gt;
== Problema de desarrollo ==&lt;br /&gt;
&lt;br /&gt;
Vale, ya parece que le hemos pillado el tranquillo a Docker y a Docker Compose. Ahora vamos a hacer algo como desarrolladores. Ve al archivo &amp;quot;app/templates/base_templates.html&amp;quot; y cambia la línea 22:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;title&amp;gt;{{ FLASK_APP_NAME }} - Repository of feature models in UVL &amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
por:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;title&amp;gt;Hello world! &amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. Fíjate en el nombre de la pestaña del navegador, ¿ha cambiado? ¿No? Tsss, esto de los contenedores... bueno, va, vamos a reiniciarlo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker restart web_app_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. ¿Tampoco? Anda que vaya idea la de usar Docker... Claro, es a la hora de construir la imagen, hicimos &amp;quot;COPY . .&amp;quot;, pero no hemos reconstruido nada, así que...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml build --no-cache web&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --force-recreate web&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Importante señalar que le pedimos que ignore la caché para que fuerce a copiar de nuevo todo el contenido (incluyendo el cambio de línea) al interior del contenedor.&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. ¡Ahora sí! Pero... ¿tengo que reconstruir TODO el contenedor en cada cambio línea? ¿Y eso es trabajar de forma ágil?&lt;br /&gt;
&lt;br /&gt;
== Crear volumen de trabajo (bind mount) ==&lt;br /&gt;
&lt;br /&gt;
Evidentemente, eso no tiene sentido, tenemos que usar un volumen de trabajo que nos ayude a sincronizar el código de nuestro host con el código interno del contenedor. Luego en nuestro docker-compose:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  web:&lt;br /&gt;
    (....)&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;5000:5000&amp;quot;&lt;br /&gt;
    volumes:&lt;br /&gt;
      - ../:/app&lt;br /&gt;
    (....)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
La línea&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
- ../:/app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Monta tu carpeta local del proyecto (../) dentro del contenedor, (/app). ¿Y por qué /app? Porque fue el espacio de trabajo que definimos en el Dockerfile. Como ves, la congruencia entre los Dockerfile y el docker-compose es importante.&lt;br /&gt;
&lt;br /&gt;
Sin embargo, no nos podemos olvidar que debemos añadirlo también como volumen al final del docker-compose:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ahora ya podemos trabajar con el código en host sin tener que reconstruir la imagen a cada cambio de línea.&lt;br /&gt;
&lt;br /&gt;
Un '''bind mount''' (o “montaje de enlace”) es un tipo de volumen en el que Docker conecta directamente una carpeta de tu máquina (el host) con una carpeta dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
== Cuidado con COPY y los bind mounts ==&lt;br /&gt;
&lt;br /&gt;
Cuando usas un bind mount (por ejemplo ../:/app), Docker monta tu carpeta local sobre la ruta /app del contenedor.&lt;br /&gt;
Eso significa que todo lo que estaba en /app dentro de la imagen desaparece al arrancar el contenedor, porque queda “oculto” por el volumen.&lt;br /&gt;
&lt;br /&gt;
Ahora bien, durante la construcción de la imagen usamos la directiva COPY. ¿Por qué? Porque necesitamos el archivo requirements.txt para instalar las dependencias de Python del proyecto. Pero... ¿realmente necesitamos copiar TODO? No, claro que no, podemos optar por copiar exclusivamente aquellos archivos que nos hagan falta durante '''LA CONSTRUCCIÓN''' de la imagen. El bind mount luego lo machacará.&lt;br /&gt;
&lt;br /&gt;
'''Muy importante:''' los bind mounts, al igual que cualquier otro volumen, están disponibles '''DESPUÉS''' de la construcción de la imagen. Es un error común creer que porque definamos un bind mount en un docker-compose, esos archivos aparecen ya para el contenedor. '''Esto es una fuente inagotable de problemas en la práctica'''.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 6: Distintas configuraciones de despliegue =&lt;br /&gt;
&lt;br /&gt;
1: Explicar las distintas imágenes, entrypoints y docker-compose.ymls&lt;br /&gt;
2: Explicar el nginx&lt;br /&gt;
3: Explicar que cuanto más se parezca el despliegue en dev y en prod, menos propenso a errores seremos&lt;br /&gt;
4: Explicar diferencia entre flask server y gunicorn. Explicar que, dado que es una configuración distinta en dev y en prod, necesitamos siempre una máquina de preproducción&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10157</id>
		<title>Tutorial-docker</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10157"/>
				<updated>2025-10-12T17:41:24Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Ejercicio 1: Empezamos desde cero =&lt;br /&gt;
&lt;br /&gt;
El objetivo es eliminar toda la configuración previa y crear desde cero la estructura de carpetas que contendrá nuestros futuros archivos Docker.&lt;br /&gt;
&lt;br /&gt;
== Eliminar la configuración existente ==&lt;br /&gt;
Antes de comenzar, asegúrate de borrar cualquier carpeta Docker existente en el proyecto. En este caso, le haremos una copia de seguridad para poder volver a ella.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mv docker docker.bk&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
También debes asegurarte que trabajas con las variables de entorno adecuadas:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
cp .env.docker.example .env&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
⚠️ Este paso es importante: partimos completamente desde cero para entender cada parte del sistema.&lt;br /&gt;
&lt;br /&gt;
== Crear un Dockerfile mínimo ==&lt;br /&gt;
&lt;br /&gt;
Ahora crea nuevamente la carpeta base donde almacenaremos los archivos relacionados con Docker:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt; mkdir -p docker/images &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vamos a explicar los distintos apartados:&lt;br /&gt;
&lt;br /&gt;
=== FROM ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Indica la imagen base sobre la que se construirá la tuya.&lt;br /&gt;
En este caso, parte de una imagen ligera que ya tiene Python 3.12 instalado.&lt;br /&gt;
&lt;br /&gt;
=== WORKDIR ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta línea define el directorio de trabajo dentro del contenedor.&lt;br /&gt;
A partir de aquí, todos los comandos que aparezcan después (COPY, RUN, CMD, etc.) se ejecutarán dentro de /app.&lt;br /&gt;
&lt;br /&gt;
Ojo: /app no es la carpeta app/ de tu proyecto local.&lt;br /&gt;
Son cosas distintas.&lt;br /&gt;
&lt;br /&gt;
En tu máquina puede existir algo como:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
uvlhub&lt;br /&gt;
   app&lt;br /&gt;
   core&lt;br /&gt;
   requirements.txt &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Pero dentro del contenedor la estructura será distinta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt; / app/ app/ ← aquí dentro se habrá copiado tu carpeta local &amp;quot;app&amp;quot; &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Cuando más adelante uses:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Docker copiará todos los archivos de tu proyecto dentro de la carpeta /app del contenedor.&lt;br /&gt;
Por eso el código quedará en /app/app/.&lt;br /&gt;
&lt;br /&gt;
=== COPY ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Copia todos los archivos del proyecto (del host) dentro del contenedor, en /app.&lt;br /&gt;
El primer punto es el origen; el segundo, el destino.&lt;br /&gt;
&lt;br /&gt;
=== RUN ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ejecuta comandos durante la construcción de la imagen.&lt;br /&gt;
Aquí instalamos las dependencias necesarias desde requirements.txt.&lt;br /&gt;
El resultado de este paso quedará guardado en la imagen.&lt;br /&gt;
&lt;br /&gt;
=== EXPOSE ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Documenta el puerto que la aplicación usa dentro del contenedor.&lt;br /&gt;
Esto no lo abre al exterior; simplemente indica que la app escucha en el 5000.&lt;br /&gt;
&lt;br /&gt;
=== CMD ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Define el comando que se ejecutará cuando el contenedor se inicie.&lt;br /&gt;
En este caso, ejecutará la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
=== RUN vs CMD ===&lt;br /&gt;
&lt;br /&gt;
Estas dos instrucciones parecen similares, pero hacen cosas muy distintas.&lt;br /&gt;
&lt;br /&gt;
==== RUN ====&lt;br /&gt;
Se ejecuta durante la construcción de la imagen (en el momento del docker build).&lt;br /&gt;
Cada RUN crea una nueva capa dentro de la imagen.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando instala las dependencias una sola vez, cuando se construye la imagen.&lt;br /&gt;
El resultado (las librerías instaladas) queda grabado dentro de la imagen final.&lt;br /&gt;
&lt;br /&gt;
Si después lanzas un contenedor nuevo a partir de esa imagen, las dependencias ya estarán instaladas y no se volverán a ejecutar.&lt;br /&gt;
&lt;br /&gt;
==== CMD ====&lt;br /&gt;
Se ejecuta cuando se inicia el contenedor (en el docker run).&lt;br /&gt;
Define el proceso principal que se ejecutará dentro del contenedor mientras esté activo.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando no se ejecuta durante la construcción, sino cada vez que arrancas el contenedor.&lt;br /&gt;
Si detienes el contenedor, este proceso también se detiene.&lt;br /&gt;
&lt;br /&gt;
==== Diferencia resumida ====&lt;br /&gt;
&lt;br /&gt;
RUN → se ejecuta al construir la imagen (fase de docker build).&lt;br /&gt;
&lt;br /&gt;
CMD → se ejecuta al iniciar el contenedor (fase de docker run).&lt;br /&gt;
&lt;br /&gt;
Solo puede haber un CMD por Dockerfile.&lt;br /&gt;
Si hay más de uno, solo se usará el último.&lt;br /&gt;
&lt;br /&gt;
== Dockerfile completo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
COPY . .&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto tiene que ir en un Dockerfile en docker/images&lt;br /&gt;
&lt;br /&gt;
== Construir la imagen ==&lt;br /&gt;
Desde la raíz del proyecto (no dentro de docker/images):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker build -t uvlhub:dev -f docker/images/Dockerfile.dev .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Ejecutar el contenedor ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Qué es lo que observas?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 2: Conectando servicios =&lt;br /&gt;
&lt;br /&gt;
¡Vaya! ¡No funciona! Can't connect to MySQL server on 'db'... ¡Ah, claro, intenta conectarse a la base de datos, que no lo tenemos! Pero claro, en un contenedor no es recomendable tener la app y la base de datos conviviendo...&lt;br /&gt;
&lt;br /&gt;
== Crear contenedor MariaDB ==&lt;br /&gt;
&lt;br /&gt;
Antes que nada, haz Ctrl + C para parar el contenedor web. En este ejercicio aprenderás a lanzar un contenedor de MariaDB 12.0.2 de forma manual, configurando todas las variables necesarias para tu aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
El objetivo es entender la complejidad de hacerlo “a mano”, antes de automatizarlo con docker-compose.&lt;br /&gt;
&lt;br /&gt;
== Descargar la imagen oficial ==&lt;br /&gt;
Primero descarga la versión exacta que usaremos:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker pull mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Crear el contenedor de MariaDB ==&lt;br /&gt;
Lanza un contenedor con todas las variables necesarias para que Flask pueda conectarse correctamente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d --name mariadb_container \&lt;br /&gt;
 -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
 -e FLASK_ENV=development \&lt;br /&gt;
 -e DOMAIN=localhost \&lt;br /&gt;
 -e MARIADB_HOSTNAME=db \&lt;br /&gt;
 -e MARIADB_PORT=3306 \&lt;br /&gt;
 -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
 -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
 -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
 -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
 -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
 -e WORKING_DIR=/app/ \&lt;br /&gt;
 -p 3306:3306 mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Explicación:&lt;br /&gt;
&lt;br /&gt;
-d: ejecuta el contenedor en segundo plano (modo detached).&lt;br /&gt;
&lt;br /&gt;
--name: asigna un nombre identificable al contenedor.&lt;br /&gt;
&lt;br /&gt;
-e: define variables de entorno dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
-p 3306:3306: publica el puerto de MariaDB en tu máquina local.&lt;br /&gt;
&lt;br /&gt;
mariadb:12.0.2: imagen exacta que vamos a usar.&lt;br /&gt;
&lt;br /&gt;
Aquí ya, de entrada, vemos que esto es más feo que una nevera por detrás. Para empezar, estamos pasando a lo loco un montón de variables que difílmente será replicable en producción, por contener información sensible. Aparte, si metemos nuevas variables, ese comando queda inservible. Necesitaremos una solución más ágil para este problema&lt;br /&gt;
&lt;br /&gt;
== Comprobar que está corriendo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver algo así:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES&lt;br /&gt;
abcd1234efgh mariadb:12.0.2 &amp;quot;docker-entrypoint.s…&amp;quot; Up 5 seconds 0.0.0.0:3306-&amp;gt;3306/tcp mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Conectarse a la base de datos ==&lt;br /&gt;
Abre una consola dentro del contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it mariadb_container bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y dentro, accede al cliente de MariaDB:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mariadb -u root -p&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Contraseña: uvlhubdb_root_password&lt;br /&gt;
&lt;br /&gt;
== Verificar la base de datos ==&lt;br /&gt;
Una vez dentro del cliente MySQL, ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
SHOW DATABASES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver que existen la bases de datos uvlhubdb porque pedimos crearla a la hora de levantar el contenedor. ¿Qué tiene la base de datos dentro? Vamos a verlo...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
USE uvlhubdb;&lt;br /&gt;
SHOW TABLES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Un total de... 0 tablas... ¡Claro! Es que solo hemos creado la base en sí, pero no las tablas. ¿Con un script SQL tal vez? ¡Espera, espera, si tenemos las migraciones! Pero claro, para eso necesito el contenedor de la app levantado. Sal de la consola SQL y del contenedor de MariaDB. Bien, ahora en el host...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta vez con el flag &amp;quot;-d&amp;quot; para que corra en segundo plano. Ahora, accedamos al contenedor y ejecutemos las migraciones. Pero... ¿cómo se llama mi contenedor?&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate que Docker asigna un nombre aleatorio a tu contenedor web porque no definiste ninguno. Una vez identificado el nombre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it &amp;lt;nombre&amp;gt; bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
What? ¿Qué pasa? ¿Otra vez error de conexión con la base? Si tenemos el contenedor funcionando, esto es un jaleo...&lt;br /&gt;
&lt;br /&gt;
== Redes internas ==&lt;br /&gt;
&lt;br /&gt;
Ahora mismo tienes dos contenedores aislados, el de la app y el la base de datos. Por defecto, cada contenedor tiene su propia red interna y no puede ver a los demás por nombre. El hostname '''db''' que usa Flask no existe en su DNS interno.&lt;br /&gt;
&lt;br /&gt;
=== Conectar servicios ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una red interna que compartan ambos contenedores&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker network create uvlhub_network&lt;br /&gt;
docker network ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vemos que ya aparece nuestra red '''uvlhub_network'''. Ahora, conectemos el contenedor de MariaDB a la red:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Claro, si te fijas, ahora hay que añadir, además de toda esa configuración engorrosa, que se añada en la misma red y que además tenga de hostname &amp;quot;db&amp;quot; para que el contenedor web entienda qué es &amp;quot;db&amp;quot;. Tenemos que hacer lo mismo para el contenedor web:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop &amp;lt;nombre&amp;gt;&lt;br /&gt;
docker rm &amp;lt;nombre&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y lo levantamos de nuevo, pero ya en la nueva red. De paso, le ponemos un nombre para identificarlo mejor.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 --name web_app_container --network uvlhub_network -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y ahora ya podemos acceder al contenedor por su nombre y ejecutar las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Mejor?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 3: Persistencia =&lt;br /&gt;
&lt;br /&gt;
Vamos a simular que hemos desplegado justo esto en producción y el servidor se reinicia por un corte de luz. Reiniciemos a mano cada contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker restart web_app_container&lt;br /&gt;
docker restart mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. What? ¿Cómo? ¿Qué ha pasado? ¿Otra vez problema de conexión? Ve dándole a F5... F5... ¡bingo! Ahora sí sale, pero menuda aleatoriedad, ¿no? Claro, es que ahora caes en que una cosa es que el contenedor haya arrancado y otra que esté listo.&lt;br /&gt;
&lt;br /&gt;
Aparte, tenemos otro problema. ¿Qué pasa con la persistencia? Porque si borro el contenedor de MariaDB, todos los datos que estén en su interior también se pierden. Y si estamos en producción, lío asegurado.&lt;br /&gt;
&lt;br /&gt;
== Volúmenes ==&lt;br /&gt;
&lt;br /&gt;
Cada vez que eliminas el contenedor de MariaDB, todas las bases de datos se pierden. Esto ocurre porque los datos se guardan dentro del sistema de archivos del contenedor, y al borrarlo desaparecen con él.&lt;br /&gt;
&lt;br /&gt;
En este ejercicio creas un volumen de Docker para que los datos persistan entre ejecuciones.&lt;br /&gt;
&lt;br /&gt;
=== Crear un volumen ===&lt;br /&gt;
Un volumen es una carpeta gestionada por Docker que vive fuera del ciclo de vida de los contenedores.&lt;br /&gt;
&lt;br /&gt;
Ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume create mariadb_data&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba que existe:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
La salida muestra algo como:&lt;br /&gt;
&lt;br /&gt;
DRIVER    VOLUME NAME&lt;br /&gt;
local     mariadb_data&lt;br /&gt;
&lt;br /&gt;
=== Crear un contenedor de MariaDB con el volumen ===&lt;br /&gt;
Lanza el contenedor y conecta el volumen a la ruta donde MariaDB guarda los datos (/var/lib/mysql):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -v mariadb_data:/var/lib/mysql \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Qué significa &amp;quot;:&amp;quot; en los volúmenes ===&lt;br /&gt;
&lt;br /&gt;
Cuando usas la opción '''-v''' o '''--volume''' en Docker, el signo dos puntos (''':''') separa dos rutas:&lt;br /&gt;
&lt;br /&gt;
'''-v origen:destino'''&lt;br /&gt;
&lt;br /&gt;
origen → es la ruta o el volumen en tu máquina (el host).&lt;br /&gt;
&lt;br /&gt;
destino → es la ruta dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 4: Orquestación de servicios =&lt;br /&gt;
&lt;br /&gt;
Hasta ahora hemos creado y conectado los contenedores manualmente.&lt;br /&gt;
Ahora vas a usar Docker Compose para definir todo en un solo archivo.&lt;br /&gt;
&lt;br /&gt;
== Orquestación básica ==&lt;br /&gt;
&lt;br /&gt;
Crea un archivo llamado docker-compose.yml en la carpeta '''docker''' con este contenido:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    container_name: web_app_container&lt;br /&gt;
    env_file:&lt;br /&gt;
      - ../.env&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;5000&amp;quot;&lt;br /&gt;
    depends_on:&lt;br /&gt;
      - db&lt;br /&gt;
    build:&lt;br /&gt;
      context: ../&lt;br /&gt;
      dockerfile: docker/images/Dockerfile&lt;br /&gt;
    networks:&lt;br /&gt;
      - uvlhub_network&lt;br /&gt;
&lt;br /&gt;
  db:&lt;br /&gt;
    container_name: mariadb_container&lt;br /&gt;
    image: mariadb:12.0.2&lt;br /&gt;
    env_file:&lt;br /&gt;
      - ../.env&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;3306:3306&amp;quot;&lt;br /&gt;
    volumes:&lt;br /&gt;
      - db_data:/var/lib/mysql&lt;br /&gt;
    networks:&lt;br /&gt;
      - uvlhub_network&lt;br /&gt;
&lt;br /&gt;
volumes:&lt;br /&gt;
  db_data:&lt;br /&gt;
&lt;br /&gt;
networks:&lt;br /&gt;
  uvlhub_network:&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate que esta orquestación de servicios me permite definir el nombre de cada servicio, su dependencia, sus puertos, sus volúmenes, redes... todo lo que hacíamos de forma manual y propenso a errores queda ahora más fácil de gestionar. Además, ahora ambos contenedores comparten el mismo archivo .env de variables de entorno.&lt;br /&gt;
&lt;br /&gt;
Bien, ahora vamos a levantar estos contenedores y acceder al contenedor web:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Inmediatamente después, ejecuta las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué ha ocurrido? Depende del tiempo que hayas tardado, puede que haya saltado un error o puede que haya funcionado. Puede que hayas intentado varias veces el comando anterior hasta que ha funcionado. ¿Por qué crees que ocurre esta aparete aleatoriedad?&lt;br /&gt;
&lt;br /&gt;
== Dependencias entre contenedores y entrypoints ==&lt;br /&gt;
&lt;br /&gt;
Fíjate que tenemos un &amp;quot;depends on&amp;quot; en el servicio web. Significa que el servicio &amp;quot;db&amp;quot; debe iniciarse antes que el servicio web. Pero, cuidado, ahí está la trampa, porque &amp;quot;servicio iniciado != servicio preparado&amp;quot;. El servicio de MariaDB tiene que arrancar, configurarse, etc...&lt;br /&gt;
&lt;br /&gt;
Cuando hicimos el comando COPY en nuestro Dockerfile, también copiamos la carpeta '''scripts'''. Uno de los scripts es '''wait-for-db.sh''', que realiza continuamente llamadas a MariaDB hasta que este servicio está listo para recibir peticiones. Ábrelo y estúdialo.&lt;br /&gt;
&lt;br /&gt;
Añade al servicio web de tu docker compose las siguientes líneas:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
entrypoint: [&amp;quot;/app/scripts/wait-for-db.sh&amp;quot;]&lt;br /&gt;
command: [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A su vez, del Dockerfile, hay que eliminar la línea de CMD que arrancaba nuestro servidor Flask.&lt;br /&gt;
&lt;br /&gt;
Luego, reinicializa todos los servicios:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. What? ¿No abre? Vamos a investigarlo, veamos el logs:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker logs web_app_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Nos debe salir un mensaje eterno de &amp;quot;/app/scripts/wait-for-db.sh: line 18: mariadb: command not found&amp;quot;. Esto ocurre porque en la imagen de nuestro servicio web NO TENEMOS el cliente de MariaDB. Debemos indicar entonces que lo instale. Añade justo debajo de FROM lo siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y --no-install-recommends mariadb-client &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Al ser un contenedor basado en Linux, y accediendo como root, es como si actualizáramos todos los paquetes y luego instalásemos el cliente de MariaDB. Intentémoslo otra vez:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. ¡Jopelines! ¿Tampoco? Si haces un &amp;quot;docker ps&amp;quot; verás el problema. El contenedor web no tiene expuesto ningún puerto.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    (...)&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;5000&amp;quot;&lt;br /&gt;
    (...)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;quot;Expose&amp;quot; hace visible el puerto '''entre contenedores''', pero '''NO entre tu contenedor y el host'''. Para eso usamos la directiva &amp;quot;ports&amp;quot;, que se encarga del mapping. En nuestro caso, será:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    (...)&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;5000:5000&amp;quot;&lt;br /&gt;
    (...)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
En ocasiones, si los dos puertos coinciden, se puede dejar uno solo con &amp;quot;5000&amp;quot; pero dejaremos los dos. El de la izquierda siempre es el puerto externo (host), el de la derecha es el puerto interno (contenedor). Así que cambia ese detalle y por última vez:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. ¡Ahora sí!&lt;br /&gt;
&lt;br /&gt;
=== Trabajando con CMD, entrypoints y commands ===&lt;br /&gt;
&lt;br /&gt;
Explicar utilidad de los entrypoints (puedo configurar el arranque sin tener que modificar la imagen)&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 5: Volumen de trabajo =&lt;br /&gt;
&lt;br /&gt;
avisar del problema del código del contenedor y del uso de un volumen para eso&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 6: Distintas configuraciones de despliegue =&lt;br /&gt;
&lt;br /&gt;
1: Explicar las distintas imágenes, entrypoints y docker-compose.ymls&lt;br /&gt;
2: Explicar el nginx&lt;br /&gt;
3: Explicar que cuanto más se parezca el despliegue en dev y en prod, menos propenso a errores seremos&lt;br /&gt;
4: Explicar diferencia entre flask server y gunicorn. Explicar que, dado que es una configuración distinta en dev y en prod, necesitamos siempre una máquina de preproducción&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10156</id>
		<title>Tutorial-docker</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10156"/>
				<updated>2025-10-12T16:45:23Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: /* Ejercicio 4: Orquestación de servicios */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Ejercicio 1: Empezamos desde cero =&lt;br /&gt;
&lt;br /&gt;
El objetivo es eliminar toda la configuración previa y crear desde cero la estructura de carpetas que contendrá nuestros futuros archivos Docker.&lt;br /&gt;
&lt;br /&gt;
== Eliminar la configuración existente ==&lt;br /&gt;
Antes de comenzar, asegúrate de borrar cualquier carpeta Docker existente en el proyecto. En este caso, le haremos una copia de seguridad para poder volver a ella.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mv docker docker.bk&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
También debes asegurarte que trabajas con las variables de entorno adecuadas:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
cp .env.docker.example .env&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
⚠️ Este paso es importante: partimos completamente desde cero para entender cada parte del sistema.&lt;br /&gt;
&lt;br /&gt;
== Crear un Dockerfile mínimo ==&lt;br /&gt;
&lt;br /&gt;
Ahora crea nuevamente la carpeta base donde almacenaremos los archivos relacionados con Docker:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt; mkdir -p docker/images &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vamos a explicar los distintos apartados:&lt;br /&gt;
&lt;br /&gt;
=== FROM ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Indica la imagen base sobre la que se construirá la tuya.&lt;br /&gt;
En este caso, parte de una imagen ligera que ya tiene Python 3.12 instalado.&lt;br /&gt;
&lt;br /&gt;
=== WORKDIR ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta línea define el directorio de trabajo dentro del contenedor.&lt;br /&gt;
A partir de aquí, todos los comandos que aparezcan después (COPY, RUN, CMD, etc.) se ejecutarán dentro de /app.&lt;br /&gt;
&lt;br /&gt;
Ojo: /app no es la carpeta app/ de tu proyecto local.&lt;br /&gt;
Son cosas distintas.&lt;br /&gt;
&lt;br /&gt;
En tu máquina puede existir algo como:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
uvlhub&lt;br /&gt;
   app&lt;br /&gt;
   core&lt;br /&gt;
   requirements.txt &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Pero dentro del contenedor la estructura será distinta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt; / app/ app/ ← aquí dentro se habrá copiado tu carpeta local &amp;quot;app&amp;quot; &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Cuando más adelante uses:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Docker copiará todos los archivos de tu proyecto dentro de la carpeta /app del contenedor.&lt;br /&gt;
Por eso el código quedará en /app/app/.&lt;br /&gt;
&lt;br /&gt;
=== COPY ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Copia todos los archivos del proyecto (del host) dentro del contenedor, en /app.&lt;br /&gt;
El primer punto es el origen; el segundo, el destino.&lt;br /&gt;
&lt;br /&gt;
=== RUN ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ejecuta comandos durante la construcción de la imagen.&lt;br /&gt;
Aquí instalamos las dependencias necesarias desde requirements.txt.&lt;br /&gt;
El resultado de este paso quedará guardado en la imagen.&lt;br /&gt;
&lt;br /&gt;
=== EXPOSE ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Documenta el puerto que la aplicación usa dentro del contenedor.&lt;br /&gt;
Esto no lo abre al exterior; simplemente indica que la app escucha en el 5000.&lt;br /&gt;
&lt;br /&gt;
=== CMD ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Define el comando que se ejecutará cuando el contenedor se inicie.&lt;br /&gt;
En este caso, ejecutará la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
=== RUN vs CMD ===&lt;br /&gt;
&lt;br /&gt;
Estas dos instrucciones parecen similares, pero hacen cosas muy distintas.&lt;br /&gt;
&lt;br /&gt;
==== RUN ====&lt;br /&gt;
Se ejecuta durante la construcción de la imagen (en el momento del docker build).&lt;br /&gt;
Cada RUN crea una nueva capa dentro de la imagen.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando instala las dependencias una sola vez, cuando se construye la imagen.&lt;br /&gt;
El resultado (las librerías instaladas) queda grabado dentro de la imagen final.&lt;br /&gt;
&lt;br /&gt;
Si después lanzas un contenedor nuevo a partir de esa imagen, las dependencias ya estarán instaladas y no se volverán a ejecutar.&lt;br /&gt;
&lt;br /&gt;
==== CMD ====&lt;br /&gt;
Se ejecuta cuando se inicia el contenedor (en el docker run).&lt;br /&gt;
Define el proceso principal que se ejecutará dentro del contenedor mientras esté activo.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando no se ejecuta durante la construcción, sino cada vez que arrancas el contenedor.&lt;br /&gt;
Si detienes el contenedor, este proceso también se detiene.&lt;br /&gt;
&lt;br /&gt;
==== Diferencia resumida ====&lt;br /&gt;
&lt;br /&gt;
RUN → se ejecuta al construir la imagen (fase de docker build).&lt;br /&gt;
&lt;br /&gt;
CMD → se ejecuta al iniciar el contenedor (fase de docker run).&lt;br /&gt;
&lt;br /&gt;
Solo puede haber un CMD por Dockerfile.&lt;br /&gt;
Si hay más de uno, solo se usará el último.&lt;br /&gt;
&lt;br /&gt;
== Dockerfile completo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
COPY . .&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto tiene que ir en un Dockerfile en docker/images&lt;br /&gt;
&lt;br /&gt;
== Construir la imagen ==&lt;br /&gt;
Desde la raíz del proyecto (no dentro de docker/images):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker build -t uvlhub:dev -f docker/images/Dockerfile.dev .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Ejecutar el contenedor ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Qué es lo que observas?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 2: Conectando servicios =&lt;br /&gt;
&lt;br /&gt;
¡Vaya! ¡No funciona! Can't connect to MySQL server on 'db'... ¡Ah, claro, intenta conectarse a la base de datos, que no lo tenemos! Pero claro, en un contenedor no es recomendable tener la app y la base de datos conviviendo...&lt;br /&gt;
&lt;br /&gt;
== Crear contenedor MariaDB ==&lt;br /&gt;
&lt;br /&gt;
Antes que nada, haz Ctrl + C para parar el contenedor web. En este ejercicio aprenderás a lanzar un contenedor de MariaDB 12.0.2 de forma manual, configurando todas las variables necesarias para tu aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
El objetivo es entender la complejidad de hacerlo “a mano”, antes de automatizarlo con docker-compose.&lt;br /&gt;
&lt;br /&gt;
== Descargar la imagen oficial ==&lt;br /&gt;
Primero descarga la versión exacta que usaremos:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker pull mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Crear el contenedor de MariaDB ==&lt;br /&gt;
Lanza un contenedor con todas las variables necesarias para que Flask pueda conectarse correctamente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d --name mariadb_container \&lt;br /&gt;
 -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
 -e FLASK_ENV=development \&lt;br /&gt;
 -e DOMAIN=localhost \&lt;br /&gt;
 -e MARIADB_HOSTNAME=db \&lt;br /&gt;
 -e MARIADB_PORT=3306 \&lt;br /&gt;
 -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
 -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
 -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
 -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
 -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
 -e WORKING_DIR=/app/ \&lt;br /&gt;
 -p 3306:3306 mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Explicación:&lt;br /&gt;
&lt;br /&gt;
-d: ejecuta el contenedor en segundo plano (modo detached).&lt;br /&gt;
&lt;br /&gt;
--name: asigna un nombre identificable al contenedor.&lt;br /&gt;
&lt;br /&gt;
-e: define variables de entorno dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
-p 3306:3306: publica el puerto de MariaDB en tu máquina local.&lt;br /&gt;
&lt;br /&gt;
mariadb:12.0.2: imagen exacta que vamos a usar.&lt;br /&gt;
&lt;br /&gt;
Aquí ya, de entrada, vemos que esto es más feo que una nevera por detrás. Para empezar, estamos pasando a lo loco un montón de variables que difílmente será replicable en producción, por contener información sensible. Aparte, si metemos nuevas variables, ese comando queda inservible. Necesitaremos una solución más ágil para este problema&lt;br /&gt;
&lt;br /&gt;
== Comprobar que está corriendo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver algo así:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES&lt;br /&gt;
abcd1234efgh mariadb:12.0.2 &amp;quot;docker-entrypoint.s…&amp;quot; Up 5 seconds 0.0.0.0:3306-&amp;gt;3306/tcp mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Conectarse a la base de datos ==&lt;br /&gt;
Abre una consola dentro del contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it mariadb_container bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y dentro, accede al cliente de MariaDB:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mariadb -u root -p&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Contraseña: uvlhubdb_root_password&lt;br /&gt;
&lt;br /&gt;
== Verificar la base de datos ==&lt;br /&gt;
Una vez dentro del cliente MySQL, ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
SHOW DATABASES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver que existen la bases de datos uvlhubdb porque pedimos crearla a la hora de levantar el contenedor. ¿Qué tiene la base de datos dentro? Vamos a verlo...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
USE uvlhubdb;&lt;br /&gt;
SHOW TABLES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Un total de... 0 tablas... ¡Claro! Es que solo hemos creado la base en sí, pero no las tablas. ¿Con un script SQL tal vez? ¡Espera, espera, si tenemos las migraciones! Pero claro, para eso necesito el contenedor de la app levantado. Sal de la consola SQL y del contenedor de MariaDB. Bien, ahora en el host...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta vez con el flag &amp;quot;-d&amp;quot; para que corra en segundo plano. Ahora, accedamos al contenedor y ejecutemos las migraciones. Pero... ¿cómo se llama mi contenedor?&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate que Docker asigna un nombre aleatorio a tu contenedor web porque no definiste ninguno. Una vez identificado el nombre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it &amp;lt;nombre&amp;gt; bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
What? ¿Qué pasa? ¿Otra vez error de conexión con la base? Si tenemos el contenedor funcionando, esto es un jaleo...&lt;br /&gt;
&lt;br /&gt;
== Redes internas ==&lt;br /&gt;
&lt;br /&gt;
Ahora mismo tienes dos contenedores aislados, el de la app y el la base de datos. Por defecto, cada contenedor tiene su propia red interna y no puede ver a los demás por nombre. El hostname '''db''' que usa Flask no existe en su DNS interno.&lt;br /&gt;
&lt;br /&gt;
=== Conectar servicios ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una red interna que compartan ambos contenedores&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker network create uvlhub_network&lt;br /&gt;
docker network ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vemos que ya aparece nuestra red '''uvlhub_network'''. Ahora, conectemos el contenedor de MariaDB a la red:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Claro, si te fijas, ahora hay que añadir, además de toda esa configuración engorrosa, que se añada en la misma red y que además tenga de hostname &amp;quot;db&amp;quot; para que el contenedor web entienda qué es &amp;quot;db&amp;quot;. Tenemos que hacer lo mismo para el contenedor web:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop &amp;lt;nombre&amp;gt;&lt;br /&gt;
docker rm &amp;lt;nombre&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y lo levantamos de nuevo, pero ya en la nueva red. De paso, le ponemos un nombre para identificarlo mejor.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 --name web_app_container --network uvlhub_network -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y ahora ya podemos acceder al contenedor por su nombre y ejecutar las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Mejor?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 3: Persistencia =&lt;br /&gt;
&lt;br /&gt;
Vamos a simular que hemos desplegado justo esto en producción y el servidor se reinicia por un corte de luz. Reiniciemos a mano cada contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker restart web_app_container&lt;br /&gt;
docker restart mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. What? ¿Cómo? ¿Qué ha pasado? ¿Otra vez problema de conexión? Ve dándole a F5... F5... ¡bingo! Ahora sí sale, pero menuda aleatoriedad, ¿no? Claro, es que ahora caes en que una cosa es que el contenedor haya arrancado y otra que esté listo.&lt;br /&gt;
&lt;br /&gt;
Aparte, tenemos otro problema. ¿Qué pasa con la persistencia? Porque si borro el contenedor de MariaDB, todos los datos que estén en su interior también se pierden. Y si estamos en producción, lío asegurado.&lt;br /&gt;
&lt;br /&gt;
== Volúmenes ==&lt;br /&gt;
&lt;br /&gt;
Cada vez que eliminas el contenedor de MariaDB, todas las bases de datos se pierden. Esto ocurre porque los datos se guardan dentro del sistema de archivos del contenedor, y al borrarlo desaparecen con él.&lt;br /&gt;
&lt;br /&gt;
En este ejercicio creas un volumen de Docker para que los datos persistan entre ejecuciones.&lt;br /&gt;
&lt;br /&gt;
=== Crear un volumen ===&lt;br /&gt;
Un volumen es una carpeta gestionada por Docker que vive fuera del ciclo de vida de los contenedores.&lt;br /&gt;
&lt;br /&gt;
Ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume create mariadb_data&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba que existe:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
La salida muestra algo como:&lt;br /&gt;
&lt;br /&gt;
DRIVER    VOLUME NAME&lt;br /&gt;
local     mariadb_data&lt;br /&gt;
&lt;br /&gt;
=== Crear un contenedor de MariaDB con el volumen ===&lt;br /&gt;
Lanza el contenedor y conecta el volumen a la ruta donde MariaDB guarda los datos (/var/lib/mysql):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -v mariadb_data:/var/lib/mysql \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Qué significa &amp;quot;:&amp;quot; en los volúmenes ===&lt;br /&gt;
&lt;br /&gt;
Cuando usas la opción '''-v''' o '''--volume''' en Docker, el signo dos puntos (''':''') separa dos rutas:&lt;br /&gt;
&lt;br /&gt;
'''-v origen:destino'''&lt;br /&gt;
&lt;br /&gt;
origen → es la ruta o el volumen en tu máquina (el host).&lt;br /&gt;
&lt;br /&gt;
destino → es la ruta dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 4: Orquestación de servicios =&lt;br /&gt;
&lt;br /&gt;
Hasta ahora hemos creado y conectado los contenedores manualmente.&lt;br /&gt;
Ahora vas a usar Docker Compose para definir todo en un solo archivo.&lt;br /&gt;
&lt;br /&gt;
Crea un archivo llamado docker-compose.yml en la raíz del proyecto con este contenido:&lt;br /&gt;
&lt;br /&gt;
1: hacer un docker compose up básico&lt;br /&gt;
2: arreglar lo del wait for db&lt;br /&gt;
3: avisar del problema del código del contenedor y del uso de un volumen para eso&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 5: Entrypoints =&lt;br /&gt;
&lt;br /&gt;
Explicar utilidad de los entrypoints (puedo configurar el arranque sin tener que modificar la imagen)&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 6: Distintas configuraciones de despliegue =&lt;br /&gt;
&lt;br /&gt;
1: Explicar las distintas imágenes, entrypoints y docker-compose.ymls&lt;br /&gt;
2: Explicar el nginx&lt;br /&gt;
3: Explicar que cuanto más se parezca el despliegue en dev y en prod, menos propenso a errores seremos&lt;br /&gt;
4: Explicar diferencia entre flask server y gunicorn. Explicar que, dado que es una configuración distinta en dev y en prod, necesitamos siempre una máquina de preproducción&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10155</id>
		<title>Tutorial-docker</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10155"/>
				<updated>2025-10-11T21:16:58Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Ejercicio 1: Empezamos desde cero =&lt;br /&gt;
&lt;br /&gt;
El objetivo es eliminar toda la configuración previa y crear desde cero la estructura de carpetas que contendrá nuestros futuros archivos Docker.&lt;br /&gt;
&lt;br /&gt;
== Eliminar la configuración existente ==&lt;br /&gt;
Antes de comenzar, asegúrate de borrar cualquier carpeta Docker existente en el proyecto. En este caso, le haremos una copia de seguridad para poder volver a ella.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mv docker docker.bk&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
También debes asegurarte que trabajas con las variables de entorno adecuadas:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
cp .env.docker.example .env&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
⚠️ Este paso es importante: partimos completamente desde cero para entender cada parte del sistema.&lt;br /&gt;
&lt;br /&gt;
== Crear un Dockerfile mínimo ==&lt;br /&gt;
&lt;br /&gt;
Ahora crea nuevamente la carpeta base donde almacenaremos los archivos relacionados con Docker:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt; mkdir -p docker/images &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vamos a explicar los distintos apartados:&lt;br /&gt;
&lt;br /&gt;
=== FROM ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Indica la imagen base sobre la que se construirá la tuya.&lt;br /&gt;
En este caso, parte de una imagen ligera que ya tiene Python 3.12 instalado.&lt;br /&gt;
&lt;br /&gt;
=== WORKDIR ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta línea define el directorio de trabajo dentro del contenedor.&lt;br /&gt;
A partir de aquí, todos los comandos que aparezcan después (COPY, RUN, CMD, etc.) se ejecutarán dentro de /app.&lt;br /&gt;
&lt;br /&gt;
Ojo: /app no es la carpeta app/ de tu proyecto local.&lt;br /&gt;
Son cosas distintas.&lt;br /&gt;
&lt;br /&gt;
En tu máquina puede existir algo como:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
uvlhub&lt;br /&gt;
   app&lt;br /&gt;
   core&lt;br /&gt;
   requirements.txt &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Pero dentro del contenedor la estructura será distinta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt; / app/ app/ ← aquí dentro se habrá copiado tu carpeta local &amp;quot;app&amp;quot; &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Cuando más adelante uses:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Docker copiará todos los archivos de tu proyecto dentro de la carpeta /app del contenedor.&lt;br /&gt;
Por eso el código quedará en /app/app/.&lt;br /&gt;
&lt;br /&gt;
=== COPY ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Copia todos los archivos del proyecto (del host) dentro del contenedor, en /app.&lt;br /&gt;
El primer punto es el origen; el segundo, el destino.&lt;br /&gt;
&lt;br /&gt;
=== RUN ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ejecuta comandos durante la construcción de la imagen.&lt;br /&gt;
Aquí instalamos las dependencias necesarias desde requirements.txt.&lt;br /&gt;
El resultado de este paso quedará guardado en la imagen.&lt;br /&gt;
&lt;br /&gt;
=== EXPOSE ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Documenta el puerto que la aplicación usa dentro del contenedor.&lt;br /&gt;
Esto no lo abre al exterior; simplemente indica que la app escucha en el 5000.&lt;br /&gt;
&lt;br /&gt;
=== CMD ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Define el comando que se ejecutará cuando el contenedor se inicie.&lt;br /&gt;
En este caso, ejecutará la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
=== RUN vs CMD ===&lt;br /&gt;
&lt;br /&gt;
Estas dos instrucciones parecen similares, pero hacen cosas muy distintas.&lt;br /&gt;
&lt;br /&gt;
==== RUN ====&lt;br /&gt;
Se ejecuta durante la construcción de la imagen (en el momento del docker build).&lt;br /&gt;
Cada RUN crea una nueva capa dentro de la imagen.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando instala las dependencias una sola vez, cuando se construye la imagen.&lt;br /&gt;
El resultado (las librerías instaladas) queda grabado dentro de la imagen final.&lt;br /&gt;
&lt;br /&gt;
Si después lanzas un contenedor nuevo a partir de esa imagen, las dependencias ya estarán instaladas y no se volverán a ejecutar.&lt;br /&gt;
&lt;br /&gt;
==== CMD ====&lt;br /&gt;
Se ejecuta cuando se inicia el contenedor (en el docker run).&lt;br /&gt;
Define el proceso principal que se ejecutará dentro del contenedor mientras esté activo.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando no se ejecuta durante la construcción, sino cada vez que arrancas el contenedor.&lt;br /&gt;
Si detienes el contenedor, este proceso también se detiene.&lt;br /&gt;
&lt;br /&gt;
==== Diferencia resumida ====&lt;br /&gt;
&lt;br /&gt;
RUN → se ejecuta al construir la imagen (fase de docker build).&lt;br /&gt;
&lt;br /&gt;
CMD → se ejecuta al iniciar el contenedor (fase de docker run).&lt;br /&gt;
&lt;br /&gt;
Solo puede haber un CMD por Dockerfile.&lt;br /&gt;
Si hay más de uno, solo se usará el último.&lt;br /&gt;
&lt;br /&gt;
== Dockerfile completo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
COPY . .&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto tiene que ir en un Dockerfile en docker/images&lt;br /&gt;
&lt;br /&gt;
== Construir la imagen ==&lt;br /&gt;
Desde la raíz del proyecto (no dentro de docker/images):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker build -t uvlhub:dev -f docker/images/Dockerfile.dev .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Ejecutar el contenedor ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Qué es lo que observas?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 2: Conectando servicios =&lt;br /&gt;
&lt;br /&gt;
¡Vaya! ¡No funciona! Can't connect to MySQL server on 'db'... ¡Ah, claro, intenta conectarse a la base de datos, que no lo tenemos! Pero claro, en un contenedor no es recomendable tener la app y la base de datos conviviendo...&lt;br /&gt;
&lt;br /&gt;
== Crear contenedor MariaDB ==&lt;br /&gt;
&lt;br /&gt;
Antes que nada, haz Ctrl + C para parar el contenedor web. En este ejercicio aprenderás a lanzar un contenedor de MariaDB 12.0.2 de forma manual, configurando todas las variables necesarias para tu aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
El objetivo es entender la complejidad de hacerlo “a mano”, antes de automatizarlo con docker-compose.&lt;br /&gt;
&lt;br /&gt;
== Descargar la imagen oficial ==&lt;br /&gt;
Primero descarga la versión exacta que usaremos:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker pull mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Crear el contenedor de MariaDB ==&lt;br /&gt;
Lanza un contenedor con todas las variables necesarias para que Flask pueda conectarse correctamente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d --name mariadb_container \&lt;br /&gt;
 -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
 -e FLASK_ENV=development \&lt;br /&gt;
 -e DOMAIN=localhost \&lt;br /&gt;
 -e MARIADB_HOSTNAME=db \&lt;br /&gt;
 -e MARIADB_PORT=3306 \&lt;br /&gt;
 -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
 -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
 -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
 -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
 -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
 -e WORKING_DIR=/app/ \&lt;br /&gt;
 -p 3306:3306 mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Explicación:&lt;br /&gt;
&lt;br /&gt;
-d: ejecuta el contenedor en segundo plano (modo detached).&lt;br /&gt;
&lt;br /&gt;
--name: asigna un nombre identificable al contenedor.&lt;br /&gt;
&lt;br /&gt;
-e: define variables de entorno dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
-p 3306:3306: publica el puerto de MariaDB en tu máquina local.&lt;br /&gt;
&lt;br /&gt;
mariadb:12.0.2: imagen exacta que vamos a usar.&lt;br /&gt;
&lt;br /&gt;
Aquí ya, de entrada, vemos que esto es más feo que una nevera por detrás. Para empezar, estamos pasando a lo loco un montón de variables que difílmente será replicable en producción, por contener información sensible. Aparte, si metemos nuevas variables, ese comando queda inservible. Necesitaremos una solución más ágil para este problema&lt;br /&gt;
&lt;br /&gt;
== Comprobar que está corriendo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver algo así:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES&lt;br /&gt;
abcd1234efgh mariadb:12.0.2 &amp;quot;docker-entrypoint.s…&amp;quot; Up 5 seconds 0.0.0.0:3306-&amp;gt;3306/tcp mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Conectarse a la base de datos ==&lt;br /&gt;
Abre una consola dentro del contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it mariadb_container bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y dentro, accede al cliente de MariaDB:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mariadb -u root -p&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Contraseña: uvlhubdb_root_password&lt;br /&gt;
&lt;br /&gt;
== Verificar la base de datos ==&lt;br /&gt;
Una vez dentro del cliente MySQL, ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
SHOW DATABASES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver que existen la bases de datos uvlhubdb porque pedimos crearla a la hora de levantar el contenedor. ¿Qué tiene la base de datos dentro? Vamos a verlo...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
USE uvlhubdb;&lt;br /&gt;
SHOW TABLES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Un total de... 0 tablas... ¡Claro! Es que solo hemos creado la base en sí, pero no las tablas. ¿Con un script SQL tal vez? ¡Espera, espera, si tenemos las migraciones! Pero claro, para eso necesito el contenedor de la app levantado. Sal de la consola SQL y del contenedor de MariaDB. Bien, ahora en el host...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta vez con el flag &amp;quot;-d&amp;quot; para que corra en segundo plano. Ahora, accedamos al contenedor y ejecutemos las migraciones. Pero... ¿cómo se llama mi contenedor?&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate que Docker asigna un nombre aleatorio a tu contenedor web porque no definiste ninguno. Una vez identificado el nombre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it &amp;lt;nombre&amp;gt; bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
What? ¿Qué pasa? ¿Otra vez error de conexión con la base? Si tenemos el contenedor funcionando, esto es un jaleo...&lt;br /&gt;
&lt;br /&gt;
== Redes internas ==&lt;br /&gt;
&lt;br /&gt;
Ahora mismo tienes dos contenedores aislados, el de la app y el la base de datos. Por defecto, cada contenedor tiene su propia red interna y no puede ver a los demás por nombre. El hostname '''db''' que usa Flask no existe en su DNS interno.&lt;br /&gt;
&lt;br /&gt;
=== Conectar servicios ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una red interna que compartan ambos contenedores&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker network create uvlhub_network&lt;br /&gt;
docker network ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vemos que ya aparece nuestra red '''uvlhub_network'''. Ahora, conectemos el contenedor de MariaDB a la red:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Claro, si te fijas, ahora hay que añadir, además de toda esa configuración engorrosa, que se añada en la misma red y que además tenga de hostname &amp;quot;db&amp;quot; para que el contenedor web entienda qué es &amp;quot;db&amp;quot;. Tenemos que hacer lo mismo para el contenedor web:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop &amp;lt;nombre&amp;gt;&lt;br /&gt;
docker rm &amp;lt;nombre&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y lo levantamos de nuevo, pero ya en la nueva red. De paso, le ponemos un nombre para identificarlo mejor.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 --name web_app_container --network uvlhub_network -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y ahora ya podemos acceder al contenedor por su nombre y ejecutar las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Mejor?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 3: Persistencia =&lt;br /&gt;
&lt;br /&gt;
Vamos a simular que hemos desplegado justo esto en producción y el servidor se reinicia por un corte de luz. Reiniciemos a mano cada contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker restart web_app_container&lt;br /&gt;
docker restart mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. What? ¿Cómo? ¿Qué ha pasado? ¿Otra vez problema de conexión? Ve dándole a F5... F5... ¡bingo! Ahora sí sale, pero menuda aleatoriedad, ¿no? Claro, es que ahora caes en que una cosa es que el contenedor haya arrancado y otra que esté listo.&lt;br /&gt;
&lt;br /&gt;
Aparte, tenemos otro problema. ¿Qué pasa con la persistencia? Porque si borro el contenedor de MariaDB, todos los datos que estén en su interior también se pierden. Y si estamos en producción, lío asegurado.&lt;br /&gt;
&lt;br /&gt;
== Volúmenes ==&lt;br /&gt;
&lt;br /&gt;
Cada vez que eliminas el contenedor de MariaDB, todas las bases de datos se pierden. Esto ocurre porque los datos se guardan dentro del sistema de archivos del contenedor, y al borrarlo desaparecen con él.&lt;br /&gt;
&lt;br /&gt;
En este ejercicio creas un volumen de Docker para que los datos persistan entre ejecuciones.&lt;br /&gt;
&lt;br /&gt;
=== Crear un volumen ===&lt;br /&gt;
Un volumen es una carpeta gestionada por Docker que vive fuera del ciclo de vida de los contenedores.&lt;br /&gt;
&lt;br /&gt;
Ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume create mariadb_data&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba que existe:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
La salida muestra algo como:&lt;br /&gt;
&lt;br /&gt;
DRIVER    VOLUME NAME&lt;br /&gt;
local     mariadb_data&lt;br /&gt;
&lt;br /&gt;
=== Crear un contenedor de MariaDB con el volumen ===&lt;br /&gt;
Lanza el contenedor y conecta el volumen a la ruta donde MariaDB guarda los datos (/var/lib/mysql):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -v mariadb_data:/var/lib/mysql \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Qué significa &amp;quot;:&amp;quot; en los volúmenes ===&lt;br /&gt;
&lt;br /&gt;
Cuando usas la opción '''-v''' o '''--volume''' en Docker, el signo dos puntos (''':''') separa dos rutas:&lt;br /&gt;
&lt;br /&gt;
'''-v origen:destino'''&lt;br /&gt;
&lt;br /&gt;
origen → es la ruta o el volumen en tu máquina (el host).&lt;br /&gt;
&lt;br /&gt;
destino → es la ruta dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 4: Orquestación de servicios =&lt;br /&gt;
&lt;br /&gt;
1: hacer un docker compose up básico&lt;br /&gt;
2: arreglar lo del wait for db&lt;br /&gt;
3: avisar del problema del código del contenedor y del uso de un volumen para eso&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 5: Entrypoints =&lt;br /&gt;
&lt;br /&gt;
Explicar utilidad de los entrypoints (puedo configurar el arranque sin tener que modificar la imagen)&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 6: Distintas configuraciones de despliegue =&lt;br /&gt;
&lt;br /&gt;
1: Explicar las distintas imágenes, entrypoints y docker-compose.ymls&lt;br /&gt;
2: Explicar el nginx&lt;br /&gt;
3: Explicar que cuanto más se parezca el despliegue en dev y en prod, menos propenso a errores seremos&lt;br /&gt;
4: Explicar diferencia entre flask server y gunicorn. Explicar que, dado que es una configuración distinta en dev y en prod, necesitamos siempre una máquina de preproducción&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10154</id>
		<title>Tutorial-docker</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10154"/>
				<updated>2025-10-11T19:32:02Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Ejercicio 1: Empezamos desde cero =&lt;br /&gt;
&lt;br /&gt;
El objetivo es eliminar toda la configuración previa y crear desde cero la estructura de carpetas que contendrá nuestros futuros archivos Docker.&lt;br /&gt;
&lt;br /&gt;
== Eliminar la configuración existente ==&lt;br /&gt;
Antes de comenzar, asegúrate de borrar cualquier carpeta Docker existente en el proyecto. En este caso, le haremos una copia de seguridad para poder volver a ella.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mv docker docker.bk&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
También debes asegurarte que trabajas con las variables de entorno adecuadas:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
cp .env.docker.example .env&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
⚠️ Este paso es importante: partimos completamente desde cero para entender cada parte del sistema.&lt;br /&gt;
&lt;br /&gt;
== Crear un Dockerfile mínimo ==&lt;br /&gt;
&lt;br /&gt;
Ahora crea nuevamente la carpeta base donde almacenaremos los archivos relacionados con Docker:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt; mkdir -p docker/images &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vamos a explicar los distintos apartados:&lt;br /&gt;
&lt;br /&gt;
=== FROM ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Indica la imagen base sobre la que se construirá la tuya.&lt;br /&gt;
En este caso, parte de una imagen ligera que ya tiene Python 3.12 instalado.&lt;br /&gt;
&lt;br /&gt;
=== WORKDIR ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt; WORKDIR /app &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta línea define el directorio de trabajo dentro del contenedor.&lt;br /&gt;
A partir de aquí, todos los comandos que aparezcan después (COPY, RUN, CMD, etc.) se ejecutarán dentro de /app.&lt;br /&gt;
&lt;br /&gt;
Ojo: /app no es la carpeta app/ de tu proyecto local.&lt;br /&gt;
Son cosas distintas.&lt;br /&gt;
&lt;br /&gt;
En tu máquina puede existir algo como:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
uvlhub&lt;br /&gt;
   app&lt;br /&gt;
   core&lt;br /&gt;
   requirements.txt &lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Pero dentro del contenedor la estructura será distinta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt; / app/ app/ ← aquí dentro se habrá copiado tu carpeta local &amp;quot;app&amp;quot; &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Cuando más adelante uses:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt; COPY . . &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Docker copiará todos los archivos de tu proyecto dentro de la carpeta /app del contenedor.&lt;br /&gt;
Por eso el código quedará en /app/app/.&lt;br /&gt;
&lt;br /&gt;
=== COPY ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Copia todos los archivos del proyecto (del host) dentro del contenedor, en /app.&lt;br /&gt;
El primer punto es el origen; el segundo, el destino.&lt;br /&gt;
&lt;br /&gt;
=== RUN ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ejecuta comandos durante la construcción de la imagen.&lt;br /&gt;
Aquí instalamos las dependencias necesarias desde requirements.txt.&lt;br /&gt;
El resultado de este paso quedará guardado en la imagen.&lt;br /&gt;
&lt;br /&gt;
=== EXPOSE ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Documenta el puerto que la aplicación usa dentro del contenedor.&lt;br /&gt;
Esto no lo abre al exterior; simplemente indica que la app escucha en el 5000.&lt;br /&gt;
&lt;br /&gt;
=== CMD ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Define el comando que se ejecutará cuando el contenedor se inicie.&lt;br /&gt;
En este caso, ejecutará la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
=== RUN vs CMD ===&lt;br /&gt;
&lt;br /&gt;
Estas dos instrucciones parecen similares, pero hacen cosas muy distintas.&lt;br /&gt;
&lt;br /&gt;
==== RUN ====&lt;br /&gt;
Se ejecuta durante la construcción de la imagen (en el momento del docker build).&lt;br /&gt;
Cada RUN crea una nueva capa dentro de la imagen.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt; RUN pip install -r requirements.txt &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando instala las dependencias una sola vez, cuando se construye la imagen.&lt;br /&gt;
El resultado (las librerías instaladas) queda grabado dentro de la imagen final.&lt;br /&gt;
&lt;br /&gt;
Si después lanzas un contenedor nuevo a partir de esa imagen, las dependencias ya estarán instaladas y no se volverán a ejecutar.&lt;br /&gt;
&lt;br /&gt;
==== CMD ====&lt;br /&gt;
Se ejecuta cuando se inicia el contenedor (en el docker run).&lt;br /&gt;
Define el proceso principal que se ejecutará dentro del contenedor mientras esté activo.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt; CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;] &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando no se ejecuta durante la construcción, sino cada vez que arrancas el contenedor.&lt;br /&gt;
Si detienes el contenedor, este proceso también se detiene.&lt;br /&gt;
&lt;br /&gt;
==== Diferencia resumida ====&lt;br /&gt;
&lt;br /&gt;
RUN → se ejecuta al construir la imagen (fase de docker build).&lt;br /&gt;
&lt;br /&gt;
CMD → se ejecuta al iniciar el contenedor (fase de docker run).&lt;br /&gt;
&lt;br /&gt;
Solo puede haber un CMD por Dockerfile.&lt;br /&gt;
Si hay más de uno, solo se usará el último.&lt;br /&gt;
&lt;br /&gt;
== Dockerfile completo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
COPY . .&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Construir la imagen ==&lt;br /&gt;
Desde la raíz del proyecto (no dentro de docker/images):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt; docker build -t uvlhub:dev -f docker/images/Dockerfile.dev . &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Ejecutar el contenedor ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Qué es lo que observas?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 2: Conectando servicios =&lt;br /&gt;
&lt;br /&gt;
¡Vaya! ¡No funciona! Can't connect to MySQL server on 'db'... ¡Ah, claro, intenta conectarse a la base de datos, que no lo tenemos! Pero claro, en un contenedor no es recomendable tener la app y la base de datos conviviendo...&lt;br /&gt;
&lt;br /&gt;
== Crear contenedor MariaDB ==&lt;br /&gt;
&lt;br /&gt;
Antes que nada, haz Ctrl + C para parar el contenedor web. En este ejercicio aprenderás a lanzar un contenedor de MariaDB 12.0.2 de forma manual, configurando todas las variables necesarias para tu aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
El objetivo es entender la complejidad de hacerlo “a mano”, antes de automatizarlo con docker-compose.&lt;br /&gt;
&lt;br /&gt;
== Descargar la imagen oficial ==&lt;br /&gt;
Primero descarga la versión exacta que usaremos:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker pull mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Crear el contenedor de MariaDB ==&lt;br /&gt;
Lanza un contenedor con todas las variables necesarias para que Flask pueda conectarse correctamente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d --name mariadb_container \&lt;br /&gt;
 -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
 -e FLASK_ENV=development \&lt;br /&gt;
 -e DOMAIN=localhost \&lt;br /&gt;
 -e MARIADB_HOSTNAME=db \&lt;br /&gt;
 -e MARIADB_PORT=3306 \&lt;br /&gt;
 -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
 -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
 -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
 -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
 -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
 -e WORKING_DIR=/app/ \&lt;br /&gt;
 -p 3306:3306 mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Explicación:&lt;br /&gt;
&lt;br /&gt;
-d: ejecuta el contenedor en segundo plano (modo detached).&lt;br /&gt;
&lt;br /&gt;
--name: asigna un nombre identificable al contenedor.&lt;br /&gt;
&lt;br /&gt;
-e: define variables de entorno dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
-p 3306:3306: publica el puerto de MariaDB en tu máquina local.&lt;br /&gt;
&lt;br /&gt;
mariadb:12.0.2: imagen exacta que vamos a usar.&lt;br /&gt;
&lt;br /&gt;
Aquí ya, de entrada, vemos que esto es más feo que una nevera por detrás. Para empezar, estamos pasando a lo loco un montón de variables que difílmente será replicable en producción, por contener información sensible. Aparte, si metemos nuevas variables, ese comando queda inservible. Necesitaremos una solución más ágil para este problema&lt;br /&gt;
&lt;br /&gt;
== Comprobar que está corriendo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver algo así:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES&lt;br /&gt;
abcd1234efgh mariadb:12.0.2 &amp;quot;docker-entrypoint.s…&amp;quot; Up 5 seconds 0.0.0.0:3306-&amp;gt;3306/tcp mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Conectarse a la base de datos ==&lt;br /&gt;
Abre una consola dentro del contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it mariadb_container bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y dentro, accede al cliente de MariaDB:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mariadb -u root -p&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Contraseña: uvlhubdb_root_password&lt;br /&gt;
&lt;br /&gt;
== Verificar la base de datos ==&lt;br /&gt;
Una vez dentro del cliente MySQL, ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
SHOW DATABASES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver que existen la bases de datos uvlhubdb porque pedimos crearla a la hora de levantar el contenedor. ¿Qué tiene la base de datos dentro? Vamos a verlo...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
USE uvlhubdb;&lt;br /&gt;
SHOW TABLES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Un total de... 0 tablas... ¡Claro! Es que solo hemos creado la base en sí, pero no las tablas. ¿Con un script SQL tal vez? ¡Espera, espera, si tenemos las migraciones! Pero claro, para eso necesito el contenedor de la app levantado. Sal de la consola SQL y del contenedor de MariaDB. Bien, ahora en el host...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta vez con el flag &amp;quot;-d&amp;quot; para que corra en segundo plano. Ahora, accedamos al contenedor y ejecutemos las migraciones. Pero... ¿cómo se llama mi contenedor?&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate que Docker asigna un nombre aleatorio a tu contenedor web porque no definiste ninguno. Una vez identificado el nombre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it &amp;lt;nombre&amp;gt; bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
What? ¿Qué pasa? ¿Otra vez error de conexión con la base? Si tenemos el contenedor funcionando, esto es un jaleo...&lt;br /&gt;
&lt;br /&gt;
== Redes internas ==&lt;br /&gt;
&lt;br /&gt;
Ahora mismo tienes dos contenedores aislados, el de la app y el la base de datos. Por defecto, cada contenedor tiene su propia red interna y no puede ver a los demás por nombre. El hostname '''db''' que usa Flask no existe en su DNS interno.&lt;br /&gt;
&lt;br /&gt;
=== Conectar servicios ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una red interna que compartan ambos contenedores&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker network create uvlhub_network&lt;br /&gt;
docker network ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vemos que ya aparece nuestra red '''uvlhub_network'''. Ahora, conectemos el contenedor de MariaDB a la red:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Claro, si te fijas, ahora hay que añadir, además de toda esa configuración engorrosa, que se añada en la misma red y que además tenga de hostname &amp;quot;db&amp;quot; para que el contenedor web entienda qué es &amp;quot;db&amp;quot;. Tenemos que hacer lo mismo para el contenedor web:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop &amp;lt;nombre&amp;gt;&lt;br /&gt;
docker rm &amp;lt;nombre&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y lo levantamos de nuevo, pero ya en la nueva red. De paso, le ponemos un nombre para identificarlo mejor.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 --name web_app_container --network uvlhub_network -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y ahora ya podemos acceder al contenedor por su nombre y ejecutar las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Mejor?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 3: Persistencia =&lt;br /&gt;
&lt;br /&gt;
Vamos a simular que hemos desplegado justo esto en producción y el servidor se reinicia por un corte de luz. Reiniciemos a mano cada contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker restart web_app_container&lt;br /&gt;
docker restart mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. What? ¿Cómo? ¿Qué ha pasado? ¿Otra vez problema de conexión? Ve dándole a F5... F5... ¡bingo! Ahora sí sale, pero menuda aleatoriedad, ¿no? Claro, es que ahora caes en que una cosa es que el contenedor haya arrancado y otra que esté listo.&lt;br /&gt;
&lt;br /&gt;
Aparte, tenemos otro problema. ¿Qué pasa con la persistencia? Porque si borro el contenedor de MariaDB, todos los datos que estén en su interior también se pierden. Y si estamos en producción, lío asegurado.&lt;br /&gt;
&lt;br /&gt;
== Volúmenes ==&lt;br /&gt;
&lt;br /&gt;
Cada vez que eliminas el contenedor de MariaDB, todas las bases de datos se pierden. Esto ocurre porque los datos se guardan dentro del sistema de archivos del contenedor, y al borrarlo desaparecen con él.&lt;br /&gt;
&lt;br /&gt;
En este ejercicio creas un volumen de Docker para que los datos persistan entre ejecuciones.&lt;br /&gt;
&lt;br /&gt;
=== Crear un volumen ===&lt;br /&gt;
Un volumen es una carpeta gestionada por Docker que vive fuera del ciclo de vida de los contenedores.&lt;br /&gt;
&lt;br /&gt;
Ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume create mariadb_data&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba que existe:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
La salida muestra algo como:&lt;br /&gt;
&lt;br /&gt;
DRIVER    VOLUME NAME&lt;br /&gt;
local     mariadb_data&lt;br /&gt;
&lt;br /&gt;
=== Crear un contenedor de MariaDB con el volumen ===&lt;br /&gt;
Lanza el contenedor y conecta el volumen a la ruta donde MariaDB guarda los datos (/var/lib/mysql):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -v mariadb_data:/var/lib/mysql \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Qué significa &amp;quot;:&amp;quot; en los volúmenes ===&lt;br /&gt;
&lt;br /&gt;
Cuando usas la opción -v o --volume en Docker, el signo dos puntos (:) separa dos rutas:&lt;br /&gt;
&lt;br /&gt;
'''-v origen:destino'''&lt;br /&gt;
&lt;br /&gt;
origen → es la ruta o el volumen en tu máquina (el host).&lt;br /&gt;
&lt;br /&gt;
destino → es la ruta dentro del contenedor.&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10153</id>
		<title>Tutorial-docker</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10153"/>
				<updated>2025-10-11T19:26:31Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Ejercicio 1: Empezamos desde cero =&lt;br /&gt;
&lt;br /&gt;
El objetivo es eliminar toda la configuración previa y crear desde cero la estructura de carpetas que contendrá nuestros futuros archivos Docker.&lt;br /&gt;
&lt;br /&gt;
== Eliminar la configuración existente ==&lt;br /&gt;
Antes de comenzar, asegúrate de borrar cualquier carpeta Docker existente en el proyecto. En este caso, le haremos una copia de seguridad para poder volver a ella.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mv docker docker.bk&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
También debes asegurarte que trabajas con las variables de entorno adecuadas:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
cp .env.docker.example .env&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
⚠️ Este paso es importante: partimos completamente desde cero para entender cada parte del sistema.&lt;br /&gt;
&lt;br /&gt;
== Crear un Dockerfile mínimo ==&lt;br /&gt;
&lt;br /&gt;
Ahora crea nuevamente la carpeta base donde almacenaremos los archivos relacionados con Docker:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt; mkdir -p docker/images &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vamos a explicar los distintos apartados:&lt;br /&gt;
&lt;br /&gt;
=== FROM ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Indica la imagen base sobre la que se construirá la tuya.&lt;br /&gt;
En este caso, parte de una imagen ligera que ya tiene Python 3.12 instalado.&lt;br /&gt;
&lt;br /&gt;
=== WORKDIR ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt; WORKDIR /app &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta línea define el directorio de trabajo dentro del contenedor.&lt;br /&gt;
A partir de aquí, todos los comandos que aparezcan después (COPY, RUN, CMD, etc.) se ejecutarán dentro de /app.&lt;br /&gt;
&lt;br /&gt;
Ojo: /app no es la carpeta app/ de tu proyecto local.&lt;br /&gt;
Son cosas distintas.&lt;br /&gt;
&lt;br /&gt;
En tu máquina puede existir algo como:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
uvlhub&lt;br /&gt;
   app&lt;br /&gt;
   core&lt;br /&gt;
   requirements.txt &lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Pero dentro del contenedor la estructura será distinta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt; / app/ app/ ← aquí dentro se habrá copiado tu carpeta local &amp;quot;app&amp;quot; &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Cuando más adelante uses:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt; COPY . . &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Docker copiará todos los archivos de tu proyecto dentro de la carpeta /app del contenedor.&lt;br /&gt;
Por eso el código quedará en /app/app/.&lt;br /&gt;
&lt;br /&gt;
=== COPY ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Copia todos los archivos del proyecto (del host) dentro del contenedor, en /app.&lt;br /&gt;
El primer punto es el origen; el segundo, el destino.&lt;br /&gt;
&lt;br /&gt;
=== RUN ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ejecuta comandos durante la construcción de la imagen.&lt;br /&gt;
Aquí instalamos las dependencias necesarias desde requirements.txt.&lt;br /&gt;
El resultado de este paso quedará guardado en la imagen.&lt;br /&gt;
&lt;br /&gt;
=== EXPOSE ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Documenta el puerto que la aplicación usa dentro del contenedor.&lt;br /&gt;
Esto no lo abre al exterior; simplemente indica que la app escucha en el 5000.&lt;br /&gt;
&lt;br /&gt;
=== CMD ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Define el comando que se ejecutará cuando el contenedor se inicie.&lt;br /&gt;
En este caso, ejecutará la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
=== RUN vs CMD ===&lt;br /&gt;
&lt;br /&gt;
Estas dos instrucciones parecen similares, pero hacen cosas muy distintas.&lt;br /&gt;
&lt;br /&gt;
==== RUN ====&lt;br /&gt;
Se ejecuta durante la construcción de la imagen (en el momento del docker build).&lt;br /&gt;
Cada RUN crea una nueva capa dentro de la imagen.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt; RUN pip install -r requirements.txt &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando instala las dependencias una sola vez, cuando se construye la imagen.&lt;br /&gt;
El resultado (las librerías instaladas) queda grabado dentro de la imagen final.&lt;br /&gt;
&lt;br /&gt;
Si después lanzas un contenedor nuevo a partir de esa imagen, las dependencias ya estarán instaladas y no se volverán a ejecutar.&lt;br /&gt;
&lt;br /&gt;
==== CMD ====&lt;br /&gt;
Se ejecuta cuando se inicia el contenedor (en el docker run).&lt;br /&gt;
Define el proceso principal que se ejecutará dentro del contenedor mientras esté activo.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt; CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;] &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando no se ejecuta durante la construcción, sino cada vez que arrancas el contenedor.&lt;br /&gt;
Si detienes el contenedor, este proceso también se detiene.&lt;br /&gt;
&lt;br /&gt;
==== Diferencia resumida ====&lt;br /&gt;
&lt;br /&gt;
RUN → se ejecuta al construir la imagen (fase de docker build).&lt;br /&gt;
&lt;br /&gt;
CMD → se ejecuta al iniciar el contenedor (fase de docker run).&lt;br /&gt;
&lt;br /&gt;
Solo puede haber un CMD por Dockerfile.&lt;br /&gt;
Si hay más de uno, solo se usará el último.&lt;br /&gt;
&lt;br /&gt;
== Dockerfile completo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
COPY . .&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Construir la imagen ==&lt;br /&gt;
Desde la raíz del proyecto (no dentro de docker/images):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt; docker build -t uvlhub:dev -f docker/images/Dockerfile.dev . &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Ejecutar el contenedor ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Qué es lo que observas?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 2: Conectando servicios =&lt;br /&gt;
&lt;br /&gt;
¡Vaya! ¡No funciona! Can't connect to MySQL server on 'db'... ¡Ah, claro, intenta conectarse a la base de datos, que no lo tenemos! Pero claro, en un contenedor no es recomendable tener la app y la base de datos conviviendo...&lt;br /&gt;
&lt;br /&gt;
== Crear contenedor MariaDB ==&lt;br /&gt;
&lt;br /&gt;
Antes que nada, haz Ctrl + C para parar el contenedor web. En este ejercicio aprenderás a lanzar un contenedor de MariaDB 12.0.2 de forma manual, configurando todas las variables necesarias para tu aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
El objetivo es entender la complejidad de hacerlo “a mano”, antes de automatizarlo con docker-compose.&lt;br /&gt;
&lt;br /&gt;
== Descargar la imagen oficial ==&lt;br /&gt;
Primero descarga la versión exacta que usaremos:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker pull mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Crear el contenedor de MariaDB ==&lt;br /&gt;
Lanza un contenedor con todas las variables necesarias para que Flask pueda conectarse correctamente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d --name mariadb_container \&lt;br /&gt;
 -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
 -e FLASK_ENV=development \&lt;br /&gt;
 -e DOMAIN=localhost \&lt;br /&gt;
 -e MARIADB_HOSTNAME=db \&lt;br /&gt;
 -e MARIADB_PORT=3306 \&lt;br /&gt;
 -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
 -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
 -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
 -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
 -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
 -e WORKING_DIR=/app/ \&lt;br /&gt;
 -p 3306:3306 mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Explicación:&lt;br /&gt;
&lt;br /&gt;
-d: ejecuta el contenedor en segundo plano (modo detached).&lt;br /&gt;
&lt;br /&gt;
--name: asigna un nombre identificable al contenedor.&lt;br /&gt;
&lt;br /&gt;
-e: define variables de entorno dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
-p 3306:3306: publica el puerto de MariaDB en tu máquina local.&lt;br /&gt;
&lt;br /&gt;
mariadb:12.0.2: imagen exacta que vamos a usar.&lt;br /&gt;
&lt;br /&gt;
Aquí ya, de entrada, vemos que esto es más feo que una nevera por detrás. Para empezar, estamos pasando a lo loco un montón de variables que difílmente será replicable en producción, por contener información sensible. Aparte, si metemos nuevas variables, ese comando queda inservible. Necesitaremos una solución más ágil para este problema&lt;br /&gt;
&lt;br /&gt;
== Comprobar que está corriendo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver algo así:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES&lt;br /&gt;
abcd1234efgh mariadb:12.0.2 &amp;quot;docker-entrypoint.s…&amp;quot; Up 5 seconds 0.0.0.0:3306-&amp;gt;3306/tcp mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Conectarse a la base de datos ==&lt;br /&gt;
Abre una consola dentro del contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it mariadb_container bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y dentro, accede al cliente de MariaDB:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mariadb -u root -p&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Contraseña: uvlhubdb_root_password&lt;br /&gt;
&lt;br /&gt;
== Verificar la base de datos ==&lt;br /&gt;
Una vez dentro del cliente MySQL, ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
SHOW DATABASES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver que existen la bases de datos uvlhubdb porque pedimos crearla a la hora de levantar el contenedor. ¿Qué tiene la base de datos dentro? Vamos a verlo...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
USE uvlhubdb;&lt;br /&gt;
SHOW TABLES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Un total de... 0 tablas... ¡Claro! Es que solo hemos creado la base en sí, pero no las tablas. ¿Con un script SQL tal vez? ¡Espera, espera, si tenemos las migraciones! Pero claro, para eso necesito el contenedor de la app levantado. Sal de la consola SQL y del contenedor de MariaDB. Bien, ahora en el host...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta vez con el flag &amp;quot;-d&amp;quot; para que corra en segundo plano. Ahora, accedamos al contenedor y ejecutemos las migraciones. Pero... ¿cómo se llama mi contenedor?&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate que Docker asigna un nombre aleatorio a tu contenedor web porque no definiste ninguno. Una vez identificado el nombre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it &amp;lt;nombre&amp;gt; bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
What? ¿Qué pasa? ¿Otra vez error de conexión con la base? Si tenemos el contenedor funcionando, esto es un jaleo...&lt;br /&gt;
&lt;br /&gt;
== Redes internas ==&lt;br /&gt;
&lt;br /&gt;
Ahora mismo tienes dos contenedores aislados, el de la app y el la base de datos. Por defecto, cada contenedor tiene su propia red interna y no puede ver a los demás por nombre. El hostname '''db''' que usa Flask no existe en su DNS interno.&lt;br /&gt;
&lt;br /&gt;
=== Conectar servicios ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una red interna que compartan ambos contenedores&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker network create uvlhub_network&lt;br /&gt;
docker network ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vemos que ya aparece nuestra red '''uvlhub_network'''. Ahora, conectemos el contenedor de MariaDB a la red:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Claro, si te fijas, ahora hay que añadir, además de toda esa configuración engorrosa, que se añada en la misma red y que además tenga de hostname &amp;quot;db&amp;quot; para que el contenedor web entienda qué es &amp;quot;db&amp;quot;. Tenemos que hacer lo mismo para el contenedor web:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop &amp;lt;nombre&amp;gt;&lt;br /&gt;
docker rm &amp;lt;nombre&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y lo levantamos de nuevo, pero ya en la nueva red. De paso, le ponemos un nombre para identificarlo mejor.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 --name web_app_container --network uvlhub_network -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y ahora ya podemos acceder al contenedor por su nombre y ejecutar las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Mejor?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 3: Persistencia =&lt;br /&gt;
&lt;br /&gt;
Vamos a simular que hemos desplegado justo esto en producción y el servidor se reinicia por un corte de luz. Reiniciemos a mano cada contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker restart web_app_container&lt;br /&gt;
docker restart mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. What? ¿Cómo? ¿Qué ha pasado? ¿Otra vez problema de conexión? Ve dándole a F5... F5... ¡bingo! Ahora sí sale, pero menuda aleatoriedad, ¿no? Claro, es que ahora caes en que una cosa es que el contenedor haya arrancado y otra que esté listo.&lt;br /&gt;
&lt;br /&gt;
Aparte, tenemos otro problema. ¿Qué pasa con la persistencia? Porque si borro el contenedor de MariaDB, todos los datos que estén en su interior también se pierden. Y si estamos en producción, lío asegurado.&lt;br /&gt;
&lt;br /&gt;
== Volúmenes ==&lt;br /&gt;
&lt;br /&gt;
Cada vez que eliminas el contenedor de MariaDB, todas las bases de datos se pierden. Esto ocurre porque los datos se guardan dentro del sistema de archivos del contenedor, y al borrarlo desaparecen con él.&lt;br /&gt;
&lt;br /&gt;
En este ejercicio creas un volumen de Docker para que los datos persistan entre ejecuciones.&lt;br /&gt;
&lt;br /&gt;
=== Crear un volumen ===&lt;br /&gt;
Un volumen es una carpeta gestionada por Docker que vive fuera del ciclo de vida de los contenedores.&lt;br /&gt;
&lt;br /&gt;
Ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume create mariadb_data&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba que existe:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
La salida muestra algo como:&lt;br /&gt;
&lt;br /&gt;
DRIVER    VOLUME NAME&lt;br /&gt;
local     mariadb_data&lt;br /&gt;
&lt;br /&gt;
=== Crear un contenedor de MariaDB con el volumen ===&lt;br /&gt;
Lanza el contenedor y conecta el volumen a la ruta donde MariaDB guarda los datos (/var/lib/mysql):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -v mariadb_data:/var/lib/mysql \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Qué significa &amp;quot;:&amp;quot; en los volúmenes ===&lt;br /&gt;
&lt;br /&gt;
Cuando usas la opción -v o --volume en Docker, el signo dos puntos (:) separa dos rutas:&lt;br /&gt;
&lt;br /&gt;
'''-v origen:destino'''&lt;br /&gt;
&lt;br /&gt;
origen → es la ruta o el volumen en tu máquina (el host).&lt;br /&gt;
&lt;br /&gt;
destino → es la ruta dentro del contenedor.&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10152</id>
		<title>Tutorial-docker</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10152"/>
				<updated>2025-10-11T19:11:51Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Ejercicio 1: Empezamos desde cero =&lt;br /&gt;
&lt;br /&gt;
El objetivo es eliminar toda la configuración previa y crear desde cero la estructura de carpetas que contendrá nuestros futuros archivos Docker.&lt;br /&gt;
&lt;br /&gt;
== Eliminar la configuración existente ==&lt;br /&gt;
Antes de comenzar, asegúrate de borrar cualquier carpeta Docker existente en el proyecto. En este caso, le haremos una copia de seguridad para poder volver a ella.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mv docker docker.bk&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
También debes asegurarte que trabajas con las variables de entorno adecuadas:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
cp .env.docker.example .env&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
⚠️ Este paso es importante: partimos completamente desde cero para entender cada parte del sistema.&lt;br /&gt;
&lt;br /&gt;
== Crear un Dockerfile mínimo ==&lt;br /&gt;
&lt;br /&gt;
Ahora crea nuevamente la carpeta base donde almacenaremos los archivos relacionados con Docker:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt; mkdir -p docker/images &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vamos a explicar los distintos apartados:&lt;br /&gt;
&lt;br /&gt;
=== FROM ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Indica la imagen base sobre la que se construirá la tuya.&lt;br /&gt;
En este caso, parte de una imagen ligera que ya tiene Python 3.12 instalado.&lt;br /&gt;
&lt;br /&gt;
=== WORKDIR ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt; WORKDIR /app &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta línea define el directorio de trabajo dentro del contenedor.&lt;br /&gt;
A partir de aquí, todos los comandos que aparezcan después (COPY, RUN, CMD, etc.) se ejecutarán dentro de /app.&lt;br /&gt;
&lt;br /&gt;
Ojo: /app no es la carpeta app/ de tu proyecto local.&lt;br /&gt;
Son cosas distintas.&lt;br /&gt;
&lt;br /&gt;
En tu máquina puede existir algo como:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
uvlhub&lt;br /&gt;
   app&lt;br /&gt;
   core&lt;br /&gt;
   requirements.txt &lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Pero dentro del contenedor la estructura será distinta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt; / app/ app/ ← aquí dentro se habrá copiado tu carpeta local &amp;quot;app&amp;quot; &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Cuando más adelante uses:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt; COPY . . &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Docker copiará todos los archivos de tu proyecto dentro de la carpeta /app del contenedor.&lt;br /&gt;
Por eso el código quedará en /app/app/.&lt;br /&gt;
&lt;br /&gt;
=== COPY ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Copia todos los archivos del proyecto (del host) dentro del contenedor, en /app.&lt;br /&gt;
El primer punto es el origen; el segundo, el destino.&lt;br /&gt;
&lt;br /&gt;
=== RUN ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ejecuta comandos durante la construcción de la imagen.&lt;br /&gt;
Aquí instalamos las dependencias necesarias desde requirements.txt.&lt;br /&gt;
El resultado de este paso quedará guardado en la imagen.&lt;br /&gt;
&lt;br /&gt;
=== EXPOSE ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Documenta el puerto que la aplicación usa dentro del contenedor.&lt;br /&gt;
Esto no lo abre al exterior; simplemente indica que la app escucha en el 5000.&lt;br /&gt;
&lt;br /&gt;
=== CMD ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Define el comando que se ejecutará cuando el contenedor se inicie.&lt;br /&gt;
En este caso, ejecutará la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
=== RUN vs CMD ===&lt;br /&gt;
&lt;br /&gt;
Estas dos instrucciones parecen similares, pero hacen cosas muy distintas.&lt;br /&gt;
&lt;br /&gt;
==== RUN ====&lt;br /&gt;
Se ejecuta durante la construcción de la imagen (en el momento del docker build).&lt;br /&gt;
Cada RUN crea una nueva capa dentro de la imagen.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt; RUN pip install -r requirements.txt &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando instala las dependencias una sola vez, cuando se construye la imagen.&lt;br /&gt;
El resultado (las librerías instaladas) queda grabado dentro de la imagen final.&lt;br /&gt;
&lt;br /&gt;
Si después lanzas un contenedor nuevo a partir de esa imagen, las dependencias ya estarán instaladas y no se volverán a ejecutar.&lt;br /&gt;
&lt;br /&gt;
==== CMD ====&lt;br /&gt;
Se ejecuta cuando se inicia el contenedor (en el docker run).&lt;br /&gt;
Define el proceso principal que se ejecutará dentro del contenedor mientras esté activo.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt; CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;] &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando no se ejecuta durante la construcción, sino cada vez que arrancas el contenedor.&lt;br /&gt;
Si detienes el contenedor, este proceso también se detiene.&lt;br /&gt;
&lt;br /&gt;
==== Diferencia resumida ====&lt;br /&gt;
&lt;br /&gt;
RUN → se ejecuta al construir la imagen (fase de docker build).&lt;br /&gt;
&lt;br /&gt;
CMD → se ejecuta al iniciar el contenedor (fase de docker run).&lt;br /&gt;
&lt;br /&gt;
Solo puede haber un CMD por Dockerfile.&lt;br /&gt;
Si hay más de uno, solo se usará el último.&lt;br /&gt;
&lt;br /&gt;
== Dockerfile completo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
COPY . .&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Construir la imagen ==&lt;br /&gt;
Desde la raíz del proyecto (no dentro de docker/images):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt; docker build -t uvlhub:dev -f docker/images/Dockerfile.dev . &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Ejecutar el contenedor ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Qué es lo que observas?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 2: Conectando servicios =&lt;br /&gt;
&lt;br /&gt;
¡Vaya! ¡No funciona! Can't connect to MySQL server on 'db'... ¡Ah, claro, intenta conectarse a la base de datos, que no lo tenemos! Pero claro, en un contenedor no es recomendable tener la app y la base de datos conviviendo...&lt;br /&gt;
&lt;br /&gt;
== Crear contenedor MariaDB ==&lt;br /&gt;
&lt;br /&gt;
Antes que nada, haz Ctrl + C para parar el contenedor web. En este ejercicio aprenderás a lanzar un contenedor de MariaDB 12.0.2 de forma manual, configurando todas las variables necesarias para tu aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
El objetivo es entender la complejidad de hacerlo “a mano”, antes de automatizarlo con docker-compose.&lt;br /&gt;
&lt;br /&gt;
== Descargar la imagen oficial ==&lt;br /&gt;
Primero descarga la versión exacta que usaremos:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker pull mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Crear el contenedor de MariaDB ==&lt;br /&gt;
Lanza un contenedor con todas las variables necesarias para que Flask pueda conectarse correctamente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d --name mariadb_container \&lt;br /&gt;
 -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
 -e FLASK_ENV=development \&lt;br /&gt;
 -e DOMAIN=localhost \&lt;br /&gt;
 -e MARIADB_HOSTNAME=db \&lt;br /&gt;
 -e MARIADB_PORT=3306 \&lt;br /&gt;
 -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
 -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
 -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
 -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
 -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
 -e WORKING_DIR=/app/ \&lt;br /&gt;
 -p 3306:3306 mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Explicación:&lt;br /&gt;
&lt;br /&gt;
-d: ejecuta el contenedor en segundo plano (modo detached).&lt;br /&gt;
&lt;br /&gt;
--name: asigna un nombre identificable al contenedor.&lt;br /&gt;
&lt;br /&gt;
-e: define variables de entorno dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
-p 3306:3306: publica el puerto de MariaDB en tu máquina local.&lt;br /&gt;
&lt;br /&gt;
mariadb:12.0.2: imagen exacta que vamos a usar.&lt;br /&gt;
&lt;br /&gt;
Aquí ya, de entrada, vemos que esto es más feo que una nevera por detrás. Para empezar, estamos pasando a lo loco un montón de variables que difílmente será replicable en producción, por contener información sensible. Aparte, si metemos nuevas variables, ese comando queda inservible. Necesitaremos una solución más ágil para este problema&lt;br /&gt;
&lt;br /&gt;
== Comprobar que está corriendo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver algo así:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES&lt;br /&gt;
abcd1234efgh mariadb:12.0.2 &amp;quot;docker-entrypoint.s…&amp;quot; Up 5 seconds 0.0.0.0:3306-&amp;gt;3306/tcp mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Conectarse a la base de datos ==&lt;br /&gt;
Abre una consola dentro del contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it mariadb_container bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y dentro, accede al cliente de MariaDB:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mariadb -u root -p&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Contraseña: uvlhubdb_root_password&lt;br /&gt;
&lt;br /&gt;
== Verificar la base de datos ==&lt;br /&gt;
Una vez dentro del cliente MySQL, ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
SHOW DATABASES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver que existen la bases de datos uvlhubdb porque pedimos crearla a la hora de levantar el contenedor. ¿Qué tiene la base de datos dentro? Vamos a verlo...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
USE uvlhubdb;&lt;br /&gt;
SHOW TABLES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Un total de... 0 tablas... ¡Claro! Es que solo hemos creado la base en sí, pero no las tablas. ¿Con un script SQL tal vez? ¡Espera, espera, si tenemos las migraciones! Pero claro, para eso necesito el contenedor de la app levantado. Sal de la consola SQL y del contenedor de MariaDB. Bien, ahora en el host...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta vez con el flag &amp;quot;-d&amp;quot; para que corra en segundo plano. Ahora, accedamos al contenedor y ejecutemos las migraciones. Pero... ¿cómo se llama mi contenedor?&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate que Docker asigna un nombre aleatorio a tu contenedor web porque no definiste ninguno. Una vez identificado el nombre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it &amp;lt;nombre&amp;gt; bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
What? ¿Qué pasa? ¿Otra vez error de conexión con la base? Si tenemos el contenedor funcionando, esto es un jaleo...&lt;br /&gt;
&lt;br /&gt;
== Redes internas ==&lt;br /&gt;
&lt;br /&gt;
Ahora mismo tienes dos contenedores aislados, el de la app y el la base de datos. Por defecto, cada contenedor tiene su propia red interna y no puede ver a los demás por nombre. El hostname '''db''' que usa Flask no existe en su DNS interno.&lt;br /&gt;
&lt;br /&gt;
=== Conectar servicios ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una red interna que compartan ambos contenedores&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker network create uvlhub_network&lt;br /&gt;
docker network ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vemos que ya aparece nuestra red '''uvlhub_network'''. Ahora, conectemos el contenedor de MariaDB a la red:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Claro, si te fijas, ahora hay que añadir, además de toda esa configuración engorrosa, que se añada en la misma red y que además tenga de hostname &amp;quot;db&amp;quot; para que el contenedor web entienda qué es &amp;quot;db&amp;quot;. Tenemos que hacer lo mismo para el contenedor web:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop &amp;lt;nombre&amp;gt;&lt;br /&gt;
docker rm &amp;lt;nombre&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y lo levantamos de nuevo, pero ya en la nueva red. De paso, le ponemos un nombre para identificarlo mejor.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 --name web_app_container --network uvlhub_network -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y ahora ya podemos acceder al contenedor por su nombre y ejecutar las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Mejor?&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10151</id>
		<title>Tutorial-docker</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10151"/>
				<updated>2025-10-11T18:10:26Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: Página creada con «= Ejercicio 1: Empezamos desde cero — creando la estructura base de Docker =  El objetivo es eliminar toda la configuración previa y crear desde cero la estructura de ca...»&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Ejercicio 1: Empezamos desde cero — creando la estructura base de Docker =&lt;br /&gt;
&lt;br /&gt;
El objetivo es eliminar toda la configuración previa y crear desde cero la estructura de carpetas que contendrá nuestros futuros archivos Docker.&lt;br /&gt;
&lt;br /&gt;
== 1. Eliminar la configuración existente ==&lt;br /&gt;
Antes de comenzar, asegúrate de borrar cualquier carpeta Docker existente en el proyecto. En este caso, le haremos una copia de seguridad para poder volver a ella.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt; mv docker docker.bk &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
⚠️ Este paso es importante: partimos completamente desde cero para entender cada parte del sistema.&lt;br /&gt;
&lt;br /&gt;
== 2. Crear un Dockerfile mínimo ==&lt;br /&gt;
&lt;br /&gt;
Ahora crea nuevamente la carpeta base donde almacenaremos los archivos relacionados con Docker:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt; mkdir -p docker/images &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vamos a explicar los distintos apartados:&lt;br /&gt;
&lt;br /&gt;
=== FROM ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Indica la imagen base sobre la que se construirá la tuya.&lt;br /&gt;
En este caso, parte de una imagen ligera que ya tiene Python 3.12 instalado.&lt;br /&gt;
&lt;br /&gt;
=== WORKDIR ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt; WORKDIR /app &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta línea define el directorio de trabajo dentro del contenedor.&lt;br /&gt;
A partir de aquí, todos los comandos que aparezcan después (COPY, RUN, CMD, etc.) se ejecutarán dentro de /app.&lt;br /&gt;
&lt;br /&gt;
Ojo: /app no es la carpeta app/ de tu proyecto local.&lt;br /&gt;
Son cosas distintas.&lt;br /&gt;
&lt;br /&gt;
En tu máquina puede existir algo como:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
uvlhub&lt;br /&gt;
   app&lt;br /&gt;
   core&lt;br /&gt;
   requirements.txt &lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Pero dentro del contenedor la estructura será distinta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt; / app/ app/ ← aquí dentro se habrá copiado tu carpeta local &amp;quot;app&amp;quot; &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Cuando más adelante uses:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt; COPY . . &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Docker copiará todos los archivos de tu proyecto dentro de la carpeta /app del contenedor.&lt;br /&gt;
Por eso el código quedará en /app/app/.&lt;br /&gt;
&lt;br /&gt;
=== COPY ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Copia todos los archivos del proyecto (del host) dentro del contenedor, en /app.&lt;br /&gt;
El primer punto es el origen; el segundo, el destino.&lt;br /&gt;
&lt;br /&gt;
=== RUN ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ejecuta comandos durante la construcción de la imagen.&lt;br /&gt;
Aquí instalamos las dependencias necesarias desde requirements.txt.&lt;br /&gt;
El resultado de este paso quedará guardado en la imagen.&lt;br /&gt;
&lt;br /&gt;
=== EXPOSE ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Documenta el puerto que la aplicación usa dentro del contenedor.&lt;br /&gt;
Esto no lo abre al exterior; simplemente indica que la app escucha en el 5000.&lt;br /&gt;
&lt;br /&gt;
=== CMD ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Define el comando que se ejecutará cuando el contenedor se inicie.&lt;br /&gt;
En este caso, ejecutará la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
== 9. Dockerfile completo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
COPY . .&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 10. Construir la imagen ==&lt;br /&gt;
Desde la raíz del proyecto (no dentro de docker/images):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt; docker build -t uvlhub:dev -f docker/images/Dockerfile.dev . &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 11. Ejecutar el contenedor ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Qué es lo que observas?&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Innosoft_-_25/26&amp;diff=10144</id>
		<title>Innosoft - 25/26</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Innosoft_-_25/26&amp;diff=10144"/>
				<updated>2025-10-07T16:52:52Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[Página_Principal]] -&amp;gt; [[2025/2026]]  -&amp;gt; [[Innosoft - 25/26]]&lt;br /&gt;
&lt;br /&gt;
En esta web encontraremos información sobre el desarrollo de las jornadas Innosoft durante el curso 25/26&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Propuestas de equipos de trabajo complementarios a los comités ==&lt;br /&gt;
&lt;br /&gt;
* [[Llamada a la propuesta de equipos de trabajo - 25/26]]&lt;br /&gt;
* [[Evaluación de las propuestas - 25/26]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
En esta página se informa sobre el proceso de restablecimiento de contraseñas en Evidentia para el curso 2025/2026.&lt;br /&gt;
&lt;br /&gt;
== Acceso a Evidentia ==&lt;br /&gt;
&lt;br /&gt;
; Restablecimiento de contraseñas  &lt;br /&gt;
El restablecimiento de contraseñas ya está funcionando correctamente en [https://evidentia.us.es Evidentia].  &lt;br /&gt;
Cada alumn@ dispone de una contraseña aleatoria asignada por el sistema, por lo que es '''necesario restablecer la contraseña''' antes de acceder.&lt;br /&gt;
&lt;br /&gt;
; Cómo hacerlo  &lt;br /&gt;
Debes hacerlo usando tu correo institucional de alumn@, el mismo que aparece en '''Sevius'''.  &lt;br /&gt;
Espera unos minutos a que llegue el correo de restablecimiento y revisa también la carpeta de '''spam'''.&lt;br /&gt;
&lt;br /&gt;
; Acceso obligatorio  &lt;br /&gt;
Aunque no participes activamente, '''tod@s l@s alumn@s deben poder entrar''', ya que las horas de asistencia también se contabilizan en este sistema.&lt;br /&gt;
&lt;br /&gt;
; Soporte técnico  &lt;br /&gt;
Si tienes algún problema, contacta con el grupo oficial de Telegram de soporte técnico: [https://t.me/evidentia_sat @evidentia_sat]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!--&lt;br /&gt;
&lt;br /&gt;
* [[Estimación inicial de tareas básicas]]&lt;br /&gt;
* [[Taller sobre pensamiento computacional y variabilidad]]&lt;br /&gt;
* Retrospectiva: &lt;br /&gt;
** [https://forms.office.com/e/n5yXafhimz Encuesta retrospectiva]&lt;br /&gt;
** [https://hdvirtual.us.es/discovirt/index.php/s/dFj78nJkgoYiSTR Información de la sesión]&lt;br /&gt;
--&amp;gt;&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Archivo:EGC_2025-26_P2.pdf&amp;diff=10087</id>
		<title>Archivo:EGC 2025-26 P2.pdf</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Archivo:EGC_2025-26_P2.pdf&amp;diff=10087"/>
				<updated>2025-10-01T10:53:12Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: Drorganvidez subió una nueva versión de Archivo:EGC 2025-26 P2.pdf&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Proyecto_InnoSoft&amp;diff=10079</id>
		<title>Proyecto InnoSoft</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Proyecto_InnoSoft&amp;diff=10079"/>
				<updated>2025-09-29T08:14:26Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[Página_Principal]] -&amp;gt; [[Proyecto - 25/26]]&lt;br /&gt;
&lt;br /&gt;
=Descripción=&lt;br /&gt;
Se trataría de hacer un proyecto que esté conformado por 1 o más equipos en el que el trabajo de todos los equipos esté integrado en un proyecto funcional. Se trataría de hacer tareas de automatización relacionadas con las jornadas como, por ejemplo: &lt;br /&gt;
&lt;br /&gt;
* Migración de funcionalidades de la versión Laravel de Evidentia (PHP) a la versión Flask de Evidentia (Python) usando la base tecnológica de UVLHub [https://github.com/EGCETSII/evidentia Evidentia (Laravel)]&lt;br /&gt;
* Automatización y gestión de la [https://institucional.us.es/innosoft/ web de de las jornadas]&lt;br /&gt;
* Web augmentation. Para sacar información relacionada con las jornadas&lt;br /&gt;
* Data analytics. Para hacer cuadros de mandos con información de la que se disponga o pueda aparecer en el futuro&lt;br /&gt;
* Automatización de las retrospectivas que se hagan&lt;br /&gt;
* Crear un generador de diplomas de participación en las jornadas&lt;br /&gt;
* Crear un sistema con API Rest para publicar y gestionar el programa de las jornadas&lt;br /&gt;
* Cualquier otro tema que se plantee automatizar&lt;br /&gt;
&lt;br /&gt;
= Enlaces de interés =&lt;br /&gt;
* Portal de Github con el código de Evidentia: https://github.com/EGCETSII/evidentia&lt;br /&gt;
* Posibles mejoras para Evidentia: https://github.com/drorganvidez/evidentia/issues&lt;br /&gt;
&lt;br /&gt;
=Impacto en la nota=&lt;br /&gt;
&lt;br /&gt;
Los componentes de estos proyectos podrán alcanzar una nota de 10 según cómo se desempeñen y la nota que el tutor decida ponerles según la evaluación hecha. &lt;br /&gt;
&lt;br /&gt;
=Tutores=&lt;br /&gt;
&lt;br /&gt;
En la medida de lo posible, el tutor será el profesor José A. Galindo o David Romero y tendrá que ponerse de acuerdo con el que se le asigne en el alcance y concreción del proyecto.&lt;br /&gt;
&lt;br /&gt;
=Política de nombre de los proyectos=&lt;br /&gt;
&lt;br /&gt;
Los proyectos se llamarán: innosoft-[sistema]-{numero-natural}, por ejemplo: &lt;br /&gt;
&lt;br /&gt;
* innosoft-evidentia&lt;br /&gt;
* innosoft-evidentia-1 innosoft-evidentia-2 (si hubiese más de un proyecto de evidentia)&lt;br /&gt;
&lt;br /&gt;
Si por algún motivo hubiera varios equipos trabajando sobre el mismo tema pero que finalmente decidieran no integrarse entre ellos, el nombre pasará a llamarse: innosoft-[sistema]-[fork]-[numero-natural], por ejemplo:&lt;br /&gt;
&lt;br /&gt;
innosoft-evidentia-fork-1&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Proyecto_InnoSoft&amp;diff=10078</id>
		<title>Proyecto InnoSoft</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Proyecto_InnoSoft&amp;diff=10078"/>
				<updated>2025-09-29T08:13:37Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[Página_Principal]] -&amp;gt; [[Proyecto - 25/26]]&lt;br /&gt;
&lt;br /&gt;
=Descripción=&lt;br /&gt;
Se trataría de hacer un proyecto que esté conformado por 1 o más equipos en el que el trabajo de todos los equipos esté integrado en un proyecto funcional. Se trataría de hacer tareas de automatización relacionadas con las jornadas como, por ejemplo: &lt;br /&gt;
&lt;br /&gt;
* Migración de funcionalidades de la versión Laravel de Evidentia (PHP) a la versión Flask de Evidentia (Python) usando la base tecnológica de UVLHub [https://github.com/EGCETSII/evidentia Evidentia (Laravel)]&lt;br /&gt;
* Automatización y gestión de la [https://institucional.us.es/innosoft/ web de de las jornadas]&lt;br /&gt;
* Web augmentation. Para sacar información relacionada con las jornadas&lt;br /&gt;
* Data analytics. Para hacer cuadros de mandos con información de la que se disponga o pueda aparecer en el futuro&lt;br /&gt;
* Automatización de las retrospectivas que se hagan&lt;br /&gt;
* Crear un generador de diplomas de participación en las jornadas&lt;br /&gt;
* Crear un sistema con API Rest para publicar y gestionar el programa de las jornadas&lt;br /&gt;
* Cualquier otro tema que se plantee automatizar&lt;br /&gt;
&lt;br /&gt;
= Enlaces de interés =&lt;br /&gt;
* Portal de Github con el código de Evidentia: https://github.com/drorganvidez/evidentia&lt;br /&gt;
* Posibles mejoras para Evidentia: https://github.com/drorganvidez/evidentia/issues&lt;br /&gt;
&lt;br /&gt;
=Impacto en la nota=&lt;br /&gt;
&lt;br /&gt;
Los componentes de estos proyectos podrán alcanzar una nota de 10 según cómo se desempeñen y la nota que el tutor decida ponerles según la evaluación hecha. &lt;br /&gt;
&lt;br /&gt;
=Tutores=&lt;br /&gt;
&lt;br /&gt;
En la medida de lo posible, el tutor será el profesor José A. Galindo o David Romero y tendrá que ponerse de acuerdo con el que se le asigne en el alcance y concreción del proyecto.&lt;br /&gt;
&lt;br /&gt;
=Política de nombre de los proyectos=&lt;br /&gt;
&lt;br /&gt;
Los proyectos se llamarán: innosoft-[sistema]-{numero-natural}, por ejemplo: &lt;br /&gt;
&lt;br /&gt;
* innosoft-evidentia&lt;br /&gt;
* innosoft-evidentia-1 innosoft-evidentia-2 (si hubiese más de un proyecto de evidentia)&lt;br /&gt;
&lt;br /&gt;
Si por algún motivo hubiera varios equipos trabajando sobre el mismo tema pero que finalmente decidieran no integrarse entre ellos, el nombre pasará a llamarse: innosoft-[sistema]-[fork]-[numero-natural], por ejemplo:&lt;br /&gt;
&lt;br /&gt;
innosoft-evidentia-fork-1&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Archivo:EGC_2025-26_P1.pdf&amp;diff=10075</id>
		<title>Archivo:EGC 2025-26 P1.pdf</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Archivo:EGC_2025-26_P1.pdf&amp;diff=10075"/>
				<updated>2025-09-24T21:51:18Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: Drorganvidez subió una nueva versión de Archivo:EGC 2025-26 P1.pdf&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Pr%C3%A1cticas_-_25/26&amp;diff=10073</id>
		<title>Prácticas - 25/26</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Pr%C3%A1cticas_-_25/26&amp;diff=10073"/>
				<updated>2025-09-23T06:05:50Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: Página creada con «Página_Principal -&amp;gt; 2025/2026 -&amp;gt; Prácticas - 25/26 * Práctica 1: '''Instalación del sistema base''' Archivo:EGC 2025-26 P1.pdf»&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[Página_Principal]] -&amp;gt; [[2025/2026]] -&amp;gt; [[Prácticas - 25/26]]&lt;br /&gt;
* Práctica 1: '''Instalación del sistema base''' [[Archivo:EGC 2025-26 P1.pdf]]&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Archivo:EGC_2025-26_P1.pdf&amp;diff=10072</id>
		<title>Archivo:EGC 2025-26 P1.pdf</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Archivo:EGC_2025-26_P1.pdf&amp;diff=10072"/>
				<updated>2025-09-23T06:05:01Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Proyecto_-_25/26&amp;diff=10071</id>
		<title>Proyecto - 25/26</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Proyecto_-_25/26&amp;diff=10071"/>
				<updated>2025-09-22T09:16:00Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[Página_Principal]] -&amp;gt; [[2025/2026]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''ESTA PÁGINA ESTÁ EN CONSTRUCCIÓN'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
= Material y enlaces relacionados con el proyecto =&lt;br /&gt;
&lt;br /&gt;
== Documentación general ==&lt;br /&gt;
&lt;br /&gt;
* Documento de descripción general del proyecto: [[Guía general del proyecto en equipo]]&lt;br /&gt;
* Inscripción de los equipos (leer instrucciones más abajo sobre el [[Proyecto_-_25/26#Milestones | M0]])&lt;br /&gt;
&lt;br /&gt;
= Tipos de proyectos según nota a la que aspira=&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;margin:auto&amp;quot;&lt;br /&gt;
|+ Tabla resumen de los tipos de proyectos&lt;br /&gt;
|-&lt;br /&gt;
! Tipos de proyectos !! Máxima nota a la que aspira !! Necesita coordinación&lt;br /&gt;
|-&lt;br /&gt;
| UVLHub single || 8 || NO&lt;br /&gt;
|-&lt;br /&gt;
| UVLHub equipo|| 10 || SI, 2 o más equipos&lt;br /&gt;
|-&lt;br /&gt;
| Innosoft || 10 || NO, pero podría hacerse&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Se podrá elegir entre los siguientes tipos de proyectos: &lt;br /&gt;
== Proyectos UVLHub ==&lt;br /&gt;
* Portal de Github con el código: https://github.com/EGCETSII/UVLHub&lt;br /&gt;
* Presentación de [https://hdvirtual.us.es/discovirt/index.php/s/eTfYiHSbMZ2aenp &amp;quot;UVLHub&amp;quot;]&lt;br /&gt;
* Breve guía para introducir cambios (es un inicio, el resto de material se ve en prácticas a lo largo del curso): https://docs.uvlhub.io/tutorials/crud_tutorial &lt;br /&gt;
&lt;br /&gt;
* Subtipos de proyectos UVLHub: &lt;br /&gt;
** UVLHub-single: un proyecto de un solo equipo derivado de UVLHub en el que no haya integración con otros equipos. &lt;br /&gt;
** UVLHub-equipo: Un proyecto de menos de 4 y más de 1 equipo derivado de UVLHub en el que todos los equipos estén integrados entre sí.&lt;br /&gt;
=== ¿Qué cambios se le puede hacer a UVLHub?===&lt;br /&gt;
* Se pueden hacer muchos cambios a UVLHub. Los cambio se basan en las [https://github.com/EGCETSII/uvlhub/issues issues] que están publicadas. Se pueden hacer otras sugerencias de cambio, pero deberán estar bien justificadas y aprobadas por su tutor. Los cambios están clasificados por su nivel de dificultad estimada (H = High; M = Medium;  L= Low).&lt;br /&gt;
* Hay dos cambios que tienen que hacer todos los equipos y que son adicionales a los demás que se definen en el siguiente apartado: &lt;br /&gt;
** WI de '''fakenodo''': para no conectarse con Zenodo y simular la llamada a una API ficticia. Es importante que entienda que NO se trata de hacer una réplica de Zenodo ([https://github.com/EGCETSII/uvlhub/issues/103 más información]).&lt;br /&gt;
** WI de '''newdataset''': para crear un nuevo hub que gestione otro tipo de datos distinto a UVL ([https://github.com/EGCETSII/uvlhub/issues/104 más información]).&lt;br /&gt;
* Además de los dos anteriores, se '''le asignarán''' tantos &amp;quot;Work Items&amp;quot; (WIs) como miembros tenga el proyecto. Los WIs que se le asignen serán en términos de dificultad al menos 2 WIs H, 2 WIs M y 2 WIs L. Si se tiene menos componentes, se irán eliminando de más simples a más complejos, por ejemplo, si termino entregando el proyecto con solo 4 componentes en el equipo, tendré que entregar, 2 WIs H y 2 WIs M; si fueran 5 componentes en el equipo 2WI h, 2 WIs M y 1 WI L. &lt;br /&gt;
* Puede haber WIs que, depende de cómo se aborden puedan ser divididos en varios WIs. Para ello, debe contar con el visto bueno de su tutor/a de proyecto.&lt;br /&gt;
* Puede haber WIs que, depende de cómo se aborden puedan bajar su dificultad o también subirla. Para ello, debe contar con el visto bueno de su tutor/a de proyecto usando para ello las tutorías o días de seguimiento.&lt;br /&gt;
* La asignación del los WIs se hará cuando se publiquen los equipos&lt;br /&gt;
&lt;br /&gt;
== Proyectos &amp;quot;InnoSoft&amp;quot;==&lt;br /&gt;
&lt;br /&gt;
* Un proyecto de uno o varios equipos relacionados con la automatización de las [[Innosoft - 25/26| jornadas]] (InnoSoft),[[Proyecto InnoSoft | más información]].&lt;br /&gt;
&lt;br /&gt;
= Equipos y proyectos =&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!--&lt;br /&gt;
M0: Este es el listado de proyectos y personas inscritas con la fecha límite establecida. Se señalan casos especiales según el color de algunas celdas. &lt;br /&gt;
&lt;br /&gt;
* [https://hdvirtual.us.es/discovirt/index.php/s/77ZmGZQaHPgswmi Listado de equipos y proyectos M0 (08/10/2024)]&lt;br /&gt;
* [https://hdvirtual.us.es/discovirt/index.php/s/esYenzRdPAAy2tX Listado de equipos y proyectos M0 - '''final''' (12/10/2024)]&lt;br /&gt;
--&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Si algún proyecto no se ha inscrito a tiempo podrá continuar con el proyecto pero no podrá presentarse a M1. Debe estar atento para una inscripción una vez haya pasado M1 (contacte con el coordinador de proyectos). Si alguna persona todavía no tiene proyecto, debe ponerse urgentemente en contacto con alguno de los proyectos que todavía tienen plazas libres/vacantes o, en su defecto, ponerse en contacto con el coordinador de proyectos (David Benavides) para solucionar la incidencia. &lt;br /&gt;
&lt;br /&gt;
Cualquier otra duda, puede dirigirse al coordinador de la asignatura.&lt;br /&gt;
&lt;br /&gt;
= Fechas Importantes =&lt;br /&gt;
&lt;br /&gt;
== Seguimientos ==&lt;br /&gt;
* [[Seguimiento Gestión del código | Seguimiento de gestión del código fuente (+info)]]  &lt;br /&gt;
* [[Seguimiento de pruebas | Seguimiento de pruebas (+info)]]  &lt;br /&gt;
* [[Seguimiento de integración/despliegue | Seguimiento de integración/despliegue (+info)]]&lt;br /&gt;
&lt;br /&gt;
== Milestones ==&lt;br /&gt;
* M0: [[M0-25_26 | Inscripción de los equipos (+info)]] &lt;br /&gt;
** V 3 de Octubre (fecha límite para rellenar el formulario de inscripción)&lt;br /&gt;
* M1: [[M1-25_26 | Sistema funcionando y pruebas (+info)]]&lt;br /&gt;
** M 21 de Octubre&lt;br /&gt;
*M2: [[M2-25_26 | Sistema funcionando y con incrementos (+info)]] &lt;br /&gt;
** M 11 de Noviembre&lt;br /&gt;
* M3: [[M3-25_26 | Entrega de proyectos y defensas (+info)]] &lt;br /&gt;
** M 16 Diciembre&lt;br /&gt;
** J 18 Diciembre&lt;br /&gt;
&lt;br /&gt;
= Calificaciones de los proyectos y los equipos =&lt;br /&gt;
&lt;br /&gt;
== Indicaciones sobre requisitos mínimos para superar el proyecto ==&lt;br /&gt;
* [[Guia-proyecto | Guía para conocer los requisitos mínimos para superar el proyecto]]&lt;br /&gt;
&lt;br /&gt;
== Cálculo de la nota del proyecto == &lt;br /&gt;
&lt;br /&gt;
Aunque la nota del proyecto está basado en el trabajo en equipo, la nota del proyecto es individual, es decir, lo ideal es que todos los componentes del equipo tengan la misma nota, pues eso sería una buena muestra de que el equipo ha trabajado de manera coordinada y equilibrada, pero '''puede darse el caso de que cada miembro del equipo tenga una nota distinta'''. &lt;br /&gt;
&lt;br /&gt;
El proyecto es parte de la evaluación continua de la asignatura y tiene el peso indicado en el proyecto docente de la asignatura. Para calcular la nota del proyecto se tienen en cuenta varios aspectos: &lt;br /&gt;
&lt;br /&gt;
* La inscripción del proyecto a tiempo (M0)&lt;br /&gt;
* La asistencia a las clases de seguimiento (seguimiento de proyectos en gestión del código fuente, en pruebas, en integración/despliegue)&lt;br /&gt;
* Las entregas y desempeño en los distintos milestones (M1, M2, M3)&lt;br /&gt;
&lt;br /&gt;
El total de la nota se calculará de la siguiente manera: &lt;br /&gt;
&lt;br /&gt;
Nota parcial del proyecto: &lt;br /&gt;
* El M3 tendrá una nota para cada integrante del proyecto&lt;br /&gt;
* El M2 podrá beneficiar hasta en 1 punto dicha nota. Si todo sale bien, podrá haber un incremento de hasta 1 punto en la nota. Si no sale bien, podrá haber hasta -1 punto en la nota. &lt;br /&gt;
* El M1 podrá beneficiar hasta en 1 punto dicha nota. Si todo sale bien, podrá haber un incremento de hasta 1 punto en la nota. Si no sale bien, podrá haber hasta -1 punto en la nota. &lt;br /&gt;
&lt;br /&gt;
Ejemplo: si una persona saca un 8 en M3 pero en M2 y M1 todo fue bien, su nota será entre 8 y 10. Sin embargo, si la misma persona tuvo algún percance en M1 o M2, podría sacar hasta un 6 en esta nota parcial.&lt;br /&gt;
&lt;br /&gt;
Penalizaciones por ausencias a seguimiento y puntualidad: &lt;br /&gt;
* La no asistencia a las sesiones de seguimiento penalizará un 5% cada una sobre la nota parcial del proyecto  (hasta un máximo de un 15%). '''¡OJO!''', eso se refiere a las sesiones de seguimiento, no a los milestones que tienen su impacto en la nota parcial del proyecto como se ha descrito en el punto anterior. &lt;br /&gt;
* La no inscripción a tiempo del equipo del proyecto penalizará un 5% sobre la nota parcial del proyecto.&lt;br /&gt;
&lt;br /&gt;
Nota final del proyecto: &lt;br /&gt;
&lt;br /&gt;
* La nota final del proyecto se calculará con la fórmula: &lt;br /&gt;
&lt;br /&gt;
'''NOTA FINAL DEL PROYECTO''' = Nota parcial del proyecto * [(100 - suma(penalizaciones_por_ausencia_y_puntualidad))/100]&lt;br /&gt;
&lt;br /&gt;
Por ejemplo, si una persona tiene una nota parcial del proyecto de 9 y no ha tenido ningún tipo de penalización por ausencia y puntualidad, su nota final del proyecto  se quedará en un 9. Si la misma persona con una nota de un 9 ha tenido alguna penalización por ausencia o puntualidad, su nota podrá bajar hasta en un 20%, es decir, hasta un 7,2.&lt;br /&gt;
&lt;br /&gt;
Tenga en cuenta que esta nota es la que se calcula para el proyecto pero después, dependiendo del proyecto, esta nota puede tener un límite (por ejemplo, de un 8 en el caso de proyectos UVLHub single).&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Archivo:EGC_2024-25_P1.pdf&amp;diff=9962</id>
		<title>Archivo:EGC 2024-25 P1.pdf</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Archivo:EGC_2024-25_P1.pdf&amp;diff=9962"/>
				<updated>2025-01-07T14:01:12Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: Drorganvidez subió una nueva versión de Archivo:EGC 2024-25 P1.pdf&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Práctica 1&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Dockerizando_una_aplicaci%C3%B3n&amp;diff=9869</id>
		<title>Tutorial Dockerizando una aplicación</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Dockerizando_una_aplicaci%C3%B3n&amp;diff=9869"/>
				<updated>2024-10-30T12:23:41Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Prerrequisitos =&lt;br /&gt;
== Instalar Docker Compose ==&lt;br /&gt;
&lt;br /&gt;
El primer paso es descargar la última versión de Docker Compose. No obstante, las últimas versiones de Docker ya incluyen Docker Compose. Antes de intentar realizar una instalación de Docker Compose, lanza el siguiente comando:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose version&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Si te funciona, puedes saltarte el paso de descargar e instalar Docker Compose. Si no, puedes descargar Docker Compose usando el siguiente comando, pero asegúrate de verificar la última versión en la página de lanzamientos de Docker Compose:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mkdir -p ~/.docker/cli-plugins/&lt;br /&gt;
&lt;br /&gt;
LATEST_VERSION=$(curl -s https://api.github.com/repos/docker/compose/releases/latest | grep '&amp;quot;tag_name&amp;quot;:' | sed -E 's/.*&amp;quot;([^&amp;quot;]+)&amp;quot;.*/\1/')&lt;br /&gt;
&lt;br /&gt;
sudo curl -SL &amp;quot;https://github.com/docker/compose/releases/download/${LATEST_VERSION}/docker-compose-$(uname -s)-$(uname -m)&amp;quot; -o ~/.docker/cli-plugins/docker-compose&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Aplicar Permisos Ejecutables: asegúrate de que el archivo de Docker Compose tenga permisos ejecutables:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt; &lt;br /&gt;
chmod +x ~/.docker/cli-plugins/docker-compose&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Verificar la instalación de Docker Compose: para confirmar que Docker Compose se ha instalado correctamente, ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose --version &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
= Dockerizando una Aplicación Flask =&lt;br /&gt;
&lt;br /&gt;
En este tutorial, aprenderemos a dockerizar la aplicación Flask `flask_testing_project` que creamos en la práctica anterior. Con esta dockerización, podrás ejecutar tu aplicación Flask en un contenedor Docker, lo que facilita la portabilidad y la ejecución en diferentes entornos. &lt;br /&gt;
&lt;br /&gt;
Para lograr esto, utilizaremos un archivo `Dockerfile` y un archivo de dependencias `requirements.txt` que especificarán cómo construir la imagen Docker y qué paquetes necesita la aplicación.&lt;br /&gt;
&lt;br /&gt;
== Estructura del Proyecto ==&lt;br /&gt;
&lt;br /&gt;
La estructura inicial del proyecto `flask_testing_project` es la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;plaintext&amp;quot;&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app.py                # Archivo principal de la aplicación Flask&lt;br /&gt;
├── templates/            # Directorio que contiene la plantilla HTML&lt;br /&gt;
│   └── tasks.html        # Plantilla para mostrar y agregar tareas&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── test_app.py       # Pruebas unitarias usando pytest&lt;br /&gt;
│   └── test_interfaz.py  # Pruebas de interfaz con Selenium&lt;br /&gt;
└── locustfile.py         # Archivo para pruebas de carga con Locust&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Para dockerizar la aplicación, añadiremos dos nuevos archivos:&lt;br /&gt;
* '''Dockerfile''': Define las instrucciones para construir la imagen de Docker.&lt;br /&gt;
* '''requirements.txt''': Contiene las dependencias de Python necesarias para ejecutar la aplicación.&lt;br /&gt;
&lt;br /&gt;
La estructura de directorios después de añadir estos archivos será:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;plaintext&amp;quot;&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app.py                # Archivo principal de la aplicación Flask&lt;br /&gt;
├── templates/            # Directorio que contiene la plantilla HTML&lt;br /&gt;
│   └── tasks.html        # Plantilla para mostrar y agregar tareas&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── test_app.py       # Pruebas unitarias usando pytest&lt;br /&gt;
│   └── test_interfaz.py  # Pruebas de interfaz con Selenium&lt;br /&gt;
├── locustfile.py         # Archivo para pruebas de carga con Locust&lt;br /&gt;
├── Dockerfile            # Archivo para construir la imagen Docker de la aplicación&lt;br /&gt;
└── requirements.txt      # Dependencias de Python necesarias para la aplicación&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Paso 1: Crear el archivo requirements.txt ==&lt;br /&gt;
&lt;br /&gt;
El archivo `requirements.txt` lista todas las dependencias de la aplicación. Es crucial para Docker porque le indica qué librerías de Python instalar. &lt;br /&gt;
&lt;br /&gt;
Dentro de `flask_testing_project`, crea un archivo llamado `requirements.txt` y copia el siguiente contenido en él:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;plaintext&amp;quot;&amp;gt;&lt;br /&gt;
attrs==24.2.0&lt;br /&gt;
blinker==1.8.2&lt;br /&gt;
Brotli==1.1.0&lt;br /&gt;
certifi==2024.8.30&lt;br /&gt;
charset-normalizer==3.4.0&lt;br /&gt;
click==8.1.7&lt;br /&gt;
ConfigArgParse==1.7&lt;br /&gt;
coverage==7.6.4&lt;br /&gt;
Flask==3.0.3&lt;br /&gt;
Flask-Cors==5.0.0&lt;br /&gt;
Flask-Login==0.6.3&lt;br /&gt;
Flask-SQLAlchemy==3.1.1&lt;br /&gt;
gevent==24.10.3&lt;br /&gt;
geventhttpclient==2.3.1&lt;br /&gt;
greenlet==3.1.1&lt;br /&gt;
h11==0.14.0&lt;br /&gt;
idna==3.10&lt;br /&gt;
iniconfig==2.0.0&lt;br /&gt;
itsdangerous==2.2.0&lt;br /&gt;
Jinja2==3.1.4&lt;br /&gt;
locust==2.32.0&lt;br /&gt;
MarkupSafe==3.0.2&lt;br /&gt;
msgpack==1.1.0&lt;br /&gt;
outcome==1.3.0.post0&lt;br /&gt;
packaging==24.1&lt;br /&gt;
pluggy==1.5.0&lt;br /&gt;
psutil==6.1.0&lt;br /&gt;
PyMySQL==1.1.1&lt;br /&gt;
PySocks==1.7.1&lt;br /&gt;
pytest==8.3.3&lt;br /&gt;
pytest-cov==5.0.0&lt;br /&gt;
python-dotenv==1.0.1&lt;br /&gt;
pyzmq==26.2.0&lt;br /&gt;
requests==2.32.3&lt;br /&gt;
selenium==4.25.0&lt;br /&gt;
setuptools==75.2.0&lt;br /&gt;
sniffio==1.3.1&lt;br /&gt;
sortedcontainers==2.4.0&lt;br /&gt;
SQLAlchemy==2.0.36&lt;br /&gt;
trio==0.27.0&lt;br /&gt;
trio-websocket==0.11.1&lt;br /&gt;
typing_extensions==4.12.2&lt;br /&gt;
urllib3==2.2.3&lt;br /&gt;
webdriver-manager==4.0.2&lt;br /&gt;
websocket-client==1.8.0&lt;br /&gt;
Werkzeug==3.0.4&lt;br /&gt;
wsproto==1.2.0&lt;br /&gt;
zope.event==5.0&lt;br /&gt;
zope.interface==7.1.1&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Recuerdas como instalar estas dependencias en un único paso?&lt;br /&gt;
&lt;br /&gt;
== Paso 2: Crear el archivo Dockerfile ==&lt;br /&gt;
&lt;br /&gt;
El `Dockerfile` es el núcleo de la dockerización, ya que define las instrucciones paso a paso para crear una imagen de Docker que ejecutará nuestra aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
Dentro de `flask_testing_project`, crea un archivo llamado `Dockerfile` con el siguiente contenido:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;docker&amp;quot;&amp;gt;&lt;br /&gt;
# Usar una imagen base de Python&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
&lt;br /&gt;
# Establecer el directorio de trabajo dentro del contenedor&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
&lt;br /&gt;
# Copiar el archivo de requisitos al directorio de trabajo del contenedor&lt;br /&gt;
COPY requirements.txt .&lt;br /&gt;
&lt;br /&gt;
# Instalar las dependencias desde requirements.txt&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
&lt;br /&gt;
# Copiar el contenido de tu aplicación al directorio de trabajo del contenedor&lt;br /&gt;
COPY . .&lt;br /&gt;
&lt;br /&gt;
# Exponer el puerto que usa Flask&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
&lt;br /&gt;
# Comando para ejecutar la aplicación Flask&lt;br /&gt;
CMD [&amp;quot;python&amp;quot;, &amp;quot;app.py&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Explicación línea a línea del Dockerfile ===&lt;br /&gt;
&lt;br /&gt;
* `FROM python:3.12-slim`: Utiliza una imagen base de Python ligera (versión 3.12) para optimizar el espacio que ocupa la imagen.&lt;br /&gt;
* `WORKDIR /app`: Crea y define `/app` como el directorio de trabajo donde se copiarán los archivos de la aplicación.&lt;br /&gt;
* `COPY requirements.txt .`: Copia el archivo `requirements.txt` desde el sistema de archivos local al directorio actual del contenedor (`/app`). Esto asegura que el contenedor tenga acceso a las dependencias de la aplicación.&lt;br /&gt;
* `RUN pip install --no-cache-dir -r requirements.txt`: Instala las dependencias especificadas en `requirements.txt` sin cachear archivos temporales, reduciendo el tamaño final de la imagen.&lt;br /&gt;
* `COPY . .`: Copia todos los archivos y carpetas desde el sistema local al directorio `/app` del contenedor, incluyendo `app.py` y otras carpetas de proyecto.&lt;br /&gt;
* `EXPOSE 5000`: Indica que el contenedor usará el puerto 5000, donde Flask ejecutará la aplicación.&lt;br /&gt;
* `CMD [&amp;quot;python&amp;quot;, &amp;quot;app.py&amp;quot;]`: Define el comando que se ejecutará cuando el contenedor inicie, en este caso, lanzando la aplicación Flask en `app.py`.&lt;br /&gt;
&lt;br /&gt;
== Paso 3: Modificar app.py para Ejecutar en Docker ==&lt;br /&gt;
&lt;br /&gt;
Flask, por defecto, ejecuta su servidor solo en `localhost`, lo que hace que el contenedor no sea accesible desde fuera. Para resolver esto, edita `app.py` y asegúrate de que contenga el siguiente código al final del archivo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
if __name__ == '__main__':&lt;br /&gt;
    app.run(host='0.0.0.0', port=5000, debug=True)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Al añadir `host='0.0.0.0'`, permitimos que Flask acepte conexiones desde cualquier IP externa, asegurando que podamos acceder a la aplicación desde fuera del contenedor Docker.&lt;br /&gt;
&lt;br /&gt;
== Paso 4: Construir y Ejecutar la Imagen Docker ==&lt;br /&gt;
&lt;br /&gt;
Con los archivos `Dockerfile`, `requirements.txt` y `app.py` preparados, estamos listos para construir la imagen Docker y ejecutar un contenedor con la aplicación.&lt;br /&gt;
&lt;br /&gt;
En la terminal, navega al directorio `flask_testing_project` y ejecuta los siguientes comandos:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Construir la imagen Docker y asignarle el nombre 'flask-testing_project'&lt;br /&gt;
docker build -t flask-testing_project .&lt;br /&gt;
&lt;br /&gt;
# Ejecutar el contenedor desde la imagen, mapeando el puerto 5000 al mismo puerto en el host&lt;br /&gt;
docker run -p 5000:5000 flask-testing_project&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* `docker build -t flask-testing_project .`: Construye una imagen Docker basada en el `Dockerfile` del directorio actual. El argumento `-t flask-testing_project` asigna un nombre a la imagen creada.&lt;br /&gt;
* `docker run -p 5000:5000 flask-testing_project`: Ejecuta la imagen en un contenedor, mapeando el puerto 5000 del contenedor al puerto 5000 de la máquina local. De esta forma, la aplicación Flask es accesible desde el navegador en `localhost:5000`.&lt;br /&gt;
&lt;br /&gt;
== Paso 5: Verificación en el Navegador ==&lt;br /&gt;
&lt;br /&gt;
Una vez que el contenedor esté en ejecución, abre tu navegador y accede a [http://localhost:5000](http://localhost:5000). Si ves la aplicación funcionando, ¡felicidades! Has dockerizado exitosamente la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
== Resumen ==&lt;br /&gt;
&lt;br /&gt;
Este tutorial te ha guiado a través del proceso completo de dockerización de una aplicación Flask:&lt;br /&gt;
* Creamos el archivo `requirements.txt` para listar las dependencias de la aplicación.&lt;br /&gt;
* Construimos un `Dockerfile` detallado para definir los pasos de construcción de la imagen.&lt;br /&gt;
* Modificamos `app.py` para hacer la aplicación accesible externamente.&lt;br /&gt;
* Construimos la imagen y ejecutamos el contenedor, logrando que la aplicación esté disponible en el navegador.&lt;br /&gt;
&lt;br /&gt;
Al seguir estos pasos, has asegurado que la aplicación Flask pueda ejecutarse en cualquier entorno compatible con Docker, simplificando la portabilidad y la escalabilidad del proyecto.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
= Dockerizando una Aplicación Flask con Base de Datos usando Docker Compose =&lt;br /&gt;
&lt;br /&gt;
En este tutorial, aprenderemos a dockerizar la aplicación Flask `flask_testing_project` que creamos en la práctica anterior y añadiremos una base de datos externa. Esto nos permitirá ejecutar la aplicación en contenedores separados para la aplicación Flask y la base de datos, facilitando la escalabilidad y el mantenimiento. &lt;br /&gt;
&lt;br /&gt;
Para lograr esto, usaremos Docker Compose y ajustaremos nuestra aplicación para que utilice una base de datos MariaDB.&lt;br /&gt;
&lt;br /&gt;
== Estructura del Proyecto ==&lt;br /&gt;
&lt;br /&gt;
La estructura inicial de `flask_testing_project` es la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;plaintext&amp;quot;&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app.py                # Archivo principal de la aplicación Flask&lt;br /&gt;
├── templates/            # Directorio con la plantilla HTML&lt;br /&gt;
│   └── tasks.html        # Plantilla para mostrar y agregar tareas&lt;br /&gt;
├── tests/                # Directorio con pruebas unitarias y de interfaz&lt;br /&gt;
│   ├── test_app.py&lt;br /&gt;
│   └── test_interfaz.py&lt;br /&gt;
├── locustfile.py         # Archivo para pruebas de carga con Locust&lt;br /&gt;
├── Dockerfile            # Archivo para construir la imagen Docker de la aplicación&lt;br /&gt;
└── requirements.txt      # Dependencias de Python necesarias para la aplicación&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Para agregar Docker Compose, crearemos un nuevo archivo `docker-compose.yml`, y realizaremos algunos cambios en `app.py` para que la aplicación se conecte a MariaDB.&lt;br /&gt;
&lt;br /&gt;
La estructura final del proyecto será:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;plaintext&amp;quot;&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app.py                # Archivo principal de la aplicación Flask&lt;br /&gt;
├── templates/            # Directorio con la plantilla HTML&lt;br /&gt;
│   └── tasks.html        # Plantilla para mostrar y agregar tareas&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── test_app.py&lt;br /&gt;
│   └── test_interfaz.py&lt;br /&gt;
├── locustfile.py         # Archivo para pruebas de carga con Locust&lt;br /&gt;
├── Dockerfile            # Archivo para construir la imagen Docker de la aplicación&lt;br /&gt;
├── requirements.txt      # Dependencias de Python necesarias para la aplicación&lt;br /&gt;
└── docker-compose.yml    # Archivo de configuración de Docker Compose&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Paso 1: Crear el archivo docker-compose.yml ==&lt;br /&gt;
&lt;br /&gt;
Docker Compose permite definir y gestionar múltiples contenedores en un solo archivo de configuración. En este archivo, definiremos dos servicios: uno para la aplicación Flask (`web`) y otro para la base de datos MariaDB (`db`).&lt;br /&gt;
&lt;br /&gt;
Dentro de `flask_testing_project`, crea un archivo llamado `docker-compose.yml` y copia el siguiente contenido:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;yaml&amp;quot;&amp;gt;&lt;br /&gt;
version: '3'&lt;br /&gt;
&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    build: .&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;5000:5000&amp;quot;&lt;br /&gt;
    environment:&lt;br /&gt;
      - FLASK_ENV=development&lt;br /&gt;
      - DATABASE_HOST=db&lt;br /&gt;
      - DATABASE_USER=root&lt;br /&gt;
      - DATABASE_PASSWORD=my-secret-pw&lt;br /&gt;
      - DATABASE_DB=flaskdb&lt;br /&gt;
    depends_on:&lt;br /&gt;
      - db&lt;br /&gt;
&lt;br /&gt;
  db:&lt;br /&gt;
    image: mariadb:10.5&lt;br /&gt;
    restart: always&lt;br /&gt;
    environment:&lt;br /&gt;
      MYSQL_ROOT_PASSWORD: my-secret-pw&lt;br /&gt;
      MYSQL_DATABASE: flaskdb&lt;br /&gt;
    volumes:&lt;br /&gt;
      - db_data:/var/lib/mysql&lt;br /&gt;
&lt;br /&gt;
volumes:&lt;br /&gt;
  db_data:&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Explicación línea a línea del docker-compose.yml ===&lt;br /&gt;
&lt;br /&gt;
* `version: '3'`: Especifica la versión de Docker Compose.&lt;br /&gt;
* `services`: Define los servicios que Docker Compose manejará, en este caso `web` y `db`.&lt;br /&gt;
* `web`: Servicio para la aplicación Flask.&lt;br /&gt;
** `build: .`: Indica que el servicio usará el Dockerfile en el directorio actual.&lt;br /&gt;
** `ports`: Mapea el puerto 5000 del contenedor al puerto 5000 de la máquina local.&lt;br /&gt;
** `environment`: Define variables de entorno para configurar la conexión a la base de datos.&lt;br /&gt;
*** `FLASK_ENV=development`: Ejecuta Flask en modo de desarrollo.&lt;br /&gt;
*** `DATABASE_HOST=db`: Dirección del contenedor `db` para la base de datos.&lt;br /&gt;
*** `DATABASE_USER=root`, `DATABASE_PASSWORD=my-secret-pw`, `DATABASE_DB=flaskdb`: Credenciales y base de datos que usará Flask.&lt;br /&gt;
** `depends_on`: Asegura que el contenedor `db` esté ejecutándose antes de iniciar `web`.&lt;br /&gt;
* `db`: Servicio para la base de datos MariaDB.&lt;br /&gt;
** `image: mariadb:10.5`: Usa la imagen de MariaDB versión 10.5.&lt;br /&gt;
** `restart: always`: Reinicia el contenedor automáticamente en caso de errores.&lt;br /&gt;
** `environment`: Define las credenciales y la base de datos por defecto.&lt;br /&gt;
** `volumes`: Almacena los datos en un volumen persistente `db_data`.&lt;br /&gt;
* `volumes`: Define `db_data` para que los datos de la base de datos persistan en el sistema anfitrión.&lt;br /&gt;
&lt;br /&gt;
== Paso 2: Modificar app.py para Conectarse a MariaDB ==&lt;br /&gt;
&lt;br /&gt;
Actualizaremos `app.py` para usar SQLAlchemy y conectarse a MariaDB mediante las variables de entorno definidas en Docker Compose.&lt;br /&gt;
&lt;br /&gt;
Reemplaza el contenido de `app.py` por el siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
from flask_sqlalchemy import SQLAlchemy&lt;br /&gt;
import os&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Configuración de la base de datos MariaDB&lt;br /&gt;
app.config['SQLALCHEMY_DATABASE_URI'] = f&amp;quot;mysql+pymysql://{os.getenv('DATABASE_USER')}:{os.getenv('DATABASE_PASSWORD')}@{os.getenv('DATABASE_HOST')}/{os.getenv('DATABASE_DB')}&amp;quot;&lt;br /&gt;
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False&lt;br /&gt;
&lt;br /&gt;
db = SQLAlchemy(app)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# Definición del modelo de Tarea&lt;br /&gt;
class Task(db.Model):&lt;br /&gt;
    id = db.Column(db.Integer, primary_key=True)&lt;br /&gt;
    title = db.Column(db.String(100), nullable=False)&lt;br /&gt;
    done = db.Column(db.Boolean, default=False)&lt;br /&gt;
&lt;br /&gt;
    def to_dict(self):&lt;br /&gt;
        return {&lt;br /&gt;
            'id': self.id,&lt;br /&gt;
            'title': self.title,&lt;br /&gt;
            'done': self.done&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# Ruta para obtener la lista de tareas (versión HTML)&lt;br /&gt;
@app.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    tasks = Task.query.all()&lt;br /&gt;
    return render_template('tasks.html', tasks=tasks)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# Ruta para obtener la lista de tareas en JSON (API)&lt;br /&gt;
@app.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    tasks = Task.query.all()&lt;br /&gt;
    return jsonify({'tasks': [task.to_dict() for task in tasks]})&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# Ruta para crear una nueva tarea desde un formulario HTML&lt;br /&gt;
@app.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    if not title:&lt;br /&gt;
        return &amp;quot;El título es necesario&amp;quot;, 400&lt;br /&gt;
    new_task = Task(title=title, done=False)&lt;br /&gt;
    db.session.add(new_task)&lt;br /&gt;
    db.session.commit()&lt;br /&gt;
    return redirect(url_for('task_list'))&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# Ruta para crear una nueva tarea (API JSON)&lt;br /&gt;
@app.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task():&lt;br /&gt;
    if not request.json or 'title' not in request.json:&lt;br /&gt;
        return jsonify({'error': 'El título es necesario'}), 400&lt;br /&gt;
    new_task = Task(title=request.json['title'], done=False)&lt;br /&gt;
    db.session.add(new_task)&lt;br /&gt;
    db.session.commit()&lt;br /&gt;
    return jsonify(new_task.to_dict()), 201&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
if __name__ == '__main__':&lt;br /&gt;
    # Crear las tablas en la base de datos si no existen&lt;br /&gt;
    with app.app_context():&lt;br /&gt;
        db.create_all()&lt;br /&gt;
&lt;br /&gt;
    app.run(host='0.0.0.0', port=5000, debug=True)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Paso 3: Construir y Ejecutar los Contenedores con Docker Compose ==&lt;br /&gt;
&lt;br /&gt;
Ahora que `docker-compose.yml` y `app.py` están configurados, estamos listos para construir y ejecutar la aplicación con Docker Compose.&lt;br /&gt;
&lt;br /&gt;
En la terminal, navega al directorio `flask_testing_project` y ejecuta los siguientes comandos:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Construir y ejecutar los contenedores&lt;br /&gt;
docker compose up&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* `docker compose up`: Construye y ejecuta los contenedores en segundo plano. Si la imagen no existe, la creará usando el Dockerfile y las configuraciones de Docker Compose.&lt;br /&gt;
&lt;br /&gt;
'''¿Aparece algún error? ¿A qué crees que se debe?'''&lt;br /&gt;
&lt;br /&gt;
Para detener los contenedores y eliminar el entorno, ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Parar y remover los contenedores&lt;br /&gt;
docker compose down&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* `docker compose down`: Detiene todos los contenedores y elimina el entorno definido en `docker-compose.yml`, sin eliminar los volúmenes de datos.&lt;br /&gt;
&lt;br /&gt;
== Paso 4: Verificación en el Navegador ==&lt;br /&gt;
&lt;br /&gt;
Una vez que los contenedores estén en ejecución, abre tu navegador y accede a [http://localhost:5000](http://localhost:5000). Si ves la aplicación funcionando y puedes agregar tareas, ¡felicidades! Has dockerizado exitosamente la aplicación Flask con una base de datos MariaDB.&lt;br /&gt;
&lt;br /&gt;
== Resumen ==&lt;br /&gt;
&lt;br /&gt;
En este tutorial:&lt;br /&gt;
* Creamos un archivo `docker-compose.yml` para gestionar múltiples contenedores.&lt;br /&gt;
* Modificamos `app.py` para usar SQLAlchemy y conectar la aplicación Flask a una base de datos MariaDB.&lt;br /&gt;
* Ejecutamos y verificamos la aplicación con Docker Compose.&lt;br /&gt;
&lt;br /&gt;
Ahora tienes una aplicación Flask lista para desarrollarse y desplegarse fácilmente en cualquier entorno.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
= Tutorial sobre Capas en Docker =&lt;br /&gt;
&lt;br /&gt;
Las capas son uno de los conceptos fundamentales de Docker, ya que permiten la eficiencia en el almacenamiento y la distribución de imágenes. En este tutorial, aprenderás sobre cómo funcionan las capas en Docker y cómo puedes trabajar con ellas utilizando comandos en la terminal.&lt;br /&gt;
&lt;br /&gt;
== ¿Qué son las Capas en Docker? ==&lt;br /&gt;
&lt;br /&gt;
Docker utiliza un sistema de capas para construir imágenes. Cada capa representa una modificación sobre la capa anterior. Esto significa que las imágenes de Docker son inmutables y se construyen de manera incremental. Cada vez que realizas un cambio, como instalar un nuevo paquete o copiar archivos, se crea una nueva capa.&lt;br /&gt;
&lt;br /&gt;
=== Ventajas de las Capas ===&lt;br /&gt;
* '''Eficiencia de almacenamiento''': Las capas son compartidas entre imágenes, lo que significa que si dos imágenes utilizan la misma capa, solo se almacena una vez en el disco.&lt;br /&gt;
* '''Despliegue rápido''': Las imágenes se pueden construir rápidamente reutilizando las capas existentes.&lt;br /&gt;
* '''Desarrollo ágil''': Las capas permiten revertir cambios fácilmente al utilizar imágenes anteriores.&lt;br /&gt;
&lt;br /&gt;
== Comandos y Trabajo en la Terminal con Capas ==&lt;br /&gt;
&lt;br /&gt;
Para interactuar y trabajar con capas en Docker, puedes utilizar varios comandos útiles en la terminal. A continuación, se presentan los pasos que puedes seguir para experimentar y observar cómo funcionan las capas en acción.&lt;br /&gt;
&lt;br /&gt;
=== Paso 1: Listar Imágenes y Capas ===&lt;br /&gt;
&lt;br /&gt;
Para ver las imágenes y sus capas, abre tu terminal y ejecuta el siguiente comando:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker images&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto mostrará una lista de las imágenes en tu sistema, incluyendo el `REPOSITORY`, `TAG`, `IMAGE ID`, `CREATED`, y `SIZE`. Observa que las imágenes más pequeñas suelen tener capas compartidas con otras.&lt;br /&gt;
&lt;br /&gt;
=== Paso 2: Inspeccionar una Imagen ===&lt;br /&gt;
&lt;br /&gt;
Selecciona una imagen de la lista y utiliza el siguiente comando para inspeccionarla y ver detalles sobre sus capas:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker inspect &amp;lt;IMAGE_ID&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Reemplaza `&amp;lt;IMAGE_ID&amp;gt;` con el ID de la imagen que te interesa. Este comando te mostrará un JSON con información sobre la imagen, incluyendo sus capas y metadatos.&lt;br /&gt;
&lt;br /&gt;
Ahora que has inspeccionado un contenedor, ¿qué otra información relevante crees que puedes obtener aquí? &lt;br /&gt;
&lt;br /&gt;
=== Paso 3: Ver el Historial de una Imagen ===&lt;br /&gt;
&lt;br /&gt;
Para ver el historial de las capas de una imagen, usa el siguiente comando:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker history &amp;lt;IMAGE_ID&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto mostrará el tamaño de cada capa, la fecha en que se creó y el comando que se utilizó para crearla. Es una excelente manera de entender cómo se construyó la imagen.&lt;br /&gt;
&lt;br /&gt;
=== Paso 4: Limpiar Capas No Utilizadas ===&lt;br /&gt;
&lt;br /&gt;
Docker almacena capas que pueden no ser necesarias después de la construcción de imágenes. Para limpiar las capas no utilizadas y liberar espacio en disco, ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker image prune&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando eliminará las capas y las imágenes no utilizadas que no tienen un contenedor asociado. Confirma la acción cuando se te pida.&lt;br /&gt;
&lt;br /&gt;
=== Paso 5: Construir Imágenes con Caching ===&lt;br /&gt;
&lt;br /&gt;
Cuando construyes una imagen, Docker utiliza caché para acelerar el proceso. Para forzar a Docker a no utilizar la caché y reconstruir todas las capas, usa el siguiente comando:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker build --no-cache -t &amp;lt;IMAGE_NAME&amp;gt; .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Reemplaza `&amp;lt;IMAGE_NAME&amp;gt;` con el nombre que deseas dar a la imagen. Esto es útil si has realizado cambios en las capas anteriores que no se reflejan debido a la caché.&lt;br /&gt;
&lt;br /&gt;
== Ejercicios Prácticos con Capas ==&lt;br /&gt;
&lt;br /&gt;
Ahora que conoces los conceptos básicos y los comandos para trabajar con capas en Docker, vamos a realizar algunos ejercicios prácticos.&lt;br /&gt;
&lt;br /&gt;
=== Ejercicio 1: Crear una Nueva Imagen ===&lt;br /&gt;
&lt;br /&gt;
1. Crea un nuevo directorio para tu proyecto y navega a él:&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
    mkdir docker-layers-demo&lt;br /&gt;
    cd docker-layers-demo&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. Crea un archivo `Dockerfile` con el siguiente contenido:&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
    FROM ubuntu:latest&lt;br /&gt;
    RUN apt-get update &amp;amp;&amp;amp; apt-get install -y curl&lt;br /&gt;
    CMD [&amp;quot;bash&amp;quot;]&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. Construye la imagen:&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
    docker build -t my-curl-image .&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. Lista las imágenes para verificar que tu nueva imagen se haya creado:&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
    docker images&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Ejercicio 2: Modificar la Imagen y Ver las Capas ===&lt;br /&gt;
&lt;br /&gt;
1. Edita el `Dockerfile` para agregar un nuevo comando que instale `git`:&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
    RUN apt-get install -y git&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. Vuelve a construir la imagen usando el comando:&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
    docker build -t my-curl-image .&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. Verifica el historial de la imagen para observar cómo se han creado las capas:&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
    docker history my-curl-image&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
'''Preguntas:'''&lt;br /&gt;
* ¿Cuántas capas se han creado después de instalar `git`?&lt;br /&gt;
* ¿Cuál es el tamaño de cada capa?&lt;br /&gt;
&lt;br /&gt;
=== Ejercicio 3: Explorar Capas de una Imagen Existente ===&lt;br /&gt;
&lt;br /&gt;
1. Elige una imagen existente en tu sistema (por ejemplo, `ubuntu`) y usa el siguiente comando para inspeccionarla:&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
    docker inspect ubuntu&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
'''Observa las capas y responde:'''&lt;br /&gt;
* ¿Cuántas capas tiene la imagen `ubuntu`?&lt;br /&gt;
* ¿Cuál es la información más relevante que puedes obtener de la inspección?&lt;br /&gt;
&lt;br /&gt;
=== Ejercicio 4: Limpiar Imágenes y Capas ===&lt;br /&gt;
&lt;br /&gt;
1. Usa el comando de limpieza para eliminar las capas no utilizadas:&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
    docker image prune&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. Verifica nuevamente la lista de imágenes para asegurarte de que las no utilizadas han sido eliminadas:&lt;br /&gt;
    &amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
    docker images&lt;br /&gt;
    &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Resumen ==&lt;br /&gt;
&lt;br /&gt;
Experimentar con estos comandos y ejercicios te permitirá entender mejor cómo funcionan las capas en Docker y cómo puedes gestionarlas eficazmente. Al manipular capas y explorar imágenes, podrás optimizar tus flujos de trabajo y crear entornos más eficientes.&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Elementos_evaluables_y_c%C3%A1lculo_de_nota_-_24/25&amp;diff=9868</id>
		<title>Elementos evaluables y cálculo de nota - 24/25</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Elementos_evaluables_y_c%C3%A1lculo_de_nota_-_24/25&amp;diff=9868"/>
				<updated>2024-10-30T12:06:28Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[Página_Principal]] -&amp;gt; [[2024/2025]] &lt;br /&gt;
&lt;br /&gt;
== Cálculo de la nota==&lt;br /&gt;
&lt;br /&gt;
El sistema de evaluación será por '''evaluación global en la primera convocatoria''' y por '''examen en la segunda y tercera''' convocatoria. &lt;br /&gt;
&lt;br /&gt;
=== Primera convocatoria ===&lt;br /&gt;
&lt;br /&gt;
Se pueden elegir entre las siguientes intensificaciones: &lt;br /&gt;
&lt;br /&gt;
(1) '''Colaborativa'''&lt;br /&gt;
&lt;br /&gt;
(2) '''Técnico-organizativa'''&lt;br /&gt;
&lt;br /&gt;
(3) '''Técnica'''&lt;br /&gt;
&lt;br /&gt;
En los tres itinerarios habrán tres actividades con pesos distintos: Participación en InnoSoft, Proyecto en equipo y Ejercicio individual. &lt;br /&gt;
&lt;br /&gt;
La siguiente tabla resume los pesos de los distintos elementos en cada intensificación:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;margin:auto&amp;quot;&lt;br /&gt;
|+ Tabla resumen de los pesos de los elementos evaluables&lt;br /&gt;
|-&lt;br /&gt;
! Itinerario !! Innosoft !! Proyecto !! Ejercicio individual&lt;br /&gt;
|-&lt;br /&gt;
| Colaborativa|| 30% || 60% || 10%&lt;br /&gt;
|-&lt;br /&gt;
| Técnico-organizativa || 20% || 60% || 20% &lt;br /&gt;
|-&lt;br /&gt;
| Técnica || 10% || 60% || 30% &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
'''En todos los casos''':&lt;br /&gt;
* El '''ejercicio individual''' constará de '''dos partes''', '''una de teoría (último día de clase)''' y '''otra de prácticas (10 de enero)''', siendo el peso de 30% de teoría y 70% de práctica. &lt;br /&gt;
* Debe sacarse como mínimo un 4 tanto en el ejercicio individual como en el proyecto para así poder hacer media con el resto de elementos. Si no se alcanza el 4 o en el ejercicio individual o en el proyecto, la nota máxima será un 4 en la evaluación global. &lt;br /&gt;
&lt;br /&gt;
'''Sobre la obtención de la matrícula de honor''':&lt;br /&gt;
Las notas de todas las posibilidades de evaluación pueden sumar hasta 10 entre los distintos ítems que las componen. Sin embargo, tendrá una nota final como mucho de un 9. Si se quiere optar a una nota entre el 9 y el 10, entonces se deben hacer al menos algunas de las siguientes actividades que solo puntuarán para esa franja de nota:&lt;br /&gt;
*	Presentar un seminario/taller.&lt;br /&gt;
*	Leer un documento técnico y/o científico, resumirlo y presentarlo.&lt;br /&gt;
*	Resolver un reto tecnológico/ de investigación.&lt;br /&gt;
*	Realizar Pull Request al proyecto original y que sea aceptado.&lt;br /&gt;
Estos trabajos serán evaluados para determinar hasta cuántos puntos sube la nota en la franja del 9 al 10. Sobre aquellos/as alumnos/as que hagan estas actividades se elegirá quienes puedan optar a matrícula de honor.&lt;br /&gt;
Quienes estén interesados/as en subir a dicha nota tendrán que ponerse en contacto con el coordinador de la asignatura una vez publicadas las notas provisionales o bien, si así lo prefiern, con anterioridad para ir trabajando con antelación. &lt;br /&gt;
&lt;br /&gt;
==Más información sobre elementos evaluables: ==&lt;br /&gt;
* [[Proyecto - 24/25]] &lt;br /&gt;
* [[Ejercicio - 24/25]]&lt;br /&gt;
* [[Innosoft - 24/25]]&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_de_Docker&amp;diff=9867</id>
		<title>Tutorial Campo de entrenamiento de Docker</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_de_Docker&amp;diff=9867"/>
				<updated>2024-10-30T08:44:36Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Prerrequisitos =&lt;br /&gt;
&lt;br /&gt;
== Instalación Docker ==&lt;br /&gt;
&lt;br /&gt;
=== Instrucciones para instalar Docker en Ubuntu 22.04 (Jammy Jellyfish) ===&lt;br /&gt;
&lt;br /&gt;
Antes de comenzar a utilizar Docker, necesitamos asegurarnos de que está instalado en nuestro sistema. Aquí te mostramos cómo hacerlo.&lt;br /&gt;
&lt;br /&gt;
Primero, actualizaremos la lista de paquetes disponibles en tu sistema. Este paso asegura que tu sistema tenga la información más reciente sobre qué paquetes se pueden instalar y actualizar:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo apt update&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A continuación, instala algunos paquetes necesarios que permiten a `apt` manejar repositorios sobre HTTPS. Esto es importante porque garantiza que tu sistema puede descargar software de fuentes seguras y confiables:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo apt install apt-transport-https ca-certificates curl software-properties-common&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ahora, es momento de añadir la clave GPG del repositorio oficial de Docker a tu sistema. Esta clave es esencial porque se utiliza para verificar que el software que descargas es auténtico y no ha sido alterado por terceros:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A continuación, añadirás el repositorio de Docker a las fuentes de `apt`. Este paso es necesario para que tu sistema sepa dónde buscar las actualizaciones y versiones de Docker:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
echo &amp;quot;deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable&amp;quot; | sudo tee /etc/apt/sources.list.d/docker.list &amp;gt; /dev/null&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Después de añadir el repositorio, es recomendable actualizar la lista de paquetes nuevamente. Esto permite que `apt` reconozca el nuevo repositorio de Docker:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo apt update&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Finalmente, puedes instalar Docker con el siguiente comando. Este comando instalará la versión más reciente de Docker Community Edition (CE) en tu sistema:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo apt install docker-ce&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Para asegurarte de que Docker se ha instalado correctamente y está funcionando, verifica el estado del servicio Docker. Al ejecutar este comando, deberías ver que el servicio está activo y en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo systemctl status docker&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Uso de Docker sin `sudo` ===&lt;br /&gt;
&lt;br /&gt;
Por defecto, Docker necesita permisos de administrador para ejecutar sus comandos. Sin embargo, puedes añadir tu usuario al grupo `docker` para evitar tener que usar `sudo` cada vez que ejecutes comandos de Docker. Esto simplifica el uso de Docker y hace que sea más conveniente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo usermod -aG docker ${USER}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Después de ejecutar este comando, necesitarás cerrar la sesión y volver a iniciarla para que los cambios surtan efecto. Si prefieres no cerrar la sesión, puedes aplicar los cambios usando:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
su - ${USER}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Si tuvieras problemas de permisos con el socket de Docker, una forma de solucionarlo es:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo chmod 666 /var/run/docker.sock&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando permite lectura y escritura en el socket para todos los usuarios, pero es una solución temporal y no la más segura. Puede ser útil para probar si el problema es de permisos.&lt;br /&gt;
&lt;br /&gt;
= Tutorial de Iniciación a Docker =&lt;br /&gt;
&lt;br /&gt;
== Docker básico: primer contacto ==&lt;br /&gt;
&lt;br /&gt;
Para comenzar a trabajar con Docker, es fundamental asegurarte de que todo está funcionando correctamente en tu sistema.&lt;br /&gt;
&lt;br /&gt;
=== 1. Ejecutar tu primer contenedor: &amp;quot;Hello World&amp;quot; ===&lt;br /&gt;
&lt;br /&gt;
El primer paso en Docker es ejecutar un contenedor básico que te confirme que todo está configurado adecuadamente. Para ello, utilizaremos la imagen &amp;quot;hello-world&amp;quot;, que está diseñada para asegurarse de que Docker está instalado y funcionando correctamente.&lt;br /&gt;
&lt;br /&gt;
Abre tu terminal y ejecuta el siguiente comando:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run hello-world&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando realiza varias acciones:&lt;br /&gt;
* Descarga la imagen: Si no tienes la imagen &amp;quot;hello-world&amp;quot; en tu máquina, Docker la descargará desde Docker Hub, que es un repositorio de imágenes Docker.&lt;br /&gt;
* Crea un contenedor: Una vez que la imagen está disponible, Docker crea un nuevo contenedor basado en ella.&lt;br /&gt;
* Ejecuta el contenedor: Finalmente, Docker ejecuta el contenedor, que mostrará un mensaje de bienvenida en tu terminal.&lt;br /&gt;
&lt;br /&gt;
Si todo ha funcionado correctamente, deberías ver un mensaje que comienza con &amp;quot;Hello from Docker!&amp;quot;. ¡Esto significa que tu instalación de Docker está en buen estado!&lt;br /&gt;
&lt;br /&gt;
=== 2. Ver las imágenes disponibles en tu sistema ===&lt;br /&gt;
&lt;br /&gt;
Después de ejecutar un contenedor, Docker guarda una copia de la imagen en tu sistema. Para ver las imágenes que tienes localmente, usa el siguiente comando:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker images&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando mostrará una lista de todas las imágenes almacenadas en tu sistema local. Aquí deberías ver la imagen de &amp;quot;hello-world&amp;quot; que acabas de descargar.&lt;br /&gt;
&lt;br /&gt;
=== 3. Descargando una imagen de Ubuntu ===&lt;br /&gt;
&lt;br /&gt;
Ahora, vamos a descargar una imagen que es más útil para el desarrollo: Ubuntu. Para hacerlo, ejecuta el siguiente comando:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker pull ubuntu&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando descargará la última versión de la imagen de Ubuntu desde Docker Hub. Tener la imagen de Ubuntu te permitirá ejecutar una versión ligera de este sistema operativo dentro de un contenedor.&lt;br /&gt;
&lt;br /&gt;
=== 4. Ejecutar un contenedor interactivo con Ubuntu ===&lt;br /&gt;
&lt;br /&gt;
Docker te permite ejecutar contenedores en modo interactivo, lo que significa que puedes acceder a una terminal dentro del contenedor y trabajar en él como si fuera una máquina independiente. Para hacerlo, ejecuta el siguiente comando:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -it ubuntu bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando hace lo siguiente:&lt;br /&gt;
* `-it`: Esta opción combina dos parámetros: `-i` (interactivo) y `-t` (terminal), permitiéndote interactuar con el contenedor a través de una terminal.&lt;br /&gt;
* `ubuntu`: Especifica que quieres crear el contenedor a partir de la imagen de Ubuntu.&lt;br /&gt;
* `bash`: Es el comando que se ejecutará dentro del contenedor. Aquí es donde decides qué shell usar.&lt;br /&gt;
&lt;br /&gt;
Al usar `bash`, estás utilizando el shell por defecto de Ubuntu, que proporciona un entorno interactivo completo con muchas características avanzadas. Puedes probar algunos comandos de Linux como `ls`, `pwd`, o incluso instalar software usando `apt`.&lt;br /&gt;
&lt;br /&gt;
Cuando termines de trabajar en el contenedor, puedes salir escribiendo `exit`. Esto detendrá el contenedor y te devolverá a tu terminal principal.&lt;br /&gt;
&lt;br /&gt;
*Nota sobre los shells*: A veces, podrías ver `bash`, `/bin/bash` o `sh` al acceder a un contenedor. &lt;br /&gt;
* `bash` y `/bin/bash` se refieren al mismo shell, pero `/bin/bash` especifica la ruta exacta al ejecutable. Esto es útil en sistemas donde la variable de entorno PATH no está configurada adecuadamente. &lt;br /&gt;
* `sh` es un shell más básico y compatible que puede no tener todas las características de `bash`, por lo que es preferible usar `bash` cuando esté disponible.&lt;br /&gt;
&lt;br /&gt;
=== 5. Ver los contenedores en ejecución ===&lt;br /&gt;
&lt;br /&gt;
Si quieres ver qué contenedores están activos en tu sistema, utiliza el siguiente comando:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando te mostrará una lista de los contenedores que están en ejecución. Si acabas de salir del contenedor de Ubuntu, no verás nada en la lista, porque el contenedor se detuvo al salir.&lt;br /&gt;
&lt;br /&gt;
Si deseas ver todos los contenedores, incluidos los que están detenidos, puedes usar:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps -a&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto te mostrará todos los contenedores, indicando su estado actual (en ejecución o detenidos).&lt;br /&gt;
&lt;br /&gt;
=== 6. Eliminar contenedores y liberar espacio ===&lt;br /&gt;
&lt;br /&gt;
Después de trabajar con contenedores, puede que desees eliminar algunos para liberar espacio en tu máquina. Para eliminar un contenedor, primero necesitas conocer su ID o nombre, que puedes obtener con `docker ps -a`. Luego, puedes eliminarlo usando:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker rm id_contenedor&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Si deseas eliminar todos los contenedores detenidos de una vez, usa este comando:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker rm $(docker ps -aq)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto es especialmente útil cuando tienes muchos contenedores detenidos y quieres limpiar tu sistema rápidamente.&lt;br /&gt;
&lt;br /&gt;
=== 7. Limpiar imágenes que ya no necesitas ===&lt;br /&gt;
&lt;br /&gt;
También puedes liberar espacio eliminando imágenes que ya no necesites. Para eliminar una imagen, primero lista todas las imágenes con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker images&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Luego, usa el siguiente comando para eliminar una imagen específica:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker rmi id_imagen&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Trabajando de manera interactiva con contenedores ==&lt;br /&gt;
&lt;br /&gt;
=== 8. Mantener un contenedor vivo en segundo plano ===&lt;br /&gt;
&lt;br /&gt;
Cuando desees ejecutar un contenedor pero no necesitas interactuar directamente con él, puedes ejecutarlo en segundo plano (modo &amp;quot;detached&amp;quot;). Esto es útil para contenedores que corren servicios como servidores web o bases de datos. Para ejecutar un contenedor en modo detached, usa el siguiente comando:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -td ubuntu bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Aquí:&lt;br /&gt;
* `-t`: Asigna un terminal virtual al contenedor, pero en este caso, como el contenedor se ejecuta en segundo plano, no podrás interactuar con él directamente.&lt;br /&gt;
* `-d`: Indica que el contenedor debe ejecutarse en modo &amp;quot;detached&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Puedes comprobar que el contenedor se está ejecutando usando `docker ps`.&lt;br /&gt;
&lt;br /&gt;
=== 9. Acceder a un contenedor en segundo plano ===&lt;br /&gt;
&lt;br /&gt;
Si necesitas acceder a un contenedor que se está ejecutando en segundo plano, puedes hacerlo usando el siguiente comando:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -ti id_contenedor bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto te permitirá interactuar con el contenedor como si estuvieras en su terminal. Recuerda que debes sustituir `id_contenedor` por el ID real de tu contenedor.&lt;br /&gt;
&lt;br /&gt;
= Trabajando con Puertos y Volúmenes en Docker =&lt;br /&gt;
&lt;br /&gt;
Uno de los conceptos más importantes en Docker es el manejo de puertos y volúmenes. Los volúmenes permiten que los datos persistan incluso cuando un contenedor se detiene o se elimina. Esto es fundamental para aplicaciones en producción donde la pérdida de datos no es aceptable.&lt;br /&gt;
&lt;br /&gt;
En este tutorial, utilizaremos un '''bind mount''' para servir archivos HTML personalizados en un contenedor NGINX y también exploraremos cómo trabajar con '''volúmenes'''.&lt;br /&gt;
&lt;br /&gt;
== Creando un Contenedor NGINX con Archivos HTML Personalizados ==&lt;br /&gt;
&lt;br /&gt;
Vamos a ejecutar un servidor web básico utilizando la imagen oficial de NGINX. Para comenzar, ejecuta el siguiente comando para iniciar el contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -it --rm -d -p 8080:80 --name web nginx&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
'''Explicación del Comando:'''&lt;br /&gt;
* `docker run`: Este comando se utiliza para crear y ejecutar un nuevo contenedor.&lt;br /&gt;
* `-it`: Esta opción combina dos parámetros:&lt;br /&gt;
  * `-i`: Mantiene la entrada estándar (stdin) abierta incluso si no estás conectado.&lt;br /&gt;
  * `-t`: Asigna un pseudo-terminal al contenedor, lo que permite interactuar con él.&lt;br /&gt;
* `--rm`: Indica que el contenedor debe ser eliminado automáticamente cuando se detiene. Esto ayuda a mantener limpio el entorno de trabajo.&lt;br /&gt;
* `-d`: Ejecuta el contenedor en modo &amp;quot;detached&amp;quot;, lo que significa que se ejecutará en segundo plano.&lt;br /&gt;
* `-p 8080:80`: Mapea el puerto 80 del contenedor al puerto 8080 de tu máquina local. Esto te permitirá acceder al servidor web en `http://localhost:8080`.&lt;br /&gt;
* `--name web`: Asigna un nombre al contenedor. En este caso, lo llamamos &amp;quot;web&amp;quot;.&lt;br /&gt;
* `nginx`: Especifica la imagen que se utilizará para crear el contenedor. En este caso, se utiliza la imagen oficial de NGINX.&lt;br /&gt;
&lt;br /&gt;
=== Servir Archivos HTML Personalizados ===&lt;br /&gt;
&lt;br /&gt;
Por defecto, NGINX busca archivos para servir en el directorio `/usr/share/nginx/html` dentro del contenedor. Necesitamos colocar nuestros archivos HTML en este directorio. Una forma sencilla de hacerlo es utilizando un '''bind mount'''. Esto nos permitirá vincular un directorio de nuestra máquina local y mapearlo dentro de nuestro contenedor en ejecución.&lt;br /&gt;
&lt;br /&gt;
==== Creando una Página HTML Personalizada ====&lt;br /&gt;
# Crea un directorio llamado `site-content` en tu máquina local.&lt;br /&gt;
# Dentro de este directorio, crea un archivo llamado `index.html` y agrega el siguiente contenido HTML:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!doctype html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
  &amp;lt;meta charset=&amp;quot;utf-8&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;title&amp;gt;Docker Nginx&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
  &amp;lt;h2&amp;gt;Hello from Nginx container&amp;lt;/h2&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Iniciando el Contenedor con el Volumen Montado ====&lt;br /&gt;
Ahora, ejecuta el siguiente comando para iniciar un nuevo contenedor NGINX que utilice el '''bind mount''':&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -it --rm -d -p 8080:80 --name web -v ~/site-content:/usr/share/nginx/html nginx&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
'''Explicación del Comando:'''&lt;br /&gt;
* `-v ~/site-content:/usr/share/nginx/html`: Esta opción crea un '''bind mount''' que mapea el directorio `~/site-content` de tu máquina local al directorio `/usr/share/nginx/html` en el contenedor. Esto permite que cualquier archivo que coloques en `~/site-content` esté disponible dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
=== Accediendo a tu Página HTML ===&lt;br /&gt;
Una vez que el contenedor esté en funcionamiento, abre tu navegador favorito y navega a `http://localhost:8080`. Deberías ver tu página HTML personalizada renderizada en la ventana del navegador.&lt;br /&gt;
&lt;br /&gt;
=== Deteniendo el Contenedor ===&lt;br /&gt;
Si deseas detener el contenedor en cualquier momento, utiliza el siguiente comando:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop web&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Persistencia de Datos con Volúmenes ===&lt;br /&gt;
Los volúmenes permiten que los datos persistan más allá del ciclo de vida del contenedor. Si detienes o eliminas el contenedor, los archivos que colocaste en un volumen seguirán estando disponibles.&lt;br /&gt;
&lt;br /&gt;
== Ejemplo de Uso de Volúmenes ==&lt;br /&gt;
&lt;br /&gt;
Ahora, veamos cómo crear un contenedor NGINX utilizando un volumen para persistir datos.&lt;br /&gt;
&lt;br /&gt;
=== Creando un Volumen ===&lt;br /&gt;
Primero, crea un volumen utilizando el siguiente comando:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume create nginx-data&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
'''Explicación del Comando:'''&lt;br /&gt;
* `docker volume create nginx-data`: Este comando crea un volumen llamado `nginx-data`, que se puede utilizar para almacenar datos de forma persistente.&lt;br /&gt;
&lt;br /&gt;
=== Iniciando el Contenedor con el Volumen ===&lt;br /&gt;
Ahora, ejecuta el siguiente comando para iniciar un nuevo contenedor NGINX utilizando el volumen que acabas de crear:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -it --rm -d -p 8080:80 --name web -v nginx-data:/usr/share/nginx/html nginx&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
'''Explicación del Comando:'''&lt;br /&gt;
* `-v nginx-data:/usr/share/nginx/html`: Esta opción mapea el volumen `nginx-data` al directorio `/usr/share/nginx/html` en el contenedor. Cualquier archivo que coloques en este volumen persistirá incluso si el contenedor se detiene o se elimina.&lt;br /&gt;
&lt;br /&gt;
=== Accediendo al Contenedor y Agregando Archivos ===&lt;br /&gt;
Para agregar archivos al volumen, puedes usar el siguiente comando para acceder a la terminal del contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it web sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Una vez dentro del contenedor, puedes crear un archivo HTML directamente en el volumen:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
echo &amp;quot;&amp;lt;!doctype html&amp;gt;&amp;lt;html lang='en'&amp;gt;&amp;lt;head&amp;gt;&amp;lt;meta charset='utf-8'&amp;gt;&amp;lt;title&amp;gt;Docker Nginx&amp;lt;/title&amp;gt;&amp;lt;/head&amp;gt;&amp;lt;body&amp;gt;&amp;lt;h2&amp;gt;Hello from Nginx Volume&amp;lt;/h2&amp;gt;&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;&amp;quot; &amp;gt; /usr/share/nginx/html/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Accediendo a tu Página HTML ===&lt;br /&gt;
Ahora abre tu navegador y navega a `http://localhost:8080`. Deberías ver el contenido del archivo HTML que acabas de crear.&lt;br /&gt;
&lt;br /&gt;
=== Conclusión ===&lt;br /&gt;
Hemos aprendido a manejar puertos y volúmenes en Docker, creando un contenedor NGINX que sirve nuestros propios archivos HTML personalizados. Hemos visto cómo utilizar '''bind mounts''' para desarrollo y '''volúmenes''' para persistencia de datos. Esto es fundamental para desarrollar aplicaciones web que requieren persistencia de datos.&lt;br /&gt;
&lt;br /&gt;
== Documentación oficial ==&lt;br /&gt;
&lt;br /&gt;
Para más información y detalles sobre todas las características de Docker, consulta la [documentación oficial de Docker](https://docs.docker.com/).&lt;br /&gt;
&lt;br /&gt;
= Conclusión =&lt;br /&gt;
&lt;br /&gt;
¡Felicidades! Ahora has completado un tutorial básico sobre Docker. Has aprendido a instalar Docker, ejecutar contenedores, administrar imágenes, y cómo utilizar volúmenes para persistencia de datos. Continúa explorando las capacidades de Docker para mejorar tu flujo de trabajo y desarrollo de software.&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_de_Docker&amp;diff=9866</id>
		<title>Tutorial Campo de entrenamiento de Docker</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_de_Docker&amp;diff=9866"/>
				<updated>2024-10-30T08:43:29Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Prerrequisitos =&lt;br /&gt;
&lt;br /&gt;
== Instalación Docker ==&lt;br /&gt;
&lt;br /&gt;
=== Instrucciones para instalar Docker en Ubuntu 22.04 (Jammy Jellyfish) ===&lt;br /&gt;
&lt;br /&gt;
Antes de comenzar a utilizar Docker, necesitamos asegurarnos de que está instalado en nuestro sistema. Aquí te mostramos cómo hacerlo.&lt;br /&gt;
&lt;br /&gt;
Primero, actualizaremos la lista de paquetes disponibles en tu sistema. Este paso asegura que tu sistema tenga la información más reciente sobre qué paquetes se pueden instalar y actualizar:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo apt update&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A continuación, instala algunos paquetes necesarios que permiten a `apt` manejar repositorios sobre HTTPS. Esto es importante porque garantiza que tu sistema puede descargar software de fuentes seguras y confiables:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo apt install apt-transport-https ca-certificates curl software-properties-common&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ahora, es momento de añadir la clave GPG del repositorio oficial de Docker a tu sistema. Esta clave es esencial porque se utiliza para verificar que el software que descargas es auténtico y no ha sido alterado por terceros:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A continuación, añadirás el repositorio de Docker a las fuentes de `apt`. Este paso es necesario para que tu sistema sepa dónde buscar las actualizaciones y versiones de Docker:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
echo &amp;quot;deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable&amp;quot; | sudo tee /etc/apt/sources.list.d/docker.list &amp;gt; /dev/null&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Después de añadir el repositorio, es recomendable actualizar la lista de paquetes nuevamente. Esto permite que `apt` reconozca el nuevo repositorio de Docker:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo apt update&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Finalmente, puedes instalar Docker con el siguiente comando. Este comando instalará la versión más reciente de Docker Community Edition (CE) en tu sistema:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo apt install docker-ce&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Para asegurarte de que Docker se ha instalado correctamente y está funcionando, verifica el estado del servicio Docker. Al ejecutar este comando, deberías ver que el servicio está activo y en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo systemctl status docker&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Uso de Docker sin `sudo` ===&lt;br /&gt;
&lt;br /&gt;
Por defecto, Docker necesita permisos de administrador para ejecutar sus comandos. Sin embargo, puedes añadir tu usuario al grupo `docker` para evitar tener que usar `sudo` cada vez que ejecutes comandos de Docker. Esto simplifica el uso de Docker y hace que sea más conveniente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo usermod -aG docker ${USER}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Después de ejecutar este comando, necesitarás cerrar la sesión y volver a iniciarla para que los cambios surtan efecto. Si prefieres no cerrar la sesión, puedes aplicar los cambios usando:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
su - ${USER}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Si tuvieras problemas de permisos con el socket de Docker, una forma de solucionarlo es:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
suod chmod 666 /var/run/docker.sock&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando permite lectura y escritura en el socket para todos los usuarios, pero es una solución temporal y no la más segura. Puede ser útil para probar si el problema es de permisos.&lt;br /&gt;
&lt;br /&gt;
= Tutorial de Iniciación a Docker =&lt;br /&gt;
&lt;br /&gt;
== Docker básico: primer contacto ==&lt;br /&gt;
&lt;br /&gt;
Para comenzar a trabajar con Docker, es fundamental asegurarte de que todo está funcionando correctamente en tu sistema.&lt;br /&gt;
&lt;br /&gt;
=== 1. Ejecutar tu primer contenedor: &amp;quot;Hello World&amp;quot; ===&lt;br /&gt;
&lt;br /&gt;
El primer paso en Docker es ejecutar un contenedor básico que te confirme que todo está configurado adecuadamente. Para ello, utilizaremos la imagen &amp;quot;hello-world&amp;quot;, que está diseñada para asegurarse de que Docker está instalado y funcionando correctamente.&lt;br /&gt;
&lt;br /&gt;
Abre tu terminal y ejecuta el siguiente comando:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run hello-world&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando realiza varias acciones:&lt;br /&gt;
* Descarga la imagen: Si no tienes la imagen &amp;quot;hello-world&amp;quot; en tu máquina, Docker la descargará desde Docker Hub, que es un repositorio de imágenes Docker.&lt;br /&gt;
* Crea un contenedor: Una vez que la imagen está disponible, Docker crea un nuevo contenedor basado en ella.&lt;br /&gt;
* Ejecuta el contenedor: Finalmente, Docker ejecuta el contenedor, que mostrará un mensaje de bienvenida en tu terminal.&lt;br /&gt;
&lt;br /&gt;
Si todo ha funcionado correctamente, deberías ver un mensaje que comienza con &amp;quot;Hello from Docker!&amp;quot;. ¡Esto significa que tu instalación de Docker está en buen estado!&lt;br /&gt;
&lt;br /&gt;
=== 2. Ver las imágenes disponibles en tu sistema ===&lt;br /&gt;
&lt;br /&gt;
Después de ejecutar un contenedor, Docker guarda una copia de la imagen en tu sistema. Para ver las imágenes que tienes localmente, usa el siguiente comando:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker images&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando mostrará una lista de todas las imágenes almacenadas en tu sistema local. Aquí deberías ver la imagen de &amp;quot;hello-world&amp;quot; que acabas de descargar.&lt;br /&gt;
&lt;br /&gt;
=== 3. Descargando una imagen de Ubuntu ===&lt;br /&gt;
&lt;br /&gt;
Ahora, vamos a descargar una imagen que es más útil para el desarrollo: Ubuntu. Para hacerlo, ejecuta el siguiente comando:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker pull ubuntu&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando descargará la última versión de la imagen de Ubuntu desde Docker Hub. Tener la imagen de Ubuntu te permitirá ejecutar una versión ligera de este sistema operativo dentro de un contenedor.&lt;br /&gt;
&lt;br /&gt;
=== 4. Ejecutar un contenedor interactivo con Ubuntu ===&lt;br /&gt;
&lt;br /&gt;
Docker te permite ejecutar contenedores en modo interactivo, lo que significa que puedes acceder a una terminal dentro del contenedor y trabajar en él como si fuera una máquina independiente. Para hacerlo, ejecuta el siguiente comando:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -it ubuntu bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando hace lo siguiente:&lt;br /&gt;
* `-it`: Esta opción combina dos parámetros: `-i` (interactivo) y `-t` (terminal), permitiéndote interactuar con el contenedor a través de una terminal.&lt;br /&gt;
* `ubuntu`: Especifica que quieres crear el contenedor a partir de la imagen de Ubuntu.&lt;br /&gt;
* `bash`: Es el comando que se ejecutará dentro del contenedor. Aquí es donde decides qué shell usar.&lt;br /&gt;
&lt;br /&gt;
Al usar `bash`, estás utilizando el shell por defecto de Ubuntu, que proporciona un entorno interactivo completo con muchas características avanzadas. Puedes probar algunos comandos de Linux como `ls`, `pwd`, o incluso instalar software usando `apt`.&lt;br /&gt;
&lt;br /&gt;
Cuando termines de trabajar en el contenedor, puedes salir escribiendo `exit`. Esto detendrá el contenedor y te devolverá a tu terminal principal.&lt;br /&gt;
&lt;br /&gt;
*Nota sobre los shells*: A veces, podrías ver `bash`, `/bin/bash` o `sh` al acceder a un contenedor. &lt;br /&gt;
* `bash` y `/bin/bash` se refieren al mismo shell, pero `/bin/bash` especifica la ruta exacta al ejecutable. Esto es útil en sistemas donde la variable de entorno PATH no está configurada adecuadamente. &lt;br /&gt;
* `sh` es un shell más básico y compatible que puede no tener todas las características de `bash`, por lo que es preferible usar `bash` cuando esté disponible.&lt;br /&gt;
&lt;br /&gt;
=== 5. Ver los contenedores en ejecución ===&lt;br /&gt;
&lt;br /&gt;
Si quieres ver qué contenedores están activos en tu sistema, utiliza el siguiente comando:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando te mostrará una lista de los contenedores que están en ejecución. Si acabas de salir del contenedor de Ubuntu, no verás nada en la lista, porque el contenedor se detuvo al salir.&lt;br /&gt;
&lt;br /&gt;
Si deseas ver todos los contenedores, incluidos los que están detenidos, puedes usar:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps -a&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto te mostrará todos los contenedores, indicando su estado actual (en ejecución o detenidos).&lt;br /&gt;
&lt;br /&gt;
=== 6. Eliminar contenedores y liberar espacio ===&lt;br /&gt;
&lt;br /&gt;
Después de trabajar con contenedores, puede que desees eliminar algunos para liberar espacio en tu máquina. Para eliminar un contenedor, primero necesitas conocer su ID o nombre, que puedes obtener con `docker ps -a`. Luego, puedes eliminarlo usando:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker rm id_contenedor&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Si deseas eliminar todos los contenedores detenidos de una vez, usa este comando:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker rm $(docker ps -aq)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto es especialmente útil cuando tienes muchos contenedores detenidos y quieres limpiar tu sistema rápidamente.&lt;br /&gt;
&lt;br /&gt;
=== 7. Limpiar imágenes que ya no necesitas ===&lt;br /&gt;
&lt;br /&gt;
También puedes liberar espacio eliminando imágenes que ya no necesites. Para eliminar una imagen, primero lista todas las imágenes con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker images&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Luego, usa el siguiente comando para eliminar una imagen específica:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker rmi id_imagen&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Trabajando de manera interactiva con contenedores ==&lt;br /&gt;
&lt;br /&gt;
=== 8. Mantener un contenedor vivo en segundo plano ===&lt;br /&gt;
&lt;br /&gt;
Cuando desees ejecutar un contenedor pero no necesitas interactuar directamente con él, puedes ejecutarlo en segundo plano (modo &amp;quot;detached&amp;quot;). Esto es útil para contenedores que corren servicios como servidores web o bases de datos. Para ejecutar un contenedor en modo detached, usa el siguiente comando:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -td ubuntu bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Aquí:&lt;br /&gt;
* `-t`: Asigna un terminal virtual al contenedor, pero en este caso, como el contenedor se ejecuta en segundo plano, no podrás interactuar con él directamente.&lt;br /&gt;
* `-d`: Indica que el contenedor debe ejecutarse en modo &amp;quot;detached&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Puedes comprobar que el contenedor se está ejecutando usando `docker ps`.&lt;br /&gt;
&lt;br /&gt;
=== 9. Acceder a un contenedor en segundo plano ===&lt;br /&gt;
&lt;br /&gt;
Si necesitas acceder a un contenedor que se está ejecutando en segundo plano, puedes hacerlo usando el siguiente comando:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -ti id_contenedor bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto te permitirá interactuar con el contenedor como si estuvieras en su terminal. Recuerda que debes sustituir `id_contenedor` por el ID real de tu contenedor.&lt;br /&gt;
&lt;br /&gt;
= Trabajando con Puertos y Volúmenes en Docker =&lt;br /&gt;
&lt;br /&gt;
Uno de los conceptos más importantes en Docker es el manejo de puertos y volúmenes. Los volúmenes permiten que los datos persistan incluso cuando un contenedor se detiene o se elimina. Esto es fundamental para aplicaciones en producción donde la pérdida de datos no es aceptable.&lt;br /&gt;
&lt;br /&gt;
En este tutorial, utilizaremos un '''bind mount''' para servir archivos HTML personalizados en un contenedor NGINX y también exploraremos cómo trabajar con '''volúmenes'''.&lt;br /&gt;
&lt;br /&gt;
== Creando un Contenedor NGINX con Archivos HTML Personalizados ==&lt;br /&gt;
&lt;br /&gt;
Vamos a ejecutar un servidor web básico utilizando la imagen oficial de NGINX. Para comenzar, ejecuta el siguiente comando para iniciar el contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -it --rm -d -p 8080:80 --name web nginx&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
'''Explicación del Comando:'''&lt;br /&gt;
* `docker run`: Este comando se utiliza para crear y ejecutar un nuevo contenedor.&lt;br /&gt;
* `-it`: Esta opción combina dos parámetros:&lt;br /&gt;
  * `-i`: Mantiene la entrada estándar (stdin) abierta incluso si no estás conectado.&lt;br /&gt;
  * `-t`: Asigna un pseudo-terminal al contenedor, lo que permite interactuar con él.&lt;br /&gt;
* `--rm`: Indica que el contenedor debe ser eliminado automáticamente cuando se detiene. Esto ayuda a mantener limpio el entorno de trabajo.&lt;br /&gt;
* `-d`: Ejecuta el contenedor en modo &amp;quot;detached&amp;quot;, lo que significa que se ejecutará en segundo plano.&lt;br /&gt;
* `-p 8080:80`: Mapea el puerto 80 del contenedor al puerto 8080 de tu máquina local. Esto te permitirá acceder al servidor web en `http://localhost:8080`.&lt;br /&gt;
* `--name web`: Asigna un nombre al contenedor. En este caso, lo llamamos &amp;quot;web&amp;quot;.&lt;br /&gt;
* `nginx`: Especifica la imagen que se utilizará para crear el contenedor. En este caso, se utiliza la imagen oficial de NGINX.&lt;br /&gt;
&lt;br /&gt;
=== Servir Archivos HTML Personalizados ===&lt;br /&gt;
&lt;br /&gt;
Por defecto, NGINX busca archivos para servir en el directorio `/usr/share/nginx/html` dentro del contenedor. Necesitamos colocar nuestros archivos HTML en este directorio. Una forma sencilla de hacerlo es utilizando un '''bind mount'''. Esto nos permitirá vincular un directorio de nuestra máquina local y mapearlo dentro de nuestro contenedor en ejecución.&lt;br /&gt;
&lt;br /&gt;
==== Creando una Página HTML Personalizada ====&lt;br /&gt;
# Crea un directorio llamado `site-content` en tu máquina local.&lt;br /&gt;
# Dentro de este directorio, crea un archivo llamado `index.html` y agrega el siguiente contenido HTML:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!doctype html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
  &amp;lt;meta charset=&amp;quot;utf-8&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;title&amp;gt;Docker Nginx&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
  &amp;lt;h2&amp;gt;Hello from Nginx container&amp;lt;/h2&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Iniciando el Contenedor con el Volumen Montado ====&lt;br /&gt;
Ahora, ejecuta el siguiente comando para iniciar un nuevo contenedor NGINX que utilice el '''bind mount''':&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -it --rm -d -p 8080:80 --name web -v ~/site-content:/usr/share/nginx/html nginx&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
'''Explicación del Comando:'''&lt;br /&gt;
* `-v ~/site-content:/usr/share/nginx/html`: Esta opción crea un '''bind mount''' que mapea el directorio `~/site-content` de tu máquina local al directorio `/usr/share/nginx/html` en el contenedor. Esto permite que cualquier archivo que coloques en `~/site-content` esté disponible dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
=== Accediendo a tu Página HTML ===&lt;br /&gt;
Una vez que el contenedor esté en funcionamiento, abre tu navegador favorito y navega a `http://localhost:8080`. Deberías ver tu página HTML personalizada renderizada en la ventana del navegador.&lt;br /&gt;
&lt;br /&gt;
=== Deteniendo el Contenedor ===&lt;br /&gt;
Si deseas detener el contenedor en cualquier momento, utiliza el siguiente comando:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop web&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Persistencia de Datos con Volúmenes ===&lt;br /&gt;
Los volúmenes permiten que los datos persistan más allá del ciclo de vida del contenedor. Si detienes o eliminas el contenedor, los archivos que colocaste en un volumen seguirán estando disponibles.&lt;br /&gt;
&lt;br /&gt;
== Ejemplo de Uso de Volúmenes ==&lt;br /&gt;
&lt;br /&gt;
Ahora, veamos cómo crear un contenedor NGINX utilizando un volumen para persistir datos.&lt;br /&gt;
&lt;br /&gt;
=== Creando un Volumen ===&lt;br /&gt;
Primero, crea un volumen utilizando el siguiente comando:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume create nginx-data&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
'''Explicación del Comando:'''&lt;br /&gt;
* `docker volume create nginx-data`: Este comando crea un volumen llamado `nginx-data`, que se puede utilizar para almacenar datos de forma persistente.&lt;br /&gt;
&lt;br /&gt;
=== Iniciando el Contenedor con el Volumen ===&lt;br /&gt;
Ahora, ejecuta el siguiente comando para iniciar un nuevo contenedor NGINX utilizando el volumen que acabas de crear:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -it --rm -d -p 8080:80 --name web -v nginx-data:/usr/share/nginx/html nginx&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
'''Explicación del Comando:'''&lt;br /&gt;
* `-v nginx-data:/usr/share/nginx/html`: Esta opción mapea el volumen `nginx-data` al directorio `/usr/share/nginx/html` en el contenedor. Cualquier archivo que coloques en este volumen persistirá incluso si el contenedor se detiene o se elimina.&lt;br /&gt;
&lt;br /&gt;
=== Accediendo al Contenedor y Agregando Archivos ===&lt;br /&gt;
Para agregar archivos al volumen, puedes usar el siguiente comando para acceder a la terminal del contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it web sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Una vez dentro del contenedor, puedes crear un archivo HTML directamente en el volumen:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
echo &amp;quot;&amp;lt;!doctype html&amp;gt;&amp;lt;html lang='en'&amp;gt;&amp;lt;head&amp;gt;&amp;lt;meta charset='utf-8'&amp;gt;&amp;lt;title&amp;gt;Docker Nginx&amp;lt;/title&amp;gt;&amp;lt;/head&amp;gt;&amp;lt;body&amp;gt;&amp;lt;h2&amp;gt;Hello from Nginx Volume&amp;lt;/h2&amp;gt;&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;&amp;quot; &amp;gt; /usr/share/nginx/html/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Accediendo a tu Página HTML ===&lt;br /&gt;
Ahora abre tu navegador y navega a `http://localhost:8080`. Deberías ver el contenido del archivo HTML que acabas de crear.&lt;br /&gt;
&lt;br /&gt;
=== Conclusión ===&lt;br /&gt;
Hemos aprendido a manejar puertos y volúmenes en Docker, creando un contenedor NGINX que sirve nuestros propios archivos HTML personalizados. Hemos visto cómo utilizar '''bind mounts''' para desarrollo y '''volúmenes''' para persistencia de datos. Esto es fundamental para desarrollar aplicaciones web que requieren persistencia de datos.&lt;br /&gt;
&lt;br /&gt;
== Documentación oficial ==&lt;br /&gt;
&lt;br /&gt;
Para más información y detalles sobre todas las características de Docker, consulta la [documentación oficial de Docker](https://docs.docker.com/).&lt;br /&gt;
&lt;br /&gt;
= Conclusión =&lt;br /&gt;
&lt;br /&gt;
¡Felicidades! Ahora has completado un tutorial básico sobre Docker. Has aprendido a instalar Docker, ejecutar contenedores, administrar imágenes, y cómo utilizar volúmenes para persistencia de datos. Continúa explorando las capacidades de Docker para mejorar tu flujo de trabajo y desarrollo de software.&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Dockerizando_una_aplicaci%C3%B3n&amp;diff=9854</id>
		<title>Tutorial Dockerizando una aplicación</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Dockerizando_una_aplicaci%C3%B3n&amp;diff=9854"/>
				<updated>2024-10-29T09:16:30Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Prerrequisitos =&lt;br /&gt;
== Instalar Docker Compose ==&lt;br /&gt;
&lt;br /&gt;
El primer paso es descargar la última versión de Docker Compose. Puedes descargar Docker Compose usando el siguiente comando, pero asegúrate de verificar la última versión en la página de lanzamientos de Docker Compose:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mkdir -p ~/.docker/cli-plugins/&lt;br /&gt;
&lt;br /&gt;
LATEST_VERSION=$(curl -s https://api.github.com/repos/docker/compose/releases/latest | grep '&amp;quot;tag_name&amp;quot;:' | sed -E 's/.*&amp;quot;([^&amp;quot;]+)&amp;quot;.*/\1/')&lt;br /&gt;
&lt;br /&gt;
sudo curl -SL &amp;quot;https://github.com/docker/compose/releases/download/${LATEST_VERSION}/docker-compose-$(uname -s)-$(uname -m)&amp;quot; -o ~/.docker/cli-plugins/docker-compose&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Aplicar Permisos Ejecutables: asegúrate de que el archivo de Docker Compose tenga permisos ejecutables:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt; &lt;br /&gt;
chmod +x ~/.docker/cli-plugins/docker-compose&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Verificar la instalación de Docker Compose: para confirmar que Docker Compose se ha instalado correctamente, ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose --version &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
= Dockerizando una Aplicación Flask =&lt;br /&gt;
&lt;br /&gt;
En este tutorial, aprenderemos a dockerizar la aplicación Flask `flask_testing_project` que creamos en la práctica anterior. Con esta dockerización, podrás ejecutar tu aplicación Flask en un contenedor Docker, lo que facilita la portabilidad y la ejecución en diferentes entornos. &lt;br /&gt;
&lt;br /&gt;
Para lograr esto, utilizaremos un archivo `Dockerfile` y un archivo de dependencias `requirements.txt` que especificarán cómo construir la imagen Docker y qué paquetes necesita la aplicación.&lt;br /&gt;
&lt;br /&gt;
== Estructura del Proyecto ==&lt;br /&gt;
&lt;br /&gt;
La estructura inicial del proyecto `flask_testing_project` es la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;plaintext&amp;quot;&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app.py                # Archivo principal de la aplicación Flask&lt;br /&gt;
├── templates/            # Directorio que contiene la plantilla HTML&lt;br /&gt;
│   └── tasks.html        # Plantilla para mostrar y agregar tareas&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── test_app.py       # Pruebas unitarias usando pytest&lt;br /&gt;
│   └── test_interfaz.py  # Pruebas de interfaz con Selenium&lt;br /&gt;
└── locustfile.py         # Archivo para pruebas de carga con Locust&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Para dockerizar la aplicación, añadiremos dos nuevos archivos:&lt;br /&gt;
* '''Dockerfile''': Define las instrucciones para construir la imagen de Docker.&lt;br /&gt;
* '''requirements.txt''': Contiene las dependencias de Python necesarias para ejecutar la aplicación.&lt;br /&gt;
&lt;br /&gt;
La estructura de directorios después de añadir estos archivos será:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;plaintext&amp;quot;&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app.py                # Archivo principal de la aplicación Flask&lt;br /&gt;
├── templates/            # Directorio que contiene la plantilla HTML&lt;br /&gt;
│   └── tasks.html        # Plantilla para mostrar y agregar tareas&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── test_app.py       # Pruebas unitarias usando pytest&lt;br /&gt;
│   └── test_interfaz.py  # Pruebas de interfaz con Selenium&lt;br /&gt;
├── locustfile.py         # Archivo para pruebas de carga con Locust&lt;br /&gt;
├── Dockerfile            # Archivo para construir la imagen Docker de la aplicación&lt;br /&gt;
└── requirements.txt      # Dependencias de Python necesarias para la aplicación&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Paso 1: Crear el archivo requirements.txt ==&lt;br /&gt;
&lt;br /&gt;
El archivo `requirements.txt` lista todas las dependencias de la aplicación. Es crucial para Docker porque le indica qué librerías de Python instalar. &lt;br /&gt;
&lt;br /&gt;
Dentro de `flask_testing_project`, crea un archivo llamado `requirements.txt` y copia el siguiente contenido en él:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;plaintext&amp;quot;&amp;gt;&lt;br /&gt;
attrs==24.2.0&lt;br /&gt;
blinker==1.8.2&lt;br /&gt;
Brotli==1.1.0&lt;br /&gt;
certifi==2024.8.30&lt;br /&gt;
charset-normalizer==3.4.0&lt;br /&gt;
click==8.1.7&lt;br /&gt;
ConfigArgParse==1.7&lt;br /&gt;
coverage==7.6.4&lt;br /&gt;
Flask==3.0.3&lt;br /&gt;
Flask-Cors==5.0.0&lt;br /&gt;
Flask-Login==0.6.3&lt;br /&gt;
Flask-SQLAlchemy==3.1.1&lt;br /&gt;
gevent==24.10.3&lt;br /&gt;
geventhttpclient==2.3.1&lt;br /&gt;
greenlet==3.1.1&lt;br /&gt;
h11==0.14.0&lt;br /&gt;
idna==3.10&lt;br /&gt;
iniconfig==2.0.0&lt;br /&gt;
itsdangerous==2.2.0&lt;br /&gt;
Jinja2==3.1.4&lt;br /&gt;
locust==2.32.0&lt;br /&gt;
MarkupSafe==3.0.2&lt;br /&gt;
msgpack==1.1.0&lt;br /&gt;
outcome==1.3.0.post0&lt;br /&gt;
packaging==24.1&lt;br /&gt;
pluggy==1.5.0&lt;br /&gt;
psutil==6.1.0&lt;br /&gt;
PyMySQL==1.1.1&lt;br /&gt;
PySocks==1.7.1&lt;br /&gt;
pytest==8.3.3&lt;br /&gt;
pytest-cov==5.0.0&lt;br /&gt;
python-dotenv==1.0.1&lt;br /&gt;
pyzmq==26.2.0&lt;br /&gt;
requests==2.32.3&lt;br /&gt;
selenium==4.25.0&lt;br /&gt;
setuptools==75.2.0&lt;br /&gt;
sniffio==1.3.1&lt;br /&gt;
sortedcontainers==2.4.0&lt;br /&gt;
SQLAlchemy==2.0.36&lt;br /&gt;
trio==0.27.0&lt;br /&gt;
trio-websocket==0.11.1&lt;br /&gt;
typing_extensions==4.12.2&lt;br /&gt;
urllib3==2.2.3&lt;br /&gt;
webdriver-manager==4.0.2&lt;br /&gt;
websocket-client==1.8.0&lt;br /&gt;
Werkzeug==3.0.4&lt;br /&gt;
wsproto==1.2.0&lt;br /&gt;
zope.event==5.0&lt;br /&gt;
zope.interface==7.1.1&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Recuerdas como instalar estas dependencias en un único paso?&lt;br /&gt;
&lt;br /&gt;
== Paso 2: Crear el archivo Dockerfile ==&lt;br /&gt;
&lt;br /&gt;
El `Dockerfile` es el núcleo de la dockerización, ya que define las instrucciones paso a paso para crear una imagen de Docker que ejecutará nuestra aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
Dentro de `flask_testing_project`, crea un archivo llamado `Dockerfile` con el siguiente contenido:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;docker&amp;quot;&amp;gt;&lt;br /&gt;
# Usar una imagen base de Python&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
&lt;br /&gt;
# Establecer el directorio de trabajo dentro del contenedor&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
&lt;br /&gt;
# Copiar el archivo de requisitos al directorio de trabajo del contenedor&lt;br /&gt;
COPY requirements.txt .&lt;br /&gt;
&lt;br /&gt;
# Instalar las dependencias desde requirements.txt&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
&lt;br /&gt;
# Copiar el contenido de tu aplicación al directorio de trabajo del contenedor&lt;br /&gt;
COPY . .&lt;br /&gt;
&lt;br /&gt;
# Exponer el puerto que usa Flask&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
&lt;br /&gt;
# Comando para ejecutar la aplicación Flask&lt;br /&gt;
CMD [&amp;quot;python&amp;quot;, &amp;quot;app.py&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Explicación línea a línea del Dockerfile ===&lt;br /&gt;
&lt;br /&gt;
* `FROM python:3.12-slim`: Utiliza una imagen base de Python ligera (versión 3.12) para optimizar el espacio que ocupa la imagen.&lt;br /&gt;
* `WORKDIR /app`: Crea y define `/app` como el directorio de trabajo donde se copiarán los archivos de la aplicación.&lt;br /&gt;
* `COPY requirements.txt .`: Copia el archivo `requirements.txt` desde el sistema de archivos local al directorio actual del contenedor (`/app`). Esto asegura que el contenedor tenga acceso a las dependencias de la aplicación.&lt;br /&gt;
* `RUN pip install --no-cache-dir -r requirements.txt`: Instala las dependencias especificadas en `requirements.txt` sin cachear archivos temporales, reduciendo el tamaño final de la imagen.&lt;br /&gt;
* `COPY . .`: Copia todos los archivos y carpetas desde el sistema local al directorio `/app` del contenedor, incluyendo `app.py` y otras carpetas de proyecto.&lt;br /&gt;
* `EXPOSE 5000`: Indica que el contenedor usará el puerto 5000, donde Flask ejecutará la aplicación.&lt;br /&gt;
* `CMD [&amp;quot;python&amp;quot;, &amp;quot;app.py&amp;quot;]`: Define el comando que se ejecutará cuando el contenedor inicie, en este caso, lanzando la aplicación Flask en `app.py`.&lt;br /&gt;
&lt;br /&gt;
== Paso 3: Modificar app.py para Ejecutar en Docker ==&lt;br /&gt;
&lt;br /&gt;
Flask, por defecto, ejecuta su servidor solo en `localhost`, lo que hace que el contenedor no sea accesible desde fuera. Para resolver esto, edita `app.py` y asegúrate de que contenga el siguiente código al final del archivo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
if __name__ == '__main__':&lt;br /&gt;
    app.run(host='0.0.0.0', port=5000, debug=True)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Al añadir `host='0.0.0.0'`, permitimos que Flask acepte conexiones desde cualquier IP externa, asegurando que podamos acceder a la aplicación desde fuera del contenedor Docker.&lt;br /&gt;
&lt;br /&gt;
== Paso 4: Construir y Ejecutar la Imagen Docker ==&lt;br /&gt;
&lt;br /&gt;
Con los archivos `Dockerfile`, `requirements.txt` y `app.py` preparados, estamos listos para construir la imagen Docker y ejecutar un contenedor con la aplicación.&lt;br /&gt;
&lt;br /&gt;
En la terminal, navega al directorio `flask_testing_project` y ejecuta los siguientes comandos:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Construir la imagen Docker y asignarle el nombre 'flask-testing_project'&lt;br /&gt;
docker build -t flask-testing_project .&lt;br /&gt;
&lt;br /&gt;
# Ejecutar el contenedor desde la imagen, mapeando el puerto 5000 al mismo puerto en el host&lt;br /&gt;
docker run -p 5000:5000 flask-testing_project&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* `docker build -t flask-testing_project .`: Construye una imagen Docker basada en el `Dockerfile` del directorio actual. El argumento `-t flask-testing_project` asigna un nombre a la imagen creada.&lt;br /&gt;
* `docker run -p 5000:5000 flask-testing_project`: Ejecuta la imagen en un contenedor, mapeando el puerto 5000 del contenedor al puerto 5000 de la máquina local. De esta forma, la aplicación Flask es accesible desde el navegador en `localhost:5000`.&lt;br /&gt;
&lt;br /&gt;
== Paso 5: Verificación en el Navegador ==&lt;br /&gt;
&lt;br /&gt;
Una vez que el contenedor esté en ejecución, abre tu navegador y accede a [http://localhost:5000](http://localhost:5000). Si ves la aplicación funcionando, ¡felicidades! Has dockerizado exitosamente la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
== Resumen ==&lt;br /&gt;
&lt;br /&gt;
Este tutorial te ha guiado a través del proceso completo de dockerización de una aplicación Flask:&lt;br /&gt;
* Creamos el archivo `requirements.txt` para listar las dependencias de la aplicación.&lt;br /&gt;
* Construimos un `Dockerfile` detallado para definir los pasos de construcción de la imagen.&lt;br /&gt;
* Modificamos `app.py` para hacer la aplicación accesible externamente.&lt;br /&gt;
* Construimos la imagen y ejecutamos el contenedor, logrando que la aplicación esté disponible en el navegador.&lt;br /&gt;
&lt;br /&gt;
Al seguir estos pasos, has asegurado que la aplicación Flask pueda ejecutarse en cualquier entorno compatible con Docker, simplificando la portabilidad y la escalabilidad del proyecto.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
= Dockerizando una Aplicación Flask con Base de Datos usando Docker Compose =&lt;br /&gt;
&lt;br /&gt;
En este tutorial, aprenderemos a dockerizar la aplicación Flask `flask_testing_project` que creamos en la práctica anterior y añadiremos una base de datos externa. Esto nos permitirá ejecutar la aplicación en contenedores separados para la aplicación Flask y la base de datos, facilitando la escalabilidad y el mantenimiento. &lt;br /&gt;
&lt;br /&gt;
Para lograr esto, usaremos Docker Compose y ajustaremos nuestra aplicación para que utilice una base de datos MariaDB.&lt;br /&gt;
&lt;br /&gt;
== Estructura del Proyecto ==&lt;br /&gt;
&lt;br /&gt;
La estructura inicial de `flask_testing_project` es la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;plaintext&amp;quot;&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app.py                # Archivo principal de la aplicación Flask&lt;br /&gt;
├── templates/            # Directorio con la plantilla HTML&lt;br /&gt;
│   └── tasks.html        # Plantilla para mostrar y agregar tareas&lt;br /&gt;
├── tests/                # Directorio con pruebas unitarias y de interfaz&lt;br /&gt;
│   ├── test_app.py&lt;br /&gt;
│   └── test_interfaz.py&lt;br /&gt;
├── locustfile.py         # Archivo para pruebas de carga con Locust&lt;br /&gt;
├── Dockerfile            # Archivo para construir la imagen Docker de la aplicación&lt;br /&gt;
└── requirements.txt      # Dependencias de Python necesarias para la aplicación&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Para agregar Docker Compose, crearemos un nuevo archivo `docker-compose.yml`, y realizaremos algunos cambios en `app.py` para que la aplicación se conecte a MariaDB.&lt;br /&gt;
&lt;br /&gt;
La estructura final del proyecto será:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;plaintext&amp;quot;&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app.py                # Archivo principal de la aplicación Flask&lt;br /&gt;
├── templates/            # Directorio con la plantilla HTML&lt;br /&gt;
│   └── tasks.html        # Plantilla para mostrar y agregar tareas&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── test_app.py&lt;br /&gt;
│   └── test_interfaz.py&lt;br /&gt;
├── locustfile.py         # Archivo para pruebas de carga con Locust&lt;br /&gt;
├── Dockerfile            # Archivo para construir la imagen Docker de la aplicación&lt;br /&gt;
├── requirements.txt      # Dependencias de Python necesarias para la aplicación&lt;br /&gt;
└── docker-compose.yml    # Archivo de configuración de Docker Compose&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Paso 1: Crear el archivo docker-compose.yml ==&lt;br /&gt;
&lt;br /&gt;
Docker Compose permite definir y gestionar múltiples contenedores en un solo archivo de configuración. En este archivo, definiremos dos servicios: uno para la aplicación Flask (`web`) y otro para la base de datos MariaDB (`db`).&lt;br /&gt;
&lt;br /&gt;
Dentro de `flask_testing_project`, crea un archivo llamado `docker-compose.yml` y copia el siguiente contenido:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;yaml&amp;quot;&amp;gt;&lt;br /&gt;
version: '3'&lt;br /&gt;
&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    build: .&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;5000:5000&amp;quot;&lt;br /&gt;
    environment:&lt;br /&gt;
      - FLASK_ENV=development&lt;br /&gt;
      - DATABASE_HOST=db&lt;br /&gt;
      - DATABASE_USER=root&lt;br /&gt;
      - DATABASE_PASSWORD=my-secret-pw&lt;br /&gt;
      - DATABASE_DB=flaskdb&lt;br /&gt;
    depends_on:&lt;br /&gt;
      - db&lt;br /&gt;
&lt;br /&gt;
  db:&lt;br /&gt;
    image: mariadb:10.5&lt;br /&gt;
    restart: always&lt;br /&gt;
    environment:&lt;br /&gt;
      MYSQL_ROOT_PASSWORD: my-secret-pw&lt;br /&gt;
      MYSQL_DATABASE: flaskdb&lt;br /&gt;
    volumes:&lt;br /&gt;
      - db_data:/var/lib/mysql&lt;br /&gt;
&lt;br /&gt;
volumes:&lt;br /&gt;
  db_data:&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Explicación línea a línea del docker-compose.yml ===&lt;br /&gt;
&lt;br /&gt;
* `version: '3'`: Especifica la versión de Docker Compose.&lt;br /&gt;
* `services`: Define los servicios que Docker Compose manejará, en este caso `web` y `db`.&lt;br /&gt;
* `web`: Servicio para la aplicación Flask.&lt;br /&gt;
** `build: .`: Indica que el servicio usará el Dockerfile en el directorio actual.&lt;br /&gt;
** `ports`: Mapea el puerto 5000 del contenedor al puerto 5000 de la máquina local.&lt;br /&gt;
** `environment`: Define variables de entorno para configurar la conexión a la base de datos.&lt;br /&gt;
*** `FLASK_ENV=development`: Ejecuta Flask en modo de desarrollo.&lt;br /&gt;
*** `DATABASE_HOST=db`: Dirección del contenedor `db` para la base de datos.&lt;br /&gt;
*** `DATABASE_USER=root`, `DATABASE_PASSWORD=my-secret-pw`, `DATABASE_DB=flaskdb`: Credenciales y base de datos que usará Flask.&lt;br /&gt;
** `depends_on`: Asegura que el contenedor `db` esté ejecutándose antes de iniciar `web`.&lt;br /&gt;
* `db`: Servicio para la base de datos MariaDB.&lt;br /&gt;
** `image: mariadb:10.5`: Usa la imagen de MariaDB versión 10.5.&lt;br /&gt;
** `restart: always`: Reinicia el contenedor automáticamente en caso de errores.&lt;br /&gt;
** `environment`: Define las credenciales y la base de datos por defecto.&lt;br /&gt;
** `volumes`: Almacena los datos en un volumen persistente `db_data`.&lt;br /&gt;
* `volumes`: Define `db_data` para que los datos de la base de datos persistan en el sistema anfitrión.&lt;br /&gt;
&lt;br /&gt;
== Paso 2: Modificar app.py para Conectarse a MariaDB ==&lt;br /&gt;
&lt;br /&gt;
Actualizaremos `app.py` para usar SQLAlchemy y conectarse a MariaDB mediante las variables de entorno definidas en Docker Compose.&lt;br /&gt;
&lt;br /&gt;
Reemplaza el contenido de `app.py` por el siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
from flask_sqlalchemy import SQLAlchemy&lt;br /&gt;
import os&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Configuración de la base de datos MariaDB&lt;br /&gt;
app.config['SQLALCHEMY_DATABASE_URI'] = f&amp;quot;mysql+pymysql://{os.getenv('DATABASE_USER')}:{os.getenv('DATABASE_PASSWORD')}@{os.getenv('DATABASE_HOST')}/{os.getenv('DATABASE_DB')}&amp;quot;&lt;br /&gt;
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False&lt;br /&gt;
&lt;br /&gt;
db = SQLAlchemy(app)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# Definición del modelo de Tarea&lt;br /&gt;
class Task(db.Model):&lt;br /&gt;
    id = db.Column(db.Integer, primary_key=True)&lt;br /&gt;
    title = db.Column(db.String(100), nullable=False)&lt;br /&gt;
    done = db.Column(db.Boolean, default=False)&lt;br /&gt;
&lt;br /&gt;
    def to_dict(self):&lt;br /&gt;
        return {&lt;br /&gt;
            'id': self.id,&lt;br /&gt;
            'title': self.title,&lt;br /&gt;
            'done': self.done&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# Ruta para obtener la lista de tareas (versión HTML)&lt;br /&gt;
@app.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    tasks = Task.query.all()&lt;br /&gt;
    return render_template('tasks.html', tasks=tasks)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# Ruta para obtener la lista de tareas en JSON (API)&lt;br /&gt;
@app.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    tasks = Task.query.all()&lt;br /&gt;
    return jsonify({'tasks': [task.to_dict() for task in tasks]})&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# Ruta para crear una nueva tarea desde un formulario HTML&lt;br /&gt;
@app.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    if not title:&lt;br /&gt;
        return &amp;quot;El título es necesario&amp;quot;, 400&lt;br /&gt;
    new_task = Task(title=title, done=False)&lt;br /&gt;
    db.session.add(new_task)&lt;br /&gt;
    db.session.commit()&lt;br /&gt;
    return redirect(url_for('task_list'))&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# Ruta para crear una nueva tarea (API JSON)&lt;br /&gt;
@app.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task():&lt;br /&gt;
    if not request.json or 'title' not in request.json:&lt;br /&gt;
        return jsonify({'error': 'El título es necesario'}), 400&lt;br /&gt;
    new_task = Task(title=request.json['title'], done=False)&lt;br /&gt;
    db.session.add(new_task)&lt;br /&gt;
    db.session.commit()&lt;br /&gt;
    return jsonify(new_task.to_dict()), 201&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
if __name__ == '__main__':&lt;br /&gt;
    # Crear las tablas en la base de datos si no existen&lt;br /&gt;
    with app.app_context():&lt;br /&gt;
        db.create_all()&lt;br /&gt;
&lt;br /&gt;
    app.run(host='0.0.0.0', port=5000, debug=True)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Paso 3: Construir y Ejecutar los Contenedores con Docker Compose ==&lt;br /&gt;
&lt;br /&gt;
Ahora que `docker-compose.yml` y `app.py` están configurados, estamos listos para construir y ejecutar la aplicación con Docker Compose.&lt;br /&gt;
&lt;br /&gt;
En la terminal, navega al directorio `flask_testing_project` y ejecuta los siguientes comandos:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Construir y ejecutar los contenedores&lt;br /&gt;
docker compose up&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* `docker compose up`: Construye y ejecuta los contenedores en segundo plano. Si la imagen no existe, la creará usando el Dockerfile y las configuraciones de Docker Compose.&lt;br /&gt;
&lt;br /&gt;
Para detener los contenedores y eliminar el entorno, ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Parar y remover los contenedores&lt;br /&gt;
docker compose down&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* `docker compose down`: Detiene todos los contenedores y elimina el entorno definido en `docker-compose.yml`, sin eliminar los volúmenes de datos.&lt;br /&gt;
&lt;br /&gt;
== Paso 4: Verificación en el Navegador ==&lt;br /&gt;
&lt;br /&gt;
Una vez que los contenedores estén en ejecución, abre tu navegador y accede a [http://localhost:5000](http://localhost:5000). Si ves la aplicación funcionando y puedes agregar tareas, ¡felicidades! Has dockerizado exitosamente la aplicación Flask con una base de datos MariaDB.&lt;br /&gt;
&lt;br /&gt;
== Resumen ==&lt;br /&gt;
&lt;br /&gt;
En este tutorial:&lt;br /&gt;
* Creamos un archivo `docker-compose.yml` para gestionar múltiples contenedores.&lt;br /&gt;
* Modificamos `app.py` para usar SQLAlchemy y conectar la aplicación Flask a una base de datos MariaDB.&lt;br /&gt;
* Ejecutamos y verificamos la aplicación con Docker Compose.&lt;br /&gt;
&lt;br /&gt;
Ahora tienes una aplicación Flask lista para desarrollarse y desplegarse fácilmente en cualquier entorno.&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento&amp;diff=9830</id>
		<title>Tutorial Campo de entrenamiento</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento&amp;diff=9830"/>
				<updated>2024-10-16T10:33:17Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: /* Pruebas de interfaz con Selenium */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Automatización de Pruebas de Software en una Aplicación Flask ==&lt;br /&gt;
&lt;br /&gt;
=== Parte 1: creamos pruebas para una aplicación sencilla ===&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación.&lt;br /&gt;
# '''Pruebas de cobertura''' para comprobar si nuestras pruebas tienen una buena cobertura de código.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
==== Requisitos previos ====&lt;br /&gt;
&lt;br /&gt;
Antes de comenzar, asegúrate de tener instalados los siguientes paquetes y herramientas:&lt;br /&gt;
&lt;br /&gt;
* Python 3&lt;br /&gt;
* Flask&lt;br /&gt;
* &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para pruebas unitarias.&lt;br /&gt;
* &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; para la cobertura de código.&lt;br /&gt;
* &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; para pruebas de interfaz.&lt;br /&gt;
* &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; para pruebas de carga.&lt;br /&gt;
* El navegador '''chromium''' y '''chromium-driver''' (para Selenium).&lt;br /&gt;
&lt;br /&gt;
Para instalar las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pip install flask pytest pytest-cov selenium locust webdriver-manager&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Estructura del proyecto =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app.py                # Archivo principal de la aplicación Flask&lt;br /&gt;
├── templates/            # Directorio que contiene la plantilla HTML&lt;br /&gt;
│   └── tasks.html        # Plantilla para mostrar y agregar tareas&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── test_app.py       # Pruebas unitarias usando pytest&lt;br /&gt;
│   └── test_interfaz.py  # Pruebas de interfaz con Selenium&lt;br /&gt;
└── locustfile.py         # Archivo para pruebas de carga con Locust&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
==== Desarrollo de la Aplicación Flask ====&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
===== Código &amp;lt;code&amp;gt;app.py&amp;lt;/code&amp;gt;: =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Lista inicial de tareas (guardada en memoria)&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
# Ruta para obtener la lista de tareas (versión HTML)&lt;br /&gt;
@app.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=tasks)&lt;br /&gt;
&lt;br /&gt;
# Ruta para obtener la lista de tareas en JSON (API)&lt;br /&gt;
@app.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': tasks})&lt;br /&gt;
&lt;br /&gt;
# Ruta para crear una nueva tarea desde un formulario HTML&lt;br /&gt;
@app.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    if not title:&lt;br /&gt;
        return &amp;quot;El título es necesario&amp;quot;, 400&lt;br /&gt;
    task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(task)&lt;br /&gt;
    return redirect(url_for('task_list'))&lt;br /&gt;
&lt;br /&gt;
# Ruta para crear una nueva tarea (API JSON)&lt;br /&gt;
@app.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task():&lt;br /&gt;
    if not request.json or 'title' not in request.json:&lt;br /&gt;
        return jsonify({'error': 'El título es necesario'}), 400&lt;br /&gt;
    task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': request.json['title'],&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(task)&lt;br /&gt;
    return jsonify(task), 201&lt;br /&gt;
&lt;br /&gt;
if __name__ == '__main__':&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver en el código, por tanto, se ofrecen dos formas para interactuar con las tareas:&lt;br /&gt;
&lt;br /&gt;
# Una página HTML que muestra la lista de tareas y un formulario para añadir nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
# Una API REST que devuelve la lista de tareas en formato JSON y permite agregar nuevas tareas mediante solicitudes POST.&lt;br /&gt;
&lt;br /&gt;
===== Plantilla HTML =====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
====== Archivo &amp;lt;code&amp;gt;templates/tasks.html&amp;lt;/code&amp;gt;: ======&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;!-- Formulario para añadir una nueva tarea --&amp;gt;&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;!-- Lista de tareas --&amp;gt;&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %} (completada) {% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Lanza la aplicación =====&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ python app.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella creando y visualizando las tareas usando primero el formulario web y luego también mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
$ curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Pruebas Unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en probar los endpoints de la API de manera independiente.&lt;br /&gt;
&lt;br /&gt;
===== Archivo &amp;lt;code&amp;gt;tests/test_app.py&amp;lt;/code&amp;gt;: =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import app&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def client():&lt;br /&gt;
    with app.test_client() as client:&lt;br /&gt;
        yield client&lt;br /&gt;
&lt;br /&gt;
def test_get_tasks(client):&lt;br /&gt;
    response = client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert 'Comprar pan' in response.get_data(as_text=True)&lt;br /&gt;
&lt;br /&gt;
def test_create_task(client):&lt;br /&gt;
    response = client.post('/tasks', json={'title': 'Aprender testing'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
    assert 'Aprender testing' in response.get_data(as_text=True)&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title(client):&lt;br /&gt;
    response = client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
def test_task_list_updates(client):&lt;br /&gt;
    response = client.post('/tasks', json={'title': 'Otra nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    response = client.get('/tasks')&lt;br /&gt;
    assert 'Otra nueva tarea' in response.get_data(as_text=True)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de las pruebas: =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
NOTA: Si recibes un error al lanzar pytest porque no se encuentra el módulo app, puedes intentarlo así (lo que añade el directorio actual (.) al PYTHONPATH):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ PYTHONPATH=. pytest&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Nota: En la segunda parte de la práctica, cuando lances las pruebas en UVLHUB, verás que esto no es necesario, ya que UVLHUB usa el archivo init.py en cada carpeta para que Python lo interprete como módulo y así cargarlo.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
Y, ¿qué es la cobertura de código?&lt;br /&gt;
&lt;br /&gt;
La cobertura de código mide el porcentaje de código ejecutado cuando se lanzan las pruebas. Esto nos ayuda a identificar las áreas de la aplicación que no están siendo probadas adecuadamente.&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código en el archivo app.py fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
------- coverage: xxx% -------&lt;br /&gt;
&lt;br /&gt;
 Name      | Stmts | Miss | Cover                      &lt;br /&gt;
-----------| ------|------|------&lt;br /&gt;
 app.py    | 26    | 8    | 69%&lt;br /&gt;
 TOTAL     | 26    | 8    | 69%&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias en el archivo app.py; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre el archivo app.py.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación.&lt;br /&gt;
&lt;br /&gt;
Importante: hay que tener instalados los siguientes paquetes y en este orden:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
sudo dpkg -i &amp;lt;archivo deb oficial descargado&amp;gt; [https://www.google.com/intl/es_es/chrome/?brand=FHFK&amp;amp;ds_kid=43700059038707842&amp;amp;gad_source=1&amp;amp;gclid=EAIaIQobChMIgKjcmdmSiQMV4jsGAB3_2AtqEAAYASAAEgJP2PD_BwE&amp;amp;gclsrc=aw.ds https://www.google.com/intl/es_es/chrome/?brand=FHFK&amp;amp;ds_kid=43700059038707842&amp;amp;gad_source=1&amp;amp;gclid=EAIaIQobChMIgKjcmdmSiQMV4jsGAB3_2AtqEAAYASAAEgJP2PD_BwE&amp;amp;gclsrc=aw.ds]&lt;br /&gt;
&lt;br /&gt;
sudo apt install chromium-browser&lt;br /&gt;
&lt;br /&gt;
sudo apt install chromium-driver&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Un pasito previo para ver &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; en acción =====&lt;br /&gt;
&lt;br /&gt;
Antes de realizar las pruebas sobre nuestra aplicación vamos a hacer un pasito previo para comprobar que tenemos chromium y chromium-driver correctamente instalados y entender el tipo de cosas que podemos hacer con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Si no tienes Chrome instalado en tu equipo puedes simplemente instalar en tu sistema los paquetes &amp;lt;code&amp;gt;chromium-brwoser&amp;lt;/code&amp;gt; y &amp;lt;code&amp;gt;chromium-driver&amp;lt;/code&amp;gt;. Pero si ya tenías Chrome instalado tendrás que instalar el paquete &amp;lt;i&amp;gt;webdriver-manager&amp;lt;/i&amp;gt; que se encargará de instalar las versiones adecuadas de Chromium y Chromium Webdriver.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;pip install webdriver-manager&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====== Archivo &amp;lt;code&amp;gt;tests/test_selenium.py&amp;lt;/code&amp;gt; : ======&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from selenium import webdriver&lt;br /&gt;
from selenium.webdriver.common.by import By&lt;br /&gt;
from selenium.webdriver.common.keys import Keys&lt;br /&gt;
from selenium.webdriver.support.ui import WebDriverWait&lt;br /&gt;
from selenium.webdriver.support import expected_conditions as EC&lt;br /&gt;
from webdriver_manager.chrome import ChromeDriverManager&lt;br /&gt;
from selenium.webdriver.chrome.service import Service&lt;br /&gt;
&lt;br /&gt;
# Configurar Selenium para usar Chromium&lt;br /&gt;
options = webdriver.ChromeOptions()&lt;br /&gt;
&lt;br /&gt;
# Quita '--headless' para ejecutar el navegador de manera visible&lt;br /&gt;
options.add_argument('--no-sandbox')&lt;br /&gt;
options.add_argument('--disable-dev-shm-usage')&lt;br /&gt;
&lt;br /&gt;
# Iniciar el driver de Chromium usando webdriver-manager&lt;br /&gt;
service = Service(ChromeDriverManager().install())&lt;br /&gt;
driver = webdriver.Chrome(service=service, options=options)&lt;br /&gt;
&lt;br /&gt;
try:&lt;br /&gt;
    # 1. Abrir Google&lt;br /&gt;
    driver.get(&amp;quot;https://www.google.com&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 2. Esperar hasta que aparezca la ventana de cookies y hacer clic en &amp;quot;Rechazar todo&amp;quot;&lt;br /&gt;
    reject_cookies_button = WebDriverWait(driver, 10).until(&lt;br /&gt;
        EC.element_to_be_clickable((By.XPATH, &amp;quot;//button[contains(., 'Rechazar todo')]&amp;quot;))&lt;br /&gt;
    )&lt;br /&gt;
    reject_cookies_button.click()&lt;br /&gt;
&lt;br /&gt;
    # 3. Esperar hasta que el campo de búsqueda sea visible&lt;br /&gt;
    search_box = WebDriverWait(driver, 10).until(&lt;br /&gt;
        EC.visibility_of_element_located((By.NAME, &amp;quot;q&amp;quot;))&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # 4. Escribir &amp;quot;Selenium&amp;quot; en la barra de búsqueda y enviar el formulario&lt;br /&gt;
    search_box.send_keys(&amp;quot;Selenium&amp;quot;)&lt;br /&gt;
    search_box.send_keys(Keys.RETURN)&lt;br /&gt;
&lt;br /&gt;
    # 5. Esperar a que el título cambie y contenga &amp;quot;Selenium&amp;quot;&lt;br /&gt;
    WebDriverWait(driver, 10).until(EC.title_contains(&amp;quot;Selenium&amp;quot;))&lt;br /&gt;
    print(&amp;quot;¡Selenium está funcionando correctamente con Chromium!&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
except Exception as e:&lt;br /&gt;
    print(f&amp;quot;Error: {e}&amp;quot;)&lt;br /&gt;
    driver.save_screenshot(&amp;quot;error_screenshot.png&amp;quot;)&lt;br /&gt;
    print(&amp;quot;Captura de pantalla guardada como 'error_screenshot.png'&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
finally:&lt;br /&gt;
    # Cerrar el navegador&lt;br /&gt;
    driver.quit()&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarla y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest -s tests/test_selenium.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_selenium.py&amp;lt;/code&amp;gt;? Pues ya tenemos todo listo para realizar las pruebas sobre nuestra aplicación.&lt;br /&gt;
&lt;br /&gt;
===== Archivo &amp;lt;code&amp;gt;tests/test_interfaz.py&amp;lt;/code&amp;gt;: =====&lt;br /&gt;
&lt;br /&gt;
Ahora sí, vamos a crear las pruebas de la vista para nuestra aplicación.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from selenium import webdriver&lt;br /&gt;
from selenium.webdriver.common.by import By&lt;br /&gt;
from selenium.webdriver.common.keys import Keys&lt;br /&gt;
from webdriver_manager.chrome import ChromeDriverManager&lt;br /&gt;
from selenium.webdriver.chrome.service import Service&lt;br /&gt;
&lt;br /&gt;
# Configurar Selenium para usar Chromium&lt;br /&gt;
options = webdriver.ChromeOptions()&lt;br /&gt;
options.add_argument('--no-sandbox')&lt;br /&gt;
options.add_argument('--disable-dev-shm-usage')&lt;br /&gt;
&lt;br /&gt;
# Usar webdriver-manager para gestionar el driver de Chromium&lt;br /&gt;
service = Service(ChromeDriverManager().install())&lt;br /&gt;
driver = webdriver.Chrome(service=service, options=options)&lt;br /&gt;
&lt;br /&gt;
try:&lt;br /&gt;
    # Abre la aplicación web&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # Verifica que el título de la página es correcto&lt;br /&gt;
    assert &amp;quot;Gestor de Tareas&amp;quot; in driver.title&lt;br /&gt;
&lt;br /&gt;
    # Buscar el campo de entrada de nueva tarea&lt;br /&gt;
    input_field = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # Escribir en el campo de entrada&lt;br /&gt;
    input_field.send_keys(&amp;quot;Tarea de Selenium&amp;quot;)&lt;br /&gt;
    input_field.send_keys(Keys.RETURN)&lt;br /&gt;
&lt;br /&gt;
    # Verificar que la tarea aparece en la lista&lt;br /&gt;
    assert &amp;quot;Tarea de Selenium&amp;quot; in driver.page_source&lt;br /&gt;
&lt;br /&gt;
finally:&lt;br /&gt;
    driver.quit()&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de las pruebas de interfaz: =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest -s tests/test_interfaz.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría mucho trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
====== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ======&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
====== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ======&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====== Ejecutar la prueba grabada ======&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
====== Exportar el test a código &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt; ======&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - pytest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
    &lt;br /&gt;
Ejecutar el test exportado:&lt;br /&gt;
&lt;br /&gt;
Y ya puedes ejecutar el test exportado utilizando pytest:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest tests/test_selenium_ide.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto ejecutará el test generado por &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en tu navegador usando &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de Carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
===== Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;: =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
# Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ python app.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura el número de usuarios -por ejemplo, 10-, la tasa de generación (cada cuánto tiempo se lanza un nuevo usuario)-por ejemplo, 1- y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;). Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
# En la terminal verás mensajes como estos hasta que se haya lanzado el número de clientes indicado:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
[2024-10-07 17:35:02,798] hostname/INFO/locust.runners: All users spawned: {&amp;quot;WebsiteTestUser&amp;quot;: 10} (10 total users)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y además en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
=== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ===&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
==== Un ejemplo sencillo para ayudarte a arrancar ====&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar pruebas unitarias para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fijate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo, ¿podrías ir diseñando las pruebas unitarias necesarias para comprobar todas las operaciones CRUD del módulo notepad?&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Archivo:EGC_2024-25_P1.pdf&amp;diff=9828</id>
		<title>Archivo:EGC 2024-25 P1.pdf</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Archivo:EGC_2024-25_P1.pdf&amp;diff=9828"/>
				<updated>2024-10-16T09:39:08Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: Drorganvidez subió una nueva versión de Archivo:EGC 2024-25 P1.pdf&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Práctica 1&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento&amp;diff=9823</id>
		<title>Tutorial Campo de entrenamiento</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento&amp;diff=9823"/>
				<updated>2024-10-16T06:10:39Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: /* Requisitos previos */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Automatización de Pruebas de Software en una Aplicación Flask ==&lt;br /&gt;
&lt;br /&gt;
=== Parte 1: creamos pruebas para una aplicación sencilla ===&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación.&lt;br /&gt;
# '''Pruebas de cobertura''' para comprobar si nuestras pruebas tienen una buena cobertura de código.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
==== Requisitos previos ====&lt;br /&gt;
&lt;br /&gt;
Antes de comenzar, asegúrate de tener instalados los siguientes paquetes y herramientas:&lt;br /&gt;
&lt;br /&gt;
* Python 3&lt;br /&gt;
* Flask&lt;br /&gt;
* &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para pruebas unitarias.&lt;br /&gt;
* &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; para la cobertura de código.&lt;br /&gt;
* &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; para pruebas de interfaz.&lt;br /&gt;
* &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; para pruebas de carga.&lt;br /&gt;
* El navegador '''chromium''' y '''chromium-driver''' (para Selenium).&lt;br /&gt;
&lt;br /&gt;
Para instalar las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pip install flask pytest pytest-cov selenium locust webdriver-manager&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Estructura del proyecto =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app.py                # Archivo principal de la aplicación Flask&lt;br /&gt;
├── templates/            # Directorio que contiene la plantilla HTML&lt;br /&gt;
│   └── tasks.html        # Plantilla para mostrar y agregar tareas&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── test_app.py       # Pruebas unitarias usando pytest&lt;br /&gt;
│   └── test_interfaz.py  # Pruebas de interfaz con Selenium&lt;br /&gt;
└── locustfile.py         # Archivo para pruebas de carga con Locust&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
==== Desarrollo de la Aplicación Flask ====&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
===== Código &amp;lt;code&amp;gt;app.py&amp;lt;/code&amp;gt;: =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Lista inicial de tareas (guardada en memoria)&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
# Ruta para obtener la lista de tareas (versión HTML)&lt;br /&gt;
@app.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=tasks)&lt;br /&gt;
&lt;br /&gt;
# Ruta para obtener la lista de tareas en JSON (API)&lt;br /&gt;
@app.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': tasks})&lt;br /&gt;
&lt;br /&gt;
# Ruta para crear una nueva tarea desde un formulario HTML&lt;br /&gt;
@app.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    if not title:&lt;br /&gt;
        return &amp;quot;El título es necesario&amp;quot;, 400&lt;br /&gt;
    task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(task)&lt;br /&gt;
    return redirect(url_for('task_list'))&lt;br /&gt;
&lt;br /&gt;
# Ruta para crear una nueva tarea (API JSON)&lt;br /&gt;
@app.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task():&lt;br /&gt;
    if not request.json or 'title' not in request.json:&lt;br /&gt;
        return jsonify({'error': 'El título es necesario'}), 400&lt;br /&gt;
    task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': request.json['title'],&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(task)&lt;br /&gt;
    return jsonify(task), 201&lt;br /&gt;
&lt;br /&gt;
if __name__ == '__main__':&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver en el código, por tanto, se ofrecen dos formas para interactuar con las tareas:&lt;br /&gt;
&lt;br /&gt;
# Una página HTML que muestra la lista de tareas y un formulario para añadir nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
# Una API REST que devuelve la lista de tareas en formato JSON y permite agregar nuevas tareas mediante solicitudes POST.&lt;br /&gt;
&lt;br /&gt;
===== Plantilla HTML =====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
====== Archivo &amp;lt;code&amp;gt;templates/tasks.html&amp;lt;/code&amp;gt;: ======&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;!-- Formulario para añadir una nueva tarea --&amp;gt;&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;!-- Lista de tareas --&amp;gt;&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %} (completada) {% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Lanza la aplicación =====&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ python app.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella creando y visualizando las tareas usando primero el formulario web y luego también mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
$ curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Pruebas Unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en probar los endpoints de la API de manera independiente.&lt;br /&gt;
&lt;br /&gt;
===== Archivo &amp;lt;code&amp;gt;tests/test_app.py&amp;lt;/code&amp;gt;: =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import app&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def client():&lt;br /&gt;
    with app.test_client() as client:&lt;br /&gt;
        yield client&lt;br /&gt;
&lt;br /&gt;
def test_get_tasks(client):&lt;br /&gt;
    response = client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert 'Comprar pan' in response.get_data(as_text=True)&lt;br /&gt;
&lt;br /&gt;
def test_create_task(client):&lt;br /&gt;
    response = client.post('/tasks', json={'title': 'Aprender testing'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
    assert 'Aprender testing' in response.get_data(as_text=True)&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title(client):&lt;br /&gt;
    response = client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
def test_task_list_updates(client):&lt;br /&gt;
    response = client.post('/tasks', json={'title': 'Otra nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    response = client.get('/tasks')&lt;br /&gt;
    assert 'Otra nueva tarea' in response.get_data(as_text=True)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de las pruebas: =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
NOTA: Si recibes un error al lanzar pytest porque no se encuentra el módulo app, puedes intentarlo así (lo que añade el directorio actual (.) al PYTHONPATH):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ PYTHONPATH=. pytest&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Nota: En la segunda parte de la práctica, cuando lances las pruebas en UVLHUB, verás que esto no es necesario, ya que UVLHUB usa el archivo init.py en cada carpeta para que Python lo interprete como módulo y así cargarlo.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
Y, ¿qué es la cobertura de código?&lt;br /&gt;
&lt;br /&gt;
La cobertura de código mide el porcentaje de código ejecutado cuando se lanzan las pruebas. Esto nos ayuda a identificar las áreas de la aplicación que no están siendo probadas adecuadamente.&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código en el archivo app.py fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
------- coverage: xxx% -------&lt;br /&gt;
&lt;br /&gt;
 Name      | Stmts | Miss | Cover                      &lt;br /&gt;
-----------| ------|------|------&lt;br /&gt;
 app.py    | 26    | 8    | 69%&lt;br /&gt;
 TOTAL     | 26    | 8    | 69%&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias en el archivo app.py; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre el archivo app.py.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación.&lt;br /&gt;
&lt;br /&gt;
===== Un pasito previo para ver &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; en acción =====&lt;br /&gt;
&lt;br /&gt;
Antes de realizar las pruebas sobre nuestra aplicación vamos a hacer un pasito previo para comprobar que tenemos chromium y chromium-driver correctamente instalados y entender el tipo de cosas que podemos hacer con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Para empezar, debemos instalar el paquete &amp;lt;i&amp;gt;webdriver-manager&amp;lt;/i&amp;gt; que se encargará de instalar las versiones adecuadas de Chromium y Chromium Webdriver.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;pip install webdriver-manager&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====== Archivo &amp;lt;code&amp;gt;tests/test_selenium.py&amp;lt;/code&amp;gt; : ======&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from selenium import webdriver&lt;br /&gt;
from selenium.webdriver.common.by import By&lt;br /&gt;
from selenium.webdriver.common.keys import Keys&lt;br /&gt;
from selenium.webdriver.support.ui import WebDriverWait&lt;br /&gt;
from selenium.webdriver.support import expected_conditions as EC&lt;br /&gt;
from webdriver_manager.chrome import ChromeDriverManager&lt;br /&gt;
from selenium.webdriver.chrome.service import Service&lt;br /&gt;
&lt;br /&gt;
# Configurar Selenium para usar Chromium&lt;br /&gt;
options = webdriver.ChromeOptions()&lt;br /&gt;
&lt;br /&gt;
# Quita '--headless' para ejecutar el navegador de manera visible&lt;br /&gt;
options.add_argument('--no-sandbox')&lt;br /&gt;
options.add_argument('--disable-dev-shm-usage')&lt;br /&gt;
&lt;br /&gt;
# Iniciar el driver de Chromium usando webdriver-manager&lt;br /&gt;
service = Service(ChromeDriverManager().install())&lt;br /&gt;
driver = webdriver.Chrome(service=service, options=options)&lt;br /&gt;
&lt;br /&gt;
try:&lt;br /&gt;
    # 1. Abrir Google&lt;br /&gt;
    driver.get(&amp;quot;https://www.google.com&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 2. Esperar hasta que aparezca la ventana de cookies y hacer clic en &amp;quot;Rechazar todo&amp;quot;&lt;br /&gt;
    reject_cookies_button = WebDriverWait(driver, 10).until(&lt;br /&gt;
        EC.element_to_be_clickable((By.XPATH, &amp;quot;//button[contains(., 'Rechazar todo')]&amp;quot;))&lt;br /&gt;
    )&lt;br /&gt;
    reject_cookies_button.click()&lt;br /&gt;
&lt;br /&gt;
    # 3. Esperar hasta que el campo de búsqueda sea visible&lt;br /&gt;
    search_box = WebDriverWait(driver, 10).until(&lt;br /&gt;
        EC.visibility_of_element_located((By.NAME, &amp;quot;q&amp;quot;))&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # 4. Escribir &amp;quot;Selenium&amp;quot; en la barra de búsqueda y enviar el formulario&lt;br /&gt;
    search_box.send_keys(&amp;quot;Selenium&amp;quot;)&lt;br /&gt;
    search_box.send_keys(Keys.RETURN)&lt;br /&gt;
&lt;br /&gt;
    # 5. Esperar a que el título cambie y contenga &amp;quot;Selenium&amp;quot;&lt;br /&gt;
    WebDriverWait(driver, 10).until(EC.title_contains(&amp;quot;Selenium&amp;quot;))&lt;br /&gt;
    print(&amp;quot;¡Selenium está funcionando correctamente con Chromium!&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
except Exception as e:&lt;br /&gt;
    print(f&amp;quot;Error: {e}&amp;quot;)&lt;br /&gt;
    driver.save_screenshot(&amp;quot;error_screenshot.png&amp;quot;)&lt;br /&gt;
    print(&amp;quot;Captura de pantalla guardada como 'error_screenshot.png'&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
finally:&lt;br /&gt;
    # Cerrar el navegador&lt;br /&gt;
    driver.quit()&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarala y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest -s tests/test_selenium.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_selenium.py&amp;lt;/code&amp;gt;? Pues ya tenemos todo listo para realizar las pruebas sobre nuestra aplicación.&lt;br /&gt;
&lt;br /&gt;
===== Archivo &amp;lt;code&amp;gt;tests/test_interfaz.py&amp;lt;/code&amp;gt;: =====&lt;br /&gt;
&lt;br /&gt;
Ahora sí, vamos a crear las pruebas de la vista para nuestra aplicación.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from selenium import webdriver&lt;br /&gt;
from selenium.webdriver.common.by import By&lt;br /&gt;
from selenium.webdriver.common.keys import Keys&lt;br /&gt;
from webdriver_manager.chrome import ChromeDriverManager&lt;br /&gt;
from selenium.webdriver.chrome.service import Service&lt;br /&gt;
&lt;br /&gt;
# Configurar Selenium para usar Chromium&lt;br /&gt;
options = webdriver.ChromeOptions()&lt;br /&gt;
options.add_argument('--no-sandbox')&lt;br /&gt;
options.add_argument('--disable-dev-shm-usage')&lt;br /&gt;
&lt;br /&gt;
# Usar webdriver-manager para gestionar el driver de Chromium&lt;br /&gt;
service = Service(ChromeDriverManager().install())&lt;br /&gt;
driver = webdriver.Chrome(service=service, options=options)&lt;br /&gt;
&lt;br /&gt;
try:&lt;br /&gt;
    # Abre la aplicación web&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # Verifica que el título de la página es correcto&lt;br /&gt;
    assert &amp;quot;Gestor de Tareas&amp;quot; in driver.title&lt;br /&gt;
&lt;br /&gt;
    # Buscar el campo de entrada de nueva tarea&lt;br /&gt;
    input_field = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # Escribir en el campo de entrada&lt;br /&gt;
    input_field.send_keys(&amp;quot;Tarea de Selenium&amp;quot;)&lt;br /&gt;
    input_field.send_keys(Keys.RETURN)&lt;br /&gt;
&lt;br /&gt;
    # Verificar que la tarea aparece en la lista&lt;br /&gt;
    assert &amp;quot;Tarea de Selenium&amp;quot; in driver.page_source&lt;br /&gt;
&lt;br /&gt;
finally:&lt;br /&gt;
    driver.quit()&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de las pruebas de interfaz: =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest -s tests/test_interfaz.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría mucho trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
====== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ======&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
====== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ======&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====== Ejecutar la prueba grabada ======&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
====== Exportar el test a código &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt; ======&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - pytest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
    &lt;br /&gt;
Ejecutar el test exportado:&lt;br /&gt;
&lt;br /&gt;
Y ya puedes ejecutar el test exportado utilizando pytest:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest tests/test_selenium_ide.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto ejecutará el test generado por &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en tu navegador usando &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de Carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
===== Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;: =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
# Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ python app.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura el número de usuarios -por ejemplo, 10-, la tasa de generación (cada cuánto tiempo se lanza un nuevo usuario)-por ejemplo, 1- y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;). Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
# En la terminal verás mensajes como estos hasta que se haya lanzado el número de clientes indicado:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
[2024-10-07 17:35:02,798] hostname/INFO/locust.runners: All users spawned: {&amp;quot;WebsiteTestUser&amp;quot;: 10} (10 total users)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y además en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
=== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ===&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
==== Un ejemplo sencillo para ayudarte a arrancar ====&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar pruebas unitarias para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fijate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo, ¿podrías ir diseñando las pruebas unitarias necesarias para comprobar todas las operaciones CRUD del módulo notepad?&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento&amp;diff=9822</id>
		<title>Tutorial Campo de entrenamiento</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento&amp;diff=9822"/>
				<updated>2024-10-16T06:09:48Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: /* Archivo tests/test_interfaz.py: */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Automatización de Pruebas de Software en una Aplicación Flask ==&lt;br /&gt;
&lt;br /&gt;
=== Parte 1: creamos pruebas para una aplicación sencilla ===&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación.&lt;br /&gt;
# '''Pruebas de cobertura''' para comprobar si nuestras pruebas tienen una buena cobertura de código.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
==== Requisitos previos ====&lt;br /&gt;
&lt;br /&gt;
Antes de comenzar, asegúrate de tener instalados los siguientes paquetes y herramientas:&lt;br /&gt;
&lt;br /&gt;
* Python 3&lt;br /&gt;
* Flask&lt;br /&gt;
* &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para pruebas unitarias.&lt;br /&gt;
* &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; para la cobertura de código.&lt;br /&gt;
* &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; para pruebas de interfaz.&lt;br /&gt;
* &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; para pruebas de carga.&lt;br /&gt;
* El navegador '''chromium''' y '''chromium-driver''' (para Selenium).&lt;br /&gt;
&lt;br /&gt;
Para instalar las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pip install flask pytest pytest-cov selenium locust&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Estructura del proyecto =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app.py                # Archivo principal de la aplicación Flask&lt;br /&gt;
├── templates/            # Directorio que contiene la plantilla HTML&lt;br /&gt;
│   └── tasks.html        # Plantilla para mostrar y agregar tareas&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── test_app.py       # Pruebas unitarias usando pytest&lt;br /&gt;
│   └── test_interfaz.py  # Pruebas de interfaz con Selenium&lt;br /&gt;
└── locustfile.py         # Archivo para pruebas de carga con Locust&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
==== Desarrollo de la Aplicación Flask ====&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
===== Código &amp;lt;code&amp;gt;app.py&amp;lt;/code&amp;gt;: =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Lista inicial de tareas (guardada en memoria)&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
# Ruta para obtener la lista de tareas (versión HTML)&lt;br /&gt;
@app.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=tasks)&lt;br /&gt;
&lt;br /&gt;
# Ruta para obtener la lista de tareas en JSON (API)&lt;br /&gt;
@app.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': tasks})&lt;br /&gt;
&lt;br /&gt;
# Ruta para crear una nueva tarea desde un formulario HTML&lt;br /&gt;
@app.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    if not title:&lt;br /&gt;
        return &amp;quot;El título es necesario&amp;quot;, 400&lt;br /&gt;
    task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(task)&lt;br /&gt;
    return redirect(url_for('task_list'))&lt;br /&gt;
&lt;br /&gt;
# Ruta para crear una nueva tarea (API JSON)&lt;br /&gt;
@app.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task():&lt;br /&gt;
    if not request.json or 'title' not in request.json:&lt;br /&gt;
        return jsonify({'error': 'El título es necesario'}), 400&lt;br /&gt;
    task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': request.json['title'],&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(task)&lt;br /&gt;
    return jsonify(task), 201&lt;br /&gt;
&lt;br /&gt;
if __name__ == '__main__':&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver en el código, por tanto, se ofrecen dos formas para interactuar con las tareas:&lt;br /&gt;
&lt;br /&gt;
# Una página HTML que muestra la lista de tareas y un formulario para añadir nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
# Una API REST que devuelve la lista de tareas en formato JSON y permite agregar nuevas tareas mediante solicitudes POST.&lt;br /&gt;
&lt;br /&gt;
===== Plantilla HTML =====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
====== Archivo &amp;lt;code&amp;gt;templates/tasks.html&amp;lt;/code&amp;gt;: ======&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;!-- Formulario para añadir una nueva tarea --&amp;gt;&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;!-- Lista de tareas --&amp;gt;&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %} (completada) {% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Lanza la aplicación =====&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ python app.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella creando y visualizando las tareas usando primero el formulario web y luego también mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
$ curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Pruebas Unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en probar los endpoints de la API de manera independiente.&lt;br /&gt;
&lt;br /&gt;
===== Archivo &amp;lt;code&amp;gt;tests/test_app.py&amp;lt;/code&amp;gt;: =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import app&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def client():&lt;br /&gt;
    with app.test_client() as client:&lt;br /&gt;
        yield client&lt;br /&gt;
&lt;br /&gt;
def test_get_tasks(client):&lt;br /&gt;
    response = client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert 'Comprar pan' in response.get_data(as_text=True)&lt;br /&gt;
&lt;br /&gt;
def test_create_task(client):&lt;br /&gt;
    response = client.post('/tasks', json={'title': 'Aprender testing'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
    assert 'Aprender testing' in response.get_data(as_text=True)&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title(client):&lt;br /&gt;
    response = client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
def test_task_list_updates(client):&lt;br /&gt;
    response = client.post('/tasks', json={'title': 'Otra nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    response = client.get('/tasks')&lt;br /&gt;
    assert 'Otra nueva tarea' in response.get_data(as_text=True)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de las pruebas: =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
NOTA: Si recibes un error al lanzar pytest porque no se encuentra el módulo app, puedes intentarlo así (lo que añade el directorio actual (.) al PYTHONPATH):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ PYTHONPATH=. pytest&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Nota: En la segunda parte de la práctica, cuando lances las pruebas en UVLHUB, verás que esto no es necesario, ya que UVLHUB usa el archivo init.py en cada carpeta para que Python lo interprete como módulo y así cargarlo.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
Y, ¿qué es la cobertura de código?&lt;br /&gt;
&lt;br /&gt;
La cobertura de código mide el porcentaje de código ejecutado cuando se lanzan las pruebas. Esto nos ayuda a identificar las áreas de la aplicación que no están siendo probadas adecuadamente.&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código en el archivo app.py fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
------- coverage: xxx% -------&lt;br /&gt;
&lt;br /&gt;
 Name      | Stmts | Miss | Cover                      &lt;br /&gt;
-----------| ------|------|------&lt;br /&gt;
 app.py    | 26    | 8    | 69%&lt;br /&gt;
 TOTAL     | 26    | 8    | 69%&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias en el archivo app.py; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre el archivo app.py.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación.&lt;br /&gt;
&lt;br /&gt;
===== Un pasito previo para ver &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; en acción =====&lt;br /&gt;
&lt;br /&gt;
Antes de realizar las pruebas sobre nuestra aplicación vamos a hacer un pasito previo para comprobar que tenemos chromium y chromium-driver correctamente instalados y entender el tipo de cosas que podemos hacer con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Para empezar, debemos instalar el paquete &amp;lt;i&amp;gt;webdriver-manager&amp;lt;/i&amp;gt; que se encargará de instalar las versiones adecuadas de Chromium y Chromium Webdriver.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;pip install webdriver-manager&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====== Archivo &amp;lt;code&amp;gt;tests/test_selenium.py&amp;lt;/code&amp;gt; : ======&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from selenium import webdriver&lt;br /&gt;
from selenium.webdriver.common.by import By&lt;br /&gt;
from selenium.webdriver.common.keys import Keys&lt;br /&gt;
from selenium.webdriver.support.ui import WebDriverWait&lt;br /&gt;
from selenium.webdriver.support import expected_conditions as EC&lt;br /&gt;
from webdriver_manager.chrome import ChromeDriverManager&lt;br /&gt;
from selenium.webdriver.chrome.service import Service&lt;br /&gt;
&lt;br /&gt;
# Configurar Selenium para usar Chromium&lt;br /&gt;
options = webdriver.ChromeOptions()&lt;br /&gt;
&lt;br /&gt;
# Quita '--headless' para ejecutar el navegador de manera visible&lt;br /&gt;
options.add_argument('--no-sandbox')&lt;br /&gt;
options.add_argument('--disable-dev-shm-usage')&lt;br /&gt;
&lt;br /&gt;
# Iniciar el driver de Chromium usando webdriver-manager&lt;br /&gt;
service = Service(ChromeDriverManager().install())&lt;br /&gt;
driver = webdriver.Chrome(service=service, options=options)&lt;br /&gt;
&lt;br /&gt;
try:&lt;br /&gt;
    # 1. Abrir Google&lt;br /&gt;
    driver.get(&amp;quot;https://www.google.com&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 2. Esperar hasta que aparezca la ventana de cookies y hacer clic en &amp;quot;Rechazar todo&amp;quot;&lt;br /&gt;
    reject_cookies_button = WebDriverWait(driver, 10).until(&lt;br /&gt;
        EC.element_to_be_clickable((By.XPATH, &amp;quot;//button[contains(., 'Rechazar todo')]&amp;quot;))&lt;br /&gt;
    )&lt;br /&gt;
    reject_cookies_button.click()&lt;br /&gt;
&lt;br /&gt;
    # 3. Esperar hasta que el campo de búsqueda sea visible&lt;br /&gt;
    search_box = WebDriverWait(driver, 10).until(&lt;br /&gt;
        EC.visibility_of_element_located((By.NAME, &amp;quot;q&amp;quot;))&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # 4. Escribir &amp;quot;Selenium&amp;quot; en la barra de búsqueda y enviar el formulario&lt;br /&gt;
    search_box.send_keys(&amp;quot;Selenium&amp;quot;)&lt;br /&gt;
    search_box.send_keys(Keys.RETURN)&lt;br /&gt;
&lt;br /&gt;
    # 5. Esperar a que el título cambie y contenga &amp;quot;Selenium&amp;quot;&lt;br /&gt;
    WebDriverWait(driver, 10).until(EC.title_contains(&amp;quot;Selenium&amp;quot;))&lt;br /&gt;
    print(&amp;quot;¡Selenium está funcionando correctamente con Chromium!&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
except Exception as e:&lt;br /&gt;
    print(f&amp;quot;Error: {e}&amp;quot;)&lt;br /&gt;
    driver.save_screenshot(&amp;quot;error_screenshot.png&amp;quot;)&lt;br /&gt;
    print(&amp;quot;Captura de pantalla guardada como 'error_screenshot.png'&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
finally:&lt;br /&gt;
    # Cerrar el navegador&lt;br /&gt;
    driver.quit()&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarala y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest -s tests/test_selenium.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_selenium.py&amp;lt;/code&amp;gt;? Pues ya tenemos todo listo para realizar las pruebas sobre nuestra aplicación.&lt;br /&gt;
&lt;br /&gt;
===== Archivo &amp;lt;code&amp;gt;tests/test_interfaz.py&amp;lt;/code&amp;gt;: =====&lt;br /&gt;
&lt;br /&gt;
Ahora sí, vamos a crear las pruebas de la vista para nuestra aplicación.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from selenium import webdriver&lt;br /&gt;
from selenium.webdriver.common.by import By&lt;br /&gt;
from selenium.webdriver.common.keys import Keys&lt;br /&gt;
from webdriver_manager.chrome import ChromeDriverManager&lt;br /&gt;
from selenium.webdriver.chrome.service import Service&lt;br /&gt;
&lt;br /&gt;
# Configurar Selenium para usar Chromium&lt;br /&gt;
options = webdriver.ChromeOptions()&lt;br /&gt;
options.add_argument('--no-sandbox')&lt;br /&gt;
options.add_argument('--disable-dev-shm-usage')&lt;br /&gt;
&lt;br /&gt;
# Usar webdriver-manager para gestionar el driver de Chromium&lt;br /&gt;
service = Service(ChromeDriverManager().install())&lt;br /&gt;
driver = webdriver.Chrome(service=service, options=options)&lt;br /&gt;
&lt;br /&gt;
try:&lt;br /&gt;
    # Abre la aplicación web&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # Verifica que el título de la página es correcto&lt;br /&gt;
    assert &amp;quot;Gestor de Tareas&amp;quot; in driver.title&lt;br /&gt;
&lt;br /&gt;
    # Buscar el campo de entrada de nueva tarea&lt;br /&gt;
    input_field = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # Escribir en el campo de entrada&lt;br /&gt;
    input_field.send_keys(&amp;quot;Tarea de Selenium&amp;quot;)&lt;br /&gt;
    input_field.send_keys(Keys.RETURN)&lt;br /&gt;
&lt;br /&gt;
    # Verificar que la tarea aparece en la lista&lt;br /&gt;
    assert &amp;quot;Tarea de Selenium&amp;quot; in driver.page_source&lt;br /&gt;
&lt;br /&gt;
finally:&lt;br /&gt;
    driver.quit()&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de las pruebas de interfaz: =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest -s tests/test_interfaz.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría mucho trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
====== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ======&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
====== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ======&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====== Ejecutar la prueba grabada ======&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
====== Exportar el test a código &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt; ======&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - pytest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
    &lt;br /&gt;
Ejecutar el test exportado:&lt;br /&gt;
&lt;br /&gt;
Y ya puedes ejecutar el test exportado utilizando pytest:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest tests/test_selenium_ide.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto ejecutará el test generado por &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en tu navegador usando &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de Carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
===== Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;: =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
# Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ python app.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura el número de usuarios -por ejemplo, 10-, la tasa de generación (cada cuánto tiempo se lanza un nuevo usuario)-por ejemplo, 1- y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;). Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
# En la terminal verás mensajes como estos hasta que se haya lanzado el número de clientes indicado:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
[2024-10-07 17:35:02,798] hostname/INFO/locust.runners: All users spawned: {&amp;quot;WebsiteTestUser&amp;quot;: 10} (10 total users)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y además en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
=== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ===&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
==== Un ejemplo sencillo para ayudarte a arrancar ====&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar pruebas unitarias para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fijate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo, ¿podrías ir diseñando las pruebas unitarias necesarias para comprobar todas las operaciones CRUD del módulo notepad?&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento&amp;diff=9821</id>
		<title>Tutorial Campo de entrenamiento</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento&amp;diff=9821"/>
				<updated>2024-10-16T06:07:03Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: /* Un pasito previo para ver Selenium en acción */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Automatización de Pruebas de Software en una Aplicación Flask ==&lt;br /&gt;
&lt;br /&gt;
=== Parte 1: creamos pruebas para una aplicación sencilla ===&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación.&lt;br /&gt;
# '''Pruebas de cobertura''' para comprobar si nuestras pruebas tienen una buena cobertura de código.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
==== Requisitos previos ====&lt;br /&gt;
&lt;br /&gt;
Antes de comenzar, asegúrate de tener instalados los siguientes paquetes y herramientas:&lt;br /&gt;
&lt;br /&gt;
* Python 3&lt;br /&gt;
* Flask&lt;br /&gt;
* &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para pruebas unitarias.&lt;br /&gt;
* &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; para la cobertura de código.&lt;br /&gt;
* &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; para pruebas de interfaz.&lt;br /&gt;
* &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; para pruebas de carga.&lt;br /&gt;
* El navegador '''chromium''' y '''chromium-driver''' (para Selenium).&lt;br /&gt;
&lt;br /&gt;
Para instalar las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pip install flask pytest pytest-cov selenium locust&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Estructura del proyecto =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app.py                # Archivo principal de la aplicación Flask&lt;br /&gt;
├── templates/            # Directorio que contiene la plantilla HTML&lt;br /&gt;
│   └── tasks.html        # Plantilla para mostrar y agregar tareas&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── test_app.py       # Pruebas unitarias usando pytest&lt;br /&gt;
│   └── test_interfaz.py  # Pruebas de interfaz con Selenium&lt;br /&gt;
└── locustfile.py         # Archivo para pruebas de carga con Locust&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
==== Desarrollo de la Aplicación Flask ====&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
===== Código &amp;lt;code&amp;gt;app.py&amp;lt;/code&amp;gt;: =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Lista inicial de tareas (guardada en memoria)&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
# Ruta para obtener la lista de tareas (versión HTML)&lt;br /&gt;
@app.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=tasks)&lt;br /&gt;
&lt;br /&gt;
# Ruta para obtener la lista de tareas en JSON (API)&lt;br /&gt;
@app.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': tasks})&lt;br /&gt;
&lt;br /&gt;
# Ruta para crear una nueva tarea desde un formulario HTML&lt;br /&gt;
@app.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    if not title:&lt;br /&gt;
        return &amp;quot;El título es necesario&amp;quot;, 400&lt;br /&gt;
    task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(task)&lt;br /&gt;
    return redirect(url_for('task_list'))&lt;br /&gt;
&lt;br /&gt;
# Ruta para crear una nueva tarea (API JSON)&lt;br /&gt;
@app.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task():&lt;br /&gt;
    if not request.json or 'title' not in request.json:&lt;br /&gt;
        return jsonify({'error': 'El título es necesario'}), 400&lt;br /&gt;
    task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': request.json['title'],&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(task)&lt;br /&gt;
    return jsonify(task), 201&lt;br /&gt;
&lt;br /&gt;
if __name__ == '__main__':&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver en el código, por tanto, se ofrecen dos formas para interactuar con las tareas:&lt;br /&gt;
&lt;br /&gt;
# Una página HTML que muestra la lista de tareas y un formulario para añadir nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
# Una API REST que devuelve la lista de tareas en formato JSON y permite agregar nuevas tareas mediante solicitudes POST.&lt;br /&gt;
&lt;br /&gt;
===== Plantilla HTML =====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
====== Archivo &amp;lt;code&amp;gt;templates/tasks.html&amp;lt;/code&amp;gt;: ======&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;!-- Formulario para añadir una nueva tarea --&amp;gt;&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;!-- Lista de tareas --&amp;gt;&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %} (completada) {% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Lanza la aplicación =====&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ python app.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella creando y visualizando las tareas usando primero el formulario web y luego también mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
$ curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Pruebas Unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en probar los endpoints de la API de manera independiente.&lt;br /&gt;
&lt;br /&gt;
===== Archivo &amp;lt;code&amp;gt;tests/test_app.py&amp;lt;/code&amp;gt;: =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import app&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def client():&lt;br /&gt;
    with app.test_client() as client:&lt;br /&gt;
        yield client&lt;br /&gt;
&lt;br /&gt;
def test_get_tasks(client):&lt;br /&gt;
    response = client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert 'Comprar pan' in response.get_data(as_text=True)&lt;br /&gt;
&lt;br /&gt;
def test_create_task(client):&lt;br /&gt;
    response = client.post('/tasks', json={'title': 'Aprender testing'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
    assert 'Aprender testing' in response.get_data(as_text=True)&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title(client):&lt;br /&gt;
    response = client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
def test_task_list_updates(client):&lt;br /&gt;
    response = client.post('/tasks', json={'title': 'Otra nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    response = client.get('/tasks')&lt;br /&gt;
    assert 'Otra nueva tarea' in response.get_data(as_text=True)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de las pruebas: =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
NOTA: Si recibes un error al lanzar pytest porque no se encuentra el módulo app, puedes intentarlo así (lo que añade el directorio actual (.) al PYTHONPATH):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ PYTHONPATH=. pytest&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Nota: En la segunda parte de la práctica, cuando lances las pruebas en UVLHUB, verás que esto no es necesario, ya que UVLHUB usa el archivo init.py en cada carpeta para que Python lo interprete como módulo y así cargarlo.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
Y, ¿qué es la cobertura de código?&lt;br /&gt;
&lt;br /&gt;
La cobertura de código mide el porcentaje de código ejecutado cuando se lanzan las pruebas. Esto nos ayuda a identificar las áreas de la aplicación que no están siendo probadas adecuadamente.&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código en el archivo app.py fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
------- coverage: xxx% -------&lt;br /&gt;
&lt;br /&gt;
 Name      | Stmts | Miss | Cover                      &lt;br /&gt;
-----------| ------|------|------&lt;br /&gt;
 app.py    | 26    | 8    | 69%&lt;br /&gt;
 TOTAL     | 26    | 8    | 69%&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias en el archivo app.py; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre el archivo app.py.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación.&lt;br /&gt;
&lt;br /&gt;
===== Un pasito previo para ver &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; en acción =====&lt;br /&gt;
&lt;br /&gt;
Antes de realizar las pruebas sobre nuestra aplicación vamos a hacer un pasito previo para comprobar que tenemos chromium y chromium-driver correctamente instalados y entender el tipo de cosas que podemos hacer con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Para empezar, debemos instalar el paquete &amp;lt;i&amp;gt;webdriver-manager&amp;lt;/i&amp;gt; que se encargará de instalar las versiones adecuadas de Chromium y Chromium Webdriver.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;pip install webdriver-manager&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====== Archivo &amp;lt;code&amp;gt;tests/test_selenium.py&amp;lt;/code&amp;gt; : ======&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from selenium import webdriver&lt;br /&gt;
from selenium.webdriver.common.by import By&lt;br /&gt;
from selenium.webdriver.common.keys import Keys&lt;br /&gt;
from selenium.webdriver.support.ui import WebDriverWait&lt;br /&gt;
from selenium.webdriver.support import expected_conditions as EC&lt;br /&gt;
from webdriver_manager.chrome import ChromeDriverManager&lt;br /&gt;
from selenium.webdriver.chrome.service import Service&lt;br /&gt;
&lt;br /&gt;
# Configurar Selenium para usar Chromium&lt;br /&gt;
options = webdriver.ChromeOptions()&lt;br /&gt;
&lt;br /&gt;
# Quita '--headless' para ejecutar el navegador de manera visible&lt;br /&gt;
options.add_argument('--no-sandbox')&lt;br /&gt;
options.add_argument('--disable-dev-shm-usage')&lt;br /&gt;
&lt;br /&gt;
# Iniciar el driver de Chromium usando webdriver-manager&lt;br /&gt;
service = Service(ChromeDriverManager().install())&lt;br /&gt;
driver = webdriver.Chrome(service=service, options=options)&lt;br /&gt;
&lt;br /&gt;
try:&lt;br /&gt;
    # 1. Abrir Google&lt;br /&gt;
    driver.get(&amp;quot;https://www.google.com&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 2. Esperar hasta que aparezca la ventana de cookies y hacer clic en &amp;quot;Rechazar todo&amp;quot;&lt;br /&gt;
    reject_cookies_button = WebDriverWait(driver, 10).until(&lt;br /&gt;
        EC.element_to_be_clickable((By.XPATH, &amp;quot;//button[contains(., 'Rechazar todo')]&amp;quot;))&lt;br /&gt;
    )&lt;br /&gt;
    reject_cookies_button.click()&lt;br /&gt;
&lt;br /&gt;
    # 3. Esperar hasta que el campo de búsqueda sea visible&lt;br /&gt;
    search_box = WebDriverWait(driver, 10).until(&lt;br /&gt;
        EC.visibility_of_element_located((By.NAME, &amp;quot;q&amp;quot;))&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # 4. Escribir &amp;quot;Selenium&amp;quot; en la barra de búsqueda y enviar el formulario&lt;br /&gt;
    search_box.send_keys(&amp;quot;Selenium&amp;quot;)&lt;br /&gt;
    search_box.send_keys(Keys.RETURN)&lt;br /&gt;
&lt;br /&gt;
    # 5. Esperar a que el título cambie y contenga &amp;quot;Selenium&amp;quot;&lt;br /&gt;
    WebDriverWait(driver, 10).until(EC.title_contains(&amp;quot;Selenium&amp;quot;))&lt;br /&gt;
    print(&amp;quot;¡Selenium está funcionando correctamente con Chromium!&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
except Exception as e:&lt;br /&gt;
    print(f&amp;quot;Error: {e}&amp;quot;)&lt;br /&gt;
    driver.save_screenshot(&amp;quot;error_screenshot.png&amp;quot;)&lt;br /&gt;
    print(&amp;quot;Captura de pantalla guardada como 'error_screenshot.png'&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
finally:&lt;br /&gt;
    # Cerrar el navegador&lt;br /&gt;
    driver.quit()&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarala y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest -s tests/test_selenium.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_selenium.py&amp;lt;/code&amp;gt;? Pues ya tenemos todo listo para realizar las pruebas sobre nuestra aplicación.&lt;br /&gt;
&lt;br /&gt;
===== Archivo &amp;lt;code&amp;gt;tests/test_interfaz.py&amp;lt;/code&amp;gt;: =====&lt;br /&gt;
&lt;br /&gt;
Ahora sí, vamos a crear las pruebas de la vista para nuestra aplicación.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from selenium import webdriver&lt;br /&gt;
from selenium.webdriver.common.by import By&lt;br /&gt;
from selenium.webdriver.common.keys import Keys&lt;br /&gt;
import pytest&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def driver():&lt;br /&gt;
    print(&amp;quot;Iniciando el navegador Chromium...&amp;quot;)&lt;br /&gt;
    driver = webdriver.Chrome()&lt;br /&gt;
    yield driver&lt;br /&gt;
    print(&amp;quot;Cerrando el navegador Chromium...&amp;quot;)&lt;br /&gt;
    driver.quit()&lt;br /&gt;
&lt;br /&gt;
def test_add_task(driver):&lt;br /&gt;
    print(&amp;quot;Abriendo la aplicación web en localhost:5000...&amp;quot;)&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    print(&amp;quot;Verificando que el título de la página es correcto...&amp;quot;)&lt;br /&gt;
    assert &amp;quot;Gestor de Tareas&amp;quot; in driver.title&lt;br /&gt;
&lt;br /&gt;
    print(&amp;quot;Buscando el campo de entrada de nueva tarea...&amp;quot;)&lt;br /&gt;
    input_field = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    print(&amp;quot;Escribiendo 'Tarea de Selenium' en el campo de entrada...&amp;quot;)&lt;br /&gt;
    input_field.send_keys(&amp;quot;Tarea de Selenium&amp;quot;)&lt;br /&gt;
    input_field.send_keys(Keys.RETURN)&lt;br /&gt;
&lt;br /&gt;
    print(&amp;quot;Verificando que 'Tarea de Selenium' aparece en la lista de tareas...&amp;quot;)&lt;br /&gt;
    assert &amp;quot;Tarea de Selenium&amp;quot; in driver.page_source&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de las pruebas de interfaz: =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest -s tests/test_interfaz.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría mucho trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
====== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ======&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
====== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ======&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====== Ejecutar la prueba grabada ======&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
====== Exportar el test a código &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt; ======&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - pytest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
    &lt;br /&gt;
Ejecutar el test exportado:&lt;br /&gt;
&lt;br /&gt;
Y ya puedes ejecutar el test exportado utilizando pytest:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest tests/test_selenium_ide.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto ejecutará el test generado por &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en tu navegador usando &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de Carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
===== Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;: =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
# Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ python app.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura el número de usuarios -por ejemplo, 10-, la tasa de generación (cada cuánto tiempo se lanza un nuevo usuario)-por ejemplo, 1- y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;). Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
# En la terminal verás mensajes como estos hasta que se haya lanzado el número de clientes indicado:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
[2024-10-07 17:35:02,798] hostname/INFO/locust.runners: All users spawned: {&amp;quot;WebsiteTestUser&amp;quot;: 10} (10 total users)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y además en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
=== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ===&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
==== Un ejemplo sencillo para ayudarte a arrancar ====&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar pruebas unitarias para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fijate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo, ¿podrías ir diseñando las pruebas unitarias necesarias para comprobar todas las operaciones CRUD del módulo notepad?&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento&amp;diff=9814</id>
		<title>Tutorial Campo de entrenamiento</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento&amp;diff=9814"/>
				<updated>2024-10-15T07:52:05Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: /* Lanza la aplicación */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Automatización de Pruebas de Software en una Aplicación Flask ==&lt;br /&gt;
&lt;br /&gt;
=== Parte 1: creamos pruebas para una aplicación sencilla ===&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación.&lt;br /&gt;
# '''Pruebas de cobertura''' para comprobar si nuestras pruebas tienen una buena cobertura de código.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
==== Requisitos previos ====&lt;br /&gt;
&lt;br /&gt;
Antes de comenzar, asegúrate de tener instalados los siguientes paquetes y herramientas:&lt;br /&gt;
&lt;br /&gt;
* Python 3&lt;br /&gt;
* Flask&lt;br /&gt;
* &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para pruebas unitarias.&lt;br /&gt;
* &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; para la cobertura de código.&lt;br /&gt;
* &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; para pruebas de interfaz.&lt;br /&gt;
* &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; para pruebas de carga.&lt;br /&gt;
* El navegador '''chromium''' y '''chromium-driver''' (para Selenium).&lt;br /&gt;
&lt;br /&gt;
Para instalar las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pip install flask pytest pytest-cov selenium locust&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Estructura del proyecto =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app.py                # Archivo principal de la aplicación Flask&lt;br /&gt;
├── templates/            # Directorio que contiene la plantilla HTML&lt;br /&gt;
│   └── tasks.html        # Plantilla para mostrar y agregar tareas&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── test_app.py       # Pruebas unitarias usando pytest&lt;br /&gt;
│   └── test_interfaz.py  # Pruebas de interfaz con Selenium&lt;br /&gt;
└── locustfile.py         # Archivo para pruebas de carga con Locust&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
==== Desarrollo de la Aplicación Flask ====&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
===== Código &amp;lt;code&amp;gt;app.py&amp;lt;/code&amp;gt;: =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Lista inicial de tareas (guardada en memoria)&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
# Ruta para obtener la lista de tareas (versión HTML)&lt;br /&gt;
@app.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=tasks)&lt;br /&gt;
&lt;br /&gt;
# Ruta para obtener la lista de tareas en JSON (API)&lt;br /&gt;
@app.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': tasks})&lt;br /&gt;
&lt;br /&gt;
# Ruta para crear una nueva tarea desde un formulario HTML&lt;br /&gt;
@app.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    if not title:&lt;br /&gt;
        return &amp;quot;El título es necesario&amp;quot;, 400&lt;br /&gt;
    task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(task)&lt;br /&gt;
    return redirect(url_for('task_list'))&lt;br /&gt;
&lt;br /&gt;
# Ruta para crear una nueva tarea (API JSON)&lt;br /&gt;
@app.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task():&lt;br /&gt;
    if not request.json or 'title' not in request.json:&lt;br /&gt;
        return jsonify({'error': 'El título es necesario'}), 400&lt;br /&gt;
    task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': request.json['title'],&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(task)&lt;br /&gt;
    return jsonify(task), 201&lt;br /&gt;
&lt;br /&gt;
if __name__ == '__main__':&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver en el código, por tanto, se ofrecen dos formas para interactuar con las tareas:&lt;br /&gt;
&lt;br /&gt;
# Una página HTML que muestra la lista de tareas y un formulario para añadir nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
# Una API REST que devuelve la lista de tareas en formato JSON y permite agregar nuevas tareas mediante solicitudes POST.&lt;br /&gt;
&lt;br /&gt;
===== Plantilla HTML =====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
====== Archivo &amp;lt;code&amp;gt;templates/tasks.html&amp;lt;/code&amp;gt;: ======&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;!-- Formulario para añadir una nueva tarea --&amp;gt;&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;!-- Lista de tareas --&amp;gt;&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %} (completada) {% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Lanza la aplicación =====&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ python app.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella creando y visualizando las tareas usando primero el formulario web y luego también mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
$ curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Pruebas Unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en probar los endpoints de la API de manera independiente.&lt;br /&gt;
&lt;br /&gt;
===== Archivo &amp;lt;code&amp;gt;tests/test_app.py&amp;lt;/code&amp;gt;: =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import app&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def client():&lt;br /&gt;
    with app.test_client() as client:&lt;br /&gt;
        yield client&lt;br /&gt;
&lt;br /&gt;
def test_get_tasks(client):&lt;br /&gt;
    response = client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert 'Comprar pan' in response.get_data(as_text=True)&lt;br /&gt;
&lt;br /&gt;
def test_create_task(client):&lt;br /&gt;
    response = client.post('/tasks', json={'title': 'Aprender testing'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
    assert 'Aprender testing' in response.get_data(as_text=True)&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title(client):&lt;br /&gt;
    response = client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
def test_task_list_updates(client):&lt;br /&gt;
    response = client.post('/tasks', json={'title': 'Otra nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    response = client.get('/tasks')&lt;br /&gt;
    assert 'Otra nueva tarea' in response.get_data(as_text=True)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de las pruebas: =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
NOTA: Si recibes un error al lanzar pytest porque no se encuentra el módulo app, puedes intentarlo así (lo que añade el directorio actual (.) al PYTHONPATH):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ PYTHONPATH=. pytest&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
Y, ¿qué es la cobertura de código?&lt;br /&gt;
&lt;br /&gt;
La cobertura de código mide el porcentaje de código ejecutado cuando se lanzan las pruebas. Esto nos ayuda a identificar las áreas de la aplicación que no están siendo probadas adecuadamente.&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código en el archivo app.py fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
------- coverage: xxx% -------&lt;br /&gt;
&lt;br /&gt;
 Name      | Stmts | Miss | Cover                      &lt;br /&gt;
-----------| ------|------|------&lt;br /&gt;
 app.py    | 26    | 8    | 69%&lt;br /&gt;
 TOTAL     | 26    | 8    | 69%&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias en el archivo app.py; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre el archivo app.py.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación.&lt;br /&gt;
&lt;br /&gt;
===== Un pasito previo para ver &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; en acción =====&lt;br /&gt;
&lt;br /&gt;
Antes de realizar las pruebas sobre nuestra aplicación vamos a hacer un pasito previo para comprobar que tenemos chromium y chromium-driver correctamente instalados y entender el tipo de cosas que podemos hacer con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====== Archivo &amp;lt;code&amp;gt;tests/test_selenium.py&amp;lt;/code&amp;gt; : ======&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from selenium import webdriver&lt;br /&gt;
from selenium.webdriver.common.by import By&lt;br /&gt;
from selenium.webdriver.common.keys import Keys&lt;br /&gt;
from selenium.webdriver.support.ui import WebDriverWait&lt;br /&gt;
from selenium.webdriver.support import expected_conditions as EC&lt;br /&gt;
&lt;br /&gt;
# Configurar Selenium para usar Chromium&lt;br /&gt;
options = webdriver.ChromeOptions()&lt;br /&gt;
# Quita '--headless' para ejecutar el navegador de manera visible&lt;br /&gt;
options.add_argument('--no-sandbox')&lt;br /&gt;
options.add_argument('--disable-dev-shm-usage')&lt;br /&gt;
&lt;br /&gt;
# Iniciar el driver de Chromium&lt;br /&gt;
driver = webdriver.Chrome(options=options)&lt;br /&gt;
&lt;br /&gt;
try:&lt;br /&gt;
    # 1. Abrir Google&lt;br /&gt;
    driver.get(&amp;quot;https://www.google.com&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 2. Esperar hasta que aparezca la ventana de cookies y hacer clic en &amp;quot;Rechazar todo&amp;quot;&lt;br /&gt;
    reject_cookies_button = WebDriverWait(driver, 10).until(&lt;br /&gt;
        EC.element_to_be_clickable((By.XPATH, &amp;quot;//button[contains(., 'Rechazar todo')]&amp;quot;))&lt;br /&gt;
    )&lt;br /&gt;
    reject_cookies_button.click()&lt;br /&gt;
&lt;br /&gt;
    # 3. Esperar hasta que el campo de búsqueda sea visible&lt;br /&gt;
    search_box = WebDriverWait(driver, 10).until(&lt;br /&gt;
        EC.visibility_of_element_located((By.NAME, &amp;quot;q&amp;quot;))&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # 4. Escribir &amp;quot;Selenium&amp;quot; en la barra de búsqueda y enviar el formulario&lt;br /&gt;
    search_box.send_keys(&amp;quot;Selenium&amp;quot;)&lt;br /&gt;
    search_box.send_keys(Keys.RETURN)&lt;br /&gt;
&lt;br /&gt;
    # 5. Esperar a que el título cambie y contenga &amp;quot;Selenium&amp;quot;&lt;br /&gt;
    WebDriverWait(driver, 10).until(EC.title_contains(&amp;quot;Selenium&amp;quot;))&lt;br /&gt;
    print(&amp;quot;¡Selenium está funcionando correctamente con Chromium!&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
except Exception as e:&lt;br /&gt;
    print(f&amp;quot;Error: {e}&amp;quot;)&lt;br /&gt;
    driver.save_screenshot(&amp;quot;error_screenshot.png&amp;quot;)&lt;br /&gt;
    print(&amp;quot;Captura de pantalla guardada como 'error_screenshot.png'&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
finally:&lt;br /&gt;
    # Cerrar el navegador&lt;br /&gt;
    driver.quit()&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarala y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest -s tests/test_selenium.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_selenium.py&amp;lt;/code&amp;gt;? Pues ya tenemos todo listo para realizar las pruebas sobre nuestra aplicación.&lt;br /&gt;
&lt;br /&gt;
===== Archivo &amp;lt;code&amp;gt;tests/test_interfaz.py&amp;lt;/code&amp;gt;: =====&lt;br /&gt;
&lt;br /&gt;
Ahora sí, vamos a crear las pruebas de la vista para nuestra aplicación.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from selenium import webdriver&lt;br /&gt;
from selenium.webdriver.common.by import By&lt;br /&gt;
from selenium.webdriver.common.keys import Keys&lt;br /&gt;
import pytest&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def driver():&lt;br /&gt;
    print(&amp;quot;Iniciando el navegador Chromium...&amp;quot;)&lt;br /&gt;
    driver = webdriver.Chrome()&lt;br /&gt;
    yield driver&lt;br /&gt;
    print(&amp;quot;Cerrando el navegador Chromium...&amp;quot;)&lt;br /&gt;
    driver.quit()&lt;br /&gt;
&lt;br /&gt;
def test_add_task(driver):&lt;br /&gt;
    print(&amp;quot;Abriendo la aplicación web en localhost:5000...&amp;quot;)&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    print(&amp;quot;Verificando que el título de la página es correcto...&amp;quot;)&lt;br /&gt;
    assert &amp;quot;Gestor de Tareas&amp;quot; in driver.title&lt;br /&gt;
&lt;br /&gt;
    print(&amp;quot;Buscando el campo de entrada de nueva tarea...&amp;quot;)&lt;br /&gt;
    input_field = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    print(&amp;quot;Escribiendo 'Tarea de Selenium' en el campo de entrada...&amp;quot;)&lt;br /&gt;
    input_field.send_keys(&amp;quot;Tarea de Selenium&amp;quot;)&lt;br /&gt;
    input_field.send_keys(Keys.RETURN)&lt;br /&gt;
&lt;br /&gt;
    print(&amp;quot;Verificando que 'Tarea de Selenium' aparece en la lista de tareas...&amp;quot;)&lt;br /&gt;
    assert &amp;quot;Tarea de Selenium&amp;quot; in driver.page_source&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de las pruebas de interfaz: =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest -s tests/test_interfaz.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría mucho trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
====== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ======&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
====== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ======&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====== Ejecutar la prueba grabada ======&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
====== Exportar el test a código &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt; ======&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - Selenium WebDriver&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
    &lt;br /&gt;
Ejecutar el test exportado:&lt;br /&gt;
&lt;br /&gt;
Y ya puedes ejecutar el test exportado utilizando pytest:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest tests/test_selenium_ide.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto ejecutará el test generado por &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en tu navegador usando &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de Carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
===== Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;: =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
# Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ python app.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura el número de usuarios -por ejemplo, 10-, la tasa de generación (cada cuánto tiempo se lanza un nuevo usuario)-por ejemplo, 1- y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;). Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
# En la terminal verás mensajes como estos hasta que se haya lanzado el número de clientes indicado:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
[2024-10-07 17:35:02,798] hostname/INFO/locust.runners: All users spawned: {&amp;quot;WebsiteTestUser&amp;quot;: 10} (10 total users)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y además en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
=== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ===&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
==== Un ejemplo sencillo para ayudarte a arrancar ====&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar pruebas unitarias para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fijate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo, ¿podrías ir diseñando las pruebas unitarias necesarias para comprobar todas las operaciones CRUD del módulo notepad?&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento&amp;diff=9813</id>
		<title>Tutorial Campo de entrenamiento</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento&amp;diff=9813"/>
				<updated>2024-10-15T07:51:32Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: /* Lanza la aplicación */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Automatización de Pruebas de Software en una Aplicación Flask ==&lt;br /&gt;
&lt;br /&gt;
=== Parte 1: creamos pruebas para una aplicación sencilla ===&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación.&lt;br /&gt;
# '''Pruebas de cobertura''' para comprobar si nuestras pruebas tienen una buena cobertura de código.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
==== Requisitos previos ====&lt;br /&gt;
&lt;br /&gt;
Antes de comenzar, asegúrate de tener instalados los siguientes paquetes y herramientas:&lt;br /&gt;
&lt;br /&gt;
* Python 3&lt;br /&gt;
* Flask&lt;br /&gt;
* &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para pruebas unitarias.&lt;br /&gt;
* &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; para la cobertura de código.&lt;br /&gt;
* &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; para pruebas de interfaz.&lt;br /&gt;
* &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; para pruebas de carga.&lt;br /&gt;
* El navegador '''chromium''' y '''chromium-driver''' (para Selenium).&lt;br /&gt;
&lt;br /&gt;
Para instalar las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pip install flask pytest pytest-cov selenium locust&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Estructura del proyecto =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app.py                # Archivo principal de la aplicación Flask&lt;br /&gt;
├── templates/            # Directorio que contiene la plantilla HTML&lt;br /&gt;
│   └── tasks.html        # Plantilla para mostrar y agregar tareas&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── test_app.py       # Pruebas unitarias usando pytest&lt;br /&gt;
│   └── test_interfaz.py  # Pruebas de interfaz con Selenium&lt;br /&gt;
└── locustfile.py         # Archivo para pruebas de carga con Locust&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
==== Desarrollo de la Aplicación Flask ====&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
===== Código &amp;lt;code&amp;gt;app.py&amp;lt;/code&amp;gt;: =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Lista inicial de tareas (guardada en memoria)&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
# Ruta para obtener la lista de tareas (versión HTML)&lt;br /&gt;
@app.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=tasks)&lt;br /&gt;
&lt;br /&gt;
# Ruta para obtener la lista de tareas en JSON (API)&lt;br /&gt;
@app.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': tasks})&lt;br /&gt;
&lt;br /&gt;
# Ruta para crear una nueva tarea desde un formulario HTML&lt;br /&gt;
@app.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    if not title:&lt;br /&gt;
        return &amp;quot;El título es necesario&amp;quot;, 400&lt;br /&gt;
    task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(task)&lt;br /&gt;
    return redirect(url_for('task_list'))&lt;br /&gt;
&lt;br /&gt;
# Ruta para crear una nueva tarea (API JSON)&lt;br /&gt;
@app.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task():&lt;br /&gt;
    if not request.json or 'title' not in request.json:&lt;br /&gt;
        return jsonify({'error': 'El título es necesario'}), 400&lt;br /&gt;
    task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': request.json['title'],&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(task)&lt;br /&gt;
    return jsonify(task), 201&lt;br /&gt;
&lt;br /&gt;
if __name__ == '__main__':&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver en el código, por tanto, se ofrecen dos formas para interactuar con las tareas:&lt;br /&gt;
&lt;br /&gt;
# Una página HTML que muestra la lista de tareas y un formulario para añadir nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
# Una API REST que devuelve la lista de tareas en formato JSON y permite agregar nuevas tareas mediante solicitudes POST.&lt;br /&gt;
&lt;br /&gt;
===== Plantilla HTML =====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
====== Archivo &amp;lt;code&amp;gt;templates/tasks.html&amp;lt;/code&amp;gt;: ======&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;!-- Formulario para añadir una nueva tarea --&amp;gt;&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;!-- Lista de tareas --&amp;gt;&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %} (completada) {% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Lanza la aplicación =====&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ python3.12 app.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella creando y visualizando las tareas usando primero el formulario web y luego también mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
$ curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Pruebas Unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en probar los endpoints de la API de manera independiente.&lt;br /&gt;
&lt;br /&gt;
===== Archivo &amp;lt;code&amp;gt;tests/test_app.py&amp;lt;/code&amp;gt;: =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import app&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def client():&lt;br /&gt;
    with app.test_client() as client:&lt;br /&gt;
        yield client&lt;br /&gt;
&lt;br /&gt;
def test_get_tasks(client):&lt;br /&gt;
    response = client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert 'Comprar pan' in response.get_data(as_text=True)&lt;br /&gt;
&lt;br /&gt;
def test_create_task(client):&lt;br /&gt;
    response = client.post('/tasks', json={'title': 'Aprender testing'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
    assert 'Aprender testing' in response.get_data(as_text=True)&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title(client):&lt;br /&gt;
    response = client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
def test_task_list_updates(client):&lt;br /&gt;
    response = client.post('/tasks', json={'title': 'Otra nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    response = client.get('/tasks')&lt;br /&gt;
    assert 'Otra nueva tarea' in response.get_data(as_text=True)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de las pruebas: =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
NOTA: Si recibes un error al lanzar pytest porque no se encuentra el módulo app, puedes intentarlo así (lo que añade el directorio actual (.) al PYTHONPATH):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ PYTHONPATH=. pytest&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
Y, ¿qué es la cobertura de código?&lt;br /&gt;
&lt;br /&gt;
La cobertura de código mide el porcentaje de código ejecutado cuando se lanzan las pruebas. Esto nos ayuda a identificar las áreas de la aplicación que no están siendo probadas adecuadamente.&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código en el archivo app.py fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
------- coverage: xxx% -------&lt;br /&gt;
&lt;br /&gt;
 Name      | Stmts | Miss | Cover                      &lt;br /&gt;
-----------| ------|------|------&lt;br /&gt;
 app.py    | 26    | 8    | 69%&lt;br /&gt;
 TOTAL     | 26    | 8    | 69%&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias en el archivo app.py; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre el archivo app.py.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación.&lt;br /&gt;
&lt;br /&gt;
===== Un pasito previo para ver &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; en acción =====&lt;br /&gt;
&lt;br /&gt;
Antes de realizar las pruebas sobre nuestra aplicación vamos a hacer un pasito previo para comprobar que tenemos chromium y chromium-driver correctamente instalados y entender el tipo de cosas que podemos hacer con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====== Archivo &amp;lt;code&amp;gt;tests/test_selenium.py&amp;lt;/code&amp;gt; : ======&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from selenium import webdriver&lt;br /&gt;
from selenium.webdriver.common.by import By&lt;br /&gt;
from selenium.webdriver.common.keys import Keys&lt;br /&gt;
from selenium.webdriver.support.ui import WebDriverWait&lt;br /&gt;
from selenium.webdriver.support import expected_conditions as EC&lt;br /&gt;
&lt;br /&gt;
# Configurar Selenium para usar Chromium&lt;br /&gt;
options = webdriver.ChromeOptions()&lt;br /&gt;
# Quita '--headless' para ejecutar el navegador de manera visible&lt;br /&gt;
options.add_argument('--no-sandbox')&lt;br /&gt;
options.add_argument('--disable-dev-shm-usage')&lt;br /&gt;
&lt;br /&gt;
# Iniciar el driver de Chromium&lt;br /&gt;
driver = webdriver.Chrome(options=options)&lt;br /&gt;
&lt;br /&gt;
try:&lt;br /&gt;
    # 1. Abrir Google&lt;br /&gt;
    driver.get(&amp;quot;https://www.google.com&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 2. Esperar hasta que aparezca la ventana de cookies y hacer clic en &amp;quot;Rechazar todo&amp;quot;&lt;br /&gt;
    reject_cookies_button = WebDriverWait(driver, 10).until(&lt;br /&gt;
        EC.element_to_be_clickable((By.XPATH, &amp;quot;//button[contains(., 'Rechazar todo')]&amp;quot;))&lt;br /&gt;
    )&lt;br /&gt;
    reject_cookies_button.click()&lt;br /&gt;
&lt;br /&gt;
    # 3. Esperar hasta que el campo de búsqueda sea visible&lt;br /&gt;
    search_box = WebDriverWait(driver, 10).until(&lt;br /&gt;
        EC.visibility_of_element_located((By.NAME, &amp;quot;q&amp;quot;))&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # 4. Escribir &amp;quot;Selenium&amp;quot; en la barra de búsqueda y enviar el formulario&lt;br /&gt;
    search_box.send_keys(&amp;quot;Selenium&amp;quot;)&lt;br /&gt;
    search_box.send_keys(Keys.RETURN)&lt;br /&gt;
&lt;br /&gt;
    # 5. Esperar a que el título cambie y contenga &amp;quot;Selenium&amp;quot;&lt;br /&gt;
    WebDriverWait(driver, 10).until(EC.title_contains(&amp;quot;Selenium&amp;quot;))&lt;br /&gt;
    print(&amp;quot;¡Selenium está funcionando correctamente con Chromium!&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
except Exception as e:&lt;br /&gt;
    print(f&amp;quot;Error: {e}&amp;quot;)&lt;br /&gt;
    driver.save_screenshot(&amp;quot;error_screenshot.png&amp;quot;)&lt;br /&gt;
    print(&amp;quot;Captura de pantalla guardada como 'error_screenshot.png'&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
finally:&lt;br /&gt;
    # Cerrar el navegador&lt;br /&gt;
    driver.quit()&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarala y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest -s tests/test_selenium.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_selenium.py&amp;lt;/code&amp;gt;? Pues ya tenemos todo listo para realizar las pruebas sobre nuestra aplicación.&lt;br /&gt;
&lt;br /&gt;
===== Archivo &amp;lt;code&amp;gt;tests/test_interfaz.py&amp;lt;/code&amp;gt;: =====&lt;br /&gt;
&lt;br /&gt;
Ahora sí, vamos a crear las pruebas de la vista para nuestra aplicación.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from selenium import webdriver&lt;br /&gt;
from selenium.webdriver.common.by import By&lt;br /&gt;
from selenium.webdriver.common.keys import Keys&lt;br /&gt;
import pytest&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def driver():&lt;br /&gt;
    print(&amp;quot;Iniciando el navegador Chromium...&amp;quot;)&lt;br /&gt;
    driver = webdriver.Chrome()&lt;br /&gt;
    yield driver&lt;br /&gt;
    print(&amp;quot;Cerrando el navegador Chromium...&amp;quot;)&lt;br /&gt;
    driver.quit()&lt;br /&gt;
&lt;br /&gt;
def test_add_task(driver):&lt;br /&gt;
    print(&amp;quot;Abriendo la aplicación web en localhost:5000...&amp;quot;)&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    print(&amp;quot;Verificando que el título de la página es correcto...&amp;quot;)&lt;br /&gt;
    assert &amp;quot;Gestor de Tareas&amp;quot; in driver.title&lt;br /&gt;
&lt;br /&gt;
    print(&amp;quot;Buscando el campo de entrada de nueva tarea...&amp;quot;)&lt;br /&gt;
    input_field = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    print(&amp;quot;Escribiendo 'Tarea de Selenium' en el campo de entrada...&amp;quot;)&lt;br /&gt;
    input_field.send_keys(&amp;quot;Tarea de Selenium&amp;quot;)&lt;br /&gt;
    input_field.send_keys(Keys.RETURN)&lt;br /&gt;
&lt;br /&gt;
    print(&amp;quot;Verificando que 'Tarea de Selenium' aparece en la lista de tareas...&amp;quot;)&lt;br /&gt;
    assert &amp;quot;Tarea de Selenium&amp;quot; in driver.page_source&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de las pruebas de interfaz: =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest -s tests/test_interfaz.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
===== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría mucho trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
====== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ======&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
====== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ======&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====== Ejecutar la prueba grabada ======&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
====== Exportar el test a código &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt; ======&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - Selenium WebDriver&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
    &lt;br /&gt;
Ejecutar el test exportado:&lt;br /&gt;
&lt;br /&gt;
Y ya puedes ejecutar el test exportado utilizando pytest:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest tests/test_selenium_ide.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto ejecutará el test generado por &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en tu navegador usando &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de Carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
===== Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;: =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
# Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ python app.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura el número de usuarios -por ejemplo, 10-, la tasa de generación (cada cuánto tiempo se lanza un nuevo usuario)-por ejemplo, 1- y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;). Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
# En la terminal verás mensajes como estos hasta que se haya lanzado el número de clientes indicado:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
[2024-10-07 17:35:02,798] hostname/INFO/locust.runners: All users spawned: {&amp;quot;WebsiteTestUser&amp;quot;: 10} (10 total users)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y además en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
=== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ===&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
==== Un ejemplo sencillo para ayudarte a arrancar ====&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar pruebas unitarias para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fijate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo, ¿podrías ir diseñando las pruebas unitarias necesarias para comprobar todas las operaciones CRUD del módulo notepad?&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Archivo:EGC_2024-25_P1.pdf&amp;diff=9788</id>
		<title>Archivo:EGC 2024-25 P1.pdf</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Archivo:EGC_2024-25_P1.pdf&amp;diff=9788"/>
				<updated>2024-09-26T11:12:07Z</updated>
		
		<summary type="html">&lt;p&gt;Drorganvidez: Drorganvidez subió una nueva versión de Archivo:EGC 2024-25 P1.pdf&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Práctica 1&lt;/div&gt;</summary>
		<author><name>Drorganvidez</name></author>	</entry>

	</feed>