¡Esta es una revisión vieja del documento!
Tabla de Contenidos
Uso de Docker en desarrollo (Docker avanzado)
Contenido perteneciente al curso Docker avanzado.
Eliminar todo lo que no está en uso (contenedores, redes, imágenes y caché):
docker system prune
Por seguridad, los volúmenes no se borrarán con el comando anterior.
Comando docker: pull, search
Lo más normal es que las imágenes estén alojadas en un registro en la nube.
Para descargarlas al equipo local tenemos el subcomando pull:
docker pull fedora
Podemos comprobar las imágenes descargadas con el comando:
docker images fedora
Si queremos consultar las imágenes disponibles en los registros que tenemos configurados, tenemos el subcomando search:
docker search debian
Comando docker: cp
Si queremos copiar ficheros entre el host y un contenedor tenemos la opción de usar el comando cp.
Copiando desde la máquina anfitriona hacia el contenedor:
docker cp foo.txt mycontainer:/foo.txt
Copiando desde el contenedor a la máquina anfitriona:
docker cp mycontainer:/foo.txt foo.txt
Comando docker: diff
Con el comando diff podemos obtener una lista de los ficheros se han alterado desde la creación del contenedor.
docker diff <ID_CONTENEDOR>|<NOMBRE_CONTENEDOR>
Ejemplo de salida:
C /home A /home/fichero.txt A /home/host.txt
Comando docker: stats
El subcomando stats nos permite conocer el estado de consumo de recursos de nuestros contenedores en tiempo real.
docker stats
Por defecto, se va actualizando la información. Si queremos que nos muestre el primer resultado y finalize, le pasamos la opción --no-stream.
Si solo queremos ver el estado de un contenedor:
docker stats <CONTENEDOR>
También admite el parámetro –a para incluir todos los contenedores inactivos.
Aplicando límites
Por defecto, en los contenedores se usan todos los recursos disponibles. Sin embargo, se pueden definir limites al uso de memoria y un valor de prioridad para obtener tiempo de CPU:
docker run -d --name nginx-test --cpu-shares 512 --memory 128M -p 8080:80 nginx
--cpu-shares: peso relativo de uso de la CPU. Por defecto es 1024. Si ponemos uno a 512, quiere decir que usará la mitad del tiempo de CPU que otros contenedores.
Si el contenedor ya está en uso, se puede modificar los valores con update:
docker update --cpu-shares 512 --memory 128M --memory-swap 256M nginx-test
Crear nuestras propias imágenes
Tenemos dos formas de crear nuestras propias imágenes:
- Usando el comando
docker commit - Usando el comando
docker buildcon un Dockerfile.
Comando docker: commit
El comando docker commit, nos permite generar una imagen a partir de un contenedor.
Los pasos serían los siguientes:
Ejecutar un contenedor de una imagen base que queramos modificar:
docker run -i -t ubuntu /bin/bash
Hacer las instalaciones y configuraciones que necesitemos
apt-get -y install apache2
Ejecutar el comando docker commit para generar la imagen:
docker commit 4aab3ce3cb76 local:apache2
docker commit -m="A new custom image" --author=“John Doe" 4aab3ce3cb76 local:apache2
El uso del comando commit no es frecuente, o mejor dicho no se aconseja su uso para la generación de imágenes. Tenemos una excepción, y es cuando algo falla durante la ejecución de un determinado contenedor y queremos hacer un volcado para su posterior análisis
En esos casos también puede llegar a resultar útil generar la exportación en tar para compartir el contenedor con otros miembros del equipo:
docker image save -o <name_of_file.tar> <REPOSITORY>:<TAG>
El Dockerfile
Archivo de texto con instrucciones para crear una imagen de Docker.
Por defecto se le suele llamar Dockerfile.
# Version: 0.0.1 FROM ubuntu:23.04 LABEL author="Pepito" # Variables de entorno ENV WWW_HOME="/var/www/html" RUN apt-get update RUN apt-get install -y vim net-tools RUN apt-get install -y nginx RUN echo 'Hi, I am in your container' > /var/www/html/index.html # La directiva ‘WORKDIR’ es el equivalente al comando ‘cd’ WORKDIR $WWW_HOME # Esto sería lo equivalente al comando ‘su'. Cambiamos el usuario # del sistema con el que estamos trabajando. Por defecto solo tenemos # el usuario ‘root’. # El usuario tiene que existir # El último ‘USER’ del Dockerfile será el usuario con el que accedamos # al contenedor # USER daemon # COPY y ADD se usa para añadir ficheros a las imágenes # ADD tiene como mejoras que puede usar un origen remoto (http, ftp...) # ADD si copiamos un .tar o .tar.gz, automáticamente lo desempaqueta # COPY origen destino # Esta directiva informa declarativamente qué puertos pueden estar # en uso en esta imagen cuando esté en ejecución (puerto dentro del contenedor) EXPOSE 80 # La directiva ENTRYPOINT es nuestro punto de entrada. Es el prefijo del CMD ENTRYPOINT ["nginx"] # El CMD es el comando que se ejecuta al arranque, lo combinamos con el ENTRYPOINT CMD ["-g", "daemon off;"]
- El comando
FROMle dice a Docker qué base desea utilizar para su imagen- Se puede indicar el nombre de la imagen seguido de su etiqueta
- El comando
LABELpuede ser usado para añadir información extra a la imagen- Puede ser cualquier cosa desde un número de versión hasta una descripción
- Se puede consultar con el subcomando
inspect
- El comando
RUNnos permite ejecutar sentencias sobre una imagen- Cada sentencia
RUNproducirá una nueva capa (layer), así que es importante tener pocas, pero agrupadas con sentido (actualización de repositorios, instalación de paquetes relacionados…)
- También podemos añadir un bloque con comandos
COPYoADDCOPY files/nginx.conf /etc/nginx/nginx.confCOPY files/default.conf /etc/nginx/conf.d/default.confADD files/html.tar.gz /usr/share/nginx/- El comando
COPYes el más sencillo, coge el fichero origen y lo copia en el destino indicado dentro de la imagen - El comando
ADDes más avanzado, automáticamente sube el paquete comprimido y lo descomprime en el destino, dentro de la imagen - El comando ADD también nos permite descargar contenido remoto:
ADD http://www.myremotesource.com/files/html.tar.gz /usr/share/nginx/
- Una de las últimas secciones de un Dockfile suele ser (si existe) el
ENTRYPOINTy/oCMD- Con estas secciones podemos indicar comandos que serán ejecutados una vez se inicie el container
ENTRYPOINTpuede ser usado en combinación conCMD, dondeENTRYPOINTestablece la base de los comandos yCMDlo completa.
Para crear la imagen a partir del Dockerfile:
docker build –t nginx-ubuntu-latest .
-t: indicamos el nombre que tendrá la imagen resultante.- El punto final indica el directorio donde está el fichero
Dockerfile.
El comando anterior descargará la imagen de base, ejecutará los comandos que hayamos indicado en RUN.
Ahora podríamos ver la nueva imagen con:
docker image ls
Para crear un contenedor a partir de esa imagen:
docker run -d --name nginx -p 80:80 nginx-ubuntu-latest
-p: asociamos un puerto de la máquina anfitriona con uno del contenedor.
Podemos indicar un rango de puertos de tal manera que Docker asigne el primero que encuentre libre: docker run -d –name nginx -p 5000-6000:80 nginx-ubuntu-latest
Se crea el contenedor y se ejecuta el comando nginx -g "daemon off;"
Lo que ponemos en CMD lo podemos sobrescribir al crear el contenedor:
docker run -d --name nginx -p 80:80 nginx-ubuntu-latest -v
Si queremos ejecutar más de un comando en el arranque del contenedor, crearíamos un script y lo llamaríamos en el ENTRYPOINT.
Otros comandos de Dockerfile que pueden ser interesantes son:
USER: define el usuario con el que se van a ejecutar los bloques RUN, CMD y ENTRYPOINT. El usuario tiene que existir dentro de la imagenWORKDIR: establece el directorio de trabajo para los los bloques RUN, CMD y ENTRYPOINTENV: El comando ENV establece las variables de entorno dentro de la imagen tanto cuando se construye como cuando se ejecuta. Estas variables pueden ser sobrescritas cuando se lanza la imagen (docker run).
Para ver los pasos de construcción de una imagen: docker image history <IMAGEN>
docker image history nginx-ubuntu-latest
Consejo – build.sh
Para estos casos en los que la generación es una ejecución repetitiva de comandos, si no disponemos de una herramienta de más alto nivel podemos crear un sencillo script para simplificar el trabajo:
vim build.sh
#!/bin/bash nombre=prac4.2 docker build -t="local:$nombre" . docker stop $nombre docker rm $nombre docker run -d --name $nombre -p 80:80 local:$nombre
chmod +x build.sh sh build.sh
El comando docker: exec
Con el subcomando exec podemos lanzar comandos dentro de un contenedor en ejecución.
Por ejemplo, para acceder a un contendor que no se inicia con terminal podemos usar:
docker exec –it static_web /bin/bash
Práctica II
Crea una imagen a partir de un Dockerfile que haga lo siguiente:
- Tome como base una imagen de Ubuntu
- Le instale un apache2 o un nGinx y los módulos básicos de php8
- Crea una página HTML sencilla
- Crea un fichero PHP con
phpinfo() - Empaqueta el código del sitio en un tar.gz para que al generar la magen se añada a la misma
- Cuando se inicie el contenedor de la imagen deberemos poder acceder a esa mini página web
Solución:
FROM ubuntu LABEL author="Pepito" RUN apt-get update RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone RUN apt-get install -y apache2 php libapache2-mod-php ADD files.tar.gz /var/www/html EXPOSE 80 ENTRYPOINT ["apachectl"] CMD ["-D", "FOREGROUND"]
Práctica II - Reflexión
Siguiendo la filosofía de contenedores crees que es la forma más cómodo de lograr una imagen capaz de ejecutar una página en PHP?
Lo mejor sería buscar si existe una imagen base que ya tenga Apache y PHP: https://hub.docker.com/layers/library/php/8.1.24-apache/images/sha256-89e8e7b89a989e2f759e7948222ae20108aaab96d79a77fa67a126150d47c498?context=explore
FROM php:8.1.24-apache ADD files.tar.gz /var/www/html
Como vemos, tampoco serían necesarias las directivas ENTRYPOINT ni CMD porque están incluidas en la imagen php:8.1.24-apache
Práctica III
Explora el registro de Docker Hub y simplifica al máximo tu Dockerfile.
Ya lo hicimos en el apartado anterior:
FROM php:8.1.24-apache ADD files.tar.gz /var/www/html
Práctica IV
- Crea una aplicación web en java sencilla, con un simple servlet que nos dé la bienvenida de forma genérica, y si pasamos el parámetro name por get nos salude de forma personalizada
- Empaqueta la aplicación en un WAR
- Crea un Dockerfile capaz de crear un contendor que ejecute ese WAR
Solución (el profesor ya nos facilita el archivo ROOT.war para ahorrarnos el paso 1 y 2) con la imagen de tomcat oficial de Docker.
Tomcat es uno de los servidores de aplicaciones web en Java más populares.
FROM tomcat:9.0-jdk8 WORKDIR /usr/local/tomcat/webapps COPY ROOT.war . # Si no indicásemos el 'WORKDIR': # COPY ROOT.war /usr/local/tomcat/webapps/
La parte interna de Tomcat se llama “Catalina”. Los scripts de arranque son “catalina” algo. Al colocar un fichero .war en la carpeta correspondiente, Tomcat realiza el desempaquetado necesario para desplegar la aplicación web y que ya esté disponible.
Creamos la imagen:
docker build -t my-java-web .
Al colocar el fichero ROOT.war en el directorio de Tomcat, este lo desempaqueta automáticamente.
Lanzamos un contenedor:
docker run --name myjavawebcontainer -d --rm -ti -p 10000:8080 my-java-web
La imagen que hemos usado ya expone el puerto 8080, por eso no lo hemos puesto en nuestro Dockerfile.
Persistencia de datos: volúmenes
Cuando hablamos de una aplicación web, es ineludible hablar de persistencia de datos
La filosofía de contenedores es que el contenedor es efímero, puede aparecer y desaparecer.
Los datos que maneja el usuario deben poder persistir más allá de la vida del contenedor.
Recordemos que las imágenes en Docker no se pueden modificar (es inmutable). Cuando ejecutamos un contenedor a partir de una imagen, Docker añade una capa sobre la última de la imagen que sí permite cambios. Al eliminar el contenedor, esta capa con modificaciones desaparece.
Para eso están los volúmenes.
- Es una zona del disco duro de la máquina host.
- Se monta dentro del contenedor como si formara parte de su sistema de ficheros.
- Puede ser compartido por varios contenedores.
- Puede ser de solo lectura, o de lectura y escritura
Diferenciamos dos tipos de montaje:
- bind: se coge un directorio externo a la gestión de Docker y se monta dentro del contenedor. Al hacer esto, reemplazamos cualquier cosa que esté dentro del contenedor con lo externo. Esto lo utilizamos para reemplazar ficheros de configuración o cuando no nos interesa lo que haya dentro del contenedor.
- named volumes: los volúmenes con nombre se utilizan cuando una “unidad de red” / directorio gestionada por Docker queremos usarla en más de un contenedor. El demonio de Docker gestiona este objeto. Otra ventaja es que si ya existen datos en el contenedor, estos pasan al volumen y quedan guardados en el volumen. A partir de entonces, la gestión de esos datos se hacen en el volumen.
Los named volumes se suelen usar con las bases de datos, para almacenar sus datos.
Se supone que el uso de named volumes facilita la gestión de backups porque los datos están centralizados en la estructura de Docker (/var/lib/docker/volumes). Con hacer un rsync serviría. Si quisiéramos restaurar la copia de seguridad, bastaría con sobrescribir ese directorio con nuestra copia (con el Docker Engine detenido).
El comando docker tiene el subcomando volume para poder trabajar con los volúmenes.
Desde desde este comando podemos crear, consultar y eliminar los volúmenes:
# Creación básica: docker volume create myVolume # Creación dándole un nombre y etiqueta docker volume create --name myVolume --label test # Creación tipo NFS (debe existir la unidad de red compartida por NFS) docker volume create --driver local \ --opt type=nfs \ --opt o=addr=192.168.1.1,rw \ --opt device=:/path/to/dir \ foo
Si no indicamos opciones, se crea un volumen de tipo local (aparecerá un nuevo directorio en /var/lib/docker/volumes/) y en modo lectura y escritura
Para listar los volúmenes creados:
docker volume ls
Para ver los detalles de un volumen
docker volume inspect <NOMBRE_VOLUMEN>|<ID_VOLUMEN>
Los volúmenes físicamente están en /var/lib/docker/volumes/<ID_O_NOMBRE_VOLUMEN>/. Y los datos que maneja el volumen están dentro de la carpeta _data.
Para eliminar un volumen
docker volume rm <NOMBRE_VOLUMEN>|<ID_VOLUMEN>
Para todos los volúmenes que no están en uso:
docker volume prune
Para usar un volumen en un contenedor:
docker run –v <NOMBRE_VOLUMEN>:<DIRECTORIO_MONTAJE_CONTENEDOR> myImagen
La opción -v usa y/o crea volúmenes locales. Si vamos a usar algo distribuido, tendremos que usar la opción --mount que veremos más adelante.
Ejemplo:
docker run -v db_data:/var/lib/mysql mysql:8.1.0
Si queremos que un volumen se monte en modo modo lectura, añadiríamos ro: /var/lib/mysql:ro. El valor por defecto es rw (lectura y escritura)
Para usar un directorio (montaje tipo “bind”) en un contenedor:
docker run –v <RUTA_ABSOLUTA_DIRECTORIO>:<DIRECTORIO_MONTAJE_CONTENEDOR> myImagen
Otra opción es usar la opción --mount:
docker run –-mount source=<NOMBRE_VOLUMEN>;target=<DIRECTORIO_MONTAJE_CONTENEDOR> myImagen
En principio son equivalentes e intercambiables, salvo que queramos iniciar un servicio de Docker Swarm (o Kubernetes), es decir, distribuido, en cuyo caso solo podemos usar --mount (es el que permite crear volúmenes no locales).
Por ejemplo, imaginemos que nuestra aplicación tiene un directorio llamado uploads en el cual guardamos los ficheros subidos por los usuarios.
Podemos usar el siguiente comando para crear el contenedor:
docker run -d –v my_uploads:/var/www/html/uploads myAppImage
Si no existe ningún volumen con el nombre de my_uploads, se creará en este momento. Si ya existe, se monta el contenido existente en el volumen .
Ejemplo con bases de datos
La mayoría de las aplicaciones web hacen uso de bases de datos para la persistencia de la información que manejan.
En Docker Hub tenemos la imagen oficial de MySQL.
Si queremos iniciar un servidor de BBDD solo necesitamos los siguiente:
docker run -d -p 33060:3306 --name mysql-db -e MYSQL_ROOT_PASSWORD=secret --mount src=mysql-db-data,dst=/var/lib/mysql mysql
-e MYSQL_ROOT_PASSWORD=secret: declaramos y establecemos una variable de entorno que será la contraseña del usuario administrador de la base de datos MySQL.src: indicamos el volumen que vamos a usardst: indicamos la carpeta que se incluirá en el volumen que creamos.
Docker Compose
Cuando una aplicación necesita de varios servicios, Docker Compose nos facilita el despliegue al poder configurarlo todo desde un único fichero. Normalmente se define en un fichero llamado docker-compose.yml (aunque se prefiere compose.yaml o compose.yml). Utiliza el formato YAML.
Cuando trabajamos con Docker Compose la filosofía es que vamos a tener un stack de contenedores (servicios) que trabajan de forma conjunta. Todos los nombres de servicios (contenedores), redes y volúmenes se prefija automáticamente (cogen el nombre de la carpeta que contiene el fichero de configuración de Docker Compose). También se crea automáticamente una red personalizada que vincula solo estos servicios (contenedores). Como si fuera una VLAN.
También vamos a tener dentro de los contenedores un DNS local que resuelve el nombre de los servicios.
Ejemplo para el despliegue de WordPress con dos servicios (base de datos y la propia aplicación web):
services: # Imagen con una base de datos db: # docker run --name db -v db_data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=somewordpress... mariadb:10.6.4-focal --default-authentication-plugin=mysql_native_password image: mariadb:10.6.4-focal # Si quisiéramos levantar 5 contenedores iguales # deploy: # replicas: 5 command: '--default-authentication-plugin=mysql_native_password' volumes: - db_data:/var/lib/mysql restart: always environment: - MYSQL_ROOT_PASSWORD=somewordpress - MYSQL_DATABASE=wordpress - MYSQL_USER=wordpress - MYSQL_PASSWORD=wordpress expose: - 3306 - 33060 # Imagen con un servidor web y la aplicación web wordpress: # docker run --name wordpress -p 80:80 -e WORDPRESS_DB_HOST=db.... wordpress:latest image: wordpress:latest ports: - 80:80 restart: always environment: - WORDPRESS_DB_HOST=db - WORDPRESS_DB_USER=wordpress - WORDPRESS_DB_PASSWORD=wordpress - WORDPRESS_DB_NAME=wordpress volumes: db_data:
Cada bloque dentro de services será una ejecución de docker run.
command es el equivalente a la directiva CMD de los Dockerfile o sencillamente lo que le pasamos a la imagen cuando ejecutamos el contenedor.
environment es lo que metemos con -e en el docker run.
expose permite exponer puertos que pueden no estar expuestos en el Dockerfile. Ahí podemos indicar la correspondencia de puertos locales con los del contenedor.
Si usamos named volumes, es necesaria la sección volumes ya que ahí indicaremos los volúmenes que se crearán, es decir, será como ejecutar docker volume create db_data. Como usamos un tipo de volumen por defecto, no es necesario indicar nada más dentro de db_data.
Docker Compose permite indicar rutas relativas para hacer montajes tipo “bind” en la sección volumes.
La opción restart sirve para indicar qué hacer con los contenedores / servicios al iniciar o reiniciar el Docker Engine (el servidor de Docker), es decir, establecer la política de reinicio de los servicios. Valores:
none: no hacer nada.on-failure: reiniciar el contenedor cuando haya fallo en el demonio de Docker.always: iniciar el contenedor siempre que se inicie el Docker Engine.unless-stopped: iniciar siempre y cuando el servicio no se haya detenido a propósito.
Para lanzar un fichero Docker Compose y que cree todos los servicios, volúmenes y redes:
docker compose up -d
up: crea y arranca y los contenedores.-d: modo dettach (para que no bloquee la consola y se vaya a segundo plano).
En versiones antiguas de Docker, Docker Compose no existía como plugin sino como un ejecutable aparte, así que se usaba docker-compose.
Si queremos detener y borrar los contenedores y redes creados por Docker Compose:
docker compose down
Podríamos lanzar solo uno de los servicios:
docker compose up -d db
Otra cosa interesante de Docker Compose es que podríamos aplicar cambios en nuestro fichero docker-compose.yml sobre servicios en funcionamiento volviendo a ejecutar:
docker compose up -d
Docker Compose compara los servicios que están arrancados con la última configuración, y si encuentra diferencias, recrea el/los contenedores.
Ejemplo con Wordpress y Docker Compose
Docker Compose es una herramienta de Docker para manejar la generación de imágenes en base a múltiples imágenes
Tiene un fichero de configuración en YAML llamado docker-compose.yml
Ejemplo de compose con Wordpress: https://docs.docker.com/compose/wordpress/ (https://github.com/docker/awesome-compose/blob/master/wordpress-mysql/compose.yaml)
# cuando trabajamos con docker compose la filosofía es que vamos a tener un # stack de contenedores (servicios) que trabajan de forma conjunta # todos los nombres de servicios (contenedores), redes y volumenes se prefijan # automáticamente (con el nombre de la carpeta donde esta el yml) # automáticamente se crea un red custom que vincula solo estos servicios (contenedores) # tambien vamos a tener dentro de los contenedores DNS local que resuelve el nombre de los servicios services: db: #docker run --name db -v db_data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=somewordpress mariadb:10.6.4-focal --default-authentication-plugin=mysql_native_password image: mariadb:10.6.4-focal command: '--default-authentication-plugin=mysql_native_password' volumes: # Además de lo que nos permite -v (named volume o ruta absoluta) compose también permite ruta relativa - db_data:/var/lib/mysql restart: always # esta directiva solo se tiene en cuenta cuando se reinicia el demonio de docker. # none -> no inicia el servicio # onfailure -> solo inicia este servicio si docker terminó por un error en el docker daemon # always -> se inicia este servicio siempre que se inicie el docker daemon environment: - MYSQL_ROOT_PASSWORD=somewordpress - MYSQL_DATABASE=wordpress - MYSQL_USER=wordpress - MYSQL_PASSWORD=wordpress expose: - 3306 - 33060 wordpress: # docker run --name wordpress -p 80:80 -e WORDPRESS_DB_HOST=db -e WORDPRESS_DB_USER=wordpress wordpress:latest image: wordpress:latest # Indicamos que el servicio con WordPress requiere que esté funcionando el servicio de base de datos, así que si solo indicásemos que queremos levantar wordpress, automáticamente levantaría también el de MySQL: depends_on: - db ports: - 80:80 restart: always environment: - WORDPRESS_DB_HOST=db - WORDPRESS_DB_USER=wordpress - WORDPRESS_DB_PASSWORD=wordpress - WORDPRESS_DB_NAME=wordpress volumes: # docker volume create db_data # todos los volumenes "named" usados en los servicios tienen que estar declarados aquí db_data: # bloque de configuración para el volumen db_data (por defecto vacio)
Ejemplo con Docker Compose y Dockerfile
Dockerfile:
FROM nginx
docker-compose.yml:
services: nginx: image: pepito/nginx build: context: webs dockerfile: Dockerfile.nginx
Si nuestro Dockerfile tiene el mismo nombre, no sería necesaria la propiedad dockerfile.
Al hacer un docker compose up -d, Docker Compose creará una imagen a partir del Dockerfile, creará un contendor a partir de ella y una red.
Si listamos las imágenes:
$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE pepito/nginx latest 2a0f562768a2 4 days ago 187MB
Si eliminamos la red y el contenedor con docker compose down y volvemos a hacer un docker compose up -d, Docker Compose ya no hará caso a la parte de “build” y no creará la imagen porque ya la tenemos.
Podemos forzar a que solamente haga la construcción de la imagen:
docker compose build
Sería equivalente a hacer docker build -t pepito/nginx -f Dockerfile.nginx ./webs
El problema es que si la imagen ya existe, Docker Compose no la creará de nuevo (aunque hayamos hecho cambios en el Dockerfile). Para forzarlo:
docker compose up --build
Práctica V
Partiendo del ejemplo anterior de wordpress con Docker Compose, modifica el script para que coja los ficheros del directorio wp-content de forma externa y persistente.
services: db: image: mariadb:10.6.4-focal command: '--default-authentication-plugin=mysql_native_password' volumes: - db_data:/var/lib/mysql restart: always environment: - MYSQL_ROOT_PASSWORD=somewordpress - MYSQL_DATABASE=wordpress - MYSQL_USER=wordpress - MYSQL_PASSWORD=wordpress expose: - 3306 - 33060 wordpress: image: wordpress:latest depends_on: - db ports: - 3080:80 restart: always environment: - WORDPRESS_DB_HOST=db - WORDPRESS_DB_USER=wordpress - WORDPRESS_DB_PASSWORD=wordpress - WORDPRESS_DB_NAME=wordpress volumes: # opción 'named volume'': - wp_data:/var/www/html/wp-content # opción 'bind': # - ./wp_data:/var/www/html/wp-content volumes: db_data: wp_data:
Práctica X
Partiendo de este Dockerfile:
FROM tomcat:9.0-jdk8 WORKDIR /usr/local/tomcat/webapps COPY ROOT.war
Transformarlo en un docker-compose.yml para poder usarlo con Docker Compose.
--- version: '3.0' services: web: image: tomcat:9.0-jdk8 volumes: - ./ROOT.war:/usr/local/tomcat/webapps
Práctica XI
Crear un docker-compose.yml que tenga un servicio web compuesto por una imagen personalizada con Apache + código (ver el Dockerfile). Además deberá ir en el mismo docker-compose.yml un contenedor de MySQL y uno con phpMyAdmin
Dockerfile:
FROM php:8.2.11-apache COPY web.tar.gz /var/www/html
Solución
Creamos un directorio web con:
Dockerfile.apachefiles.tar.gz(los archivos de nuestra web)
En el directorio padre, creamos un fichero docker-compose.yml:
services: web: image: pepito/apache build: context: web dockerfile: Dockerfile.apache ports: - 80:80 db: image: mysql volumes: - db_data:/var/lib/mysql environment: MYSQL_ROOT_PASSWORD: abcd1234. MYSQL_DATABASE: dbname MYSQL_USER: db_user MYSQL_PASSWORD: dbpass restart: always dbadmin: image: phpmyadmin restart: always environment: - PMA_ARBITRARY=1 # - PMA_HOST=db # - PMA_USER=db_user # - PMA_PASSWORD=dbpass ports: - 8080:80 volumes: db_data:
Práctica guiada
Creando una imagen paso a paso
Partiendo de un Dockerfile:
# # Comments start with an octothorpe, as you might expect # # Specify the 'base image' FROM ubuntu:latest # # Naming the maintainer is good practice LABEL Author="Your Name" Email="your@email.address" # # The 'LABEL' directive takes arbitrary key=value pairs LABEL Description="This is my personal flavor of Ubuntu" Vendor="Your Name" Version="1.0" # # Now tell ubuntu to update itself RUN apt-get update -y
You can have multiple RUN commands, though you should check out the Best practices for a comment about that.
You tell docker to build an image with that dockerfile by using the docker build command. We’ll give it a tag with the --tag option, and we tell it which dockerfile to use with the --file option.
We also have to give it a context to build from, so we give it the current directory .. If we add files, they will be taken relative to that context. The context can also be a URL, see https://docs.docker.com/engine/reference/commandline/build/ for full details.
N.B. This assumes you have the USER environment variable set in your environment
docker build --tag $USER:ubuntu --file Dockerfile.01 .
Now, you can see your image with the docker images command:
docker images
Our new image is there, and it’s about 40 MB bigger than the image we started from, because of the updates we applied.
We can now run that image and check that it really is updated by trying to apply the updates again, there should be nothing new to do:
docker run -t -i $USER:ubuntu /bin/bash
root@4989d23e6e8b:/# apt-get update -y Hit:1 http://archive.ubuntu.com/ubuntu xenial InRelease Hit:2 http://archive.ubuntu.com/ubuntu xenial-updates InRelease Hit:3 http://archive.ubuntu.com/ubuntu xenial-security InRelease Reading package lists... Done root@4989d23e6e8b:/# exit
As expected, there’s nothing new to apply.
Inspecting an image to find out how it was built
A brief aside, if you want to find out how a container was built, you can use the docker inspect command. It gives full details as a JSON document, more than you’d normally want to know, but we can at least use it to get back the MAINTAINER and LABELS we added:
docker inspect $USER:ubuntu | grep --after-context=6 Labels
Salida:
"Labels": {
"Author": "Your Name",
"Description": "This is my personal flavor of Ubuntu",
"Email": "your@email.address",
"Vendor": "Your Name",
"Version": "1.0"
}
--
"Labels": {
"Author": "Your Name",
"Description": "This is my personal flavor of Ubuntu",
"Email": "your@email.address",
"Vendor": "Your Name",
"Version": "1.0"
}
Why do the LABELS we specified appear twice? I don’t know…
Adding our own programs to the image
Tenemos el siguiente script de Perl:
#!/usr/bin/perl # Modules used use strict; use warnings; # Print function print("Hello World\n");
Let’s tell docker to add that script to the image, so we can run it as an application.
We’ll use Dockerfile.02, which has the following content:
FROM ubuntu:latest LABEL Author="Your Name" Email="your@email.address" RUN apt-get update -y RUN apt-get install -y -perl # # Set an environment variable in the container ENV MY_NAME Tony # Add our perl script ADD hello-user.pl /app/hello.pl RUN chmod +x /app/hello-user.pl
You can see that we’ve set an environment variable in our image (MY_NAME) and we’ve added our script as /app/hello.pl. You can have as many ENV and ADD sections as you like, though as with the RUN section, it’s worth learning about the best practices before adding too many.
Now build the image:
docker build --tag $USER:ubuntu --file Dockerfile.02 .
Ejemplo de salida:
Sending build context to Docker daemon 95.74kB Step 1/6 : FROM ubuntu:latest ---> 7698f282e524 Step 2/6 : LABEL Author="Your Name" Email="your@email.address" ---> Using cache ---> 4da140dc87fa Step 3/6 : LABEL Description="This is my personal flavor of Ubuntu" Vendor="Your Name" Version="1.0" ---> Using cache ---> a6f0cc9d1234 Step 4/6 : RUN apt-get update -y ---> Using cache ---> 2f162cdbcc1e Step 5/6 : ENV MY_NAME Tony ---> Running in b166b73c2eb0 Removing intermediate container b166b73c2eb0 ---> 4d2ba043c256 Step 6/6 : ADD hello-user.pl /app/hello.pl ---> d83241a70a07 Successfully built d83241a70a07 Successfully tagged wildish:ubuntu
Note steps 1 through 4, where the cache was used to save time building the image. I.e. we didn’t have to build the entire image from scratch, and apply the updates again.
We’ve re-used the tag ($USER:ubuntu), so this version will replace the old one. That’s not a good idea if the image is already in use in production, of course!
Now let’s run the app in the image:
docker run -t -i --rm $USER:ubuntu /app/hello.pl
Salida:
Hello Tony
What happens if we update our script, will docker be smart enough to pick up the changes? Yes, up to a point.
Let’s start by copying a new version of the script in place, and re-build the image:
#!/usr/bin/perl # Modules used use strict; use warnings; use Env; # Print function print("Hello $ENV{MY_NAME}\n");
cp hello-user-with-args.pl hello-user.pl
docker build --tag $USER:ubuntu --file Dockerfile.02 .
Ejemplo de salida:
Sending build context to Docker daemon 81.92 kB Step 1 : FROM ubuntu:latest ---> 4ca3a192ff2a Step 2 : MAINTAINER Your Name "your@email.address" ---> Using cache ---> ea59cb99c816 Step 3 : LABEL Description "This is my personal flavor of Ubuntu" Vendor "Your Name" Version "1.0" ---> Using cache ---> 241e336f1ef1 Step 4 : RUN apt-get update -y ---> Using cache ---> 312bd6b10add Step 5 : ENV MY_NAME Tony ---> Using cache ---> 0857feeb7bb0 Step 6 : ADD hello-user.pl /app/hello.pl ---> ae442bdee840 Removing intermediate container 5fe5c7d58e0d Successfully built ae442bdee840
Step 6 didn’t use the cache, because docker noticed the script had been updated.
However, if the script itself hadn’t changed, but modules or libraries that it uses have changed, docker wouldn’t be able to pick that up on its own. Put differently, the build process can’t ‘see through’ commands like apt-get update -y to know that there are changes since it was last run.
In case you want to, you can force a re-build from the start by telling docker not to use the cache:
docker build --no-cache --tag $USER:ubuntu --file Dockerfile.02 .
Passing arguments to an application in an image
Can we change who it says hello to? Yes, we can! We can set environment variables in the container before the application runs by using the --env flag with docker run:
docker run -t -i --rm --env MY_NAME=Whoever $USER:ubuntu /app/hello.pl
La versión abreviada de --env es -e. Si queremos poner más de una variable, tenemos que ir poniendo tantos -e como variables necesitemos.
Respuesta:
Hello Whoever
The new version uses the environment variable MY_NAME by default, as before, but also allows you to override that by giving command-line options. To do that, simply append the arguments to the end of the docker run command:
docker run --rm -ti $USER:ubuntu /app/hello.pl someone
Salida:
Hello someone
Running an application by default
Finally, let’s try getting our application to run by default, so we don’t have to remember the path to it whenever we want to run it. Dockerfile.03 shows how to do that:
FROM ubuntu:latest LABEL Author="Your Name" Email="your@email.address" # # The 'LABEL' directive takes arbitrary key=value pairs LABEL Description="This is my personal flavor of Ubuntu" Vendor="Your Name" Version="1.0" # # Now tell ubuntu to update itself RUN apt-get update -y # RUN apt-get install -y perl # Set an environment variable in the container ENV MY_NAME Tony ADD hello-user.pl /app/hello.pl # Specify the command to run! RUN chmod +x /app/hello.pl CMD /app/hello.pl
So, build it, then run it:
docker build --tag $USER:ubuntu --file Dockerfile.03 .
docker run --rm -ti $USER:ubuntu
Salida:
Hello Tony
Optimizing builds
We saw that docker build --no-cache … solves the problem of docker not knowing if something was updated, but doing everything from scratch can be a bit expensive. The obvious solution is to build intermediate images, and move the more stable stuff into the earlier images. Take a look at Dockerfile.04.base:
FROM ubuntu:latest LABEL Author="Your Name" Email="your@email.address" RUN apt-get update -y RUN apt-get install -y perl
and Dockerfile.04.app:
FROM usuario:ubuntu ENV MY_NAME Tony ADD hello-user.pl /app/hello.pl RUN chmod +x /app/hello.pl CMD /app/hello.pl
They’re just Dockerfile.03 split into two parts. Dockerfile.04.base builds an updated ubuntu image, while Dockerfile.04.app uses that image as its base. As long as we tag the base image as $USER:ubuntu, and refer to it correctly in the FROM statement for the app, the app will find it correctly. We can’t use the environment variable in the FROM statement for the app, so we have to hard-code the user name there. Change it to your own user name before building the image.
Note also that Dockerfile.04.app doesn’t have a MAINTAINER or LABEL section, which means it will inherit them from the base image.
Now we can build our app in two stages:
docker build --tag $USER:ubuntu --file Dockerfile.04.base .
docker build --tag $USER:hello --file Dockerfile.04.app .
Creamos un contenedor a partir de la última imagen:
docker run --rm -ti $USER:hello
Salida:
Hello Tony
If we force a rebuild of the app, it’s very quick now, because it doesn’t have to update the base ubuntu operating system.
Ficheros de entorno
dbname=valor1 dbuser=valor2 dbpass=valor3
En el docker-compose.yml:
services: (...) db: env_file: - .env.desarrollo environment: - MYSQL_DATABASE=$dbname - MYSQL_USER=$dbuser - MYSQL_PASSWORD=$dbpass
En línea de comandos también lo podríamos indicar:
docker compose up --env-file=.env.desarrollo
Por defecto, el fichero de configuración que cogerá Compose se llamará .env
