Diferencia entre revisiones de «Tutorial-docker»

De Wiki de EGC
Saltar a: navegación, buscar
Línea 35: Línea 35:
  
 
=== WORKDIR ===
 
=== WORKDIR ===
<syntaxhighlight lang="dockerfile"> WORKDIR /app </syntaxhighlight>
+
<syntaxhighlight lang="dockerfile">
 +
WORKDIR /app
 +
</syntaxhighlight>
  
 
Esta línea define el directorio de trabajo dentro del contenedor.
 
Esta línea define el directorio de trabajo dentro del contenedor.
Línea 50: Línea 52:
 
   core
 
   core
 
   requirements.txt  
 
   requirements.txt  
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
Línea 59: Línea 60:
 
Cuando más adelante uses:
 
Cuando más adelante uses:
  
<syntaxhighlight lang="dockerfile"> COPY . . </syntaxhighlight>
+
<syntaxhighlight lang="dockerfile">
 +
COPY . .
 +
</syntaxhighlight>
  
 
Docker copiará todos los archivos de tu proyecto dentro de la carpeta /app del contenedor.
 
Docker copiará todos los archivos de tu proyecto dentro de la carpeta /app del contenedor.
Línea 107: Línea 110:
 
Ejemplo:
 
Ejemplo:
  
<syntaxhighlight lang="dockerfile"> RUN pip install -r requirements.txt </syntaxhighlight>
+
<syntaxhighlight lang="dockerfile">
 +
RUN pip install -r requirements.txt
 +
</syntaxhighlight>
  
 
Este comando instala las dependencias una sola vez, cuando se construye la imagen.
 
Este comando instala las dependencias una sola vez, cuando se construye la imagen.
Línea 120: Línea 125:
 
Ejemplo:
 
Ejemplo:
  
<syntaxhighlight lang="dockerfile"> CMD ["flask", "run", "--host=0.0.0.0", "--port=5000"] </syntaxhighlight>
+
<syntaxhighlight lang="dockerfile">
 +
CMD ["flask", "run", "--host=0.0.0.0", "--port=5000"]
 +
</syntaxhighlight>
  
 
Este comando no se ejecuta durante la construcción, sino cada vez que arrancas el contenedor.
 
Este comando no se ejecuta durante la construcción, sino cada vez que arrancas el contenedor.
Línea 143: Línea 150:
 
CMD ["flask", "run", "--host=0.0.0.0", "--port=5000", "--reload", "--debug"]
 
CMD ["flask", "run", "--host=0.0.0.0", "--port=5000", "--reload", "--debug"]
 
</syntaxhighlight>
 
</syntaxhighlight>
 +
 +
Esto tiene que ir en un Dockerfile en docker/images
  
 
== Construir la imagen ==
 
== Construir la imagen ==
 
Desde la raíz del proyecto (no dentro de docker/images):
 
Desde la raíz del proyecto (no dentro de docker/images):
  
<syntaxhighlight lang="bash"> docker build -t uvlhub:dev -f docker/images/Dockerfile.dev . </syntaxhighlight>
+
<syntaxhighlight lang="bash">
 +
docker build -t uvlhub:dev -f docker/images/Dockerfile.dev .
 +
</syntaxhighlight>
  
 
== Ejecutar el contenedor ==
 
== Ejecutar el contenedor ==
 +
 
<syntaxhighlight lang="bash">
 
<syntaxhighlight lang="bash">
 
docker run -p 5000:5000 uvlhub:dev
 
docker run -p 5000:5000 uvlhub:dev
Línea 397: Línea 409:
 
== Qué significa ":" en los volúmenes ===
 
== Qué significa ":" en los volúmenes ===
  
Cuando usas la opción -v o --volume en Docker, el signo dos puntos (:) separa dos rutas:
+
Cuando usas la opción '''-v''' o '''--volume''' en Docker, el signo dos puntos (''':''') separa dos rutas:
  
 
'''-v origen:destino'''
 
