9.2.2.2.2.2. Construcción

La función de Docker no es sólo aislar una aplicación tomando una imagen y alterando su configuración, sino también la de distribuir aplicaciones propias o servicios preconfigurados. Esto implica generar nuestras propias imágenes para que terceros sean capaces de crear sus propios contenedores a partir de ellas.

Para ello necesitamos:

  • Un registro con el que poder hacer la distribución y docker push para exportar la imagen desde nuestro repositorio local. Lo más sencillo es usar una cuenta gratuita en el propio Docker Hub

    Nota

    Alternativamente, se puede funcionar sin registro creando localmente archivos tar con las imágenes usando docker-save e importar tales archivos con docker-load. La distribución de las imáegnes, no obstante, se complica mucho.

  • Generar en sí la imagen para lo cual hay dos vías:

    • Convertir en imagen un contenedor.

    • La más apropiada que consiste en construir la imagen indicando a Docker cómo llevarlo a cabo.

9.2.2.2.2.2.1. Conversión a imágenes

El primer método consiste en convertir un contenedor en una imagen, para lo cual se usa docker commit. Para ilustrarlo consideremos que queremos generar una imagen de nginx en la que servimos una página estática que se alberga en /srv/www:

# docker run -d --name="nginx-test" -p 80:80 nginx:alpine
f7fc06d5ed70910bcf837c427775c894a0b21d4fc22c1f50ffcc9d079d910e12
# docker exec -ti nginx-test sh
/ # mkdir /srv/www
/ # cat > /srv/www/index.html
<!DOCTYPE html>
<html lang="es">
<meta charset="utf-8">
<title>Página de prueba</title>

<h1>Página de prueba</h1>
</html>

/ # sed -ir '1,/root\s/{s:root.*:root /srv/www\;:}' /etc/nginx/conf.d/default.conf
/ # exit
# docker stop nginx-test

En este punto tenemos el contenedor con los cambios que pretendíamos. Ahora es momento de generar a partir de él la imagen y ponerle un nombre y un a etiqueta:

# docker commit -a "Perico de los Palotes <perico@mail.org>" \
                -m "nginx que sirve el contenido de /srv/www" nginx-test
# docker image ls
REPOSITORY           TAG            IMAGE ID          CREATED              SIZE
<none>               <none>         da408ad65b98      About a minute ago   19.7M
# docker image tag da408ad65b98 miusuario/nginx-srv:v1
# docker image ls
REPOSITORY           TAG            IMAGE ID          CREATED              SIZE
miusuario/nginx-srv  v1             da408ad65b98      4 minutes ago        19.7M

Obsérvese que hemos dado al contenedor el nombre miusuario/nginx-test donde «miusuario» es el usuario del que disponemos en Docker Hub.

Finalmente, ya podemos subir la imagen al repositorio[1]:

# docker login -u miusuario
# docker push miusuario/nginx-srv:v1

Es interesante que comparemos la imagen original (nginx:alpine) con nuestra imagen, cuya diferencia es únicamente una simple manipulación de ficheros. Eso se traduce en que el almacenamiento crea una capa adicional:

# docker image inspect -f '{{json .RootFS.Layers}}' nginx:alpine
["sha256:531743b7098cb2aaf615641007a129173f63ed86ca32fe7b5a246a1c47286028",
 "sha256:6f23cf4d16deb170554e0237bec12e4fb488c78222a20e172462ba4776affb3d"]
# docker image inspect -f '{{json .RootFS.Layers}}' miusuario/nginx-srv:v1
["sha256:531743b7098cb2aaf615641007a129173f63ed86ca32fe7b5a246a1c47286028",
 "sha256:6f23cf4d16deb170554e0237bec12e4fb488c78222a20e172462ba4776affb3d"
 "sha256:eb7259d6e25c133fc5f662d2eb25b02c24194f58694f948fa596c722d0fbcc81"]

