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