'''-v origen:destino'''
Línea 404: Línea 416:
  
 
destino → es la ruta dentro del contenedor.
 
destino → es la ruta dentro del contenedor.
 +
 +
= Ejercicio 4: Orquestación de servicios =
 +
 +
1: hacer un docker compose up básico
 +
2: arreglar lo del wait for db
 +
3: avisar del problema del código del contenedor y del uso de un volumen para eso
 +
 +
= Ejercicio 5: Entrypoints =
 +
 +
Explicar utilidad de los entrypoints (puedo configurar el arranque sin tener que modificar la imagen)
 +
 +
= Ejercicio 6: Distintas configuraciones de despliegue =
 +
 +
1: Explicar las distintas imágenes, entrypoints y docker-compose.ymls
 +
2: Explicar el nginx
 +
3: Explicar que cuanto más se parezca el despliegue en dev y en prod, menos propenso a errores seremos
 +
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

Revisión del 23:16 11 oct 2025

Ejercicio 1: Empezamos desde cero

El objetivo es eliminar toda la configuración previa y crear desde cero la estructura de carpetas que contendrá nuestros futuros archivos Docker.

Eliminar la configuración existente

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.

mv docker docker.bk

También debes asegurarte que trabajas con las variables de entorno adecuadas:

cp .env.docker.example .env

⚠️ Este paso es importante: partimos completamente desde cero para entender cada parte del sistema.

Crear un Dockerfile mínimo

Ahora crea nuevamente la carpeta base donde almacenaremos los archivos relacionados con Docker:

 mkdir -p docker/images

Vamos a explicar los distintos apartados:

FROM

FROM python:3.12-slim

Indica la imagen base sobre la que se construirá la tuya. En este caso, parte de una imagen ligera que ya tiene Python 3.12 instalado.

WORKDIR

WORKDIR /app

Esta línea define el directorio de trabajo dentro del contenedor. A partir de aquí, todos los comandos que aparezcan después (COPY, RUN, CMD, etc.) se ejecutarán dentro de /app.

Ojo: /app no es la carpeta app/ de tu proyecto local. Son cosas distintas.

En tu máquina puede existir algo como:

uvlhub
   app
   core
   requirements.txt

Pero dentro del contenedor la estructura será distinta:

 / app/ app/ ← aquí dentro se habrá copiado tu carpeta local "app"

Cuando más adelante uses:

COPY . .

Docker copiará todos los archivos de tu proyecto dentro de la carpeta /app del contenedor. Por eso el código quedará en /app/app/.

COPY

COPY . .

Copia todos los archivos del proyecto (del host) dentro del contenedor, en /app. El primer punto es el origen; el segundo, el destino.

RUN

RUN pip install --no-cache-dir -r requirements.txt

Ejecuta comandos durante la construcción de la imagen. Aquí instalamos las dependencias necesarias desde requirements.txt. El resultado de este paso quedará guardado en la imagen.

EXPOSE

EXPOSE 5000

Documenta el puerto que la aplicación usa dentro del contenedor. Esto no lo abre al exterior; simplemente indica que la app escucha en el 5000.

CMD

CMD ["flask", "run", "--host=0.0.0.0", "--port=5000", "--reload", "--debug"]

Define el comando que se ejecutará cuando el contenedor se inicie. En este caso, ejecutará la aplicación Flask.

RUN vs CMD

Estas dos instrucciones parecen similares, pero hacen cosas muy distintas.

RUN

Se ejecuta durante la construcción de la imagen (en el momento del docker build). Cada RUN crea una nueva capa dentro de la imagen.

Ejemplo:

RUN pip install -r requirements.txt

Este comando instala las dependencias una sola vez, cuando se construye la imagen. El resultado (las librerías instaladas) queda grabado dentro de la imagen final.

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.

CMD

