¿Cómo evitar reinstalar paquetes al crear una imagen de Docker para proyectos de Python?

128

Mi Dockerfile es algo como

FROM my/base

ADD . /srv
RUN pip install -r requirements.txt
RUN python setup.py install

ENTRYPOINT ["run_server"]

Cada vez que construyo una nueva imagen, las dependencias deben reinstalarse, lo que podría ser muy lento en mi región.

Una forma en la que pienso en los cachepaquetes que se han instalado es anular la my/baseimagen con imágenes más nuevas como esta:

docker build -t new_image_1 .
docker tag new_image_1 my/base

Entonces, la próxima vez que construya con este Dockerfile, mi / base ya tiene algunos paquetes instalados.

Pero esta solución tiene dos problemas:

  1. No siempre es posible anular una imagen base
  2. La imagen base crece cada vez más a medida que se superponen imágenes más nuevas.

Entonces, ¿qué mejor solución podría usar para resolver este problema?

EDITAR##:

Alguna información sobre la ventana acoplable en mi máquina:

  test  docker version
Client version: 1.1.2
Client API version: 1.13
Go version (client): go1.2.1
Git commit (client): d84a070
Server version: 1.1.2
Server API version: 1.13
Go version (server): go1.2.1
Git commit (server): d84a070
  test  docker info
Containers: 0
Images: 56
Storage Driver: aufs
 Root Dir: /var/lib/docker/aufs
 Dirs: 56
Execution Driver: native-0.2
Kernel Version: 3.13.0-29-generic
WARNING: No swap limit support
satoru
fuente
¿Elimina la imagen intermedia después de terminar de construir su imagen?
Regan
Por supuesto que no, pero esto es irrelevante porque cuando reconstruyo una imagen, todavía me baso en el originalmy/base
satoru

Respuestas:

139

Intente construir un Dockerfile que se parezca a esto:

FROM my/base

WORKDIR /srv
ADD ./requirements.txt /srv/requirements.txt
RUN pip install -r requirements.txt
ADD . /srv
RUN python setup.py install

ENTRYPOINT ["run_server"]

Docker usará la caché durante la instalación de pip siempre que no realice ningún cambio en el requirements.txt, independientemente de si .se cambiaron o no otros archivos de código en . He aquí un ejemplo.


Aquí tienes un Hello, World!programa simple :

$ tree
.
├── Dockerfile
├── requirements.txt
└── run.py   

0 directories, 3 file

# Dockerfile

FROM dockerfile/python
WORKDIR /srv
ADD ./requirements.txt /srv/requirements.txt
RUN pip install -r requirements.txt
ADD . /srv
CMD python /srv/run.py

# requirements.txt
pytest==2.3.4

# run.py
print("Hello, World")

La salida de la compilación de Docker:

Step 1 : WORKDIR /srv
---> Running in 22d725d22e10
---> 55768a00fd94
Removing intermediate container 22d725d22e10
Step 2 : ADD ./requirements.txt /srv/requirements.txt
---> 968a7c3a4483
Removing intermediate container 5f4e01f290fd
Step 3 : RUN pip install -r requirements.txt
---> Running in 08188205e92b
Downloading/unpacking pytest==2.3.4 (from -r requirements.txt (line 1))
  Running setup.py (path:/tmp/pip_build_root/pytest/setup.py) egg_info for package pytest
....
Cleaning up...
---> bf5c154b87c9
Removing intermediate container 08188205e92b
Step 4 : ADD . /srv
---> 3002a3a67e72
Removing intermediate container 83defd1851d0
Step 5 : CMD python /srv/run.py
---> Running in 11e69b887341
---> 5c0e7e3726d6
Removing intermediate container 11e69b887341
Successfully built 5c0e7e3726d6

Modifiquemos run.py:

# run.py
print("Hello, Python")

Intente construir de nuevo, a continuación se muestra el resultado:

Sending build context to Docker daemon  5.12 kB
Sending build context to Docker daemon 
Step 0 : FROM dockerfile/python
---> f86d6993fc7b
Step 1 : WORKDIR /srv
---> Using cache
---> 55768a00fd94
Step 2 : ADD ./requirements.txt /srv/requirements.txt
---> Using cache
---> 968a7c3a4483
Step 3 : RUN pip install -r requirements.txt
---> Using cache
---> bf5c154b87c9
Step 4 : ADD . /srv
---> 9cc7508034d6
Removing intermediate container 0d7cf71eb05e
Step 5 : CMD python /srv/run.py
---> Running in f25c21135010
---> 4ffab7bc66c7
Removing intermediate container f25c21135010
Successfully built 4ffab7bc66c7

Como puede ver arriba, esta vez la ventana acoplable usa caché durante la compilación. Ahora, actualice requirements.txt:

# requirements.txt

pytest==2.3.4
ipython

A continuación se muestra el resultado de la compilación de Docker:

Sending build context to Docker daemon  5.12 kB
Sending build context to Docker daemon 
Step 0 : FROM dockerfile/python
---> f86d6993fc7b
Step 1 : WORKDIR /srv
---> Using cache
---> 55768a00fd94
Step 2 : ADD ./requirements.txt /srv/requirements.txt
---> b6c19f0643b5
Removing intermediate container a4d9cb37dff0
Step 3 : RUN pip install -r requirements.txt
---> Running in 4b7a85a64c33
Downloading/unpacking pytest==2.3.4 (from -r requirements.txt (line 1))
  Running setup.py (path:/tmp/pip_build_root/pytest/setup.py) egg_info for package pytest

Downloading/unpacking ipython (from -r requirements.txt (line 2))
Downloading/unpacking py>=1.4.12 (from pytest==2.3.4->-r requirements.txt (line 1))
  Running setup.py (path:/tmp/pip_build_root/py/setup.py) egg_info for package py

Installing collected packages: pytest, ipython, py
  Running setup.py install for pytest

Installing py.test script to /usr/local/bin
Installing py.test-2.7 script to /usr/local/bin
  Running setup.py install for py

Successfully installed pytest ipython py
Cleaning up...
---> 23a1af3df8ed
Removing intermediate container 4b7a85a64c33
Step 4 : ADD . /srv
---> d8ae270eca35
Removing intermediate container 7f003ebc3179
Step 5 : CMD python /srv/run.py
---> Running in 510359cf9e12
---> e42fc9121a77
Removing intermediate container 510359cf9e12
Successfully built e42fc9121a77

Observe cómo Docker no usó el caché durante la instalación de pip. Si no funciona, verifique su versión de Docker.

