Table of Contents

Dockerfile: Guía rápida y completa

Directivas de un fichero Dockerfile

ARG inicializa variables que solo estarán operativas en la creación de la imagen, no en los contenedores basados en la misma. Antes del FROM se pueden indicar directivas de parseo y/o ARG para inicializar alguna variable.

ARG VERSION=latest

FROM indica la imagen que se va a usar como base. Es bueno escribir una versión si la imagen va a perdurar en el tiempo y será distribuida. No se recomienda usar “latest” ya que nunca sabremos cuando cambiará la imagen en remoto.

FROM alpine:$VERSION

RUN ejecuta comandos para la construcción de la imagen, NO en el contenedor. En este caso instala socat y nginx, además de crear el directorio “/run/nginx”. Suele utilizarse para instalar paquetes, descargar contenido, correr scripts necesarios para la construcción etc

RUN apk add socat nginx
RUN mkdir -p /run/nginx

Si nombramos de nuevo con ARG la variable definida en la primera linea antes del “FROM” podemos sobrescribirla desde la linea de comandos o bien inicializarla con un valor (denominado valor predeterminado). No se pueden definir directamente desde la linea de comandos, siempre deben estar en el Dockerfile primeramente.

ARG VERSION

Por ejemplo ARG puede usarse para desactivar instalaciones interactivas en diversos sistemas,en sistemas Debian se podría usar la siguiente variable DEBIAN_FRONTEND=noninteractive“.

ARG DEBIAN_FRONTEND=noninteractive

Con ARG no deben usarse credenciales ni llaves criptográficas ya que estas estarían visibles al ejecutar “docker history”. Las variables son únicamente validas en el momento de definirse. La última definición de la variable en el Dockerfile será la que puede ser sobrescrita desde la linea de comandos. Si se usan múltiples FROM (multi-stage builds), ARG debe estar también definida en cada stage. La construcción por etapas se explica más adelante.

Las variables definidas von ARG pueden ser sobrescritas siempre con ENV (se verá más adelante), haciendo inútil por tanto el uso de la sustitución por linea de comando de variables definidas mediante ARG. Ejemplo de sustitución de variable definida con ARG.

ARG CONT_IMG_VER
ENV CONT_IMG_VER=v1.0.0

Docker incluye una serie de variables predefinidas, que no necesitan introducirse en el Dockerfile primeramente y pueden ser inicializadas desde el Dockerfile o directamente con ”--build-arg“. Estas variables NO se incluyen en la salida de docker history. Recordar que si no se definen las variables, a excepción de estas, mediante ARG en el Dockerfile, estás no pueden ser inicializadas desde la linea de comandos.

HTTP_PROXY
http_proxy
HTTPS_PROXY
https_proxy
FTP_PROXY
ftp_proxy
NO_PROXY
no_proxy

EXPOSE define el puerto expuesto por el comando ejecutado, es solo informativo para que el usuario que use la imagen utilice las opciones de mapeo de puertos que quiera. El mapeo, al igual que con los volúmenes de Docker, no pueden ser mapeados desde le fichero Dockerfile y se hace siempre desde la linea de comando u otras aplicaciones externas de gestión de contenedores.

EXPOSE 80/tcp

USER facilita configurar el propietario (UID/Grupo) usado por los comandos RUN, CMD y ENTRYPOINT. Solo puede definirse un grupo de usuario, no varios. Si no se define grupo, este será root. Al configurar un usuario para correr la aplicación, NO es necesario que se cree el id de usuario previamente en la imagen (al no ser que se especifiquen nombres en vez de ids).

# Usando IDs.
USER 567:567
# Usando nombre de usuario.
USER manolo

NOTA: Se debe tener en cuenta que el sistema de ficheros estará como root, por lo que si el proceso interactúa con ficheros y directorios, habrá que preocuparse de configurar permisos previamente.

RUN groupadd -g 567 user && \
    useradd -r -u 567 -g user user
USER 567:567
# Si queremos la coniguración 567:0 simplemente no debe definirse grupo.
#USER 567

Si el comando ejecutado usa ficheros de configuración para establecer el usuario, como por ejemplo Apache o Nginx, el uso de USER no será efectivo.