Se ejecuta cuando se inicia el contenedor (en el docker run). Define el proceso principal que se ejecutará dentro del contenedor mientras esté activo.

Ejemplo:

CMD ["flask", "run", "--host=0.0.0.0", "--port=5000"]

Este comando no se ejecuta durante la construcción, sino cada vez que arrancas el contenedor. Si detienes el contenedor, este proceso también se detiene.

Diferencia resumida

RUN → se ejecuta al construir la imagen (fase de docker build).

CMD → se ejecuta al iniciar el contenedor (fase de docker run).

Solo puede haber un CMD por Dockerfile. Si hay más de uno, solo se usará el último.

Dockerfile completo

FROM python:3.12-slim
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir -r requirements.txt
EXPOSE 5000
CMD ["flask", "run", "--host=0.0.0.0", "--port=5000", "--reload", "--debug"]

Esto tiene que ir en un Dockerfile en docker/images

Construir la imagen

Desde la raíz del proyecto (no dentro de docker/images):

docker build -t uvlhub:dev -f docker/images/Dockerfile.dev .

Ejecutar el contenedor

docker run -p 5000:5000 uvlhub:dev

Abre el navegador en http://localhost:5000. ¿Qué es lo que observas?

Ejercicio 2: Conectando servicios

¡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...

Crear contenedor MariaDB

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.

El objetivo es entender la complejidad de hacerlo “a mano”, antes de automatizarlo con docker-compose.

Descargar la imagen oficial

Primero descarga la versión exacta que usaremos:

docker pull mariadb:12.0.2

Crear el contenedor de MariaDB

Lanza un contenedor con todas las variables necesarias para que Flask pueda conectarse correctamente:

docker run -d --name mariadb_container \
 -e FLASK_APP_NAME="UVLHUB.IO(dev)" \
 -e FLASK_ENV=development \
 -e DOMAIN=localhost \
 -e MARIADB_HOSTNAME=db \
 -e MARIADB_PORT=3306 \
 -e MARIADB_DATABASE=uvlhubdb \
 -e MARIADB_TEST_DATABASE=uvlhubdb_test \
 -e MARIADB_USER=uvlhubdb_user \
 -e MARIADB_PASSWORD=uvlhubdb_password \
 -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \
 -e WORKING_DIR=/app/ \
 -p 3306:3306 mariadb:12.0.2

Explicación:

-d: ejecuta el contenedor en segundo plano (modo detached).

--name: asigna un nombre identificable al contenedor.

-e: define variables de entorno dentro del contenedor.

-p 3306:3306: publica el puerto de MariaDB en tu máquina local.

mariadb:12.0.2: imagen exacta que vamos a usar.

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

Comprobar que está corriendo

docker ps

Deberías ver algo así:

CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES
abcd1234efgh mariadb:12.0.2 "docker-entrypoint.s…" Up 5 seconds 0.0.0.0:3306->3306/tcp mariadb_container

Conectarse a la base de datos

Abre una consola dentro del contenedor:

docker exec -it mariadb_container bash

Y dentro, accede al cliente de MariaDB:

mariadb -u root -p

Contraseña: uvlhubdb_root_password

Verificar la base de datos

Una vez dentro del cliente MySQL, ejecuta:

SHOW DATABASES;

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...

USE uvlhubdb;
SHOW TABLES;

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...

docker run -p 5000:5000 -d uvlhub:dev

Esta vez con el flag "-d" para que corra en segundo plano. Ahora, accedamos al contenedor y ejecutemos las migraciones. Pero... ¿cómo se llama mi contenedor?

docker ps

Fíjate que Docker asigna un nombre aleatorio a tu contenedor web porque no definiste ninguno. Una vez identificado el nombre:

docker exec -it <nombre> bash
flask db upgrade

What? ¿Qué pasa? ¿Otra vez error de conexión con la base? Si tenemos el contenedor funcionando, esto es un jaleo...

Redes internas

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.