9.2.2.2.2.2.2. Generación de imágenes

La otra alternativa es más limpia y más recomendable, y consiste en generar una imagen indicando cuáles son las acciones que deben llevarse a cabo para obtener la imagen deseada. Para ello debe crear un directorio de trabajo y dentro de él un fichero Dockerfile con las instrucciones.

Para ilustrar el procedimiento crearemos una imagen equivalente a la generada bajo el epígrafe anterior:

# mkdir /tmp/nginx-test
# cd /tmp/nginx-text
# cat > index.html
<!DOCTYPE html>
<html lang="es">
<meta charset="utf-8">
<title>Página de prueba</title>

<h1>Página de prueba</h1>
</html>

# vim Dockerfile

Y dentro de este fichero Dockerfile escribiremos lo siguiente:

FROM nginx:alpine

RUN  sed -ir '1,/root\s/{s:root.*:root /srv/www\;:}' /etc/nginx/conf.d/default.conf ;\
     mkdir /srv/www

COPY index.html /srv/www

No es excesivamente complicado entender qué hace casa línea. Sí es interesante tener presente que cada directiva RUN o COPY COPY genera una capa distinta para el driver de almacenamiento y, en consecuencia, es conveniente minimizarlas. Por ese motivo la directiva RUN contiene dos órdenes, en vez de haber definido dos directivas RUN para cada orden.

Con todo, ya solo falta generar la imagen:

# docker build -t miusuario/nginx-test:v1b .

y subir la imagen. Es importante tener presente también que partir de la imagen nginx:alpine no sólo implica partir del sistema de archivos de ese contenedor, sino también del resto de configuración. Por ese motivo, no es necesario indicar qué deseamos exponer el puerto 80 o que queremos que se ejecute nginx. Por eso, aunque a efectos prácticos no tenga sentido alguno, ilustremos cómo obtener una imagen semejante partiendo de la imagen original Alpine, lo cual implica instalar nginx y hacer una configuración adicional.

Para ello tomemos otro directorio de trabajo en el que incluyamos un Dockerfile:

# mkdir /tmp/nginx-test.2
# cd /tmp/nginx-test.2
# mkdir -p archives/srv/www archives/etc/nginx/conf.d
# vim archives/srv/www/index.html
# vim archives/etc/nginx/conf.d/default.conf
# vim Dockerfile

El fichero index.html puede ser el mismo que el anterior; default.conf puede ser, simplemente, este:

server {
   listen 80;

   root  /srv/www;
   try_files  $uri $uri/ =404;
}

y Dockerfile, el siguiente:

FROM    alpine
RUN     apk update && apk add nginx && \
        ln -s /dev/stdout /var/log/nginx/access.log;\
        ln -s /dev/stderr /var/log/nginx/error.log;\
        mkdir /srv/www;\
        mkdir /run/nginx

COPY    ./archives /

EXPOSE  80/tcp
CMD     ["nginx", "-g", "daemon off;"]

Con lo cual, ya podemos generar la imagen:

# docker build -t miusuario/nginx-test:v1c .

cuyo almacenamiento debe tener tres capas: la generada por la imagen de Alpine, la generada por la directiva RUN y la generada por la directiva COPY.

Nota

El demonio usa una caché que almacena los resultados intermedios, por lo que puede interesar durante la fase de desarrollo de la imagen, descomponer las acciones en múltiples directivas RUN y solo al final minimizar el número de capas.

Las principales directivas que contiene un archivo Dockerfile son las siguientes:

# Preámbulo
FROM      imagenbase
ARG       VERSION=3.9
ARG       PASSWORD
LABEL     clave1=valor1 clave2="valor 2"
LABEL     clave3=valor3

# Construcción
RUN       ordenes...
WORKDIR   directorio/de/trabajo/en/la/construccion
COPY      anfitrion/archivo http://servidor/archivo contenedor/directorio/
ADD       semejante a copy, pero desempaqueta si el archivo es un paquete.