ENTRYPOINT y CMD permiten ejecutar un comando predeterminado al iniciar el contenedor. Como funcionalidad agregada usando la instrucción CMD junto con ENTRYPOINT, se puede usar para pasar o completar parámetros al ejecutable configurado en ENTRYPOINT. Esto facilita sobrescribir los parámetros desde la linea de comandos, cosa que con las opciones de linea de comandos de “entrypoint” es algo más laborioso.

ENTRYPOINT modo shell, este método arranca un subproceso de ”/bin/sh -c“ para ejecutar lo definido. No es recomendable su uso ya que el que recibe las señales del sistema es el proceso shell y no el programa que ejecutamos en ENTRYPOINT. Tampoco es compatible con el uso de CMD para asignar parámetros.

ENTRYPOINT top

Para solventar el problema de la no recepción de señales del sistema (ej docker stop XXX), es recomendable usar el comando exec para sustituir al proceso padre shell por el comando a ejecutar.

ENTRYPOINT exec top

NOTA: Si en vez de una aplicación en el contenedor se debe usar un script que arranque varios ejecutables en el contenedor, en la documentación oficial hay varios hacks de interés: https://docs.docker.com/engine/reference/builder/#exec-form-entrypoint-example

ENTRYPOINT modo Exec no arranca el proceso a través de shell si no directamente, es el uso recomendado de ENTRYPOINT y el que se usa con el uso de la linea de comandos mediante ”--entrypoint“. CMD deben usarse también en esta sintaxis cuando se usa como argumentos de ENTRYPOINT.

ENTRYPOINT ["nginx"]
CMD ["-g", "daemon off;"]

ENTRYPOINT sin el uso de CMD para parámetros. EL uso de CMD junto con ENTRYPOINT es interesante si se quieren sobrescribir cómodamente los parámetros desde la linea de comandos. Si esto no es requerido, puede usarse ENTRYPOINT unicamente.

ENTRYPOINT ["nginx","-g", "daemon off;"]

CMD también tiene su modo shell y Exec. Al igual que ENTRYPOINT, si no se usan corchetes estamos ejecutando el comando a partir de un proceso shell padre.

# Modo Exec.
CMD ["nginx","-g", "daemon off;"]
# Modo shell.
CMD nginx -g 'daemon off;'

Si hay varios ENTRYPOINT solo el último tiene efecto. Lo mismo pasa con CMD. En un Dockerfile unicamente puede haber una instrucción CMD y ENTRYPOINT.

Tabla con todas las combinaciones posibles entre ENTRYPOINT y CMD en base al uso de parámetros y a sus diferentes modos de ejecución (shell / Exec):

No ENTRYPOINT ENTRYPOINT exec_entry p1_entry ENTRYPOINT [“exec_entry”, “p1_entry”]
No CMD error, not allowed /bin/sh -c exec_entry p1_entry exec_entry p1_entry
CMD [“exec_cmd”, “p1_cmd”] exec_cmd p1_cmd /bin/sh -c exec_entry p1_entry exec_entry p1_entry exec_cmd p1_cmd
CMD [“p1_cmd”, “p2_cmd”] p1_cmd p2_cmd /bin/sh -c exec_entry p1_entry exec_entry p1_entry p1_cmd p2_cmd
CMD exec_cmd p1_cmd /bin/sh -c exec_cmd p1_cmd /bin/sh -c exec_entry p1_entry exec_entry p1_entry /bin/sh -c exec_cmd p1_cmd

LABEL permite especificar etiquetas de varias formas. Pueden ser visualizadas con el comando “inspect”.

LABEL "Entorno"=":Imagen de prueba"
LABEL "Descripción"="Vamos a probar un poco esto de Docker a ver cómo fuciona." "Version"="1.0"
LABEL multi.label1="value1" \
      multi.label2="value2" \
      other="value3"

ENV permite establecer variables de entorno que a diferencia de ARG perduran en el contenedor. Se pueden usar múltiples veces incluso en la misma linea. También puede usarse como vimos anteriormente para sobrescribir variables inicializadas con ARG,

ENV variable="variable de entorno pasada por ENV" variable2=variable2\ de\ entorno\ pasada\ por\ ENV 

