Herramientas de usuario

Herramientas del sitio


informatica:sistemas_operativos:cursos:docker_avanzado:uso_de_docker_en_desarrollo

¡Esta es una revisión vieja del documento!


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.

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 build con 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 FROM le dice a Docker qué base desea utilizar para su imagen
    • Se puede indicar el nombre de la imagen seguido de su etiqueta
  • El comando LABEL puede 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 RUN nos permite ejecutar sentencias sobre una imagen
    • Cada sentencia RUN producirá 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 COPY o ADD
    • COPY files/nginx.conf /etc/nginx/nginx.conf
    • COPY files/default.conf /etc/nginx/conf.d/default.conf
    • ADD files/html.tar.gz /usr/share/nginx/
    • El comando COPY es el más sencillo, coge el fichero origen y lo copia en el destino indicado dentro de la imagen
    • El comando ADD es 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:
  • Una de las últimas secciones de un Dockfile suele ser (si existe) el ENTRYPOINT y/o CMD
    • Con estas secciones podemos indicar comandos que serán ejecutados una vez se inicie el container
    • ENTRYPOINT puede ser usado en combinación con CMD, donde ENTRYPOINT establece la base de los comandos y CMD lo 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 imagen
  • WORKDIR: establece el directorio de trabajo para los los bloques RUN, CMD y ENTRYPOINT
  • ENV: 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

  1. 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
  2. Empaqueta la aplicación en un WAR
  3. 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 usar
  • dst: 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.apache
  • files.tar.gz (los archivos de nuestra web)

En el directorio padre, creamos un fichero docker-compose.yml:

services:
  web:
    image: pepito/php-web
    build:
      context: web
      dockerfile: Dockerfile
    ports:
      - 80:80

  db:
    image: mysql
    environment:
      MYSQL_ROOT_PASSWORD: abcd1234.
    restart: always

  phpmyadmin:
    image: phpmyadmin
    restart: always
    environment:
      - PMA_ARBITRARY=1
    ports:
      - 8080:80

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.

informatica/sistemas_operativos/cursos/docker_avanzado/uso_de_docker_en_desarrollo.1697470976.txt.gz · Última modificación: por tempwin