Client version: 1.1.2
Client API version: 1.13
Go version (client): go1.2.1
Git commit (client): d84a070
Server version: 1.1.2
Server API version: 1.13
Go version (server): go1.2.1
Git commit (server): d84a070
nacyot
fuente
2
Esto no parece funcionar, porque siempre que la ventana acoplable ve una ADDinstrucción, la caché se invalida.
Satoru
1
No estoy seguro de por qué no funciona. Pero no hay ningún cambio en requirements.txt (el <src> ADD ./requirements.txt /srv/requirements.txtactivado), entonces la ventana acoplable debe usar la caché. Consulte agregar sección en el documento Dockerfile.
Nacyot
16
Sí, utilizará el caché si no cambia requirements.txt. Pero si el archivo requirements.txt cambia, se descargan todos los requisitos. ¿Hay alguna forma de que pueda montar un volumen de caché de pip en el contenedor de la ventana acoplable para cargar desde la caché?
Jitu
7
La clave de esta respuesta es que agrega requirements.txt ( ADD requirements.txt /srvantes de ejecutar pip ( RUN pip install -r requirements.txt) y agrega todos los demás archivos después de ejecutar pip. Por lo tanto, deben estar en el siguiente orden: (1) ADD requirements.txt /srv; (2) RUN pip install -r requirements.txt; ( 3)ADD . /srv
engelen
2
Tenga en cuenta que esto no funciona cuando se usa COPY en lugar de ADD
veuncent
29

Para minimizar la actividad de la red, puede apuntar pipa un directorio de caché en su máquina host.

Ejecute su contenedor de la ventana acoplable con el enlace del directorio de caché pip de su host montado en el directorio de caché pip de su contenedor. docker runEl comando debería verse así:

docker run -v $HOME/.cache/pip-docker/:/root/.cache/pip image_1

Luego, en su Dockerfile, instale sus requisitos como parte de la ENTRYPOINTdeclaración (o CMDdeclaración) en lugar de como un RUNcomando. Esto es importante, porque (como se señaló en los comentarios) el montaje no está disponible durante la construcción de la imagen (cuando RUNse ejecutan las declaraciones). El archivo Docker debería verse así:

FROM my/base

ADD . /srv

ENTRYPOINT ["sh", "-c", "pip install -r requirements.txt && python setup.py install && run_server"]
Jakub Kukul
fuente
4
No es lo que el OP estaba buscando en su caso de uso, pero si está creando un servidor de compilación, esta es una gran idea
oden
2
Esto parece una receta para problemas, particularmente la sugerencia de apuntar a la caché de host predeterminada. Potencialmente está mezclando paquetes específicos de arco.
Giacomo Lacava
@GiacomoLacava gracias, ese es un muy buen punto. Ajusté mi respuesta y eliminé la parte que sugería usar la reutilización del directorio de caché de los hosts.
Jakub Kukul
24

Entiendo que esta pregunta ya tiene algunas respuestas populares. Pero hay una forma más nueva de almacenar archivos en caché para los administradores de paquetes. Creo que podría ser una buena respuesta en el futuro cuando BuildKit se vuelva más estándar.

A partir de Docker 18.09, existe soporte experimental para BuildKit . BuildKit agrega soporte para algunas características nuevas en Dockerfile, incluido el soporte experimental para montar volúmenes externos en RUNpasos. Esto nos permite crear cachés para cosas como $HOME/.cache/pip/.

Usaremos el siguiente requirements.txtarchivo como ejemplo:

Click==7.0
Django==2.2.3
django-appconf==1.0.3
django-compressor==2.3
django-debug-toolbar==2.0
django-filter==2.2.0
django-reversion==3.0.4
django-rq==2.1.0
pytz==2019.1
rcssmin==1.0.6
redis==3.3.4
rjsmin==1.1.0
rq==1.1.0
six==1.12.0
sqlparse==0.3.0

Un ejemplo típico de Python Dockerfilepodría verse así:

FROM python:3.7
WORKDIR /usr/src/app
COPY requirements.txt /usr/src/app/
RUN pip install -r requirements.txt
COPY . /usr/src/app

Con BuildKit habilitado usando la DOCKER_BUILDKITvariable de entorno, podemos construir el pippaso sin caché en aproximadamente 65 segundos:

$ export DOCKER_BUILDKIT=1
$ docker build -t test .
[+] Building 65.6s (10/10) FINISHED                                                                                                                                             
 => [internal] load .dockerignore                                                                                                                                          0.0s
 => => transferring context: 2B                                                                                                                                            0.0s
 => [internal] load build definition from Dockerfile                                                                                                                       0.0s
 => => transferring dockerfile: 120B                                                                                                                                       0.0s
 => [internal] load metadata for docker.io/library/python:3.7                                                                                                              0.5s
 => CACHED [1/4] FROM docker.io/library/python:3.7@sha256:6eaf19442c358afc24834a6b17a3728a45c129de7703d8583392a138ecbdb092                                                 0.0s
 => [internal] load build context                                                                                                                                          0.6s
 => => transferring context: 899.99kB                                                                                                                                      0.6s
 => CACHED [internal] helper image for file operations                                                                                                                     0.0s
 => [2/4] COPY requirements.txt /usr/src/app/                                                                                                                              0.5s
 => [3/4] RUN pip install -r requirements.txt                                                                                                                             61.3s
 => [4/4] COPY . /usr/src/app                                                                                                                                              1.3s
 => exporting to image                                                                                                                                                     1.2s
 => => exporting layers                                                                                                                                                    1.2s
 => => writing image sha256:d66a2720e81530029bf1c2cb98fb3aee0cffc2f4ea2aa2a0760a30fb718d7f83                                                                               0.0s
 => => naming to docker.io/library/test                                                                                                                                    0.0s

Ahora, agreguemos el encabezado experimental y modifiquemos el RUNpaso para almacenar en caché los paquetes de Python:

# syntax=docker/dockerfile:experimental

FROM python:3.7
WORKDIR /usr/src/app
COPY requirements.txt /usr/src/app/
RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt
COPY . /usr/src/app

Adelante, haz otra compilación ahora. Debería llevar la misma cantidad de tiempo. Pero esta vez está almacenando en caché los paquetes de Python en nuestro nuevo montaje de caché:

$ docker build -t pythontest .
[+] Building 60.3s (14/14) FINISHED                                                                                                                                             
 => [internal] load build definition from Dockerfile                                                                                                                       0.0s
 => => transferring dockerfile: 120B                                                                                                                                       0.0s
 => [internal] load .dockerignore                                                                                                                                          0.0s
 => => transferring context: 2B                                                                                                                                            0.0s
 => resolve image config for docker.io/docker/dockerfile:experimental                                                                                                      0.5s
 => CACHED docker-image://docker.io/docker/dockerfile:experimental@sha256:9022e911101f01b2854c7a4b2c77f524b998891941da55208e71c0335e6e82c3                                 0.0s
 => [internal] load .dockerignore                                                                                                                                          0.0s
 => [internal] load build definition from Dockerfile                                                                                                                       0.0s
 => => transferring dockerfile: 120B                                                                                                                                       0.0s
 => [internal] load metadata for docker.io/library/python:3.7                                                                                                              0.5s
 => CACHED [1/4] FROM docker.io/library/python:3.7@sha256:6eaf19442c358afc24834a6b17a3728a45c129de7703d8583392a138ecbdb092                                                 0.0s
 => [internal] load build context                                                                                                                                          0.7s
 => => transferring context: 899.99kB                                                                                                                                      0.6s
 => CACHED [internal] helper image for file operations                                                                                                                     0.0s
 => [2/4] COPY requirements.txt /usr/src/app/                                                                                                                              0.6s
 => [3/4] RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt                                                                                  53.3s
 => [4/4] COPY . /usr/src/app                                                                                                                                              2.6s
 => exporting to image                                                                                                                                                     1.2s
 => => exporting layers                                                                                                                                                    1.2s
 => => writing image sha256:0b035548712c1c9e1c80d4a86169c5c1f9e94437e124ea09e90aea82f45c2afc                                                                               0.0s
 => => naming to docker.io/library/test                                                                                                                                    0.0s

Aproximadamente 60 segundos. Similar a nuestra primera construcción.

Realice un pequeño cambio en requirements.txt(como agregar una nueva línea entre dos paquetes) para forzar una invalidación de caché y ejecutar nuevamente:

$ docker build -t pythontest .
[+] Building 15.9s (14/14) FINISHED                                                                                                                                             
 => [internal] load build definition from Dockerfile                                                                                                                       0.0s
 => => transferring dockerfile: 120B                                                                                                                                       0.0s
 => [internal] load .dockerignore                                                                                                                                          0.0s
 => => transferring context: 2B                                                                                                                                            0.0s
 => resolve image config for docker.io/docker/dockerfile:experimental                                                                                                      1.1s
 => CACHED docker-image://docker.io/docker/dockerfile:experimental@sha256:9022e911101f01b2854c7a4b2c77f524b998891941da55208e71c0335e6e82c3                                 0.0s
 => [internal] load build definition from Dockerfile                                                                                                                       0.0s
 => => transferring dockerfile: 120B                                                                                                                                       0.0s
 => [internal] load .dockerignore                                                                                                                                          0.0s
 => [internal] load metadata for docker.io/library/python:3.7                                                                                                              0.5s
 => CACHED [1/4] FROM docker.io/library/python:3.7@sha256:6eaf19442c358afc24834a6b17a3728a45c129de7703d8583392a138ecbdb092                                                 0.0s
 => CACHED [internal] helper image for file operations                                                                                                                     0.0s
 => [internal] load build context                                                                                                                                          0.7s
 => => transferring context: 899.99kB                                                                                                                                      0.7s
 => [2/4] COPY requirements.txt /usr/src/app/                                                                                                                              0.6s
 => [3/4] RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt                                                                                   8.8s
 => [4/4] COPY . /usr/src/app                                                                                                                                              2.1s
 => exporting to image                                                                                                                                                     1.1s
 => => exporting layers                                                                                                                                                    1.1s
 => => writing image sha256:fc84cd45482a70e8de48bfd6489e5421532c2dd02aaa3e1e49a290a3dfb9df7c                                                                               0.0s
 => => naming to docker.io/library/test                                                                                                                                    0.0s

¡Solo unos 16 segundos!

Estamos obteniendo esta aceleración porque ya no estamos descargando todos los paquetes de Python. Fueron almacenados en caché por el administrador de paquetes ( pipen este caso) y almacenados en un montaje de volumen de caché. El montaje de volumen se proporciona al paso de ejecución para que pippueda reutilizar nuestros paquetes ya descargados. Esto sucede fuera de cualquier almacenamiento en caché de la capa de Docker .

Las ganancias deberían ser mucho mejores a mayor tamaño requirements.txt.

Notas:

  • Esta es una sintaxis experimental de Dockerfile y debe tratarse como tal. Es posible que no desee construir con esto en producción en este momento.
  • Las cosas de BuildKit no funcionan bajo Docker Compose u otras herramientas que usan directamente la API de Docker en este momento. Ahora hay soporte para esto en Docker Compose a partir de la versión 1.25.0. Consulte ¿Cómo se habilita BuildKit con docker-compose?
  • No hay ninguna interfaz directa para administrar la caché en este momento. Se purga cuando realiza un docker system prune -a.

Con suerte, estas características se convertirán en Docker para compilar y BuildKit se convertirá en el predeterminado. Si / cuando eso suceda, intentaré actualizar esta respuesta.

Andy Shinn
fuente
Puedo confirmar que esta solución funciona muy bien. Mi construcción bajó de más de un minuto a solo 2.2 segundos. Gracias @ andy-shinn.
Kwuite
2
Ahora también Docker-Compose: stackoverflow.com/questions/58592259/…
Rexcirus
Nota: Si está utilizando SUDO para ejecutar la ventana acoplable, probablemente deba hacer: sudo DOCKER_BUILDKIT = 1 ...
Vinícius M
Recibo este error: - no se pudo resolver con la interfaz dockerfile.v0: no se pudo crear la definición de LLB: Dockerfile parse error línea 10: Bandera desconocida: montaje
Mayur Dangar
Suena como si se hubiera perdido el comentario en la parte superior del Dockerfileo la versión de Docker es demasiado antigua. Crearía una nueva pregunta con toda su información de depuración.
Andy Shinn
-10

Descubrí que una mejor manera es simplemente agregar el directorio de paquetes de sitio de Python como un volumen.

services:
    web:
        build: .
        command: python manage.py runserver 0.0.0.0:8000
        volumes:
            - .:/code
            -  /usr/local/lib/python2.7/site-packages/

De esta manera, puedo instalar nuevas bibliotecas sin tener que hacer una reconstrucción completa.

EDITAR : Ignore esta respuesta, la respuesta de jkukul anterior funcionó para mí. Mi intención era almacenar en caché la carpeta de paquetes del sitio . Eso habría parecido algo más parecido a:

volumes:
   - .:/code
   - ./cached-packages:/usr/local/lib/python2.7/site-packages/

Sin embargo, almacenar en caché la carpeta de descarga es mucho más limpio. Eso también almacena en caché las ruedas, por lo que realiza correctamente la tarea.

jaywhy13
fuente
2
¿Y qué sucede cuando intentas compilar este dockerfile en una máquina diferente? Ésta no es una solución sostenible.
Aaron McMillin
Realmente confundido, esto resultó tener errores y no estaba seguro de por qué. ¿Podría darnos más detalles?
jaywhy13
3
La imagen de la ventana acoplable depende del estado del sistema host. Esto anula la mayor parte de la utilidad de Docker. Todo lo que necesita la imagen debe estar instalado en él. use el Dockerfile para instalar todas las dependencias. Si desea evitar volver a descargar los paquetes cada vez que crea la respuesta desde jkukul para montar el caché de pip, es el camino a seguir.
Aaron McMillin
2
La bombilla se acaba de apagar gracias. De hecho, estaba intentando montar el directorio de paquetes de sitio desde la máquina virtual, no desde el host. Todo un descuido. Creo que en espíritu estaba tratando de hacer lo mismo que sugirió jkulkul. ¡Gracias por la claridad!
jaywhy13
@AaronMcMillin En realidad, no depende de una ruta en el host. Está montando los paquetes del sitio en el contenedor en un volumen anónimo. Sin embargo
sigue