Las buenas prácticas oficiales para Dockerfile tienen muchísimo 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 expiren por límite de tiempo.
Medir el tiempo de ejecución con frecuencia es una excelente manera de hacerte una idea del rendimiento de tus herramientas. Haz que sea un hábito medir el tiempo de ejecución tanto después como antes de un cambio. Incluso cuando sientes «cierto» que un cambio va a mejorar el rendimiento, igual deberías medir el tiempo de ejecución.
Cuando sea posible, crea scripts para medir el rendimiento automáticamente (también conocido 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 de track más nuevos tendrán acceso a los siguientes dos scripts:
./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 Docker de las herramientas del track (código fuente)Si estás trabajando en un repositorio de herramientas de track que no tiene estos archivos, siéntete libre de copiarlos a tu repositorio usando los enlaces al código fuente de arriba.
Los scripts de benchmarking pueden ayudar a estimar el rendimiento de las herramientas. Ten en cuenta, eso sí, que el rendimiento en los servidores de producción de Exercism suele ser menor.
Prueba a experimentar con distintas imágenes base (por ejemplo, Alpine en lugar de Ubuntu), para ver si una supera (significativamente) a la otra. Si el rendimiento es más o menos igual, elige la imagen que sea más pequeña.
Comprueba si usar la red internal en lugar de none mejora el rendimiento.
Consulta la documentación sobre redes para más información.
Las herramientas del track ejecutan un contenedor Docker único 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 excelente manera de mejorar el rendimiento. Una forma de lograrlo es trasladar código del tiempo de ejecución al tiempo de construcció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 construcción solo se ejecuta una vez (cuando se construye la imagen Docker).
El código de tiempo de construcción se ejecuta una vez como parte de un flujo de trabajo de GitHub Actions. Por lo tanto, no hay problema si el código que se ejecuta en tiempo de construcció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 pruebas ocurre en un contenedor nuevo, ¡esto significa que esa compilación se hacía en todas y cada una de las ejecuciones de pruebas! Para evitar esto, el Dockerfile del test runner de Haskell tiene los siguientes dos comandos:
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 prueba y depende de las mismas bibliotecas base de las que depende el ejercicio real.
Luego ejecutamos las pruebas sobre ese directorio, de forma similar a como 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 construcción.
Así, la imagen Docker resultante tendrá sus bibliotecas base ya compiladas.
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 en el momento. Esto es una disyuntiva entre tiempo de construcción y tiempo de ejecución y, de nuevo, preferimos la ejecución en tiempo de construcción por motivos de rendimiento.
El Dockerfile del test runner de C# usa este enfoque, donde el test runner se compila a un binario por adelantado (en tiempo de construcción) en lugar de compilar el código en el momento (en tiempo de ejecución). Esto significa que hay menos trabajo por 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 significa que esta:
Las distintas imágenes de distribución tendrán tamaños distintos.
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, así que muchas imágenes de herramientas se basan en Alpine.
Algunas imágenes tienen variantes «slim» especiales, en las que se han eliminado algunas funcionalidades, lo que da como resultado imágenes más pequeñas.
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 funcionalidades. Puede que tu imagen no necesite esas funcionalidades adicionales y, si no las necesita, considera usar la variante «slim».
Una manera 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 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 construcción (ya que no hay conexión a internet disponible en tiempo de ejecución). Por lo tanto, cualquier archivo de caché o de registro 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 en varias etapas. Estas te permiten dividir tu Dockerfile en etapas separadas, y solo la última etapa termina en la imagen Docker producida (el resto solo está ahí para ayudar a construir la última etapa). Puedes pensar en cada etapa como su propio mini Dockerfile; las etapas pueden usar imágenes base distintas.
Las compilaciones en varias etapas son especialmente útiles cuando tu Dockerfile necesita instalar paquetes que solo se necesitan en tiempo de construcción. En esta situación, la estructura general de tu Dockerfile se ve 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 terminarán en la imagen 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 en varias etapas.
Primero, su Dockerfile define una etapa (llamada «build») en la que se instala el paquete curl.
Luego 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 requeridas (gemas).
Su Dockerfile empieza con una etapa (a la que se le da el nombre build) que instala esos paquetes (mediante apk add) y luego 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
Luego define la etapa que formará la imagen 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 de construcción 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 de construcción puede usar una imagen Docker existente que ya tiene preinstalados los paquetes adicionales necesarios para instalar bibliotecas.
Las pruebas unitarias pueden ser muy útiles, pero recomendamos centrarte en escribir pruebas de integración. Su principal beneficio es que prueban mejor cómo se ejecutan las herramientas en producción y, por lo 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 el entorno de producción. Esto significa construir la imagen Docker y luego ejecutar la imagen construida sobre 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 guarda en un archivo. Esto es perfecto para las pruebas de integración de las herramientas de un track, ya que la salida de las herramientas también son archivos.
Cuando se ejecuta el test runner sobre una solución, su salida es un archivo results.json.
Luego podemos comparar este archivo con un archivo de salida de referencia (es decir, el «esperado»), llamado expected_results.json, para comprobar si el test runner funciona como se pretende.
La seguridad es una de las razones principales por las que usamos contenedores Docker para ejecutar nuestras herramientas.
Hay muchas imágenes Docker en Docker Hub, pero intenta usar las oficiales. Estas imágenes están curadas y tienen (muchas) menos probabilidades de ser inseguras.
Para asegurarte de que las construcciones sean estables (es decir, que no se rompan de repente), siempre deberías fijar tus imágenes base a etiquetas específicas. Eso significa que en lugar de:
FROM alpine:latest
deberías usar:
FROM alpine:3.20.2
Con esta última, las construcciones siempre usarán la misma versión.
Por defecto, muchas imágenes se ejecutarán con un usuario que tiene privilegios de root. Deberías considerar ejecutar 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
Es (casi) siempre una buena idea instalar las últimas versiones
RUN apt-get update && \
apt-get install curl
Animamos a que los archivos Docker se escriban usando un sistema de archivos de solo lectura. Los únicos directorios que deberías asumir como escribibles son:
/tmp
Nuestro entorno de producción actualmente no aplica un sistema de archivos de solo lectura, pero podríamos hacerlo en el futuro. Por este motivo, la plantilla base de un nuevo test runner/analyzer/representer empieza con un sistema de archivos de solo lectura. Si no logras que las cosas funcionen en un archivo de solo lectura, siéntete libre de (por ahora) asumir un sistema de archivos escribible.