Introducción a Dockerfile

Agustin Bassi

May 2, 2021 ‧ 30 min estimados  ‧  #docker #dockerfile #custom-image

Contenido

Objetivos

Introducción a Dockerfile

¿Cuándo conviene utilizar un Dockerfile?

¿Cómo se utiliza un Dockerfile?

Ejemplo de aplicación de Dockerfile

Caché de imágenes

Recomendaciones generales

Crear cuenta en Dockerhub

Conclusiones

Bibliografía

Licencia

Objetivos

Lo que vas a ver en este documento son los siguientes temas:

Introducción a Dockerfile

Docker es una herramienta que permite automatizar la creación y el despliegue de aplicaciones utilizando contenedores de software. A la hora de trabajar con Docker podés utilizar imágenes de registros públicos como Dockerhub y también podés crear tus propias imágenes con las necesidades de cada proyecto.

Para crear una imagen personalizada es necesario que utilices Dockerfile, un archivo que contiene las instrucciones necesarias para la creación de la imagen, para que luego de compilarla puedas ejecutar sus contenedores. En la figura siguiente podés ver un diagrama.

Una imagen es un snapshot (captura) de un sistema de archivos creado a partir de distintas capas. Internamente Docker utiliza el sistema de archivos UnionFS para unir las capas y crear las imágenes.

Lo más común a la hora de trabajar con imágenes personalizadas de Docker es partir de una imagen base que posea la mayor cantidad de funcionalidades que necesitas, y luego ir agregando capas adicionales con tus necesidades, a las capas que ya existen en la imagen base. Este mecanismo resulta bastante eficiente, ya que una imágen personalizada de Docker sólo ocupa la diferencia con la imagen inicial.

También es posible crear tu propia imagen base. Este es un paso avanzado, y podés encontrar más información al respecto en este link de documentación oficial de Docker.

Para que tengas una idea general, el proceso de crear una imagen de Docker a medida se compone de los siguientes pasos:

  1. Crear un archivo Dockerfile.
  2. Definir las instrucciones para construir la imagen dentro del archivo Dockerfile.
  3. Ejecutar el comando para compilar la imagen.
  4. Ejecutar el comando para iniciar contenedores a partir de la nueva imagen.

¿Cuándo conviene utilizar un Dockerfile?

Los proveedores y desarrolladores de tecnología (bases de datos, servidores, herramientas, etc.) proporcionan sus imágenes genéricas.

Si bien para varios proyectos estas imágenes por defecto cumplen con lo que necesitas, cuando querés personalizar algún funcionamiento es necesario que armes una imagen a medida.

Esto sucede bastante seguido a la hora de trabajar con Docker, por lo que aprender a utilizar Dockerfile es una parte esencial del proceso.

¿Cómo se utiliza un Dockerfile?

Docker provee un conjunto de instrucciones que deben ser utilizadas dentro de los archivos Dockerfile, para que se pueda crear una imagen.

Básicamente, dentro del Dockerfile tenés que realizar una descripción de los pasos y comandos necesarios para construir la imagen. Una vez que tenés listo el archivo Dockerfile con los comandos necesarios, lo podés compilar y luego crear contenedores de esa imagen con el comando docker run.

Además de utilizar los comandos de Docker dentro de un Dockerfile, es necesario que conozcas el contexto que necesitan las diferentes tecnologías o herramientas que utilices, y eso depende de cada proyecto en particular.

Por ejemplo, para crear una imagen de PHP para un proyecto web, además de configurar el Dockerfile, es necesario que conozcas los detalles específicos como métodos de configuración, extensiones, variables de entorno, entre otros.

Yendo a lo pragmático, antes de crear una imagen a medida es conveniente que primero busques en Dockerhub si ya no existe eso que estás buscando, o al menos ver si hay una imagen lo más parecida posible a lo que necesitas. Si tenés suerte comenzá descargandola a tu máquina con docker pull. También en caso que la imagen tenga un buen README leelo bien para informarte de todos los detalles que necesitás saber. Cuando ya estés más o menos en tema empezá a experimentar ejecutando la imagen de distintas maneras, probando puertos, volúmenes, redes y demás. Esto te permite comenzar con una tecnología en poco tiempo ya que la podés utilizar sin entender todos los detalles. A medida que la vas usando vas a entender los detalles más específicos.

Si no tuviste suerte a la hora de encontrar una imagen parecida, podés seguir la guía de pasos a continuación que te van a llevar por el proceso de crear una imagen a medida, que puede ser un buen punto de partida para que tengas un mayor entendimiento del ecosistema Docker.