# Compartición
EXPOSE    80/tcp 443/tcp
VOLUME    /tmp

# Ejecución
USER        www-data
ENV         DEBUG=True
ENTRYPOINT  ["orden", "param1", "param2"]
CMD         ["param3", "param4"]
ARG

Permite definir variables para tiempo de compilación (docker build) que pueden usarse en otras directivas: Si las variables se pasan a través de la opción --build-arg de docker build se sobrescribirán los valores indicados en el archivo. Por ejemplo[2]:

ARG   VERSION
FROM  alpine${VERSION:+:$VERSION}

En este caso, la orden:

# docker build -t miusuario/alpine:propio .

usará la última versión de alpine, puesto que no se ha especificado versión. En cambio:

# docker build -t miusuario/nginx-test:v1v --build-arg VERSION=3.9 .

utilizará la versión 3.9.

WORKDIR

Define cuál es el directorio de trabajo dentro del contenedor.

VOLUME

permite definir volúmenes anónimos que se crearán automáticamente al generar un contenedor a partir de la imagen sin que sea necesario declararlos con -v. Por ejemplo, si hubiéramos querido hacer permanentes los registros podríamos haber utilizado este Dockerfile:

FROM    alpine
RUN     apk update && apk add nginx && \
        mkdir /srv/www;\
        mkdir /run/nginx

COPY    ./archives /

VOLUME  /var/lob/nginx

EXPOSE  80/tcp
CMD     ["nginx", "-g", "daemon off;"]
ENV

Permite definir variables de entorno. Por ejemplo:

ENV   DEBUG=True
ENTRYPOINT

Define una orden (con argumentos si así se desea) que se eejcutará al arrancar el contenedor. Esta orden no es sobrescrita por los argumentos posicionales de docker run, sino que tales argumentos se añaden a la definición de ENTRYPOINT. Por ejemplo:

FROM alpine
ENTRYPOINT ["echo"]

Indefectiblemente ejecutará echo al arrancar el contenedor:

$ docker build -t alpine:lorito .
$ docker run alpine:lorito Mensaje del contenedor
Mensaje del contenedor
CMD

Es semejante a ENTRYPOINT, pero los argumentos de docker run sobrescriben lo que se haya dispuesto en la directiva. Si también se dispuso una directiva ENTRYPOINT, se añade a la orden que determina ésta.

Nota

DockerHub permite asociar a una imagen un repositorio de GitHub para que al actualizar el repositorio, se regenere automáticamente la imagen.

Cnstrucción multi-stage

Hay por último un concepto bastante interesante que es el de la cosntrucción multistage de una imagen que se requiere cuando para crear una imagen necesitamos la creación de otras imágenes previas intermedias. Por ejemplo, imaginemos que en nuestra imagen necesitamos incluir un programa compilado con gcc. Dado que nuestra imagen necesita únicamente el ejecutable, no tiene sentido que incluyamos el compilador en ella, sino solamente el resultado de la compilación; así que pudemos crear una imagen intermedia previa con el compilador que genere el código compilado y la imagen definitiva que, simplemente, obtenga el resultado de esta compilación de esa primera imagen. Para ilustrarlo supongamos que creamos un directorio de trabajo:

# mkdir /tmp/multistage
# cd /tmp/multistage
# vim app.c
# vim Dockerfile

donde el código fuente app.c es simplemente el código del «Hola, mundo»:

#include <stdio.h>

int main() {
   printf("Hola, mundo\n");
   return 0;
}

y el Dockerfile este:

FROM     gcc as builder
WORKDIR  /tmp
COPY     app.c .
RUN      gcc -static -o app app.c

FROM     alpine
COPY     --from=builder /tmp/app /bin
CMD      ["app"]

Como resulta de utlizar este Dockerfile, obtendremos una imagen basada en Alpine que contiene y ejecuta nuestra aplicación compilada.

Notas al pie