Dockerfile: qué hay tras este formato

El software de código abierto Docker se ha establecido como estándar para la visualización de contenedores. La virtualización de contenedores supone una continuación para el desarrollo de las máquinas virtuales, pero con una diferencia importante: en vez de simular un sistema operativo completo, se virtualiza una aplicación en un contenedor. Hoy día, los contenedores Docker se utilizan en todas las fases del ciclo vital del software, como el desarrollo, el testeo y la ejecución.

Dentro del ecosistema Docker existen distintos conceptos. Reconocer y entender estos componentes es esencial para trabajar de manera fluida con Docker. Además del Dockerfile, otros componentes importantes son la Docker Image y el Docker Container. En este artículo, te explicamos el Dockerfile en profundidad y te damos consejos prácticos para utilizarlo.

¿Qué es un Dockerfile?

El Dockerfile es la unidad fundamental del ecosistema Docker. Describe los pasos para crear una imagen de Docker. El flujo de información sigue este esquema central: Dockerfile > Docker Image > Docker Container.

Un contenedor Docker tiene una vida limitada e interactúa con su entorno. Imagina que contenedor es como un organismo vivo. Piensa en un organismo unicelular, como una célula de levadura. Siguiendo esta analogía, una imagen Docker equivale, digamos, a la información genética: todos los contenedores creados a partir de una imagen son iguales, como todos los organismos unicelulares clonados a partir de una unidad de información genética. Entonces, ¿cómo entra el Dockerfile en este esquema?

El Dockerfile define los pasos a seguir para crear una nueva imagen. Hay que entender que se empieza siempre con una imagen base existente. La nueva imagen nace de la imagen base. Además, hay ciertos cambios puntuales. En nuestro ejemplo de la célula de levadura, los cambios serían mutaciones. Un Dockerfile es determinante en dos aspectos de la nueva imagen de Docker:

  1. La imagen base de la que procede la nueva imagen. Esto ancla la nueva imagen al árbol genealógico del ecosistema Docker.
  2. Un conjunto de cambios específicos que diferencian la nueva imagen de la imagen base.

¿Cómo funciona un Dockerfile y cómo se crea una imagen a partir de él?

En el fondo, el Dockerfile es un archivo de texto totalmente normal. El Dockerfile contiene un conjunto de instrucciones, cada una en una línea distinta. Para crear una Docker Image, las instrucciones se ejecutan una tras otra. Quizás te suene este esquema de la ejecución de un script por lotes. Durante la ejecución, se añaden paso por paso más capas a la imagen. Te explicamos cómo funciona exactamente en nuestro artículo temático sobre Docker Image.

Una imagen Docker se crea ejecutando las instrucciones de un Dockerfile. Este paso se conoce como el proceso build y empieza con la ejecución del comando “docker build”. El contexto de construcción es un concepto crucial: define a qué archivos y directorios tiene acceso el proceso de construcción, donde un directorio local hace las veces de fuente. El contenido del directorio fuente se transfiere al Docker Daemon al accionar “docker build”. Las instrucciones contenidas en el Dockerfile reciben acceso a los archivos y directorios contenidos en el contexto de construcción.

A veces no queremos iniciar todos los archivos del directorio fuente local en el contexto build. Para estos casos existe el archivo .dockerignore, que sirve para excluir archivos y directorios del contexto de construcción y cuyo nombre se basa en el archivo .gitignore de Git. El punto antes del nombre del archivo indica que se trata de un archivo oculto.

¿Cómo se construye un Dockerfile?

Un Dockerfile es un archivo de texto simple que lleva el nombre de archivo “Dockerfile”. Ten en cuenta que es obligatorio que la primera letra sea mayúscula. El archivo contiene una entrada por fila. Te mostramos a continuación cómo se construye generalmente un Dockerfile:

# Comentario
INSTRUCCIONES Argumentos

Además de los comentarios, el Dockerfile contiene instrucciones y argumentos, que describen la construcción de las imágenes.

Comentarios e instrucciones del analizador