Ejemplo de aplicación de Dockerfile

El ejemplo de uso de Dockerfile que vamos a ver consiste en partir de una imagen base de Alpine Linux obtenida de Dockerhub, a la cual le vamos a instalar las herramientas Vim, Curl y Git.

La imagen a crear no cumple ninguna función real en una aplicación, pero muestra todos los pasos necesarios para crear una imagen a medida de manera sencilla.

0. Probar la imagen base

Una buena forma de entender lo que vamos a hacer es que primero descargues la imagen base y veas si las herramientas Vim, Curl o Git se encuentran en la imagen base. Para ello comenzá descargando la imagen base.

docker pull alpine:3.4

Una vez que se haya descargado, ejecuta este comando para correr el shell dentro del contenedor.

docker run --rm --interactive alpine:3.4 /bin/sh

Una vez que ejecutes el comando ya vas a estar dentro del contenedor. Ahora lo que vamos a probar es chequear las versiones de Vim, Curl y Git. Para cada uno de los comandos deberías ver una salida como la siguiente.

/ # vim --version

sh: vim: not found

/ # curl --version

sh: curl: not found

/ # git --version

sh: git: not found

Como podrás notar, la imagen base de Alpine no cuenta con ninguna de estas herramientas. Ahora sí podemos comenzar con el proceso.

1. Crear el archivo Dockerfile

Como primer paso crea una carpeta llamada dockerfile_test y dentro un archivo llamado Dockerfile (sin extensión). Desde Linux podés utilizar estos comandos.

mkdir dockerfile_test && cd dockerfile_test/
touch Dockerfile

2. Definir la imagen base con la instrucción FROM

Todo Dockerfile debe comenzar con la instrucción FROM, que indica el punto de partida para comenzar a construirse. Generalmente se comienza la construcción a partir de otras imágenes aunque también es posible comenzar de una imagen vacía.

Las imágenes las podés descargar de cualquier registry, ya sea público o privado. La imagen desde donde se comienza a construir se llama “imagen base” y para este ejemplo es alpine:3.4. Agregá la instrucción FROM con la imagen base al Dockerfile.

FROM alpine:3.4

3. Agregar los paquetes a instalar

Ahora que ya tenemos la base es necesario instalar los paquetes que queremos. Agreguemos las líneas para descargar los paquetes. En este punto, el archivo debería verse de la siguiente manera.

FROM alpine:3.4

RUN apk update && \
   apk
add vim && \
   apk add curl

4. Agregar la acción por defecto

Toda imagen de Docker debe realizar alguna acción al iniciar si no se le especifica ninguna acción en particular al momento de ejecutar el comando docker run. Para este caso vamos a hacer que se corra la shell al iniciar el contenedor a través de la instrucción CMD. Agregando esta línea, el archivo Dockerfile debería verse así.

FROM alpine:3.4

RUN apk update && \
   apk
add vim && \
   apk add curl

CMD ["/bin/sh"]

5. Construir la imagen

Con el archivo Dockerfile listo ya podemos construir la imagen. Abrí una terminal dentro de la carpeta dockerfile_test y corré el siguiente comando para construir la imagen.

docker build --tag local/alpine-iot:latest .

El comando se estructura de la siguiente manera:

En la terminal deberías ver una salida similar a la siguiente.

Sending build context to Docker daemon  2.048kB
Step 1/3 : FROM alpine:3.4
3.4: Pulling from library/alpine
Step 2/3 : RUN apk update && apk add vim && apk add curl && apk add git
---> Running in e2dd06c49534
Step 3/3 : CMD ["/bin/sh"]
---> Running in 9c8e5d212b88
Removing intermediate container 9c8e5d212b88

 ---> 9d8ed3dbe13b

Successfully built 9d8ed3dbe13b

Successfully tagged local/alpine-iot:latest

6. Correr la imagen

En el paso anterior Docker creó una imagen a partir del Dockerfile. En la lista de imágenes del sistema debería aparecer la imagen que recién creaste. Ejecuta en la terminal el comando docker images para ver sus detalles.

REPOSITORY         TAG     IMAGE ID         CREATED             SIZE
local/alpine-iot   latest  31b378c0129e     4 minutes ago       32.2MB

La imagen ya está creada. Ahora vamos a correr el contenedor para ver lo que hay dentro con este comando.

docker run --rm --interactive local/alpine-iot:latest