ADD permite copiar ficheros del host a la imagen y los contenedores dispondrán de esos ficheros, permite configurar propietario y grupo. Si se especifica el nombre de usuario, este debe estar en ”/etc/passwd“ y ”/etc/group“. Se debe de usar siempre rutas relativas al directorio donde creemos la imagen con el Dockerfile. Para el destino sí se deben usar rutas absolutas. Se pueden especificar varios orígenes pero NO sin son remotos.

La diferencia con COPY es que ADD descomprime automáticamente ficheros comprimidos y permite descargar de internet archivos. Los permisos predeterminados son 600 para los ficheros obtenidos remotamente con ADD.

ADD --chown=XXXX:XXXX "alpine-minirootfs-3.13.0-x86_64.tar.gz" /opt/codigoalpine
# ADD decomprimirá el tar.gz en /opt/codigoalpine, si no se desea la descompresión automática se debe usar COPY.
# La aplicación de permisos en este caso no se aplican a los ficheros descomprimidos.

ADD no es compatible con procesos de autenticación cuando usamos orígenes remotos. En esos casos se puede usar RUN von wget o cualquier otro software que permite descargar mediante autenticación.

ADD https://freetsa.org/files/cacert.pem /opt/

COPY no permite usar ficheros remotos y no descomprime los ficheros de manera automáticamente como si hace ADD.

COPY --chown=XXXX:XXXX "alpine-minirootfs-3.13.0-x86_64.tar.gz" /opt/codigoalpine
# El propietario de /opt/codigoalpine/alpine-minirootfs-3.13.0-x86_64.tar.gz sera XXXX.

La ruta origen es relativa siempre al directorio de trabajo donde estemos (PWD).

COPY directorio_local/ /opt/codigoalpine/

NOTA: Si en el destino se usa una ruta relativa, será en base al WORKDIR usado por la imagen. Si se copia un directorio del anfitrión (host) los permisos son también copiados (no así el propietario).

VOLUME permite crear volúmenes al arrancar el contenedor. En este ejemplo se crearán dos volúmenes para esos directorios del contenedor (Para montar directorios del host se usa ”-v“ en linea de comandos, no es posible definir eso desde el Dockerfile). Esto se debe a que podría provocar problemas al cambiar de host si este no tiene la misma carpeta.

VOLUME /opt/volumen1 /opt/volumen2
COPY "alpine-minirootfs-3.13.0-x86_64.tar.gz" /opt/volumen2/

Si se quiere controlar el montaje de volúmenes con un determinado usuario que no sea root, este puede ser primeramente definido en el fichero Dockerfile. Una vez se monte el volumen, heredará los permisos y propietario definidos previamente en la imagen. En el ejemplo se crea una imagen docker llamada alpine_volumen que usará un usuario no privilegiado “testuser”.

ARG USR=testuser
FROM alpine
RUN addgroup -S $USR && adduser -S $USR -G $USR           # Se crea el usuario y el grupo para luego aplicarlo al punto de montaje del volumen.
RUN mkdir /DIR_testuser && chown $USR:$USR /DIR_testuser  # Se crea el directorio donde será montado el volumen y su propietario
VOLUME /DIR_testuser                                      # Se hace una referencia al volumen que será montado desde linea de comando o docker-compose.
USER $USR                                                 # Usuario del proceso.

Linea de comando (se creará el volumen XXXX si no existe).

docker run --rm -it -v XXXX:/DIR_testuser alpine_volumen

Montar el volumen desde docker-compose, el volumen se crea automáticamente si no existe. El contenedor creado solo ejecutará una shell.

version: "3"
services:
  web:
    image: alpine_volumen  # Imagen creada a partir del Dockerfile mostrado anteriormente.
    stdin_open: true # docker run -i
    tty: true        # docker run -t
    command: /bin/sh
    volumes:
      - XXXX:/DIR_testuser  # El volumen será montado como el usuario testuser.
volumes:
    XXXX:

NOTA: El traspaso automático de la configuración de usuario y permisos al montar el volumen sucede únicamente, cuando el volumen no ha sido usado previamente por otro contenedor. Esto es solo aplicable a volúmenes, NO a directorios (bind mounts) del host. Para cpntrolar los permisos de “bind mounts” con aplicaciones como docker-compose, habría que usar en ENTRYPOINT comandos chmod / chown para adaptar los permisos a las necesidades o bien adaptarlos en el anfitrión manualmente (leer).