Los comentarios contienen información pensada principalmente para las personas. Conocido por los lenguajes de programación Python o Perl, los comentarios en un Dockerfile empiezan con una almohadilla (#). Durante el proceso de construcción, las filas de comentarios se eliminan antes del procesamiento, por lo que es importante tener en cuenta que solo se consideran como filas de comentarios las que empiezan con la almohadilla.

Este sería un comentario válido:

# nuestra Base Image
FROM busybox

En cambio, aquí se generaría un fallo, ya que la almohadilla no está al principio de la línea:

FROM busybox # nuestra Base Image

Como variante de los comentarios, están las instrucciones del analizador. Van dentro de las líneas de comentario y deben aparecer siempre al principio del Dockerfile. Si no, se tratarán como comentarios y se eliminarán durante el build. También hay que tener en cuenta que Dockerfile solo puede utilizar una vez una determinada instrucción del analizador.

En el momento de escribir este artículo, solo existen dos tipos de instrucciones de análisis: “syntax” y “escape”. La instrucción de análisis “escape” define el símbolo de escape que debe utilizarse. Se utiliza para escribir instrucciones en varias filas y para expresar caracteres especiales. La instrucción de análisis “syntax” define las reglas según las cuales el analizador tiene que procesar las instrucciones del Dockerfile. He aquí un ejemplo:

# syntax=docker/dockerfile:1
# escape=\

Instrucciones, argumentos y variables

Las instrucciones conforman la parte principal del contenido del Dockerfile. Ejecutadas una tras otra, las instrucciones describen la estructura específica de una imagen Docker. Al igual que los comandos de la línea de comandos, las instrucciones tienen argumentos. Algunas son directamente comparables a comandos específicos de la línea de comandos. De esta manera, existe una instrucción COPY, que copia archivos y directorios y corresponde aproximadamente al comando cp de la fila de comandos. A diferencia de la línea de comandos, para algunas instrucciones del Dockerfile hay reglas específicas para su secuencia. Además, ciertas instrucciones solo pueden ocurrir una vez por Dockerfile.

Nota

No es obligatorio escribir en mayúsculas las instrucciones, pero has de seguir la convención para crear un Dockerfile.

En el caso de los argumentos, hay que distinguir entre las partes de código fijo y variable. Según la metodología de la app de 12 factores, Docker utiliza variables de entorno para la configuración de los contenedores. Dentro de un Dockerfile, las variables de entorno se definen con la instrucción ENV. Así se asigna un valor a la variable de entorno:

Los valores almacenados en las variables de entorno pueden leerse y utilizarse como partes variables de los argumentos. Además, se utiliza una sintaxis especial que recuerda al script de Shell. El nombre de la variable de entorno se indica con un signo de dólar: $env_var. También existe una escritura alternativa para delimitar explícitamente el nombre de la variable. En este caso, el nombre de la variable va entre corchetes: ${env_var}. Veamos un ejemplo concreto:

# Establecer variable 'user' como valor 'admin' 
ENV user="admin"
# Establecer nombre de usuario como 'admin_user'
USER ${user}_user

Las instrucciones Dockerfile más importantes

A continuación, te presentamos las instrucciones de Dockerfile más importantes. Tradicionalmente, algunas instrucciones, sobre todo FROM, solo podían utilizarse una vez por Dockerfile. Con el paso del tiempo, ha surgido la construcción en múltiples etapas, o multi-stage builds, que describe múltiples imágenes en un Dockerfile, por lo que la limitación se refiere a cada etapa individual.

Instrucción Descripción Comentario
FROM Establecer imagen base Debe presentarse como primera instrucción; solo una entrada por etapa
ENV Variable de entorno para el proceso de construcción y establecer vida del contenedor
ARG Declarar parámetros de la línea de comandos para el proceso de construcción Debe aparecer antes que la instrucción FROM
WORKDIR Cambiar de directorio actual
USER Cambiar usuario y pertenencia al grupo
COPY Copiar los archivos y directorios de una Image Crea una nueva capa
ADD Copiar los archivos y directorios de una Image Crea una nueva capa; uso no recomendable
RUN Ejecutar el comando de Image durante el proceso de construcción Crea una nueva capa
CMD Establecer argumentos estándar para el inicio del contenedor Solo una entrada por build stage
ENTRYPOINT Establecer comando estándar para el inicio del contenedor Solo una entrada por etapa de construcción
EXPOSE Definir asignación de puerto para contenedores en ejecución Los puertos deben estar activos al iniciar el contenedor
VOLUME Integrar como volumen directorio de Image al iniciar el contenedor en el sistema anfitrión

Instrucción FROM

La instrucción FROM establece la imagen base sobre la que operan las instrucciones posteriores. Esta directiva solo puede incluirse una vez por etapa de construcción y debe ser la primera instrucción. Con una restricción: la instrucción ARG puede presentarse antes que FROM. Esto permite especificar exactamente qué imagen se utiliza como imagen base a través de un argumento de línea de comandos al iniciar el proceso build.

Cada imagen Docker debe basarse en una imagen base. En otras palabras, todas las imágenes Docker tienen exactamente una imagen predecesora. Esto da lugar al clásico dilema “del huevo y la gallina”: la cadena debe empezar en algún sitio. En el universo Docker, el linaje comienza con la imagen “scratch”. Esta imagen mínima sirve como origen de toda imagen Docker.

Nota

En inglés, “from scratch” significa que algo está hecho con ingredientes básicos. El término se utiliza en repostería y cocina. Si un Dockerfile comienza con la línea “FROM scratch”, alude a que la imagen se compila desde cero.

Instrucciones ENV y ARV

Estas dos instrucciones asignan un valor a una variable. La distinción entre ambas afirmaciones radica principalmente en el origen de los valores y en el contexto en el que están disponibles las variables. Veamos primero la instrucción ARG.

Con la instrucción ARG, se declara dentro del Dockerfile una variable que solo está disponible mientras dure el proceso de construcción. El valor de una variable declarada con ARG se pasa como argumento de la línea de comandos cuando se inicia el proceso build. Veamos un ejemplo declarando la variable de build “user”:

ARG user

Al iniciar el proceso build pasamos el valor real de la variable:

docker build --build-arg user=admin

Al declarar la variable, existe la opción de especificar un valor por defecto. Si no se pasa ningún argumento adecuado al iniciar el proceso build, la variable recibe el valor por defecto:

ARG user=tester

Si no se utiliza “--build-arg”, la variable “user” contiene por defecto el valor “tester”:

docker build

Mediante la instrucción ENV, definimos una variable de entorno. A diferencia de la instrucción ARG, una variable definida con ENV existe tanto durante el proceso de construcción como durante la ejecución del contenedor. Hay dos escrituras posibles para la instrucción ENV.

  1. Escritura recomendada:
ENV version="1.0"

2. Escritura alternativa para compatibilidad con el pasado:

ENV version="1.0"
Consejo

La funcionalidad de la instrucción ENV se corresponde más o menos a la del comando “export” de la línea de comandos.

Instrucciones WORKDIR y USER

La instrucción WORKDIR sirve para cambiar los directorios durante el proceso de construcción, así como al iniciar el contenedor. Al activar WORKDIR, esta se aplica a todas las instrucciones posteriores. Durante el proceso de construcción, se ven afectadas las instrucciones RUN, COPY y ADD; durante la ejecución del contenedor, las instrucciones CMD y ENTRYPOINT.

Consejo

La instrucción WORKDIR se corresponde más o menos con el comando cd de la línea de comandos.

Similarmente al cambio de directorio, la instrucción USER permite cambiar el usuario actual (de Linux). Existe la opción de especificar el grupo al que pertenece el usuario. La llamada a USER se aplica a todas las instrucciones posteriores. Durante el proceso de construcción, las instrucciones RUN se ven influenciadas por su pertenencia al usuario y al grupo; durante el tiempo de ejecución del contenedor, esto se aplica a las instrucciones CMD y ENTRYPOINT.

Consejo

La instrucción USER corresponde aproximadamente al comando su de la línea de comandos.

Instrucciones COPY y ADD

Las instrucciones COPY y ADD sirven ambas para añadir archivos y directorios a la Docker Image. Ambas instrucciones crean una nueva capa que se apila a la imagen existente. En la instrucción COPY, la fuente siempre es el contexto de construcción. En el siguiente ejemplo, copiamos un archivo readme del subdirectorio “doc” del contexto build en el directorio de nivel superior “app” de la imagen:

COPY ./doc/readme.md /app/
Consejo

La instrucción COPY corresponde aproximadamente al comando cp de la línea de comandos.

La instrucción ADD se comporta de forma casi idéntica, pero puede recuperar recursos URL fuera del contexto de construcción y descomprimir archivos comprimidos. En la práctica, esto puede conllevar efectos secundarios inesperados, por lo tanto, se desaconseja totalmente el uso de la instrucción ADD. En la mayoría de los casos debe utilizarse exclusivamente la instrucción COPY.

Instrucción RUN

La instrucción RUN es una de las más comunes de Dockerfile. Con ella, indicamos a Docker que ejecute un comando de la línea de comandos durante el proceso de construcción. Los cambios resultantes se apilan como una nueva capa sobre la imagen existente. Hay dos escrituras para la instrucción RUN:

  1. Escritura Shell: los argumentos pasados a RUN se ejecutan en el Shell estándar de la Image. Los símbolos especiales y las variables de entorno se sustituyen según las reglas de Shell. He aquí un ejemplo de una llamada que saluda al usuario actual y utilizar una subshell “$()”:
RUN echo "Hello $(whoami)"

2. Escritura “Exec”: en vez de pasar un comando a la Shell, se activa directamente un archivo ejecutable. En el proceso, pueden pasarse argumentos adicionales. Este es un ejemplo de una llamada a la herramienta de desarrollo “npm” indicándole que ejecute el script “build”:

CMD ["npm", "run", " build"]
Nota

En principio, la instrucción RUN puede sustituir a algunas de las otras instrucciones de Docker. Por ejemplo, la llamada “RUN cd src” es más o menos equivalente a “WORKDIR src”. No obstante, este enfoque crea Dockerfiles, que conforme van aumentando de tamaño se vuelven más difíciles de leer y gestionar, por lo que es mejor utilizar instrucciones especializadas si es posible.

Instrucciones CMD y ENTRYPOINT

La instrucción RUN ejecuta un comando durante el proceso build y crea una nueva capa en la Docker Image. En cambio, las instrucciones CMD o ENTRYPOINT ejecutan un comando cuando se inicia el contenedor. La diferencia entre ambas afirmaciones es sutil:

  • ENTRYPOINT se utiliza para crear un contenedor que siempre realiza la misma acción cuando se inicia, por lo que el contenedor se comporta como un archivo ejecutable.
  • CMD se utiliza para crear un contenedor que ejecuta una acción definida cuando se inicia sin más parámetros. La acción preestablecida se puede sobrescribir fácilmente mediante parámetros adecuados.

Lo que ambas instrucciones tienen en común es que solo pueden darse una vez por Dockerfile. Sin embargo, es posible combinar ambas instrucciones. En este caso, ENTRYPOINT define la acción estándar que se realizará al iniciar el contenedor, mientras que CMD define parámetros fácilmente anulables para la acción.

Nuestra entrada en Dockerfile:

ENTRYPOINT ["echo", "Hello"]
CMD ["World"]

Los comandos correspondientes de la línea de comandos:

# Salida "Hello World"
docker run my_image
# Salida "Hello Moon"
docker run my_image Moon

Instrucción EXPOSE

Los contenedores Docker se comunican a través de la red. Los servicios que se ejecutan en el contenedor se dirigen a través de los puertos especificados. La instrucción EXPOSE documenta la asignación de puertos y soporta los protocolos TCP y UDP. Si un contenedor se inicia con “docker run -P”, el contenedor escucha en los puertos definidos a través de EXPOSE. De manera alternativa, los puertos asignados pueden ser sobrescritos con “docker run -p”.

He aquí un ejemplo. Nuestro Dockerfile contiene la siguiente instrucción EXPOSE:

EXPOSE 80/tcp
EXPOSE 80/udp

Luego, existen las siguientes vías para hacer que los puertos se activen al iniciar el contenedor:

# Container escucha para TCP / UDP Traffic en Port 80
docker run -P
# Container escucha para TCP Traffic en Port 81
docker run -p 81:81/tcp

Instrucción VOLUME

Un Dockerfile define una imagen Docker que consiste en capas apiladas las unas sobre las otras. Las capas son solo de lectura para que se garantice siempre el mismo estado cuando se inicie un contenedor. Necesitamos un mecanismo para intercambiar datos entre el contenedor en ejecución y el sistema anfitrión. La instrucción VOLUME define un “mount point” dentro del contenedor.

Consideremos el siguiente fragmento de un archivo Docker. Creamos un directorio “shared” en el directorio de nivel superior de la imagen. También especificamos que este directorio se incluya en el sistema anfitrión cuando se inicie el contenedor:

RUN mkdir /shared
VOLUME /shared

Ten en cuenta que dentro del Dockerfile no podemos definir la ruta real en el host system. De manera estándar, los directorios definidos mediante la instrucción VOLUME se incluyen en el sistema anfitrión bajo "/var/lib/docker/volumes/".

¿Cómo se modifica un Dockerfile?

Recuerda: un Dockerfile es un archivo de texto (plano). Se puede editar con los métodos típicos; un editor de texto plano es probablemente el más común. Puede ser un editor con una interfaz gráfica de usuario. ¡Será por opciones! Los editores más populares son VSCode, Sublime Text, Atom y Notepad++. Como alternativa, hay varios editores disponibles en la línea de comandos. Además de los originales Vim o Vi, los editores mínimos Pico y Nano también se usan mucho.

Nota

Edita archivos de texto plano únicamente con editores adecuados. No uses bajo ningún concepto procesadores de textos como Microsoft Word, Apple Pages o Libre u OpenOffice para editar un Dockerfile.