Buenas prácticas con Docker

Este es uno de los capítulos del tutorial Tutorial de Docker. Encontrarás los enlaces a todos los de capítulos, al final de este artículo.

Durante estos capítulos del tutorial de docker has visto con detalle como levantar contenedores. Por supuesto, has creado tus propias imágenes para poder distribuir tus aplicaciones con facilidades, y sin tener que sufrir la incomodidad de las dependencias. Desde luego que se trata de una tecnología muy sencilla de implementar, y que te puede ahorrar muchos disgustos y sufrimiento. Sin embargo, esto no quita a que si no aplicas unas mínimas buenas prácticas con Docker a la hora de crear esas imágenes, sea contraproducente. Es decir, que en lugar de ahorrarte algún sufrimiento, este aparezca. Algunas de estas buenas prácticas, son simples recomendaciones que te van a venir bien a la hora de actualizar o mantener esas imágenes. Sin embargo, otras de esta recopilación de mejores prácticas con docker, son imprescindibles, puesto que pueden representar un problema de seguridad

Mejores prácticas con Docker

Buenas prácticas con Docker

Como te decía en la introducción, este artículo es una recopilación de buenas prácticas con docker. Encontrarás al final de este artículo las referencias de donde he recopilado estas buenas prácticas con Docker. Desde luego que es un artículo recopilatorio, de forma que si encuentras que falta alguna recomendación, no dudes en comentarlo para completar este artículo y sea verdaderamente un artículo sobre buenas prácticas con Docker, y sobre todo que sea de utilidad para todo aquel que lo lea.

Para darle una razón a este artículo, partiré del capítulo 6 de este tutorial dedicado a crear una imagen docker desde cero y paso a paso.

En este capítulo te propongo como Dockerfile, lo siguiente,