WORKDIR configura el directorio de trabajo, sencillamente es la carpeta donde ejecutaran RUN, CMD, ENTRYPOINT, COPY y ADD sus instrucciones. Si no existe el directorio es creado automáticamente. Puede usarse varias veces y puede ampliarse múltiples veces si la primera inicialización usa una ruta directa y las siguientes relativas. También puede interactuar con ENV. Los WORKDIR no crean nuevas capas a la hora de crear imágenes.

 # En este ejemplo el COMANDO1 será ejecutado en el WORKDIR /root/carpeta1/subcarpeta1 y el COMANDO2 en /opt.
 
ENV DIRPATH=/root
WORKDIR $DIRPATH/carpeta1
WORKDIR subcarpeta1
RUN COMANDO1
WORKDIR /opt/
RUN COMANDO2

ONBUILD se usa sobre todo en creación de imágenes base y orientado a imágenes cuya finalidad sea compilar/construir algo. Funciona como una especie de include de comandos que se ejecutarán sobre las imágenes derivadas. Por ejemplo permite ahorrar espacio en los Dockerfile y no repetir configuraciones. Veamos un ejemplo simple.

# Se construye la imagen base con el siguiente Dockerfile.
FROM maven:3.6.0-alpine
WORKDIR app
ONBUILD COPY . /app
ONBUILD RUN mvn package
# Se crea la imagen de nombre mavenbase.
docker build -f Dockerfile -t mavenbase .

En otro Dockerfile configuramos el siguiente FROM especificando la imagen anterior y se procederá a ejecutar esos comandos sobre la imagen. De esa manera no se repetiría tanto comando “COPY” y “RUN” al tener muchos Dockerfiles similares.

FROM mavenbase:latest

STOPSIGNAL facilita especificar la señal con la que el contenedor debe terminar.De manera predeterminada, mediante “docker stop” se envían la señal “SIGTERM” y si no termina, después de un periodo de gracia de 10 segundos de forma predeterminada, SIGKILL.

STOPSIGNAL signal

HEALTHCHECK Permite especificar nuestros propios checks de comprobación. Solo puede haber uno por Dockerfile.

HEALTHCHECK --interval=DURATION CMD curl -f http://localhost/ || exit 1
    --timeout=DURATION
    --start-period=DURATION
    --retries=N

Construcción de imágenes Docker mediante etapas (multi-stage)

Para poder disponer de imágenes pequeñas se necesita que estas tengan el mínimo indispensables de ficheros para correr la aplicación deseada. Para ello en los Dockerfiles es posible utilizar y trabajar sobre varias imágenes base para que al final, una última imagen pueda copiar los ficheros ya construidos en esas etapas de construcción anteriores (imágenes temporales). Esas imágenes intermedias (etapas de construcción) se reerencian mediante la instrucción AS y desaparecen al terminar el proceso de construcción.

Otro uso que se le suele dar a los Dockerfile con multi-stage es el de definir diferentes construcciones de imágenes en un solo Dockerfile y usar ”--target“ en el proceso de creación para especificar una contrucción en concreto. Normalmente se usar para tener varias versiones de una misma imagen, pero variando la cantidad de cosas instaladas en ellas. Por ejemplo la primera construcción de imagen podría ser la versión liviana, luego una normal basada en la anterior y la completa (siendo esta ultima la suma de las otras dos).

El proceso es simple, cada etapa empieza con FROM, se asigna mediante AS un nombre que luego es referenciado mediante COPY --from en la imagen final. “COPY --from=0” Permite referenciar la etapa anterior y no necesita definir nombres de etapas mediante AS.

En este ejemplo se crea una imagen a partir de apine:latest, la cual copia sin sentido contenido de dos distros diferentes. Estas son debian:latest (referenciada como “busi”) y “nginx:latest” (Al no estar definida definida en el Dockerfile, usará Dockerhub o cualquier otro repositorio remoto configurado).

ARG VERSION=latest
FROM debian:$VERSION as busi
RUN apt-get update && \
    apt-get install -y nginx
 
ARG VERSION=latest
FROM alpine:$VERSION
COPY --from=nginx:latest /etc/nginx/nginx.conf /opt/imagen_nginx/
COPY cacert.pem /opt/
COPY --from=busi /etc/apt/ /etc/nginx/nginx.conf /opt/imagen_debian/
docker build -t alpine_multistage .

