¿Cómo evitar reinstalar paquetes al crear una imagen de Docker para proyectos de Python?
Mi Dockerfile es algo así como
FROM my/base
ADD . /srv
RUN pip install -r requirements.txt
RUN python setup.py install
ENTRYPOINT ["run_server"]
Cada vez que creo una nueva imagen, es necesario reinstalar las dependencias, lo que podría ser muy lento en mi región.
Una forma en que pienso en los cache
paquetes que se han instalado es anular la my/base
imagen 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:
- No siempre es posible anular una imagen base
- 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 utilizar 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
Intente crear 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á el caché durante la instalación de pip siempre que no realice ningún cambio en requirements.txt
, independientemente de si .
se cambiaron otros archivos de código o no. He aquí un ejemplo.
Aquí tienes un Hello, World!
programa sencillo:
$ 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")
El resultado 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 compilar nuevamente, 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 Docker usa el caché durante la compilación. Ahora, actualicemos 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
Entiendo que esta pregunta ya tiene algunas respuestas populares. Pero existe 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 hay soporte experimental para BuildKit . BuildKit agrega soporte para algunas funciones nuevas en Dockerfile, incluido el soporte experimental para montar volúmenes externos en RUN
pasos. Esto nos permite crear cachés para cosas como $HOME/.cache/pip/
.
Usaremos el siguiente requirements.txt
archivo 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 Dockerfile
podrí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_BUILDKIT
variable de entorno, podemos construir el pip
paso 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 RUN
paso 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
Continúe y haga otra compilación ahora. Debería llevar la misma cantidad de tiempo. Pero esta vez se están 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
Unos 60 segundos. Similar a nuestra primera construcción.
Realice un pequeño cambio 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
¡Sólo unos 16 segundos!
Obtenemos esta aceleración porque ya no descargamos todos los paquetes de Python. El administrador de paquetes los almacenó en caché ( pip
en este caso) y se almacenaron en un montaje de volumen de caché. El montaje del volumen se proporciona en el paso de ejecución para que pip
podamos reutilizar nuestros paquetes ya descargados. Esto sucede fuera del almacenamiento en caché de cualquier capa de Docker .
Las ganancias deberían ser mucho mejores en los más grandes requirements.txt
.
Notas:
- Esta es una sintaxis experimental de Dockerfile y debe tratarse como tal. Es posible que no quieras construir con esto en producción en este momento.
El material de BuildKit no funciona en 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 1.25.0. Consulte ¿Cómo se habilita BuildKit con Docker-Compose?- Por el momento no existe ninguna interfaz directa para gestionar el caché. Se elimina cuando haces un
docker system prune -a
.
Con suerte, estas funciones se incluirán en Docker para la compilación y BuildKit se convertirá en el valor predeterminado. Si eso sucede, intentaré actualizar esta respuesta.
Para minimizar la actividad de la red, puede apuntar pip
a un directorio de caché en su máquina host.
Ejecute su contenedor acoplable con el enlace del directorio de caché pip de su host montado en el directorio de caché pip de su contenedor. docker run
El 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 ENTRYPOINT
una declaración (o CMD
declaración) en lugar de como un RUN
comando. Esto es importante porque (como se señaló en los comentarios) el montaje no está disponible durante la creación de la imagen (cuando RUN
se 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"]
pipenv install
de forma predeterminada intenta volver a bloquearse. Cuando lo hace, la capa almacenada en caché de la compilación de Docker no se usa porque Pipfile.lock ha cambiado. Ver los documentos
Una solución para esto es versionar Pipfile.lock y usar
RUN pipenv sync
en cambio.
Gracias a JFG Piñeiro.