Conectar servicios

Vamos a crear una red interna que compartan ambos contenedores

docker network create uvlhub_network
docker network ls

Vemos que ya aparece nuestra red uvlhub_network. Ahora, conectemos el contenedor de MariaDB a la red:

docker stop mariadb_container
docker rm mariadb_container
docker run -d \
  --name mariadb_container \
  --hostname db \
  --network uvlhub_network \
  -e FLASK_APP_NAME="UVLHUB.IO(dev)" \
  -e FLASK_ENV=development \
  -e DOMAIN=localhost \
  -e MARIADB_HOSTNAME=db \
  -e MARIADB_PORT=3306 \
  -e MARIADB_DATABASE=uvlhubdb \
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \
  -e MARIADB_USER=uvlhubdb_user \
  -e MARIADB_PASSWORD=uvlhubdb_password \
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \
  -e WORKING_DIR=/app/ \
  -p 3306:3306 \
  mariadb:12.0.2

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 "db" para que el contenedor web entienda qué es "db". Tenemos que hacer lo mismo para el contenedor web:

docker stop <nombre>
docker rm <nombre>

Y lo levantamos de nuevo, pero ya en la nueva red. De paso, le ponemos un nombre para identificarlo mejor.

docker run -p 5000:5000 --name web_app_container --network uvlhub_network -d uvlhub:dev

Y ahora ya podemos acceder al contenedor por su nombre y ejecutar las migraciones:

docker exec -it web_app_container bash
flask db upgrade

Abre el navegador en http://localhost:5000. ¿Mejor?

Ejercicio 3: Persistencia

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:

docker restart web_app_container
docker restart mariadb_container

Abre de nuevo 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.

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.

Volúmenes

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.

En este ejercicio creas un volumen de Docker para que los datos persistan entre ejecuciones.

Crear un volumen

Un volumen es una carpeta gestionada por Docker que vive fuera del ciclo de vida de los contenedores.

Ejecuta:

docker volume create mariadb_data

Comprueba que existe:

docker volume ls

La salida muestra algo como:

DRIVER VOLUME NAME local mariadb_data

Crear un contenedor de MariaDB con el volumen

Lanza el contenedor y conecta el volumen a la ruta donde MariaDB guarda los datos (/var/lib/mysql):

docker stop mariadb_container
docker rm mariadb_container
docker run -d \
  --name mariadb_container \
  --hostname db \
  --network uvlhub_network \
  -v mariadb_data:/var/lib/mysql \
  -e FLASK_APP_NAME="UVLHUB.IO(dev)" \
  -e FLASK_ENV=development \
  -e DOMAIN=localhost \
  -e MARIADB_HOSTNAME=db \
  -e MARIADB_PORT=3306 \
  -e MARIADB_DATABASE=uvlhubdb \
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \
  -e MARIADB_USER=uvlhubdb_user \
  -e MARIADB_PASSWORD=uvlhubdb_password \
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \
  -e WORKING_DIR=/app/ \
  -p 3306:3306 \
  mariadb:12.0.2

Qué significa ":" en los volúmenes =

Cuando usas la opción -v o --volume en Docker, el signo dos puntos (:) separa dos rutas:

-v origen:destino

origen → es la ruta o el volumen en tu máquina (el host).

destino → es la ruta dentro del contenedor.

Ejercicio 4: Orquestación de servicios

1: hacer un docker compose up básico 2: arreglar lo del wait for db 3: avisar del problema del código del contenedor y del uso de un volumen para eso

Ejercicio 5: Entrypoints

Explicar utilidad de los entrypoints (puedo configurar el arranque sin tener que modificar la imagen)

Ejercicio 6: Distintas configuraciones de despliegue

1: Explicar las distintas imágenes, entrypoints y docker-compose.ymls 2: Explicar el nginx 3: Explicar que cuanto más se parezca el despliegue en dev y en prod, menos propenso a errores seremos 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