En un artículo anterior ya vimos las bases de cómo usar imágenes de Docker. Pero en aquel caso usamos imágenes construidas por otros. Eso está muy bien si encuentras la imagen que estás buscando, pero qué pasa si ninguna cubre tus necesidades?
Para cocinar necesitas una receta
Partiendo de que hayas seguido el tutorial que enlazaba al principio, ya tendrás Docker instalado en tu Linux. Con eso, necesitarás una receta para decirle a Docker los pasos para "cocinar" tu imagen. Esa receta es un fichero llamado Dockerfile que hay que crear en la carpeta que más te convenga en la que habrás de incluir cualquier fichero que quiera meter en tu imagen.
¿Qué cocinas con esa receta? una imagen, ¿pero qué es una imagen de Docker?. Una imagen de docker es un sistema operativo virtual con una configuración específica y un conjunto concreto de dependencias instaladas. Cómo utilices esa imagen depende de ti. Algunas imágenes están pensadas para ser usadas como base en las que instalar nuestra aplicación para que se ejecute dentro. Otras imágenes son más específicas y ya traen una aplicación instalada para ser ejecutada en cuanto se arranque la imagen. Haciendo una analogía con la Programación Orientada a Objetos (OOP en inglés), una imagen de Docker sería una clase, mientras que un contenedor sería una instancia de esa clase (también denominada "objeto" en OOP). No es raro encontrar múltiples contenedores ejecutándose simultáneamente después de ser iniciados de la misma imagen.
Para entender cómo funciona un Dockerfile, vamos a analizar algunos ejemplos.
Vamos a empezar con este Dockerfile del proyecto vdist. La imagen que se crea con ese Dockerfile está pensada para compilar una distribución de Python y ejecutar una aplicación llamada fpm sobre una carpeta creada sobre la marcha cl arrancar el contenedor basado en la imagen. Por lo tanto, este Dockerfile se encarga de instalar todas las dependencias necesarias para que la imagen quede preparada para que sus contenedores puedan compilar la distribución de Python y ejecutar fpm.
El primer paso en todos los Dockerfile es definir qué imagen vamos a usar como punto de partida. Continuando la metáfora de la OOP, si nuestra imagen es una clase, en este punto definimos de qué otra clase hereda la nuestra.
En este caso, nuestra imagen se deriva de la imagen ubuntu:latest, pero para tus imágenes puedes usar cualquier otra imagen disponible en Docker Hub. Sólo asegúrate de echarle un ojo a la página de la imagen en Docker Hub para averiguar cual es la etiqueta que más se ajuste a lo que necesites. Una vez que subas tu imagen a Docker Hub otros podrán usarla como punto de partida para sus respectivas imágenes.
Todos las obras de arte deberían firmarse. A tu imagen le pasa lo mismo, por lo que debemos definir metadatos para hacerlo.
La miga empieza realmente con los comandos RUN. Esos son lo que usas para configurar tu imagen.
Algunas personas que crean una imagen por primera vez confunden lo que hacen realmente los comandos RUN. Estos comandos no se ejecutan cuando se crea un contenedor de una imagen, con el comando "docker run". En vez de eso, se ejecutan sólo una vez por parte del comando "docker build", para crear una imagen de un Dockerfile.
El conjunto exacto de comandos RUN para una imagen dependerá de los que necesite el proyecto respectivo. En este ejemplo, los comando RUN comprueban que la lista de fuentes de apt son las correctas, actualizan la base de datos de apt e instalan un conjunto de dependencias usando tanto "apt-get" como "gem install".
Te recomiendo que comiences el apartado de los comandos RUN con un "RUN set -e" de manera que el proceso de construcción finalice con un error si cualquiera de los comandos RUN devuelve un error. Esto puede parecer una medida extrema, pero es la mejor para asegurar que no subas una imagen con un error inadvertido.
Además de eso, cuando revises Dockerfiles de otros proyectos te darás cuenta que muchos de ellos incluyen varios comandos de shell dentro del mismo comando RUN, como pasa en las líneas 14 a 16 de nuestro ejemplo. La gente de Docker recomienda juntar dentro de un mismo comando RUN aquellos comandos de shell que están relacionados entre si (es decir, si dos comandos no tienen sentido separados, siendo ejecutados sin el otro, deberían ser incluidos dentro del mismo comando RUN). Esto se debe a la estructura en capas con la que se construyen las imágenes de Docker, en las que cada comando RUN conforma una capa separada. Si sigues el consejo de la gente de Docker, tus imágenes serán más rápidas de reconstruir cuando realices un cambio sobre su Dockerfile. Para incluir varios comandos de shell en el mismo comando RUN, recuerda separar esos comandos en una línea por cada comando de shell y pon al final de cada línea un "&& \" para encadenar las sucesivas líneas (excepto en la última, como se puede ver en el ejemplo).
Aparte de los comandos RUN, hay otros que deberías conocer. Vamos a analizar el Dockerfile de mi proyecto markdown2man. El propósito de esa imagen es ejecutar un script de Python para hacer que Pandoc realice la conversión de un fichero usando los argumentos pasados por el usuario al arrancar el contenedor con "docker run". Para ello, verás que algunos de los comandos empleados en el Dockerfile ya resultan familiares:
Hora de cocinar
dante@Camelot:~/Projects/markdown2man$ docker build -t dantesignal31/markdown2man:latest . Sending build context to Docker daemon 20.42MB Step 1/14 : FROM python:3.8 ---> 67ec76d9f73b Step 2/14 : LABEL maintainer="dante-signal31 (dante.signal31@gmail.com)" ---> Using cache ---> ca94c01e56af Step 3/14 : LABEL description="Image to run markdown2man GitHub Action." ---> Using cache ---> b749bd5d4bab Step 4/14 : LABEL homepage="https://github.com/dante-signal31/markdown2man" ---> Using cache ---> 0869d30775e0 Step 5/14 : RUN set -e ---> Using cache ---> 381750ae4a4f Step 6/14 : RUN apt-get update && apt-get install pandoc -y ---> Using cache ---> 8538fe6f0c06 Step 7/14 : ENV SCRIPT_PATH /script ---> Using cache ---> 25b4b27451c6 Step 8/14 : COPY requirements.txt $SCRIPT_PATH/ ---> Using cache ---> 03c97cc6fce4 Step 9/14 : RUN pip install --no-cache-dir -r $SCRIPT_PATH/requirements.txt ---> Using cache ---> ccb0ee22664d Step 10/14 : COPY src/lib/* $SCRIPT_PATH/lib/ ---> d447ceaa00db Step 11/14 : COPY src/markdown2man.py $SCRIPT_PATH/ ---> 923dd9c2c1d0 Step 12/14 : RUN chmod 755 $SCRIPT_PATH/markdown2man.py ---> Running in 30d8cf7e0586 Removing intermediate container 30d8cf7e0586 ---> f8386844eab5 Step 13/14 : RUN ln -s $SCRIPT_PATH/markdown2man.py /usr/bin/markdown2man ---> Running in aa612bf91a2a Removing intermediate container aa612bf91a2a ---> 40da567a99b9 Step 14/14 : ENTRYPOINT ["markdown2man"] ---> Running in aaa4774f9a1a Removing intermediate container aaa4774f9a1a ---> 16baba45e7aa Successfully built 16baba45e7aa Successfully tagged dantesignal31/markdown2man:latest
dante@Camelot:~$
Subir tu imagen a Docker Hub
dante@Camelot:~/Projects/markdown2man$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE dantesignal31/markdown2man latest 16baba45e7aa 15 minutes ago 1.11GB
dante@Camelot:~$
dante@Camelot:~/Projects/markdown2man$ docker login Authenticating with existing credentials... WARNING! Your password will be stored unencrypted in /home/dante/.docker/config.json. Configure a credential helper to remove this warning. See https://docs.docker.com/engine/reference/commandline/login/#credentials-store Login Succeeded dante@Camelot:~$
dante@Camelot:~/Projects/markdown2man$ docker push dantesignal31/markdown2man:latest The push refers to repository [docker.io/dantesignal31/markdown2man] 05271d7777b6: Pushed 67b7e520b6c7: Pushed 90ccec97ca8c: Pushed f8ffd19ea9bf: Pushed d637246b9604: Pushed 16c591e22029: Pushed 1a4ca4e12765: Pushed e9df9d3bdd45: Mounted from library/python 1271cc224a6b: Mounted from library/python 740ef99eafe1: Mounted from library/python b7b662b31e70: Mounted from library/python 6f5234c0aacd: Mounted from library/python 8a5844586fdb: Mounted from library/python a4aba4e59b40: Mounted from library/python 5499f2905579: Mounted from library/python a36ba9e322f7: Mounted from library/debian latest: digest: sha256:3e2a65f043306cc33e4504a5acb2728f675713c2c24de0b1a4bc970ae79b9ec8 size: 3680 dante@Camelot:~$