FROM ubuntu
RUN apt-get update -y
COPY ./requirements.txt /app/requirements.txt
WORKDIR /app
RUN apt-get install -y $(grep -vE "^\s*#" requirements.txt  | tr "\n" " ")
COPY src/* /app/
ENTRYPOINT ["python3"]
CMD ["app.py"]

Voy a repasar contigo este Dockerfile, de forma que al final, tendremos algo mejor de lo que te has encontrado aquí. Indicarte, que si lo construyes tal y como lo ves aquí, la imagen resultante tiene un tamaño de 137 MB.

El orden de los factores no altera el producto, pero molesta

La imagen resultante va a ser igual, con independencia de donde encuentres COPY src/* /app/. Sin embargo, si esta instrucción la pones al principio, te vas a encontrar con el problema de que si cambias cualquier cosa en src/* esto te va a obligar a reconstruir toda la imagen. Al modificar una capa de la imagen, las siguientes capas se tienen que reconstruir. Por ello, es mejor dejar las capas, inmutables o que no vayan a ser modificadas, al menos inicialmente, se encuentren las primeras.

La avaricia rompe el saco

A la hora de copiar vale la pena ser lo mas específico posible. En el caso de cualquier archivo se modifique se tendrá que cambiar esa capa, y evidentemente las consecuentes. Solo copia lo que necesites, ni mas ni menos. En este caso, de nuevo me refiero a la línea COPY src/* /app/. Cualquier archivo que se modifique en src/* afectará a la imagen resultante.

Piensa que en src/* a lo mejor tienes archivos que no necesitas para tu imagen resultante. Esto no es nada extraño. Puedes tener unas notas referentes a la ejecución de la imagen o cualquier cosa.

Pasito a pasito no siempre es la mejor solución

Podría parecerte que ejecutar dos instrucciones RUN como en el ejemplo anterior es una buena solución, sin embargo, no es así. Como te decía anteriormente, cada instrucción es una capa. ¿Porqué no todo en una capa?. Así, he modificado el archivo para que quede de la siguiente forma, que además es mas legible, y por supuesto mas sencillo de mantener.

FROM ubuntu
MAINTAINER Lorenzo Carbonell <a.k.a. atareao> "lorenzo.carbonell.cerezo@gmail.com"
RUN apt-get update -y && \
    apt-get install -y \
    python3-flask \
    python3-itsdangerous \
    python3-pyinotify  \
    python3-werkzeug
WORKDIR /app
COPY src/* /app/
ENTRYPOINT ["python3"]
CMD ["app.py"]

Consejos vendo que para mi no tengo

No instales dependencias que no necesites, instala lo justo y necesario. Esto ya te lo imaginas, no es necesario que instales aquellos paquetes que no vas a necesitar. No solo por el problema del espacio, sino también para el caso de las posibles vulnerabilidades. Además es interesante utilizar la opción --no-install-recommends, que evita, añadir posibles paquetes recomendados, pero que no necesitarás.

En este caso el archivo quedará de la siguiente forma,

FROM ubuntu
MAINTAINER Lorenzo Carbonell <a.k.a. atareao> "lorenzo.carbonell.cerezo@gmail.com"
RUN apt-get update -y && \
    apt-get install -y --no-install-recommends\
    python3-flask \
    python3-itsdangerous \
    python3-pyinotify  \
    python3-werkzeug
WORKDIR /app
COPY src/* /app/
ENTRYPOINT ["python3"]
CMD ["app.py"]

En este caso, la imagen ha pasado de 137 MB a 126 MB, que no está nada mal.

Borra lo que no necesites

Una vez terminada la actualización e instalación de paquetes, no necesitas tener esa información que no te aporta nada. Por esto, simplemente borra ese contenido. De nuevo, aplicando esto al ejemplo sobre el que estás trabajando,

FROM ubuntu
MAINTAINER Lorenzo Carbonell <a.k.a. atareao> "lorenzo.carbonell.cerezo@gmail.com"
RUN apt-get update -y && \
    apt-get install -y --no-install-recommends\
    python3-flask \
    python3-itsdangerous \
    python3-pyinotify  \
    python3-werkzeug &&
    rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY src/* /app/
ENTRYPOINT ["python3"]
CMD ["app.py"]

Con este nuevo paso, tu imagen habrá pasado de los 126 MB anteriores a 98.7 MB, que no es ninguna broma.

Parte de imágenes oficiales

En lugar de partir de una imagen de Ubuntu, es mejor partir de una imagen preparada para ello. Porque estas imágenes siempre se encontrarán lo mejor optimizadas posible. Pero no solo esto, sino que es recomendable que utilices imágenes lo mas pequeñas posibles, normalmente aparecen con la etiqueta slim. Así, para el caso del ejemplo, voy a partir de la 3.7.5-alpine3.10

FROM python:3.7.5-alpine3.10
RUN pip install --no-cache-dir \
    flask \
    itsdangerous \
    pyinotify  \
    werkzeug
WORKDIR /app
COPY src/* /app/
ENTRYPOINT ["python"]
CMD ["app.py"]

En este caso, a pesar de partir de una imagen oficial es un poquito mayor que la anterior, 109 MB

Usa etiquetas específicas

Esto ya lo has visto recogido en el ejemplo anterior. En lugar de utilizar simplemente FROM ubuntu, he pasado a utilizar python:3.8-slim-buster. Esto tiene varias ventajas. Por un lado, lo que te he comentado anteriormente, sobre utilizar imágenes oficiales, que seguro estarán optimizadas. Pero otra interesante ventaja de utilizar una imagen versionada, es el hecho de que te aseguras la compatibilidad con tu aplicación. De otra forma, cuando se construye la imagen, en el caso de que partas de la etiqueta latest, puedes encontrarte con que algunas de las dependencias de tu aplicación no se encuentre en la imagen de la que estás partiendo.

Multistage builds

Este tipo de Dockerfile es realmente interesante porque te evita tener que reconstruir todo una y otra vez. Lo reconocerás por que en la primera línea donde se indica la imagen de partida FROM, se añade AS donde pones una etiqueta que utilizarás en la siguiente etapa.

Esto te puede resultar muy interesante en el caso de que necesites compilar tu aplicación. En esa primera etapa utilizarás una imagen para compilar tu aplicación. Sin embargo, no necesitas el JDK de Java, por ejemplo, para luego ejecutar tu aplicación, donde solo necesitas el JRE

No utilices root

Por supuesto que los primeros pasos en la creación de tu imagen requieren de que tengas derechos de administrador. Sin embargo, no lo dejes al azar. En cuanto puedas define el usuario de la aplicación. Pero no solo esto, además define el uid del usuario, de lo contrario coincidirá con el tuyo y no habrás conseguido gran cosa.

Sigue con atención estos pasos. Primero crea /tmp/secreto.txt, con el contenido esto es un secreto, que se resume en,

echo "esto es un secreto" > /tmp/secreto.txt
chmod 400 /tmp/secreto.txt

Ahora crea una imagen con el contenido FROM ubuntu, con la instrucción docker build -t ejemplo . Levanta un contenedor con docker run -it -v /tmp:/tmp --name ejemplo ejemplo bash. Ejecuta cat /tem/secreto.txtSi estás viendo el contenido.

¿Como puedes evitar esto? Añadiendo esto a tu imagen,

FROM ubuntu
RUN useradd -r -u 5000 userapp
USER userapp

De esta forma, si ahora levantas de nuevo el contenedor e intentas ver el contenido te darás cuenta de que no puedes verlo. Pero, acuérdate de definir el uid, de otra forma será el 1000, que normalmente coincidirá con el tuyo, con lo que habrás hecho el pan como unas tortas.

De cualquier forma, ten en cuenta que cualquier usuario que pertenezca al grupo docker, siempre puede levantar un contenedor con derechos de administrador. Así que es fundamental tener en cuenta este detalle. Eso no quita a que le prestes atención a estas buenas prácticas con Docker.

Conclusión

Indicarte que este artículo sobre mejores prácticas con docker está en proceso de continua creación. Desde luego, las buenas prácticas es algo que no tiene fin, sin embargo, en algún momento, tenía que parar de escribir… Aunque no dudes que cualquier sugerencia la tendré en cuenta para ampliar este artículo. Así como que iré completando el mismo para que sea de ayuda a la hora de hacer tus imágenes docker.


Más información,

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *