Como evitar a reinstalação de pacotes ao construir uma imagem Docker para projetos Python?

128

Meu Dockerfile é algo como

FROM my/base

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

ENTRYPOINT ["run_server"]

Cada vez que construo uma nova imagem, as dependências precisam ser reinstaladas, o que pode ser muito lento na minha região.

Uma maneira que penso em cachepacotes que foram instalados é substituir a my/baseimagem por imagens mais recentes como esta:

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

Então, da próxima vez que eu construir com este Dockerfile, meu / base já tem alguns pacotes instalados.

Mas esta solução tem dois problemas:

  1. Nem sempre é possível substituir uma imagem de base
  2. A imagem de base fica cada vez maior conforme novas imagens são colocadas em camadas

Então, que solução melhor eu poderia usar para resolver esse problema?

EDITAR##:

Algumas informações sobre o docker em minha 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
fonte
Você exclui a imagem intermediária após terminar de construir sua imagem?
Regan de
Claro que não, mas isso é irrelevante porque quando eu reconstruo uma imagem, ainda estou me baseando no originalmy/base
satoru

Respostas:

139

Tente construir um Dockerfile que se pareça com isto:

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"]

O Docker usará o cache durante a instalação do pip, desde que você não faça alterações no requirements.txt, independentemente do fato de outros arquivos de código em .terem sido alterados ou não. Aqui está um exemplo.


Aqui está um Hello, World!programa simples :

$ 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")

A saída do docker build:

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

Vamos modificar run.py:

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

Tente construir novamente, abaixo está o 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 você pode ver acima, desta vez, o docker usa o cache durante a construção. Agora, vamos atualizar requirements.txt:

# requirements.txt

pytest==2.3.4
ipython

Abaixo está a saída do docker build:

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 como o docker não usou o cache durante a instalação do pip. Se não funcionar, verifique a versão do 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
fonte
2
Isso não parece funcionar, porque sempre que o docker vê uma ADDinstrução, o cache é invalidado.
satoru de
1
Não sei por que não funciona. Mas não há nenhuma mudança em requirements.txt (o <src> ativado ADD ./requirements.txt /srv/requirements.txt), então o docker deve usar o cache. Consulte adicionar seção no documento Dockerfile.
nacyot de
16
Sim, ele usará o cache se o arquivo requirements.txt não for alterado. Mas se o requirements.txt for alterado, todos os requisitos serão baixados. Existe alguma maneira de montar um volume de cache pip no contêiner do docker para carregar do cache?
Jitu
7
A chave para essa resposta é adicionar requirements.txt ( ADD requirements.txt /srvantes de executar pip ( RUN pip install -r requirements.txt) e adicionar todos os outros arquivos após executar pip. Portanto, eles devem estar na seguinte ordem: (1) ADD requirements.txt /srv; (2) RUN pip install -r requirements.txt; ( 3)ADD . /srv
engelen 01 de
2
Observe que isso não funciona ao usar COPY em vez de ADD
veuncent
29

Para minimizar a atividade da rede, você pode apontar pippara um diretório de cache em sua máquina host.

Execute seu contêiner do docker com o vínculo do diretório de cache do seu host montado no diretório de cache do seu contêiner. docker runcomando deve ser semelhante a este:

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

Em seguida, em seu Dockerfile, instale seus requisitos como parte da ENTRYPOINTinstrução (ou CMDinstrução) em vez de como um RUNcomando. Isso é importante porque (conforme apontado nos comentários) a montagem não está disponível durante a construção da imagem (quando as RUNinstruções são executadas). O arquivo Docker deve ser semelhante a este:

FROM my/base

ADD . /srv

ENTRYPOINT ["sh", "-c", "pip install -r requirements.txt && python setup.py install && run_server"]
Jakub Kukul
fonte
4
Não é o que o OP estava procurando em seu caso de uso, mas se você está criando um servidor de compilação, essa é uma ótima ideia
oden
2
Esta parece ser uma receita para problemas, especialmente a sugestão de apontar para o cache do host padrão. Você está potencialmente misturando pacotes específicos do arco.
Giacomo Lacava
@GiacomoLacava obrigado, é um ponto muito bom. Eu ajustei minha resposta e removi a parte que sugeria usar a reutilização do diretório de cache dos hosts.
Jakub Kukul
24

Eu entendo que esta pergunta já tem algumas respostas populares. Mas existe uma maneira mais recente de armazenar arquivos em cache para gerenciadores de pacotes. Acho que pode ser uma boa resposta no futuro, quando o BuildKit se tornar mais padrão.

A partir do Docker 18.09, há suporte experimental para BuildKit . BuildKit adiciona suporte para alguns novos recursos no Dockerfile, incluindo suporte experimental para montar volumes externos em RUNetapas. Isso nos permite criar caches para coisas como $HOME/.cache/pip/.

Usaremos o seguinte requirements.txtarquivo como exemplo:

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

Um exemplo típico de Python Dockerfilepode ser semelhante a:

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

Com BuildKit ativado usando a DOCKER_BUILDKITvariável de ambiente, podemos construir a pipetapa sem cache em cerca de 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

Agora, vamos adicionar o cabeçalho experimental e modificar a RUNetapa para armazenar em cache os pacotes 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

Vá em frente e faça outra construção agora. Deve demorar o mesmo tempo. Mas desta vez ele está armazenando em cache os pacotes Python em nossa nova montagem de cache:

$ 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

Cerca de 60 segundos. Semelhante à nossa primeira construção.

Faça uma pequena alteração no requirements.txt(como adicionar uma nova linha entre dois pacotes) para forçar uma invalidação do cache e execute novamente:

$ 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

Apenas cerca de 16 segundos!

Estamos obtendo essa velocidade porque não estamos mais baixando todos os pacotes Python. Eles foram armazenados em cache pelo gerenciador de pacotes ( pipneste caso) e armazenados em uma montagem de volume de cache. A montagem do volume é fornecida para a etapa de execução para que pippossa reutilizar nossos pacotes já baixados. Isso acontece fora de qualquer cache de camada do Docker .

Os ganhos devem ser muito melhores em maiores requirements.txt.

Notas:

  • Esta é a sintaxe experimental do Dockerfile e deve ser tratada como tal. Você pode não querer construir com isso em produção no momento.
  • As coisas do BuildKit não funcionam no Docker Compose ou em outras ferramentas que usam diretamente a API Docker no momento. Agora há suporte para isso no Docker Compose a partir de 1.25.0. Veja como você habilita BuildKit com docker-compose?
  • Não há nenhuma interface direta para gerenciar o cache no momento. Ele é eliminado quando você faz um docker system prune -a.

Esperançosamente, esses recursos chegarão ao Docker para construção e o BuildKit se tornará o padrão. Se / quando isso acontecer, tentarei atualizar esta resposta.

Andy Shinn
fonte
Posso confirmar que esta solução funciona muito bem. Minha construção caiu de mais de um minuto para apenas 2,2 segundos. Obrigado @ andy-shinn.
Kwuite
2
Agora também Docker-Compose: stackoverflow.com/questions/58592259/…
Rexcirus
Obs: Se você estiver usando o SUDO para rodar o docker, provavelmente precisará fazer: sudo DOCKER_BUILDKIT = 1 ...
Vinícius M
Estou recebendo este erro: - falha ao resolver com frontend dockerfile.v0: falha ao criar a definição LLB: Dockerfile analisa a linha de erro 10: Sinalizador desconhecido: montagem
Mayur Dangar
Parece que você perdeu o comentário na parte superior da Dockerfileou a versão do Docker é muito antiga. Gostaria de criar uma nova pergunta com todas as suas informações de depuração.
Andy Shinn
-10

Descobri que a melhor maneira é simplesmente adicionar o diretório Python site-packages como um volume.

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

Desta forma, posso instalar novas bibliotecas sem ter que fazer uma reconstrução completa.

EDIT : Desconsidere esta resposta, a resposta de jkukul acima funcionou para mim. Minha intenção era armazenar em cache a pasta de pacotes do site . Isso seria mais parecido com:

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

Armazenar a pasta de download em cache é muito mais limpo. Isso também armazena em cache as rodas, de modo que realiza a tarefa corretamente.

jaywhy13
fonte
2
E o que acontece quando você tenta construir este dockerfile em uma máquina diferente. Esta não é uma solução sustentável.
Aaron McMillin
Genuinamente confuso, acabou sendo um bug e eu não tinha certeza do porquê. Você poderia dar mais alguns detalhes.
jaywhy13 de
3
A imagem do docker depende do estado do sistema host. Isso anula a maior parte da utilidade do docker. Tudo o que a imagem precisa deve ser instalado nele. use o Dockerfile para instalar todas as dependências. Se você quiser evitar baixar novamente os pacotes toda vez que construir, a resposta do jkukul para montar o cache do pip é o caminho a percorrer.
Aaron McMillin de
2
A lâmpada acabou de desligar, obrigado. Na verdade, eu estava tentando montar o diretório de pacotes de sites da VM, não do host. Um grande descuido. Acho que, em espírito, estava tentando fazer o mesmo que jkulkul sugeriu. Obrigado pela clareza!
jaywhy13
@AaronMcMillin Na verdade, ele não depende de um caminho no host. Ele está montando os pacotes de sites no contêiner em um volume anônimo. Ainda é uma má ideia
ruohola