Buenas Prácticas al Escribir Dockerfiles


Antes de comenzar, quería contarles que hay una promoción en DigitalOcean donde te dan un crédito de USD 100.00 durante 60 días para que puedas probar los servicios que este Proveedor Cloud ofrece. Lo único que tienes que hacer es suscribirte a DigitalOcean con el siguiente botón:

DigitalOcean Referral Badge

O a través del siguiente enlace: https://bit.ly/digitalocean-itsm


En este artículo les voy a hablar de algunos consejos y/o buenas prácticas al momento de escribir Dockerfiles y, de esta manera, evitar problemas de seguridad y optimizar la construcción en nuestras imágenes.


Comenzamos en materia de seguridad y es que recordemos, que a menos que indiquemos lo contrario, los contenedores Docker se ejecutan bajo usuario root, cosa que es muy peligrosa, ya que si un atacante logra comprometer un contenedor, puede tomar control del host. Es por ello que debes agregar usuarios cuando construyas imágenes Docker y así evitar que los contenedores se ejecuten como usuario root:

1
2
3
RUN addgroup --system app && add user --system --group app

USER app

De preferencia usa COPY en lugar de ADD cuando copies archivos desde el host hacia la imagen. Utiliza ADD solamente cuando:

  • Descargues archivos desde una ubicación remota.
  • Extraes un archivo comprimido hacia el destino.
1
2
3
4
5
6
7
8
COPY /ruta/origen /ruta/destino
ADD /ruta/origen /ruta/destino

# Cuando descargamos un archivo externo y lo copiamos al destino
ADD http://www.ruta.ext/archivo.ext /ruta/destino

# Cuando copiamos y extraemos archivos comprimidos desde el host
ADD archivo-comprimido.tar.gz /ruta/destino

Combina comandos para reducir al mínimo el número de capas que contendrá la imagen:

1
2
3
4
5
6
# 2 comandos, 2 capas:
RUN apt-get update
RUN apt-get install -y apache2

# combinamos 2 comandos en una misma linea, una capa:
RUN apt-get update && apt-get install -y apache2

Siempre que sea posible, apoyate del backslash ( \ ) para mejorar la lectura cuando se están instalando muchos paquetes:

1
2
3
4
5
6
7
RUN apt-get update && apt-get install -y \
  bzr \
  cvs \
  git \
  mercurial \
  subversion \
  && rm -rf /var/lib/apt/lists/* 

Cachea paquetes de Python en el host y móntalos a un volumen (o en su defecto, usa BuildKit):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Utilizando la opción de montar un volumen
-V $HOME/.cache/pip-docker/:/root/.cache/pip

## BuildKit
# syntax = docker/dockerfile:1.2

...

COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
      pip install -r requirements.txt

Limita el uso de CPU y Memoria de tus contenedores para que no sobreconsuman los recursos del host (muy apreciado en aplicaciones Java) y que los demás contenedores en ejecución puedan funcionar de manera normal.

En la CLI:

1
docker run --cpus=2 -m 512m nginx

En Docker Compose:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
version: "3.9"
services:
  redis:
    image: redis:alpine
    deploy:
      resources:
        limits:
          cpus: 2
          memory: 512M
        reservations: # Recursos Garantizados al iniciar el contenedor
          cpu: 1
          memory: 256M

Escanea regularmente o cada vez que construyas una imagen, tales como: Synk, Trivy o Anchore, lo que te permitirá hacer chequeo de vulnerabilidades e informarte de malas prácticas al escribir Dockerfiles. Además te recomiendo hadolint para validar la sintaxis en tus Dockerfiles.

Es buena práctica firmar y verificar tus imágenes Docker para prevenir ejecutar imágenes de dudosa procedencia o que hayan sido corrompidos. Para esto agregamos la siguiente variable de entorno:

1
DOCKER_CONTENT_TRUST = 1

Utiliza multi-stage builds, te va a permitir reducir drasticamente el tamaño de la imagen final de tu aplicación, sin tener que reducir el número de capas intermediarias y archivos.

Utilizaríamos entonces una imagen con todas las dependencias que necesitaramos para poder compilar el proyecto y otra imagen mínima para copiar el ejecutable/componentes de aplicación.

El siguiente Dockerfile ilustra la construcción y posterior copiado de una aplicación en Go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# syntax=docker/dockerfile:1
FROM golang:1.16-alpine AS build

# Instalamos las herramientas requeridas para compilar la aplicación
# Ejecuta `docker build --no-cache .` para actualizar dependencias
RUN apk add --no-cache git
RUN go get github.com/golang/dep/cmd/dep

# Lista de las dependencias del proyecto en Gopkg.toml y Gopkg.lock
# Esas capas se recontruirán cuando los archivos Gopkg se actualicen
COPY Gopkg.lock Gopkg.toml /go/src/project/
WORKDIR /go/src/project/
# Instalando dependencias
RUN dep ensure -vendor-only

# Copiamos el proyecto completo y construimos
# Esta capa se recontruirá cuando un archivo cambie en el directorio del proyecto
COPY . /go/src/project/
RUN go build -o /bin/project

# Y ahora copiamos el ejecutable hacia una imagen nueva
FROM scratch
COPY --from=build /bin/project /bin/project
ENTRYPOINT ["/bin/project"]
CMD ["--help"]

Nunca, nunca coloques secretos o credenciales en Dockerfiles (variables de entorno, argumentos, o credenciales en texto plano en cualquier comando)

Ten cuidado con aquellos archivos que se copian dentro de la imagen. Aún si el archivo es eliminado en alguna instrucción posterior en el Dockerfile, puede ser accesado por las capas anteriores, ya que no está realmente eliminado, sino “escondido” en el sistema de archivos final. Así, que cuando estés construyendo imágenes, toma en cuenta lo siguiente:

  • Si la aplicación soporta configuración vía variables de entorno, usalas para configurar los secretos en tiempo de ejecución (opción “-e” en docker run), o usa Docker secrets, Kubernetes secrets para proveer los valores como variales de entorno.

  • Usa archivos de configuración y móntalas como volúmenes en docker, o móntalas como secretos de Kubernetes.

Además, tus imágenes no deberían tener ninguna información sensible o confidencial o valores de configuración que muestren algún ambiente de desarrollo (por ejemplo, production, staging, etc.).

Espero les haya gustado y nos vemos en la próxima! Happy Dockering 😄


Otros Articulos