Cuando se tiene más de una etapa definida con AS, es posible parar a partir de una determinada etapa e ignorar el resto. Para ello se debe usar ”-- docker build –target“ y referenciar qué etapa.

docker build --target builder -t nombre_imagen .

En este absurdo ejemplo, la imagen de nombre “alpine_multistage” creada tendrá en ”/opt/“ el fichero cacert.pem.procedente del host. En /opt/imagen_nginx el fichero nginx.conf de la imagen remota “nginx:latest” obtenida desde Dockerhub automáticamente.Y para finalizar en el directorio /opt/imagen_debian/ encontraremos el contenido de /etc/apt/ y el fichero nginx.conf de la imagen debian.

Las etapas pueden ser también referenciadas desde otras etapas, se puede usar como un “include” de instrucciones que otras etapas pueden aprovechar. En este ejemplo las etapas “build1” y “build2” usan la etapa definida como “build” para poder tener disponible el paquete build-base y puedan usar “gcc++”.

FROM alpine:latest as builder
RUN apk --no-cache add build-base
 
FROM builder as build1
COPY source1.cpp source.cpp
RUN g++ -o /binary source.cpp
 
FROM builder as build2
COPY source2.cpp source.cpp
RUN g++ -o /binary source.cpp

Consejos / Buenas prácticas

El consejo general al crear Dockerfiles es usar el mínimo de lineas necesarias para crear menos capas. También se debe prestar atención al orden de las instrucciones para hacer eficiente el uso de cache.

El orden de los comandos influye en el uso de la cache, al cambiar una linea las lineas posteriores quedarían invalidadas para usar en cache y deben ser procesadas de nuevo. Por lo tanto es mejor poner al final las lineas que vayan a ser modificadas más frecuentemente.

Si se van a instalar paquetes, es mejor todo en una linea que no en varias.

Al copiar ficheros siempre es mejor especificar varios origines si el destino es siempre el mismo. De esa manera se ahorran lineas con ADD y COPY. También es interesante el uso de ficheros .dockerignore para evitar copiar determinados ficheros sin necesidad de esquivarlos usando múltiples ADD y/o COPY.

Si se puede usar una versión oficial de una imagen docker suele ser mejor opción que construirla uno mismo.

Usar WORKDIR con los valores de rutas para usar con COPY, ADD y RUN y no estar especificando en cada linea una ruta. (Los WORKDIR además no usan capas).

Para producción se debería tener el mínimo instalado, es decir, a la hora de crear imágenes personalizadas, no agregar paquetes que no se necesitan. Para depurar siempre pueden ser instalados posteriormente. También sería recomendable limpiar la cache de paquetes, etc. El uso de varias etapas (multi-stage) permite incluir varios FROM y poder crear etapas intermedias de las que copiar unicamente los ficheros necesarios para la imagen final.

El uso de ONBUILD permite también ahorrarnos repetir instrucciones cuando se necesitan muchos ficheros Dockerfile similares.

Se debería especificar la versión concreta de una imagen y evitar uso de versiones “latest” ya que esta cambiará en el tiempo.

Las imágenes Alpine no usa libc, por lo que no siempre sirve para todo, pero si en la mayoría de casos, por lo que es una buena imagen para usar como base.

Si se puede evitar el uso de root (usuario predeterminado) mejor.

Utilizar algún sistema de escaneo de vulnerabilidades por ejemplo Clair o Docker Bech Security pero cada día salen nuevas herramientas / Plugins de Frameworks para dicha finalidad y otras son consideradas “deprecated”.

Nunca debe cambiarse el propietario del socket de docker. Si un usuario no root necesita hacer uso de comandos docker, simplemente se agrega el grupo “docker” al usuario pertinente.

usermod -aG docker ${USER}

Evitar la activación del modo privilegiado de docker (por defecto desactivado). De necesitarlo deben usarse imágenes oficiales en la medida de lo posible. El uso de la opción “no-new-privileges:true” en estos casos puede ser de interés.

Para tareas de autenticación, autorización, cifrado,etc es recomendable usar “secretos”. Esto permite gestionar ese tipo de información que se necesita en tiempo de ejecución pero que no se quiere almacenar en la imagen de Docker o en el repositorio de código fuente.

Crear una imagen con nombre personalizado.

 docker build -t alpine_busi .