Las buenas prácticas oficiales para Dockerfile contienen un montón de contenido excelente sobre cómo mejorar tus Dockerfiles.
Deberías optimizar principalmente el rendimiento (sobre todo en el caso de los test runners). Así te asegurarás de que tus herramientas se ejecuten lo más rápido posible y no se agote el tiempo de espera.
Medir el tiempo de ejecución a menudo es una forma estupenda de hacerse una idea del rendimiento de las herramientas. Adquiere el hábito de medir el tiempo de ejecución tanto después como antes de un cambio. Aunque creas que «estás seguro» de que un cambio mejorará el rendimiento, deberías medir igualmente el tiempo de ejecución.
Cuando sea posible, crea scripts que midan el rendimiento automáticamente (lo que también se conoce como benchmarking). Una herramienta de línea de comandos muy útil es hyperfine, pero siéntete libre de usar lo que tenga más sentido para tus herramientas.
Los repositorios de herramientas del track más recientes tendrán acceso a los dos scripts siguientes:
./bin/benchmark.sh: mide el rendimiento del código de las herramientas del track (código fuente)./bin/benchmark-in-docker.sh: mide el rendimiento de la imagen de Docker de las herramientas del track (código fuente)Si trabajas en un repositorio de herramientas del track que no tenga estos archivos, puedes copiarlos a tu repositorio usando los enlaces al código fuente de arriba.
Los scripts de benchmarking pueden ayudarte a estimar el rendimiento de las herramientas. Ten en cuenta, no obstante, que el rendimiento en los servidores de producción de Exercism suele ser inferior.
Intenta experimentar con distintas imágenes base (p. ej., Alpine en lugar de Ubuntu) para ver si una tiene un rendimiento (significativamente) mejor que la otra. Si el rendimiento es más o menos el mismo, quédate con la imagen más pequeña.
Comprueba si usar la red internal en lugar de none mejora el rendimiento.
Consulta la documentación sobre redes para obtener más información.
Las herramientas del track ejecutan un contenedor de Docker puntual y de corta duración que lleva a cabo los siguientes pasos.
Por lo tanto, el código que se ejecuta en el paso 2 se ejecuta en todas y cada una de las ejecuciones de las herramientas. Por este motivo, reducir la cantidad de código que se ejecuta en el paso 2 es una forma estupenda de mejorar el rendimiento. Una manera de conseguirlo es trasladar código del tiempo de ejecución al tiempo de compilación. Mientras que el código de tiempo de ejecución se ejecuta en todas y cada una de las ejecuciones de las herramientas, el código de tiempo de compilación solo se ejecuta una vez (cuando se compila la imagen de Docker).
El código de tiempo de compilación se ejecuta una vez como parte de un flujo de trabajo de GitHub Actions. Por lo tanto, no pasa nada si el código que se ejecuta en tiempo de compilación es (relativamente) lento.
Al ejecutar las pruebas en el test runner de Haskell, este necesita que se compilen algunas bibliotecas base. Como cada ejecución de las pruebas tiene lugar en un contenedor nuevo, ¡esto significa que esa compilación se hacía en todas y cada una de las ejecuciones de las pruebas! Para evitarlo, el Dockerfile del test runner de Haskell tiene los dos comandos siguientes:
COPY pre-compiled/ .
RUN stack build --resolver lts-20.18 --no-terminal --test --no-run-tests
Primero, el directorio pre-compiled se copia en la imagen.
Este directorio está configurado como un ejercicio de pruebas y depende de las mismas bibliotecas base de las que depende el ejercicio real.
Después ejecutamos las pruebas en ese directorio, lo que es similar a cómo se ejecutan las pruebas de un ejercicio real.
Ejecutar las pruebas hará que se compile la base, pero la diferencia es que esto ocurre en tiempo de compilación.
Por lo tanto, la imagen de Docker resultante tendrá ya compiladas sus bibliotecas base.
Esto significa que no hace falta compilar en tiempo de ejecución, lo que da como resultado una ejecución (mucho) más rápida.
Algunos lenguajes permiten compilar el código por adelantado o sobre la marcha. Se trata de una disyuntiva entre tiempo de compilación y tiempo de ejecución y, de nuevo, nos decantamos por la ejecución en tiempo de compilación por motivos de rendimiento.
El Dockerfile del test runner de C# usa este enfoque, en el que el test runner se compila a un binario por adelantado (en tiempo de compilación) en lugar de compilar el código sobre la marcha (en tiempo de ejecución). Esto significa que hay menos trabajo que hacer en tiempo de ejecución, lo que debería ayudar a aumentar el rendimiento.
Deberías intentar reducir el tamaño de la imagen, lo que hará que esta:
Las distintas imágenes de distribución tendrán tamaños diferentes.
Por ejemplo, la imagen alpine:3.20.2 es diez veces más pequeña que la imagen ubuntu:24.10:
REPOSITORY TAG SIZE
alpine 3.20.2 8.83MB
ubuntu 24.10 101MB
En general, las imágenes basadas en Alpine están entre las más pequeñas, por lo que muchas imágenes de herramientas se basan en Alpine.
Algunas imágenes tienen variantes «slim» especiales, en las que se han eliminado algunas características, lo que da como resultado tamaños de imagen más pequeños.
Por ejemplo, la imagen node:20.16.0-slim es cinco veces más pequeña que la imagen node:20.16.0:
REPOSITORY TAG SIZE
node 20.16.0 1.09GB
node 20.16.0-slim 219MB
La razón por la que las variantes «slim» son más pequeñas es que tienen menos características. Puede que tu imagen no necesite las características adicionales y, si no es así, plantéate usar la variante «slim».
Una forma obvia, pero excelente, de reducir el tamaño de tu imagen es eliminar todo lo que no necesites. Esto puede incluir cosas como:
La mayoría de las imágenes de Docker necesitan instalar paquetes adicionales, lo que normalmente se hace a través de un gestor de paquetes. Estos paquetes deben instalarse en tiempo de compilación (ya que no hay conexión a internet disponible en tiempo de ejecución). Por lo tanto, cualquier archivo de caché o de control interno del gestor de paquetes debería eliminarse después de instalar los paquetes adicionales.
Las distribuciones que usan el gestor de paquetes apk (como Alpine) deberían usar la opción --no-cache al usar apk add para instalar paquetes:
RUN apk add --no-cache curl
Las distribuciones que usan el gestor de paquetes apt-get/apk (como Ubuntu) deberían ejecutar los comandos apt-get autoremove -y y rm -rf /var/lib/apt/lists/* después de instalar los paquetes y en el mismo comando RUN:
RUN apt-get update && \
apt-get install curl -y && \
apt-get autoremove -y && \
rm -rf /var/lib/apt/lists/*
Docker tiene una funcionalidad llamada compilaciones de varias etapas. Estas te permiten dividir tu Dockerfile en etapas separadas, y solo la última etapa acaba formando parte de la imagen de Docker producida (el resto solo está ahí para ayudar a compilar la última etapa). Puedes pensar en cada etapa como en su propio mini Dockerfile; las etapas pueden usar imágenes base distintas.
Las compilaciones de varias etapas son especialmente útiles cuando tu Dockerfile necesita instalar paquetes que solo se necesitan en tiempo de compilación. En esta situación, la estructura general de tu Dockerfile es así:
Con esta configuración, los paquetes adicionales se instalan solo en la etapa «build» y no en la etapa «runtime», lo que significa que no acabarán en la imagen de Docker que se produce.
El test runner de Fortran necesita curl para descargar algunos archivos.
Sin embargo, su imagen de tiempo de ejecución no necesita curl, lo que hace de este un caso de uso perfecto para una compilación de varias etapas.
Primero, su Dockerfile define una etapa (llamada «build») en la que se instala el paquete curl.
Después usa curl para descargar archivos en esa etapa.
FROM alpine:3.15 AS build
RUN apk add --no-cache curl
WORKDIR /opt/test-runner
COPY bust_cache .
WORKDIR /opt/test-runner/testlib
RUN curl -R -O https://raw.githubusercontent.com/exercism/fortran/main/testlib/CMakeLists.txt
RUN curl -R -O https://raw.githubusercontent.com/exercism/fortran/main/testlib/TesterMain.f90
WORKDIR /opt/test-runner
RUN curl -R -O https://raw.githubusercontent.com/exercism/fortran/main/config/CMakeLists.txt
La segunda parte del Dockerfile define una nueva etapa y copia los archivos descargados de la etapa «build» a su propia etapa usando el comando COPY:
FROM alpine:3.15
RUN apk add --no-cache coreutils jq gfortran libc-dev cmake make
WORKDIR /opt/test-runner
COPY --from=build /opt/test-runner/ .
COPY . .
ENTRYPOINT ["/opt/test-runner/bin/run.sh"]
El test runner de Ruby necesita que se instalen los paquetes git, openssh, build-base, gcc y wget antes de poder instalar sus bibliotecas (gemas) requeridas.
Su Dockerfile comienza con una etapa (a la que se da el nombre build) que instala esos paquetes (mediante apk add) y después instala las dependencias (mediante bundle install):
FROM ruby:3.2.2-alpine3.18 AS build
RUN apk update && apk upgrade && \
apk add --no-cache git openssh build-base gcc wget git
COPY Gemfile Gemfile.lock .
RUN gem install bundler:2.4.18 && \
bundle config set without 'development test' && \
bundle install
Después define la etapa que formará la imagen de Docker resultante.
Esta etapa no instala las dependencias que instaló la etapa anterior; en su lugar, usa el comando COPY para copiar las bibliotecas instaladas de la etapa «build» a su propia etapa:
FROM ruby:3.2.2-alpine3.18
RUN apk add --no-cache bash
WORKDIR /opt/test-runner
COPY --from=build /usr/local/bundle /usr/local/bundle
COPY . .
ENTRYPOINT [ "sh", "/opt/test-runner/bin/run.sh" ]
El Dockerfile del test runner de C# hace algo similar, solo que en este caso la etapa «build» puede usar una imagen de Docker existente que ya tiene preinstalados los paquetes adicionales necesarios para instalar bibliotecas.
Las pruebas unitarias pueden ser muy útiles, pero te recomendamos centrarte en escribir pruebas de integración. Su principal ventaja es que comprueban mejor cómo se ejecutan las herramientas en producción y, por tanto, ayudan a aumentar la confianza en la implementación de tus herramientas.
Para imitar lo mejor posible el entorno de producción, las pruebas de integración deberían ejecutar las herramientas como en el entorno de producción. Esto significa compilar la imagen de Docker y luego ejecutar la imagen compilada con una solución para verificar su salida.
Las pruebas de integración deberían definirse como pruebas golden, que son pruebas en las que la salida esperada se almacena en un archivo. Esto es perfecto para las pruebas de integración de las herramientas del track, ya que la salida de las herramientas también son archivos.
Al ejecutar el test runner con una solución, su salida es un archivo results.json.
Después podemos comparar este archivo con un archivo de salida «conocido como bueno» (es decir, «esperado») (llamado expected_results.json) para comprobar si el test runner funciona como se pretende.
La seguridad es uno de los principales motivos por los que usamos contenedores de Docker para ejecutar nuestras herramientas.
Hay muchas imágenes de Docker en Docker Hub, pero intenta usar las oficiales. Estas imágenes están seleccionadas y tienen (muchas) menos probabilidades de ser inseguras.
Para asegurarte de que las compilaciones sean estables (es decir, que no se rompan de repente), deberías fijar siempre tus imágenes base a etiquetas concretas. Eso significa que, en lugar de:
FROM alpine:latest
deberías usar:
FROM alpine:3.20.2
Con esta última, las compilaciones siempre usarán la misma versión.
De forma predeterminada, muchas imágenes se ejecutan con un usuario que tiene privilegios de root. Deberías plantearte ejecutarlas como un usuario sin privilegios.
FROM alpine
RUN groupadd -r myuser && useradd -r -g myuser myuser
# RUN <COMMANDS THAT REQUIRE ROOT USER, E.G. INSTALLING PACKAGES>
USER myuser
(Casi) siempre es buena idea instalar las últimas versiones
RUN apt-get update && \
apt-get install curl
Animamos a que los archivos de Docker se escriban usando un sistema de archivos de solo lectura. Los únicos directorios que debes asumir que son escribibles son:
/tmp
Nuestro entorno de producción actualmente no exige un sistema de archivos de solo lectura, pero es posible que lo hagamos en el futuro. Por este motivo, la plantilla base de un nuevo test runner/analyzer/representer parte de un sistema de archivos de solo lectura. Si no consigues que las cosas funcionen en un sistema de archivos de solo lectura, puedes asumir (por ahora) un sistema de archivos escribible.