Despliegue de aplicaciones web
UD 05. Servidores de
aplicaciones
Docker. Docker Compose
Tutorial
Actualizado Enero 2024
Licencia
Reconocimiento – NoComercial - Compartir Igual (BY-NC-SA): No se permite un uso
comercial de la obra original ni de las posibles obras derivadas, la distribución de las cuales
se debe hacer con una licencia igual a la que regula la obra original.
Nomenclatura
A lo largo de este tema se utilizarán distintos símbolos para distinguir elementos importantes
dentro del contenido. Estos símbolos son:
📖 Importante
❕ Atención
💬 Interesante
ÍNDICE DE CONTENIDO
1. Introducción 4
2. Ejemplo sencillo Mysql 4
3. Ejemplo Nginx 10
4. Ejemplo Aplicación completa 13
5. Bibliografía 20
6. Autores (en orden alfabético) 20
UD05. SERVIDORES DE APLICACIONES
1. INTRODUCCIÓN
En el anterior documento, hemos visto una simple explicación de Docker, ahora vamos a ponerlo
en práctica donde realmente entenderás su utilidad y funcionamiento.
2. EJEMPLO SENCILLO MYSQL
Vamos a ir viendo ejemplos e ir aumentando la dificultad poco a poco.
En este ejemplo simplemente vamos a lanzar un contenedor de Mysql (si solo uno, también es
recomendable utilizar docker compose aunque solo tengamos un solo contenedor, ya que nos
facilita las cosas). Y ahora vas a comprobar de primera mano la diferencia entre el ejemplo de
Mysql que vimos en el tutorial de Dockerfile pero haciendo uso de Docker Compose.
Por lo tanto, crea un nuevo directorio y crea el fichero “[Link]”:
IMPORTANTE: en YAML la indentación es IMPORTANTÍSIMA, llevad cuidado con esto, si no, os
dará error de formato.
Vamos por partes:
● Indicamos la versión y seguidamente la sección services, en la cual se definen los
contenedores (en nuestro caso solo hay un contenedor).
● Ahora viene la definición del contenedor, empezamos por especificar el identificador del
contenedor “db”.
● Podemos de forma opcional especificar un nombre para cuando el contendor se cree
identificarlo más fácilmente (si no, se añadirá un por defecto, normalmente le pone el
nombre del directorio del proyecto + el identificador del contenedor “db”).
● En este caso, vamos a partir de la imagen por defecto de mysql ya que nos sirve, es decir,
no vamos a construir una de la cual partir (en el caso de que no nos sirva, sí habrá que
crear el Dockerfile e indicarlo aquí, pero eso siempre que la imagen de Docker Hub no nos
sirva, haremos un ejemplo al final).
● Y por último, especifico el mapeo de puertos. IMPORTANTE: esto ya no es el Dockerfile, es
decir, aquí SÍ estamos mapeando el puerto a diferencia del EXPOSE que servía solo a modo
de información, por lo tanto, en nuestro Host el puerto 3307 escuchará el 3306 del
contenedor de Docker.
● Aquí específico las variables de entorno que va a necesitar la imagen del contendor que
estoy utilizando, en este caso estoy creando también un segundo usuario a parte del root.
Es una buena práctica no trabajar directamente con el usuario root en las aplicaciones.
● Por último, defino el volumen. Aquí puede crear confusión, pero lo explico más abajo.
● Si te fijas bien, esta opción se encuentra al mismo nivel que services (indentación), no
pertenece al contenedor “db”, por lo tanto, en esta sección, definimos TODOS los
volúmenes que pueden ser utilizados por los contenedores.
● Posteriormente, voy al contendor en el cuál quiero utilizar ese volumen, y ahora sí, este
segundo volumes sí está dentro de “db”, por lo tanto, lo asocio e indico qué quiero
almacenar en ese volumen, que en nuestro caso es la ruta donde mysql guarda las bases de
datos.
● Si recuerdas la teoría de volumes de Docker, dijimos que había varias formas de trabajar
con ellos, esto equivale a la primera forma que vimos, persistencia de información. Con
esto, hago que si el contendor se elimina, las bases de datos se mantienen en el volumen
“mysql-data”.
Y esto sería todo, ahora simplemente ejecutaríamos el comando up:
Y ya tenemos el contenedor funcionando (acuérdate el comando docker run que utilizamos con
este ejemplo, docker compose no ha simplificado mucho, no tenemos que escribir ese comando
tan largo cada vez).
Si queremos para el contenedor o arrancarlo:
Vamos a MysqlWorkbench o a vuestro gestor de BBDD favorito y comprobemos que podemos
acceder. Hay que recordar que nuestro equipo está escuchando en el puerto 3307, por lo tanto:
Ahora vamos a suponer que la imagen original de Mysql no nos sirve y queremos construirla
nosotros. Vamos a ello, primero vamos a limpiar todo lo que acabamos de hacer con el comando
“down”:
(en mi caso me ha dado un warning porque ha intentado eliminar la imagen mysql:8.0.35-debian
de mi repositorio de imágenes al añadir la opción --rmi all, pero como tengo otros contendores que
utilizan esa imagen en mi sistema no me ha dejado, a ti te puede haber pasado lo mismo o si tienes
el sistema limpio sin más contendores que dependan de esa imagen se te habrá eliminado).
Como en este caso la imagen no nos sirve, tenemos que construir nosotros la imagen a utilizar, por
lo tanto, necesitamos un Dockerfile, para ello, como vamos a partir del ejemplo visto en el tema
anterior, reutilizamos ese Dockerfile:
Realmente el único cambio respecto a la imagen original es que estamos actualizando los
repositorios e instalando el editor vim, pero vamos a hacerlo para entender el concepto.
Vamos a hacer unos pequeños cambios, vamos a cambiar el nombre del volumen ya que habíamos
puesto otro en el docker-compose y quitamos la variables de entorno:
● Las variables de entorno las vamos a definir a partir de ahora en el docker-compose.
● Y recuerda que EXPOSE y VOLUME en el Dockerfile eran informativos, me van a informar
sobre qué puertos tengo que mapear en el docker-compose y que volúmenes tengo que
crear y qué ruta persistir.
Ahora volvemos a docker-compose ya que hay que cambiar la opción de image. Ahora ya no
apuntamos a una imagen de docker hub, si no que tenemos que construirla, para ello, debemos
especificarlo en el docker-compose con la opción build:
El “.” indicará a docker que para construir la imagen, busque un fichero llamado “Dockerfile” en la
misma ruta en la que se encuentra el fichero docker-compose. Si el fichero no tiene el nombre
“Dockerfile” no funcionará, para ello, habría que ponerlo de la siguiente forma:
Por lo tanto, el Dockerfile y [Link] quedarán de la siguiente forma:
Ejecutamos el comando “up”:
Como vemos, aparece un log más largo ya que está haciendo la acción de construir la imagen.
3. EJEMPLO NGINX
Vamos a ver ahora un ejemplo con un servidor web para posteriormente ver un ejemplo más
completo.
Vamos a partir del ejemplo del tema anterior relacionado con NGINX, pero vamos a realizar
cambios para organizar mejor el proyecto, puedes descargarlo desde aquí:
[Link]
Vamos a analizar el contenido, esta estructura es la que utilizaremos a partir de ahora en los
proyectos:
● app: en esta carpeta es donde va el código
fuente del proyecto.
● config: añadimos configuraciones que vayan a
ser necesitadas en el despliegue del proyecto.
○ virtualhosts: en este caso,
necesitamos establecer el virtualhost
de nuestra app.
El Dockerfile como verás se ha simplificado, hemos dejado lo referente a la construcción de la
imagen. La copia del proyecto y la configuración del virtualhost se lo dejamos a docker-compose ya
que se compartirá de otra forma.
(Hasta aquí no hay nada diferente, los Dockerfile contendrán todo lo que ya hemos visto en el tema
anterior, no hay nada nuevo).
El fichero docker-compose es muy sencillo como verás, pero hay un elemento nuevo que todavía
no hemos visto de esta forma, los volúmenes.
IMPORTANTE: Para este proyecto NO utilizamos los volúmenes para persistir información, si no,
como una especie de carpeta compartida, lo que hacemos es compartir datos de la máquina
host con el contenedor, de esta forma, todo lo que modifiquemos de esos datos compartidos en
el host, se aplicarán también en el contendor, por lo tanto, como habrás podido pensar, vamos a
poder desarrollar desde el host y los cambios se aplicarán en el entorno que realmente ejecuta
la aplicación, es decir, el contendor.
● Como puedes ver, compartimos nuestra carpeta app (contiene el proyecto) y hacemos que
se sincronice con la ruta /var/www/html que es donde deben estar los proyectos para que
el servidor web pueda servirlos.
● Y también compartimos el virtualhost, ya que también queremos utilizar un nombre de
dominio para acceder a través de él.
● Con estas dos acciones estamos haciendo algo parecido a lo que hicimos con el Dockerfile,
copiar lo que necesitamos al contenedor. Pero en esta ocasión, lo estamos haciendo como
realmente se trabaja con Docker.
● Nota: como puedes observar, podemos tanto compartir una carpeta completa como
únicamente un archivo.
Antes de lanzar el proyecto, vamos a añadir el dominio [Link] a nuestro /etc/hosts para que el
host pueda resolverlo (tendrás que poner la IP de tu equipo anfitrión).
Y lanzamos el proyecto con el comando “up”:
Ahora accede al navegador y verás que podemos acceder a la web ya desplegada:
Y como la app (código fuente) la tenemos en el host y está compartida con el contenedor,
cualquier cambio que hagamos se aplicará en el contenedor, por lo tanto, tenemos listo un entorno
de desarrollo preparado con docker. Ya podríamos seguir
desarrollando la app con VSCode, crear un repositorio con git y
trabajar con ramas como hemos hecho hasta ahora. La idea sería
añadir git al proyecto completo, de esa forma, cualquier nuevo
desarrollador clonaría el proyecto, haría “docker compose up -d” y a
trabajar, ya que tendría los archivos de configuración de Docker.
Vamos a hacer un cambio en el código para que puedas ver que cambia y que todo está bien
preparado.
Simplemente he cambiado el color del título:
Recomendación: siempre que estéis desarrollando en un entorno web, os recomiendo que
refresquéis el navegador con la combinación de teclas ctrl + shift + R, esto hace que refresque y al
mismo tiempo limpie caché, cuando se realizan cambios en HTML y CSS afecta.
RETO: Ahora te reto a lo siguiente, partiendo del mismo proyecto cambia lo que necesites para
hacerlo funcionar con un contendor de Apache en lugar de con NGINX. En este caso, parte de la
imagen de PHP de docker hub (php:apache), no es necesario que crees tu la imagen ya que esta
serviría, o también puedes crearla tú si prefieres practicar, lo dejo a tu decisión.
IMPORTANTE: los archivos compartidos con los volúmenes deben tener permisos 755 para que
puedan ser utilizados dentro de los contenedores. Como en estos ejemplos estás clonando los
proyectos no tendrás problemas, pero es posible que los tengas cuando crees tu propio proyecto.
4. EJEMPLO APLICACIÓN COMPLETA
Ahora, vamos a ver un ejemplo con una aplicación completa, es decir, el código va a ser simple,
HTML, CSS, Javascript y PHP, pero la infraestructura no.
Vamos a tener la aplicación FrontEnd desarrollada en HTML, CSS y Javascript, una aplicación
Backend (API) desarrollada en PHP y MYSQL como gestor de bases de datos.
Como tenemos dos partes bien diferenciadas, vamos a dividirlo entre el proyecto Frontend y el
proyecto Backend el cual cada uno levantará la infraestructura que le toque.
El proyecto ya está desarrollado, vamos primero a hacerlo funcionar y luego os explico en detalle,
pero quiero que veáis que fácil sería desplegar todo con docker para comenzar a trabajar.
Primero, clona estos repositorios:
● [Link]
● [Link]
Una vez clonados, verás que he creado un README que
explican cómo hacer funcionar cada proyecto, es simple,
basta con clonar, entrar en cada proyecto y ejecutar el
comando “up”.
Vamos a ello:
Nota: los servicios están preparados para mapear los puertos 80, 8080 y 3306, si en tu host tienes
algún servicio que utilice estos puertos apágalo para que no tengas conflictos.
BACKEND
FRONTEND
Comprobamos los servicios que están levantados:
Añadimos los dominios a nuestro archivo “hosts”:
Y probamos que funcione:
Como habéis visto, ha sido muy fácil desplegar nuestro entorno de desarrollo con la app lista,
ahora como desarrollador del proyecto, podría crear mis ramas y comenzar a desarrollar (este es
un proyecto de prueba, solo está la rama master, pero en uno real, debería estar develop también
como ya vimos en el tema 2).
Este sería el esquema de lo que hemos levantado:
Vamos a explicar el proyecto en detalle:
Lo primero, comentar que se ha dividido la aplicación en dos proyectos ya que cada desarrollador
se clonará la parte que le toque desarrollar, de esta forma tenemos el proyecto frontend y por otro
lado, el proyecto backend.
En el caso de que un desarrollador tenga que trabajar en ambos proyectos, haría justo lo que
hemos hecho nosotros.
BACKEND
Ambos proyectos tienen exáctamente la misma estructura, esto es una estructura que yo he
definido, inspirada en los frameworks usuales de PHP, pero si no te gusta, puedes cambiarla, pero
lo importante es que aprendamos a organizarnos bien:
● Lo que estoy haciendo es meter la aplicación (código
fuente) en una carpeta llamada “app” (lo dicho, puedes
llamarla como quieras), esta carpeta contiene
realmente el proyecto de la aplicación, pero al trabajar
con docker, necesitamos todo lo demás para montar
adecuadamente la infraestructura y que sea tan fácil de
ejecutar como habéis visto.
● En “config”, meto todos los archivos de configuración que se necesitan para hacer
funcionar el proyecto. (Si quisiéramos partir de un archivo de configuración del servidor
web diferente al original, se podría meter aquí y utilizar un volumen para sustituir al archivo
del contenedor).
○ Aquí he puesto un dump de la base de datos,
esto permitirá crear la estructura de la base de
datos que utiliza la aplicación (puede tener
también datos por defecto, no solo la estructura).
○ Y el virtualhost como ya vimos en el ejemplo
anterior.
● Tenemos también un gitignore, lo he dejado preparado
porque lo utilizaremos más adelante, pero de momento está vacío.
● Tenemos un [Link] con las instrucciones sobre qué pasos hay que llevar a cabo para
hacer funcionar el proyecto.
● El Dockerfile, que no cambia respecto al ejemplo anteriormente visto.
● Y por último el [Link] que define la estructura de los contenedores, este
vamos a verlo más en detalle porque aquí sí han habido cambios:
Como vemos, tenemos dos contenedores:
● db: contendor de la base de datos, aquí hay poco que comentar, únicamente vemos algo
nuevo en los volumes.
○ Lo que estoy haciendo es compartir el dump en la ruta /docker-entrypoint-initdb.d
○ Esto hace que cuando el contendor sea arrancado, docker busca en esa ruta los
archivos .sql y los ejecuta, con esto estoy consiguiendo que la base de datos no
empiece vacía, si no que se creen la estructura de tablas que necesito y además,
puedo aprovechar y meter algún dato de prueba para que no esté vacía.
● app: contendor correspondiente al backend. Como verás es igual que el que hemos hecho
en el ejemplo anterior, lo único que hemos añadido es el “depends_on”:
○ Esto indica que el contendor app depende del contenedor db, por lo tanto, modifica
el orden de creación, hasta que no esté listo el contendor db, no se crea el
contenedor app.
¿Cómo hacemos referencia al contenedor “db” desde “app”?
Esto se hace como siempre, hay que configurar la conexión con la base de datos en el código de la
aplicación. En mi caso y en casi el 99% de aplicaciones, debe haber un fichero de configuración en
el cual especificar esta información, en este caso se llama “app/[Link]”:
Aquí indicamos:
● Nombre de la base de datos a la cual nos
queremos conectar.
● Usuario y contraseña que tiene permisos
para acceder a esa base de datos (aquí lo
ideal es no poner root, sino un usuario
que únicamente tenga permisos sobre esa
base de datos, que es lo que estamos
haciendo). Estos datos son los definidos
en las variables de entorno.
● Y lo más importante la “IP”, es decir, la ip del equipo que tiene el servicio de base de datos
funcionando. En este caso, es un contenedor, por lo tanto, ¿Qué IP ponemos? como ambos
contendores están en la misma red, con poner el nombre del contenedor docker ya sabe
cómo redirigir el tráfico, por lo tanto, indicamos su nombre (el container_name no).
Y es:o es todo, simplemente destacar lo siguiente, el entorno va a depender de la app, es decir,
este backend accede a una base de datos haciendo uso de PHP y lee a través de código PHP un
archivo YAML, esto la librería de PHP que viene por defecto no tiene estas funcionalidades, por lo
tanto, es necesario instalar las librerías adicionales que necesite nuestro proyecto, en este caso, yo
lo he indicado en el Dockerfile:
A donde voy a parar con esto: es posible que con la imagen original no te sea suficiente y tengas
que finalmente construir la imagen que cumpla con tus requisitos.
FRONTEND
Y ya estaría todo comentado, del frontend hay poco que decir, es más de lo mismo, simplemente
un aspecto importante, la comunicación entre el frontend y el backend es diferente a la
comunicación entre el backend y la base de datos:
● El backend y la base de datos se comunican internamente en la red interna de los
contenedores.
● El frontend y el backend no, es cierto que el frontend hace peticiones a recursos del
backend, pero piensa que esas peticiones se harán desde el navegador del host, no
directamente desde el contendor del frontend.
● El navegador hace la petición al frontend obteniendo y ejecutando el código cliente, si ese
código cliente realiza una petición al backend, la petición saldrá desde el navegador, por lo
tanto, en el frontend pondremos el dominio que hemos creado para el backend, porque el
que realiza la petición es el host, no el contendor del frontend:
(No ponemos el nombre del contenedor como hicimos con el backend y la base de datos).
IMPORTANTE: recuerda que cuando vimos apache vimos el tema del CORS, es decir, las
conexiones cruzadas (cross-origin), en este caso tenemos ese problema, el backend va a recibir
peticiones desde un origen distinto, por lo tanto, es importante que configuremos el CORS en el
backend para que todo funcione.
● Para ello, puedes hacerlo de dos formas:
○ Si estás en apache puedes hacerlo en un .htaccess o bien directamente en el
virtualhost (recomiendo que lo hagáis en el virtualhost ya que es algo que siempre
tiene que estar configurado).
○ Si estás en nginx, se hace directamente en el virtualhost (abre el archivo del
virtualhost del backend y podrás ver que se ha añadido la configuración del CORS).
5. BIBLIOGRAFÍA
[1] [Link]
[2] [Link]
[3] Curso de Introducción y de desarrolladores a Docker de OpenWebinars
[4] [Link]
[5] [Link]
[6] [Link]
[7] [Link]
[8] [Link]
6. AUTORES (EN ORDEN ALFABÉTICO)
A continuación ofrecemos en orden alfabético el listado de autores que han hecho aportaciones a
este documento.
● David Folgado De la Rosa
● Miguel Mira Flor