Si no hubo errores te debería aparecer un prompt (#) en la terminal que indica que estás dentro del contenedor (en algunos casos puede que el # no aparezca pero de todas maneras estás dentro del contenedor). Chequeá las versiones de vim y curl ejecutando los comandos vim --version y curl --version. En la terminal debería verse una salida como esta:

/ # vim --version
VIM - Vi IMproved 7.4 (2013 Aug 10, compiled Feb 16 2017 11:25:35)
/ # curl --version
curl 7.60.0 (x86_64-alpine-linux-musl)

Como podés ver, dentro de la imagen que recién creamos existen estas dos herramientas nuevas que no existían en la imagen base (cuando ejecutamos el paso 0).

Caché de imágenes

Hasta ahora instalamos Vim y Curl dentro de la imagen y aún queda pendiente instalar Git. La razón por la que no lo instalamos todavía fue para demostrar el funcionamiento del caché de Docker.

Como podés ver en el proceso de construcción (punto 5), los pasos necesarios para construir la imagen en ese punto eran 3 (uno para cada comando especificado en el Dockerfile). Si miras en detalle, para cada paso se ejecuta un nuevo contenedor como capa intermedia que queda guardada en caché.

Podés ver esto cuando se muestra en la salida "---> Running in e2dd06c49534". Por ejemplo, la primera capa es descargar la imagen base de Alpine, luego se actualiza el repositorio de aplicaciones y se instalan Vim y Curl, y finalmente se crea una capa para CMD.

Si corrieras nuevamente el comando verías que el tiempo de ejecución es prácticamente instantáneo, ya que los pasos intermedios están guardados en la memoria caché. Para corroborarlo, corré el comando docker images -a, y chequeá que las imágenes <none> tienen los mismos IDs que los mostrados en el proceso de construcción.

REPOSITORY         TAG     IMAGE ID         CREATED              SIZE
local/alpine-iot   1.0     31b378c0129e     About an hour ago    32.2MB
<none>             <none>  ef72ae3086ea     About an hour ago    30.6MB
<none>             <none>  31817a6cdbf5     About an hour ago    5.58MB

Finalmente, agregá la línea para instalar Git al Dockerfile. Debería quedarte así:

FROM alpine:3.4

RUN apk update && \

    apk add vim && \

    apk add curl && \

    apk add git


CMD ["/bin/sh"]

Ahora corré nuevamente el comando para construir la imagen.

docker build --tag local/alpine-iot:latest .

Como podés ver en la salida, Docker hace uso del caché para las capas ya existentes (fijate que en la salida aparece Using cache para los pasos intermedios).

En caso que no quieras utilizar el caché de imágenes de Docker, podés especificar la opción --no-cache=True al momento de ejecutar el comando docker build.

Una vez compilado accedé nuevamente al contenedor para ver lo que hay dentro.

docker run --rm --interactive local/alpine-iot:latest

Chequeá la versión de Git con el comando a continuación, que te va a demostrar que la herramienta está instalada dentro de la nueva imagen construida.

/ # git --version
git version 2.8.6

Si listás nuevamente las imágenes con docker images -a vas a ver que hay una nueva imagen sin nombre ni tag (<none>), que es la imagen que se construyó previamente al instalar Git. Docker llama a estas imágenes Dangling images, y lo mejor es borrarlas una vez que terminaste de construir la imagen ya que no cumplen ninguna función. Corré el siguiente comando para eliminarlas todas juntas en un mismo paso.

docker rmi -f $(docker images -f "dangling=true" -q)

A través de los pasos que fuimos realizando pudiste ver cómo crear una imagen de Docker a medida. Como nombramos anteriormente, saber crear imágenes es una parte esencial a la hora de trabajar con el ecosistema Docker.

Recomendaciones generales

En esta sección vamos a ver algunas recomendaciones generales a la hora de utilizar Dockerfiles y crear imágenes a medida. Si querés ampliar en el tema podés encontrar más información en este link.

Flujo de trabajo

Escribir Dockerfiles es relativamente simple. La parte más compleja es saber los detalles necesarios para crear correctamente un entorno de trabajo. Esto depende fuertemente de la tecnología que estés utilizando, por lo que es necesario leer su documentación.

Más allá de la tecnología, una buena metodología de trabajo es la siguiente:

  1. Partir de la imagen correcta: El primer paso es chequear si la imagen necesaria existe en algún registry. En el caso negativo, chequeá las imágenes propias creadas previamente para extraer información útil sobre comandos.
  2. Construir el entorno: Descargá la imagen que mejor te parezca y explorará de manera interactiva. Probá diferentes configuraciones desde la ejecución del contenedor, por ejemplo compartir volúmenes, exposición de puertos, comandos internos, entre otros.
  3. Agregar los pasos al Dockerfile para construir la imagen: A medida que se prueban los comandos desde la terminal ir agregandolos al Dockerfile. Esto se debe ir haciendo a medida que se prueban los comandos de manera interactiva. Cuando agregues nuevos comandos probá el proceso de construcción nuevamente, ya que puede dañarse con comandos erróneos. Cuando construiste la imagen, probá que las nuevas configuraciones funcionan como se espera.
  4. Subir los nuevos cambios (opcional): En caso que quieras subir los cambios a un registry, ya sea público o privado, es en este momento donde debes realizarlo.
  5. Repetir los pasos 2 y 3: Repetí los pasos anteriores hasta llegar al punto deseado de manera iterativa.

Un contenedor no es una máquina virtual

Idealmente un contenedor debería ejecutar únicamente un solo proceso. Deberías diseñarlos como si se tratase de un binario ejecutable, esto facilita su portabilidad y despliegue.

También tenés que tener en cuenta lo que se instala dentro de la imagen (NO ES UNA MÁQUINA VIRTUAL). Cualquier herramienta que no sea indispensable para la ejecución del proceso no deberías incluirla.

Minimizar el contenido a lo estrictamente indispensable

Durante el proceso de creación de la imagen todo el directorio donde se encuentre el Dockerfile es cargado en el Build Context, aunque como mencionamos en el punto anterior, deberías incluir solo lo necesario para la correcta ejecución del contenedor. Si hay más archivos o directorios no indispensables es conveniente utilizar un archivo .dockerignore (análogo a .gitignore) para evitar incluir archivos o directorios innecesarios.

Optimizar Instrucciones

Si utilizas el menor número de instrucciones posible la imagen tendrá menos capas y será más liviana. Una opción es encadenar comandos en una sola instrucción.

Internamente Docker crea cachés de cada capa que utiliza para la creación de una imagen específica. Si utilizas esta técnica, podés lograr mejores tiempos de compilación y menos espacio para construir una imagen. Por ejemplo, en lugar de realizar estas instrucciones dentro del Dockerfile.

RUN apk update
RUN apk add vim
RUN apk add curl

RUN apk add git

Podés reescribirlas de la siguiente manera, generando así una única capa y optimizando así el tiempo de ejecución.

RUN apk update && \

    apk add vim && \

    apk add curl && \

    apk add git

Ejecutable por defecto

Todas las imágenes deben indicar siempre una acción por defecto al iniciar un contenedor. En caso que la imagen esté diseñada para ejecutarse como un binario, la instrucción a utilizar debe ser ENTRYPOINT. De otro modo debes utilizar la instrucción CMD.

Crear cuenta en Dockerhub

Para que puedas subir imágenes personalizadas y que otras personas puedan descargar tus imágenes creadas, podés crear una cuenta en el registry público Dockerhub (opción Sign Up). Es necesario que verifiques tu email, y luego podes seguir esta secuencia de pasos:

  1. Loggearse a Dockerhub con el usuario y contraseña que asignaste.
  2. Crear un repositorio en Dockerhub (ir a la pestaña Repositories).
  3. Elegir un nombre y una descripción para el repositorio (puede ser alpine-iot).
  4. Loggearse en Dockerhub desde la terminal (te pedirá la contraseña).

docker login --username=yourhubusername

Aparecerá un warning que la contraseña está alojada sin encriptar. Si querés podes crear una key para securizar el proceso.

  1. Taggear la imagen a pushear con el repositorio donde se va a subir.

docker tag local/alpine-iot:latest username/repository:latest

  1. Finalmente pushear la imagen.

docker tag local/alpine-iot:latest username/repository:latest

Con los pasos anteriores, la imagen estará disponible en Dockerhub para ser descargada. Intentá ahora descargarla con el siguiente comando.

docker pull username/repository:latest

Una vez que termine el proceso, en el listado de imágenes del sistema debería aparecer la nueva imagen. Corré el comando docker images para chequearlo.

Conclusiones

En este artículo vimos una parte muy importante a la hora de trabajar con el ecosistema Docker que se puede resumir en los siguientes temas:

Bibliografía

Licencia

Este material es distribuido bajo licencia Creative Commons BY-SA 4.0. Podés encontrar detalles sobre el uso del material en este link.