Resumen
Docker es un proyecto open source creado en 2013 y que ha supuesto una revolución para el desarrollo y despliegue de operaciones. Docker abstrae el hardware y el sistema operativo del host ejecutando las aplicaciones en contenedores, compartimentos aislados que contienen todos los recursos para una aplicación o servicio.
En este seminario veremos cómo usar Docker para el desarrollo de aplicaciones sencillas, aprendiendo a crear servicios con Docker Compose y concer la técnica de desarrollo de aplicaciones multicontenedor con Docker Compose
-
Conocer los componentes básicos de Docker
-
Crear contenedores a partir de imágenes de Docker Hub
-
Aprender a usar
Dockerfile
para la creación de imágenes -
Usar volúmenes para almacenamiento persistente
-
Usar Docker Compose para construir entornos de contenedores
-
Aprender la técnica de desarrollo de aplicaciones multicontenedor con Docker Compose
Disponibles los repositorios usados en este seminario: |
1. Conceptos básicos
1.1. Qué es Docker
-
Docker es una plataforma para que desarrolladores y administradores puedan desarrollar, desplegar y ejecutar aplicaciones en un entorno aislado denominado contenedor.
-
Docker permite separar las aplicaciones de la infraestructura acelerando el proceso de entrega de software a producción.
-
Proyecto open source creado en 2013 que hace uso de LXC (Linux Containers). LXC es un método de virtualización de a nivel de S.O.
Docker permite empaquetar una aplicación con todas sus dependencias para que pueda ser ejecutada en plataformas diferentes. El proceso de despliegue es rápido y repetible. |
Basta con ejecutar los tres comandos siguientes en una máquina con Docker instalado para tener una aplicación web que muestra un catálogo de clientes almacenados en una base de datos MySQL.
$ git clone https://github.com/ualmtorres/docker_customer_catalog.git
$ cd docker_customer_catalog
$ docker-compose up -d
Para detener el ejemplo anterior, desde la carpeta donde se ha desplegado la aplicación ejecutaremos
$ docker-compose down
Esto eliminará todo lo que se ha creado para este ejemplo (contenedores y red de interconexión de dichos contenedores).
1.2. Docker vs Máquinas virtuales
-
Una máquina virtual proporciona un entorno con más recursos de los que necesitan la mayoría de las aplicaciones
-
Mayor número de contenedores que de MV en el mismo hardware.
-
Los contenedores se pueden ejecutar en hosts que sean máquinas virtuales.
1.3. Ventajas
-
Ligeros: Los contenedores comparten el kernel del host.
-
Intercambiables: Depliegue de actualizaciones en caliente.
-
Portables: Build local y ejecución en cualquier lugar.
-
Escalables: Aumento y distribución automática de réplicas de contenedores.
-
Apilables: Aumento del stack de servicios en caliente.
Docker supone una revolución en los entornos de CI/CD. Tras la actualización del repositorio de proyecto, se crean contenedores para pasar las pruebas, se construyen las nuevas imágenes y se despliega la nueva versión de la aplicación sin parada del sistema. |
2. Un ejemplo sencillo
2.1. Antes de nada
2.1.2. Crear cuenta en Docker Hub
Docker Hub es un registro público de imágenes (Lugar donde se almacenan imágenes): https://hub.docker.com
Docker Hub permite en su plan libre tener un repositorio privado de imágenes. También permite automatizar la construcción de imágenes y su despliegue con repositorios GitHub y Bitbucket |
2.3. El Hola mundo
$ docker --version
Docker version 18.09.2, build 6247962
$ docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
9bb5a5d4561a: Pull complete
Digest: sha256:f5233545e43561214ca4891fd1157e1c3c563316ed8e237750d59bde73361e77
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
This message shows that your installation appears to be working correctly.
....
2.4. Crear un contenedor Apache
$ docker run -d -p 82:80 --name apache httpd
-
Descarga una imagen Apache (
httpd
) si no existe localmente, lanza un contenedor y asocia el puerto 82 del host al puerto 80 del contenedor -
-d
lanza el contenedor en modo dettached y libera la terminal -
-p 82:80
asocia el puerto local 82 al puerto 80 del contenedor -
-name apache
asigna el nombreapache
al contenedor para que luego se más fácil interactuar con él (p.e. para ver sus logs, iniciar una sesión interactiva, eliminarlo, …)
El primer puerto que aparece es el del host y el segundo el del contenedor |
También podemos usar el parámetro |
2.6. Imágenes interesantes de Docker
En https://hub.docker.com/explore/ se encuentran las imágenes ordenadas por popularidad. Destacamos:
-
alpine: Linux reducido
-
nginx: Servidor web Nginx
-
httpd: Servidor web Apache
-
ubuntu: Ubuntu
-
redis: Base de datos Redis (clave-valor)
-
mongo: Base de datos MongoDB (documentos)
-
mysql: Base de datos MySQL (relacional)
-
postgres: Base de datos PostgreSQL (relaional)
-
node: Node.js
-
registry: Registro de imágenes on-premise
-
php, elasticsearch, haproxy, wordpress, rabbitmq, python, openjdk, tomcat, jenkins, redmine, flink, spark, …
2.7. Operaciones sobre contenedores
2.7.1. Mostrar contenedores
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
99f6727e2506 httpd "httpd-foreground" 4 seconds ago Up 3 seconds 0.0.0.0:82->80/tcp apache
Los nombres generados para los contenedores son aleatorios si no se usa el parámetro |
2.7.2. Detener y reanudar contenedores
Primero, obtener con docker ps
el CONTAINER ID
o el nombre del contenedor que queremos detener.
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
99f6727e2506 httpd "httpd-foreground" 4 seconds ago Up 3 seconds 0.0.0.0:82->80/tcp apache
Detener el contenedor
Podemos detener el contenedor de dos formas, bien a partir de su nombre, que es más sencillo localizarlo, o bien a partir de su CONTAINER ID
-
Detener el contenedor mediante su nombre:
docker stop apache
-
Detener el contenedor mediante su nombre:
docker stop 99f6727e2506
Al hacer |
Mostrar todos los contenedores, también los detenidos
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
99f6727e2506 httpd "httpd-foreground" 20 minutes ago Exited (0) 2 minutes ago apache
Reanudar un contenedor
$ docker start apache
También se podría haber reanudado a partir de su CONTAINER ID
$ docker start 99f6727e2506
Tras reanudar el contenedor, vuelve a aparecer cuando hacemos docker ps
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
99f6727e2506 httpd "httpd-foreground" 9 hours ago Up 10 seconds 0.0.0.0:82->80/tcp apache
Detener todos los contenedores en ejecución
Primero obtenenemos los identificadores de los contenedores en ejecución con docker ps -q
. Ese comando lo podemos encerrar entre apóstrofes y pasar su resultado a otro comando en la misma línea.
$ docker stop `docker ps -q`
Iniciar una lista de contenedores
$ docker start 99f6727e2506 9811efbf6e45 178c2d03f2e7
2.7.3. Abrir un terminal en un contenedor
Se puede iniciar especificando el nombre del contenedor (apache
) o bien su CONTAINER ID
. En este ejemplo se abre el terminal usando el CONTAINER ID
$ docker exec -it 99f6727e2506 bash
root@99f6727e2506:/usr/local/apache2#
Git Bash no permite abrir un terminal en un contenedor en Windows. Usar PowerShell o Símbolo del sistema. |
Se inicia una sesión como root
en el contenedor. En la terminal del contenedor podemos ejecutar comandos del sistema operativo (ls, df -h, cat /proc/cpuinfo, …
). La cantidad y el tipo de comandos dependerá de la imagen usada para crear el contenedor.
2.7.4. Copia de datos
El almacenamiento en un contenedor no es persistente. Se eliminan los datos escritos en él tras su eliminación. |
docker cp [OPTIONS] CONTAINER:SRC_PATH DEST_PATH|-
docker cp [OPTIONS] SRC_PATH|- CONTAINER:DEST_PATH
Como ejemplo vamos a crear en nuestro host un archivo index.html
y lo copiaremos en el contenedor para sustituir la página de inicio del servidor Apache.
<!-- Ejemplo de archivo index.html -->
<html>
<body>
<h1>Docker es una maravilla</h1>
</body>
</html>
Ahora copiamos el archivo index.html
al contenedor con docker cp
. Se usará el nombre del contenedor o su CONTAINER ID
para hacer referencia al contenedor.
$ docker cp index.html apache:/usr/local/apache2/htdocs/
2.7.5. Eliminación de un contenedor
Primero paramos el contenedor con docker stop
y luego lo eliminamos con docker rm
$ docker stop apache
$ docker rm apache
También se puede eliminar directamente un contenedor en ejecución forzando su eliminación
$ docker rm -f <name-or-container-id>
Al crear un nuevo contenedor a partir de la imagen httpd
comprobamos que la página de inicio modificada anteriormente se eliminó junto al contenedor eliminado.
$ docker run -d -p 82:80 httpd
Podemos eliminar todos los contenedores creados a partir de una imagen con la secuencia de comandos siguiente (p.e. eliminar todos los contenedores creados a partir de una imagen
|
Para eliminar todos los contenedores parados ejecutaremos
$ docker container prune
2.8. Resumen de comandos básicos para contenedores
$ docker info
$ docker version
$ docker run <image> // Crea un contenedor a partir de una imagen. Si no tenemos la imagen en local, la descarga
$ docker run -d -p 82:80 --name my-nginx nginx: Crea un contenedor denominado my-nginx en modo deattached accesible desde el puerto 82
$ docker stop|start <name-or-id>: Detiene|Continúa un contenedor
$ docker ps -a: Listado de contenedores (-a muestra también los parados)
$ docker ps -q: Listado de los ids de los contenedores
$ docker stop `docker ps -q`: Para todos los contenedores que devuelve el subcomando `docker ps -q`
$ docker rm <name-or-id>: Borra un contenedor si está parado
$ docker rm -f <name-or-id>: Fuerza el borrado de un contenedor aunque esté parado
$ docker container prune: Elimina todos los contenedores parados
$ docker exec -it <name-or-id> sh: Abre una terminal en el contenedor
$ docker exec <name-or-id> ls: Ejecuta el comando ls en el contenedor para mostrar sus archivos
$ docker cp <name-or-id>:./dockerenv .: Copia el fichero dockerenv del contenedor en nuestro sistema de archivos local
$ docker rm -f `docker ps -a | grep "wordpress" | awk '{print $1}'`: Eliminar todos los contenedores creados a partir de una imagen
Hay muchas Cheat Sheets con resumen de los comandos principales de Docker. Aquí puedes encontrar una que está bastante bien. |
3. Bind mounts
Un bind mount permite montar un archivo o directorio de nuestro sistema en un contenedor.
Dado que los contenedores no ofrecen almacenamiento persistente, todo lo que se almacene en ellos se perderá al eliminar el contenedor. A continuación se ilustran algunas situaciones habituales y cómo los bind mounts resultan útiles:
-
Uso de contenedores para el desarrollo de aplicaciones. El código de desarrollo estará en el sistema de archivos de nuestro host y usaremos un bind mount que permite ejecutar en el contenedor el código almacenado en nuestro host.
-
Uso de contenedores de bases de datos. La base de datos tiene que estar en el sistema de archivos de nuestro host y usaremos un bind mount para ejecutar el contenedor con la base de datos almacenada en nuestro host.
Los bind mounts (se puede usar más de uno) se definen en el momento de lanzar el contenedor con el parámetro -v
, indicando en primer lugar la ruta del sistema de archivo local y en segundo lugar la ruta del sistema de archivos del contenedor. Por ejemplo
-v /home/ubuntu/webEstaticaBasica:/usr/local/apache2/htdocs
indica un bind mount que monta la carpeta local /home/ubuntu/webEstaticaBasica
en la carpeta /usr/local/apache2/htdocs
del contenedor.
-
Crear una carpeta para este ejemplo y entrar en ella.
-
Descargar este repositorio. Contiene una web estática sencilla con un único archivo (
index.html
) -
Lanzar un contenedor Apache con un bind mount sobre la carpeta de la aplicación. Asignaremos el nombre
my-web
al contenedor
$ git clone https://github.com/ualmtorres/webEstaticaBasica.git
$ docker run -d \
-p 80:80 \ (1)
--name my-web \ (2)
-v $(pwd)/webEstaticaBasica:/usr/local/apache2/htdocs \ (3)
httpd (4)
1 | Conservar el puerto original del contenedor |
2 | Asignar el nombre my-web al contenedor |
3 | Crear un bind mount entre la carpeta webEstaticaBasica del host a la carpeta /usr/local/apache2/htdocs del contenedor. |
4 | Usar la imagen httpd de Apache |
El resultado sería el siguiente
-
Crear una carpeta para este ejemplo y entrar en ella.
-
Descargar este script de inicialización de la base de datos Sporting Goods
$ curl https://gist.githubusercontent.com/ualmtorres/eb328b653fcc5964f976b22c320dc10f/raw/448b00c44d7102d66077a393dad555585862f923/init.sql --output init.sql
-
Lanzar un contenedor MySQL con dos bind mounts, uno para inyectar el archivo de inicialización anterior, y otro para la carpeta de datos
$ docker run -d \
-p 3306:3306 \ (1)
--name my-mysql \ (2)
-v $(pwd)/init.sql:/docker-entrypoint-initdb.d/init.sql \ (3)
-v $(pwd)/data:/var/lib/mysql \ (4)
-e MYSQL_ROOT_PASSWORD=secret \ (5)
mysql (6)
1 | Conservar los puertos del contenedor |
2 | Asignar el nombre my-mysql al contenedor |
3 | bind mount para pasar un script SQL de inicialización de una base de datos |
4 | bind mount para almacenar los datos del contenedor localmente en la carpeta data |
5 | Inicialización de la contraseña del root . Se configura inicializando una variable de entorno en el contenedor. |
6 | Usar la imagen de MySQL |
En Windows (con Git Bash o PowerShell) sería:
docker run -d \
-p 3306:3306 \
--name my-mysql \
-v /"$(pwd)"/init.sql:/docker-entrypoint-initdb.d/init.sql \
-v /"$(pwd)"/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=secret \
mysql
En Windows (con Git Bash o PowerShell) el comando sería
|
A partir de este ejemplo, usando un cliente local de MySQL se podría acceder al contenedor directamente como localhost
.
Si no se dispone de un cliente MySQL para ver si se ha inicializado correctamente la base de datos, se puede iniciar una sesión interactiva en el contenedor creado
$ docker exec -it my-mysql bash (1)
root@3c51f13a1046:/# mysql -u root -p (2)
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 9
Server version: 8.0.19 MySQL Community Server - GPL
Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> show databases; (3)
+--------------------+
| Database |
+--------------------+
| SG | (4)
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
5 rows in set (0.01 sec)
1 | Iniciar una sesión interactiva en el contenedor MySQL |
2 | Iniciar una sesión como root en MySQL. Recordar la contraseña facilitada al crear el contenedor (secret ) |
3 | Mostrar las bases de datos |
4 | Base de datos inializada por el script durante la creación del contenedor |
4. Creación de imágenes propias
4.1. El Dockerfile
-
Para construir una imagen, se crea un
Dockerfile
con las instrucciones que especifican lo que va a ir en el entorno, dentro del contenedor (redes, volúmenes, puertos al exterior, archivos que se incluyen. -
Indica cómo y con qué construir la imagen.
-
Conseguimos que el build de la aplicación definida en el contenedor se comporte de la misma forma en cualquier lugar que se ejecute. Hacemos que sea repetible.
Ejemplo de Dockerfile
# Use an official Python runtime as a parent image
FROM python:2.7-slim
# Set the working directory to /app
WORKDIR /app
# Copy the current directory contents into the container at /app
ADD . /app
# Install any needed packages specified in requirements.txt
RUN pip install --trusted-host pypi.python.org -r requirements.txt
# Make port 80 available to the world outside this container
EXPOSE 80
# Define environment variable
ENV NAME World
# Run app.py when the container launches
CMD ["python", "app.py"]
Fragmento de Dockerfile
para construir una imagen con Ubuntu como base y definiendo dónde se montará un volumen externo
FROM ubuntu:latest
RUN apt-get update -y
RUN apt-get install -y python-pip python-dev
WORKDIR /app
ENV DEBUG=True
EXPOSE 80
VOLUME /data (1)
1 | Crea un punto de montaje en el contenedor. A la hora de crearlo le haremos corresponder normalmente un directorio del host |
4.2. Imágenes
-
Se construyen con
docker build
a partir de unDockerfile
-
Se crean en un contexto (normalmente añadiendo archivos del directorio de trabajo del host a la imagen -p.e. el código fuente de la aplicación)
-
Con
FROM
(normalmente primera instrucción delDockerfile
) inicializamos el sistema de archivos de la imagen (p.e. si es ubuntu obtenemos el sistema de archivos de Ubuntu) -
Muchas imágenes disponibles en Docker Hub usan Alpine (una distribución ligera de Linux) en lugar de Ubuntu, Fedora o CentOS, debido a su menor tamaño
-
Cada instrucción del
Dockerfile
genera una nueva capa (con la diferencia) en ese sistema de archivos -
Al hacer
build
las capas existentes en el registro local no se vuelven a crear
Una imagen comprimida de Alpine está en torno a los 2 MB, mientras que una imagen comprimida de Ubuntu está entre 40 y 80 MB |
4.3. Distribución y ejecución de aplicaciones mediante Dockerfile
Supongamos el siguiente escenario. Contamos con el código de una aplicación disponible en un repositorio. Con la ayuda de Dockerfile
podemos crear una imagen local con todo el software y configuración que necesita la aplicación para ejecutarse y crear después un contenedor a partir de dicha imagen. El código de la aplicación podrá ser copiado directamete al contenedor o se podrá montar un volumen en el sistema de archivos del host de forma que se pueda editar el código y no se pierdan los cambios al eliminar el contenedor.
Por tanto, una buena forma de distribuir una aplicación puede ser incluir un Dockerfile
con la configuración de software que necesita para ejecutarse junto con el código de la aplicación.
El Dockerfile
siguiente contiene los pasos a seguir para:
-
Crear una imagen con Apache, PHP y el framework Phalcon.
-
Incluir el código de la aplicación en el contenedor.
-
Exponer el puerto deseado.
-
Crear un punto de montaje en el contenedor. Este punto de montaje se podrá conectar al sistema de archivos del host con un bind mount al iniciar el contenedor.
FROM ualmtorres/phalcon-apache-ubuntu (1)
ADD webEstaticaBasica /var/www/html (2)
EXPOSE 80 (3)
VOLUME /var/www/html (4)
1 | Imagen de base. Incluye Apache, PHP y el framework Phalcon |
2 | Añade el código de la carpeta webEstaticaBasica del sistema de archivos local a la carpeta /var/www/html del contenedor |
3 | Informa del puerto en el que escucha el contenedor |
4 | Ofrece la carpeta /var/www/html como punto de montaje |
Si se quieren ofrecer varios puntos de montaje se hará a través de un array.
Si se quieren exponer varios puertos se hará enumerando la lista de puertos
|
Configura tus propias imagen de base siguiendo el ejemplo del anexo Configuración de imágenes de base |
En un caso sencillo, podríamos reducir a que una aplicación está formada su base de código y el entorno en el que se ejecuta. El código de esa aplicación posiblemente esté en un repositorio Vamos a construir un contenedor a partir del código del repositorio y lo ofrezca al host como un volumen. El proceso a seguir es:
-
Descargar el repositorio del código de la aplicación.
$ git clone https://github.com/ualmtorres/webEstaticaBasica.git
-
Creación del
Dockerfile
en la carpetawebEstaticaBasica
para la construcción de la imagen.FROM ualmtorres/phalcon-apache-ubuntu ADD index.html /var/www/html EXPOSE 80 VOLUME /var/www/html
Es buena idea incluir en el repositorio de la aplicación el
Dockerfile
. Así se contará tanto con el código de la aplicación como con las instrucciones (en forma deDockerfile
) para crear el contenedor de la aplicación con todo lo necesario.
4.3.1. Construcción de la imagen
El comando docker build
crea una imagen nueva usando las instrucciones del Dockerfile
.
$ docker build -t ualmtorres/web-estatica-basica:v0 .
-
Con
-t
definimos una etiqueta o nombre de la imagen. Al construir la imagen pasa a nuestro registro local. -
Con
.
indicamos a Docker que utilice el directorio actual como contexto para hace el build
Es buena práctica crear etiquetas con el nombre de usuario el Docker Hub, el nombre de la imagen y la versión. |
4.4. Ejecución de la aplicación a partir de la imagen creada
Usaremos un bind mount para poder modificar el código de la aplicación y poder conservar los cambios. Posteriormente, podremos subir los cambios de la aplicación al repositorio.
$ docker run -d \
-p 83:80 \
--name webEstaticaBasica \
-v $(pwd):/var/www/html \
ualmtorres/web-estatica-basica:v0
En Windows sería
|
A la hora de distribuir y actualizar aplicaciones podemos incluir la aplicación en la imagen. Con un ciclo de CI/CD tendríamos la aplicación actualizada al actualizar su repositorio. |
4.5. Subida de imágenes a Docker Hub
Hasta ahora la imagen creada está en el repositorio local de imágenes. Para subirla a un repositorio remoto, como Docker Hub, primero iniciaremos sesión con docker login
y después podremos subir la imagen con el comando siguiente
docker push <user>/<image>:<tag>
-
Al hacer
push
las capas que ya estén subidas no se vuelven a subir. En cuanto una instrucción delDockerfile
cambia una capa, invalida al resto y se volcerán a crear las capas restantes. Por tanto, colocaremos antes en elDockerfile
lo que menos cambie. -
Al hacer
pull
sólo se descargan las capas nuevas. -
Si cambiamos en el host archivos de los que se incluyen en la imagen se genera una capa nueva invalidando la caché.
$ docker pull wordpress
$ docker run -d -p 80:80 --name my_wordpress wordpress
5. Aplicaciones con varios contenedores
-
Docker Compose es una herramienta para definir y ejecutar aplicaciones Docker con varios contenedores.
-
De forma predeterminada, usaremos un archivo
docker-compose.yml
para configurar los servicios de la aplicación. Los servicios son los componentes de la aplicación (p.e. un servicio para el almacenamiento de los datos y otro para el front-end) -
En un mismo host podemos tener varios entornos aislados. Compose usa nombres de proyecto para mantener a los entornos aislados. De forma predeterminada, Compose usa el nombre del directorio desde donde se lanza la aplicación.
-
docker-compose --version
para obtener la versión y saber si está instalado. -
Instalación desde https://docs.docker.com/compose/install
5.1. Flujo de trabajo básico con Docker Compose
-
Crear el archivo
docker-compose.yml
con los servicios de la aplicación (p.e. php y mysql) -
Construir y lanzar el entorno en modo dettached con
docker-compose up -d
-
Echar abajo el entorno con
docker-compose down
El comando El script siguiente devuelve los nombres de directorio desde los que se hayan lanzado todos los entornos de Compose que se tengan en ejecución.
A partir de ahí, se trata de buscar en el sistema de archivos del host los nombres de directorio devueltos. |
5.2. Comandos básicos para Docker Compose
$ docker-compose up -d Construye y lanza el entorno en modo dettached
$ docker-compose pull Descarga las imágenes pero no inicia los contenedores
$ docker-compose rm [-fs] Borra los contedores parados. Con -fs los detiene y fuerza su borrado
5.3. Ejemplo: Aplicación web (PHP) con soporte de Base de datos (MySQL)
-
Aplicación que muestra un listado de clientes almacenado en una base de datos MySQL.
-
Podemos distribuirla con un repositorio que incluya una carpeta
html
con la aplicación PHP. -
Al iniciar el servicio MySQL se ejecutará un script de inicialización de la base de datos.
-
Usaremos volúmenes externos para la base de datos y para la aplicación web para asegurar la persistencia de los cambios.
Comencemos clonando el repositorio de la aplicación:
$ git clone https://github.com/ualmtorres/docker_customer_catalog.git
En ese repositorio se encuentra:
-
Un archivo
docker-compose.yml
que configura dos servicios: un servicio para almacenamiento de datos con MySQL y otro servicio para la aplicación PHP. -
Una carpeta
html
con la aplicación. Esta carpeta será la que monte la aplicación PHP de forma que el código de la aplicación no esté almacenada en el contenedor. -
Un script SQL
init.sql
que inicializa la base de datos de nuestra aplicación. La base de datos se almacena en nuestro host, garantizando almacenamiento persistente.
docker-compose.yml
version: '2'
services:
mysql:
container_name: mysql (1)
restart: always
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: 'secret' # TODO: Change this
ports:
- "3306:3306"
volumes:
- ./data:/var/lib/mysql (2)
- ./init.sql:/docker-entrypoint-initdb.d/init.sql (3)
php:
container_name: php
restart: always
image: ualmtorres/phalcon-apache-ubuntu
ports:
- "80:80"
volumes:
- ./html:/var/www/html (4)
1 | Nombre del contenedor. Este será el nombre que usará el contendor de la aplicación PHP para poder acceder a este contenedor |
2 | Montar una carpeta data de nuestro host en la ruta en la que el servicio mysql almacena la base de datos |
3 | La imagen de MySQL ejecutará al inicio cualquier script que encuentre en /docker-entrypoint-initdb.d/ |
4 | Montar una carpeta html de nuestro host en la ruta en la que el servicio php almacena la aplicación |
Para lanzar la aplicación multicontenedor ejecutaremos el comando
$ cd docker_customer_catalog
$ docker-compose up -d
Este archivo despliega un contenedor denominado |
Esto creará un contenedor para cada servicio y una red para que los contenedores de los servicios se puedan comunicar entre sí. El nombre de la red vendrá determinado por el nombre del directorio desde donde se lance Docker Compose. Los contenedores podrán referenciarse unos a otros por el nombre del contenedor.
Para probar esto, abrir una sesión interactiva en el contenedor PHP y hacer ping mysql
$ docker exec -it php bash
root@61d202a9f1bf:/app# ping mysql (1)
PING mysql (192.168.32.3) 56(84) bytes of data.
64 bytes from mysql.docker_customer_catalog_default (192.168.32.3): icmp_seq=1 ttl=64 time=0.132 ms
64 bytes from mysql.docker_customer_catalog_default (192.168.32.3): icmp_seq=2 ttl=64 time=0.185 ms
64 bytes from mysql.docker_customer_catalog_default (192.168.32.3): icmp_seq=3 ttl=64 time=0.120 ms
1 | Se usa el nombre del contenedor asignado en docker-compose.yml para referenciarlo |
Aunque en el |
index.php
con el código de la aplicación<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Web PHP-MySQL con Docker</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">
</head>
<body>
<div class = "container">
<div class="jumbotron">
<h1 class="display-4">Docker app</h1>
<p class="lead">Ejemplo de aplicacion PHP y MySQL con contenedores</p>
<hr class="my-4">
<p>Usa un contenedor para Apache/PHP y otro para MySQL con almacenamiento de aplicación y de datos en volúmenes externos</p>
</div>
<table class="table table-striped table-responsive">
<thead>
<tr>
<th>Name</th>
<th>Credit Rating</th>
<th>Address</th>
<th>City</th>
<th>State</th>
<th>Country</th>
<th>Zip</th>
</tr>
</thead>
<tbody>
<?php
$conexion = new mysqli("mysql", "root", "secret", "SG"); (1)
$cadenaSQL = "select * from s_customer";
$resultado = $conexion->query($cadenaSQL);
while ($fila = $resultado->fetch_object()) {
echo "<tr><td> " .$fila->name .
"</td><td>" . $fila->credit_rating .
"</td><td>" . $fila->address .
"</td><td>" . $fila->city .
"</td><td>" . $fila->state .
"</td><td>" . $fila->country .
"</td><td>" . $fila->zip_code .
"</td></tr>";
}
?>
</tbody>
</table>
</div>
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js" integrity="sha384-smHYKdLADwkXOn1EmN1qk/HfnUcbVRZyYmZ4qpPea6sjB/pTJ0euyQp0Mk8ck+5T" crossorigin="anonymous"></script>
</body>
</html>
1 | El nombre con el que se accede a la base de datos MySQL es el nombre del contenedor usado en docker-compose.yml |
Archivo init.sql
para inicializar la base de datos Descargar init.sql
La aplicación quedará disponible
6. Desarrollo con Docker Compose
A la hora de desarrollar una aplicación con varios contenedores que tienen que trabajar de forma coordinada (p.e. una aplicación de frontend y backend) usaremos Docker Compose creando un servicio para cada componente (p.e. uno para el front y otro para la API). Cada uno de esos componentes es susceptible de empaquetarse como una imagen Docker. Por tanto, cada componente debería incluir su Dockerfile
para construir su imagen correspondiente. Así, la estructura recomendada para desarrollar con Docker Compose podría ser algo parecido a esto:
.
├── docker-compose.yml (1)
├── servicio-1 (2)
│ ├── Dockerfile (3)
│ └── Base de código del servicio 1
├── servicio-2
│ ├── Dockerfile
│ └── Base de código del servicio 2
...
└── servicio-n
├── Dockerfile
└── Base de código del servicio n
1 | Archivo con la configuración de ejecución |
2 | Directorio para cada servicio/componente |
3 | Instrucciones para la creación de la imagen del servicio |
Para ilustrar esto usaremos un ejemplo ficticio que desarrolle una API con calificaciones y un front para presentar los datos de la API. A continuación se muestran los pasos:
$ mkdir calificaciones
$ cd calificaciones
6.1. Desarrollo de la API
Se trata de una API en PHP con Phalcon. La API contiene los datos directamente en JSON para no añadir otro componente de bases de datos al ejemplo, conseguir un ejemplo más sencillo y no perdernos en los detalles. El objetivo es ver cómo desarrollar con Docker Compose.
-
Desde el directorio del proyecto crear una carpeta
api
para la API. -
Crear un archivo
.htaccess
que es necesario para rescribir las rutas en las peticiones a la API -
Crear un archivo
index.php
con el código de la API. -
Crear el
Dockerfile
con las instrucciones para crear la imagen de la APIFROM ualmtorres/phalcon-apache-ubuntu (1) ADD . /var/www/html (2) VOLUME /var/www/html (3) EXPOSE 80 (4)
1 Imagen de base para la ejecución de la API. Incluye Apache, PHP y el framework Phalcon 2 Incluir el código de la API en la carpeta de publicación de Apache 3 Crear un punto de montaje para que se pueda tener la base de código fuera del contenedor 4 Indicar el puerto por el que escucha el contenedor (80 por ser Apache) -
Crear en el directorio de la aplicación (un nivel por encima de
api
) el archivodocker-compose.yml
para poder ejecutar la API. El archivodocker-compose.yml
lo creamos un directorio por encima deapi
porque la especificación del volumen del código de la API en el archivodocker-compose.yml
se hace un nivel por encima del directorioapi
version: '2' services: calificaciones-api: container_name: calificaciones-api restart: always image: ualmtorres/phalcon-apache-ubuntu (1) ports: - "80:80" volumes: - ./api:/var/www/html (2)
1 Imagen base para ejecutar la API 2 Volumen en el host montado en el directorio /var/www/html
del contenedor
Si desplegamos el docker-compose.yml
veremos la API ejecutándose en el puerto 80 (http://localhost/calification
)
6.2. Desarrollo del front
-
Desde el directorio del proyecto crear una carpeta
front
para el código del front. -
Crear un archivo
index.php
con el código del front. -
Crear el
Dockerfile
con las instrucciones para crear la imagen del frontFROM php:7.2-apache (1) ADD . /var/www/html (2) VOLUME /var/www/html (3) EXPOSE 80
1 Imagen de base para la ejecución del front. Incluye Apache y PHP 2 Incluir el código del front en la carpeta de publicación de Apache 3 Crear un punto de montaje para que se pueda tener la base de código fuera del contenedor 4 Indicar el puerto por el que escucha el contenedor (80 por ser Apache)
Modificar el archivo docker-compose.yml
(un nivel por encima de front
) añadiéndole el servicio para poder ejecutar el front.
version: '2'
services:
calificaciones-api:
container_name: calificaciones-api
restart: always
image: ualmtorres/phalcon-apache-ubuntu
ports:
- "80:80"
volumes:
- ./api:/var/www/html
calificaciones-front: (1)
container_name: calificaciones-front
restart: always
image: php:7.2-apache (2)
ports:
- "8088:80" (3)
volumes:
- ./front:/var/www/html (4)
1 | Servicio para el front |
2 | Imagen base para ejecutar el front |
3 | Mapping de puertos para el front (El 80 está ocupado con la API) |
4 | Volumen en el host montado en el directorio /var/www/html del contenedor |
Si volvemos a desplegar el docker-compose.yml
con docker-compose up -d
, se mantiene intacto lo ya desplegado y se despliegan las modificaciones. El front se verá ejecutándose en el puerto 8088
6.3. Construcción de las imágenes
Una vez desarrollados los servicios de la aplicación podríamos proceder a la construcción de sus imágenes. Se trataría de un proceso manual en el que ejecutaríamos el comando docker build
en cada una de los directorios de los servicios desarrollados
-
En el directorio de la API:
docker build -t ualmtorres/calificaciones-api:v0 .
-
En el directorio del front:
docker build -t ualmtorres/calificaciones-front:v0 .
6.4. Prueba de la aplicación
Si ahora quisiéramos desplegar la aplicación con Docker Compose nos encontramos con el problema que el archivo docker-compose.yml
usa como imagen base las imágenes base creadas para el proceso de desarrollo. Sin embargo, ahora queremos desplegar la aplicación usando las imágenes creadas para la API y el front. Este problema lo podemos solventar rápidamente creando otro archivo para Docker Compose (p.e. docker-compose-produccion.yml
) y lanzar Docker Compose con ese archivo. docker-compose-produccion.yml
incluiría las imágenes creadas montando igualmente los directorios de las bases de código de la API y del front.
version: '2'
services:
calificaciones-api:
container_name: calificaciones-api
restart: always
image: ualmtorres/calificaciones-api:v0 (1)
ports:
- "80:80"
volumes:
- ./api:/var/www/html
calificaciones-front:
container_name: calificaciones-front
restart: always
image: ualmtorres/calificaciones-front:v0 (2)
ports:
- "8088:80"
volumes:
- ./front:/var/www/html
1 | Imagen de la API como base |
2 | Imagen del front como base |
6.5. Docker Compose con la opción build
El tener varios archivos para Docker Compose puede resultar confuso y en algunas ocasiones nos puede llevar a errores pensando que hemos usado uno cuando realmente estábamos usando el otro. Además, la creación de imágenes en un despliegue con gran cantidad de servicios implica la construcción manual de cada una de sus imágenes.
Para evitar esta situación podemos usar la opción build
. La opción build
crea las imágenes a partir de los archivos Dockerfile
que indiquemos y posteriormente crea el contenedor a partir de esa imagen. Para hacer esto, incluiremos dos particularidades en el archivo docker-compose.yml
con respecto a los que hemos estado usando hasta ahora:
-
Incluir una opción
build
que indica el contexto donde encontrar elDockerfile
para crear la imagen del servicio. -
El nombre de la imagen ahora no es el nombre de la imagen de base para crear el servicio, sino el nombre que se dará a la imagen construida para desplegar el servicio.
docker-compose.yml
para la construcción de imágenesversion: '2'
services:
calificaciones-api:
build: (1)
context: ./api (2)
container_name: calificaciones-api
restart: always
image: ualmtorres/calificaciones-api:v0 (3)
ports:
- "80:80"
volumes:
- ./api:/var/www/html
calificaciones-front:
build: (4)
context: ./front (5)
container_name: calificaciones-front
restart: always
image: ualmtorres/calificaciones-front:v0 (6)
ports:
- "8088:80"
volumes:
- ./front:/var/www/html
1 | Opción build para construir la imagen |
2 | Ruta del Dockerfile de la API |
3 | Nombre para la imagen de la API |
4 | Opción build para construir la imagen |
5 | Ruta del Dockerfile del front |
6 | Nombre para la imagen del front |
Para comprobar el funcionamiento de este nuevo procedimiento, eliminaremos todo rastro de lo anterior echando abajo el despliegue anterior y borrando las imágenes creadas anteriormente.
$ docker-compose down
$ docker image rm ualmtorres/calificaciones-api:v0
$ docker image rm ualmtorres/calificaciones-front:v0
A continuación, basta con volver a levantar el entorno com docker-compose up -d
y veremos como se construyen las imágenes para el despliegue de los contenedores y los servicios de Docker Compose funcionan correctamte.
Cuando introduzcamos cambios en la base de código y queramos volver a desplegarlos sobre las imágenes podremos ejecutar cualquiera de estos dos comandos:
$ docker-compose build
$ docker-compose up --build
7. Otras cosas interesantes
7.1. Configuración de imágenes de base
Aquí se muestra cómo configurar una imagen con Ubuntu 18.04, PHP y el framework Phalcon. También se muestran los dos archivos auxiliares necesarios para la configuración de Apache que necesita Phalcon.
FROM ubuntu:18.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -yq --no-install-recommends \
apt-utils \
curl \
# Install git
git \
# Install apache
apache2 \
# Install last version of PHP
php \
libapache2-mod-php \
php-mcrypt \
php-mysql \
php-curl \
nano \
ca-certificates \
locales \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
RUN curl -s "https://packagecloud.io/install/repositories/phalcon/stable/script.deb.sh" | /bin/bash
RUN apt-get install -y php7.0-phalcon
# Set locales
RUN locale-gen en_US.UTF-8 en_GB.UTF-8 es_ES.UTF-8
COPY conf/apache2.conf /etc/apache2/apache2.conf
COPY conf/dir.conf /etc/apache2/mods-available/dir.conf
RUN a2enmod rewrite
RUN a2enmod headers
RUN service apache2 restart
EXPOSE 80 443
WORKDIR /var/www/html
RUN rm /var/www/html/index.html
HEALTHCHECK --interval=5s --timeout=3s --retries=3 CMD curl -f http://localhost || exit 1
CMD apachectl -D FOREGROUND
conf/apache2.conf
Mutex file:${APACHE_LOCK_DIR} default
PidFile ${APACHE_PID_FILE}
Timeout 300
KeepAlive On
MaxKeepAliveRequests 100
KeepAliveTimeout 5
User ${APACHE_RUN_USER}
Group ${APACHE_RUN_GROUP}
HostnameLookups Off
ErrorLog ${APACHE_LOG_DIR}/error.log
LogLevel warn
IncludeOptional mods-enabled/*.load
IncludeOptional mods-enabled/*.conf
Include ports.conf
<Directory />
Options FollowSymLinks
AllowOverride None
Require all denied
</Directory>
<Directory /usr/share>
AllowOverride None
Require all granted
</Directory>
<Directory /var/www/>
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
Header set Access-Control-Allow-Origin "*"
</Directory>
AccessFileName .htaccess
<FilesMatch "^\.ht">
Require all denied
</FilesMatch>
LogFormat "%v:%p %h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined
LogFormat "%h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined
LogFormat "%h %l %u %t \"%r\" %>s %O" common
LogFormat "%{Referer}i -> %U" referer
LogFormat "%{User-agent}i" agent
IncludeOptional conf-enabled/*.conf
IncludeOptional sites-enabled/*.conf
conf/dir.conf
<IfModule mod_dir.c>
DirectoryIndex index.php index.html index.cgi index.pl index.xhtml index.htm
</IfModule>
7.2. Microservicios y contenedores
Con microservicios:
-
Establecemos un contrato, normalmente mediante una API REST, versionada para no romper funcionalidad a usuarios anteriores
-
Ocupan un tamaño reducido y suelen realizar una tarea muy concreta
-
Autenticación,
-
API REST. Toda la API vs cada endpoint
-
Estadísticas consumo de recursos
-
Exportar salida a central de logs
-
…
-
-
Dockerizar con cabeza
-
Comenzamos pasando todo nuestro sistema o MV a un contenedor Docker. Con sólo eso ya conseguimos ejecutar nuestra sistema en distintas máquinas con distintos SO y configuraciones.
-
No intentar pasar de una vez de aplicación monlítica a microservicios diminutos
-
7.4. Instalar un registro de imágenes propio
Es posible tener un registro propio para imágenes por cuestiones de seguridad y confidencialidad. Veamos cómo crear un registro propio mediante contenedores (uno para el registro en sí y otro cono Web UI).
El ejemplo será obtener una imagen Alpine de Docker Hub y subirla a nuestro propio registro
// En el servidor (p.e. 192.168.65.103)
$ docker run -d -p 5000:5000 --restart always --name registry registry:2
$ docker run \
-d \
-e ENV_DOCKER_REGISTRY_HOST=192.168.65.103 \
-e ENV_DOCKER_REGISTRY_PORT=5000 \
-p 8080:80 \
konradkleine/docker-registry-frontend:v2
// En nuestro equipo
$ docker pull alpine (1)
$ docker image list | grep alpine (2)
$ docker tag 3e640a41799a 192.168.65.103:5000/alpine (3)
$ docker push 192.168.65.103:5000/alpine (4)
1 | Descargar una imagen de prueba de Alpine al registro local |
2 | Obtener el identificador de la imagen Alpine descargada (p.e. 3e640a41799a ) |
3 | La imagen se etiqueta añadiéndole como prefijo host:puerto de nuestro registro |
4 | Subida de la imagen al registro |
Harbor es una opción muy interesante para disponer de un registro propio de imágenes. Permite definir reglas de control de acceso, analizar imágenes en busca de vulnerabilidades y añadir una firma de confianza a las imágenes.
Para subir una imagen a Harbor:
-
Es necesario pertenecer a un proyecto
-
Iniciar sesión
$ docker login <harbor-server>
-
Etiquetar la imagen siguiendo este patrón
$ docker tag <image> <server>/<project>/<image>[:<version>]
-
Subir imagen
$ docker push <server>/<project>/<image>[:<version>]