Jak uniknąć ponownej instalacji pakietów podczas tworzenia obrazu platformy Docker dla projektów w języku Python?

128

Mój plik Dockerfile jest podobny do

FROM my/base

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

ENTRYPOINT ["run_server"]

Za każdym razem, gdy tworzę nowy obraz, zależności muszą być ponownie instalowane, co może być bardzo powolne w moim regionie.

Jednym ze sposobów, w jaki myślę o cachepakietach, które zostały zainstalowane, jest zastąpienie my/baseobrazu nowszymi obrazami, takimi jak ten:

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

Więc następnym razem, gdy będę budować z tym plikiem Dockerfile, mój / base ma już zainstalowane pakiety.

Ale to rozwiązanie ma dwa problemy:

  1. Nie zawsze jest możliwe zastąpienie obrazu podstawowego
  2. Obraz podstawowy staje się coraz większy w miarę nakładania na niego nowszych obrazów

Więc jakie lepsze rozwiązanie mógłbym użyć do rozwiązania tego problemu?

EDYTOWAĆ##:

Niektóre informacje o dokerze na moim komputerze:

  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
źródło
Czy usuwasz obraz pośredni po zakończeniu tworzenia obrazu?
Regan
Oczywiście, że nie, ale nie ma to znaczenia, ponieważ odbudowując obraz nadal my/base
bazuję

Odpowiedzi:

139

Spróbuj zbudować plik Dockerfile, który wygląda mniej więcej tak:

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 będzie używał pamięci podręcznej podczas instalacji pip, o ile nie wprowadzisz żadnych zmian w requirements.txtpliku, niezależnie od tego, czy inne pliki kodu w witrynie .zostały zmienione, czy nie. Oto przykład.


Oto prosty Hello, World!program:

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

Dane wyjściowe kompilacji dockera:

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

Zmodyfikujmy run.py:

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

Spróbuj ponownie zbudować, poniżej jest wynik:

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

Jak widać powyżej, tym razem docker używa pamięci podręcznej podczas kompilacji. Teraz zaktualizujmy requirements.txt:

# requirements.txt

pytest==2.3.4
ipython

Poniżej znajduje się wynik kompilacji dockera:

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

Zwróć uwagę, że docker nie używał pamięci podręcznej podczas instalacji pip. Jeśli to nie zadziała, sprawdź wersję dockera.

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
źródło
2
Wydaje się, że to nie działa, ponieważ ilekroć docker widzi ADDinstrukcję, pamięć podręczna jest unieważniana.
satoru
1
Nie wiem, dlaczego to nie działa. Ale nie ma żadnej zmiany w Requirements.txt (<src> on ADD ./requirements.txt /srv/requirements.txt), wtedy docker musi używać cache. Zobacz add seciton on Dockerfile document.
nacyot
16
Tak, użyje pamięci podręcznej, jeśli require.txt nie ulegnie zmianie. Jeśli jednak plik Requirements.txt ulegnie zmianie, wszystkie wymagania zostaną pobrane. Czy istnieje sposób, w jaki mogę zamontować wolumin pamięci podręcznej pip w kontenerze Dockera, aby załadować go z pamięci podręcznej?
Jitu
7
Kluczem do tej odpowiedzi jest dodanie Requirements.txt ( ADD requirements.txt /srvprzed uruchomieniem pip ( RUN pip install -r requirements.txt) i dodaniem wszystkich innych plików po uruchomieniu pip. Dlatego powinny one być w następującej kolejności: (1) ADD requirements.txt /srv; (2) RUN pip install -r requirements.txt; ( 3)ADD . /srv
engelen
2
Należy pamiętać, że to nie działa, gdy używasz KOPIUJ zamiast DODAJ
veuncent
29

Aby zminimalizować aktywność sieciową, możesz wskazać pipkatalog pamięci podręcznej na komputerze głównym.

Uruchom kontener Docker z powiązaniem katalogu pamięci podręcznej pip hosta podłączonym do katalogu pamięci podręcznej pip kontenera. docker runpolecenie powinno wyglądać następująco:

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

Następnie w pliku Dockerfile zainstaluj wymagania jako część ENTRYPOINTinstrukcji (lub CMDinstrukcji), a nie jako RUNpolecenie. Jest to ważne, ponieważ (jak wskazano w komentarzach) mount nie jest dostępny podczas budowania obrazu (podczas RUNwykonywania instrukcji). Plik Dockera powinien wyglądać następująco:

FROM my/base

ADD . /srv

ENTRYPOINT ["sh", "-c", "pip install -r requirements.txt && python setup.py install && run_server"]
Jakub Kukul
źródło
4
Nie to, czego szukał OP w swoim przypadku użycia, ale jeśli tworzysz serwer kompilacji, to świetny pomysł
oden
2
Wydaje się, że jest to recepta na problemy, szczególnie sugestia wskazująca domyślną pamięć podręczną hosta. Potencjalnie mieszasz pakiety specyficzne dla arch.
Giacomo Lacava
@GiacomoLacava dzięki, to bardzo dobra uwaga. Dostosowałem odpowiedź i usunąłem część, która sugerowała ponowne użycie katalogu pamięci podręcznej hostów.
Jakub Kukul
24

Rozumiem, że to pytanie ma już kilka popularnych odpowiedzi. Ale jest nowszy sposób buforowania plików dla menedżerów pakietów. Myślę, że może to być dobra odpowiedź w przyszłości, gdy BuildKit stanie się bardziej standardowy.

Od Dockera 18.09 dostępna jest eksperymentalna obsługa BuildKit . BuildKit dodaje obsługę niektórych nowych funkcji w pliku Dockerfile, w tym eksperymentalną obsługę montażu woluminów zewnętrznych w RUNkrokach. To pozwala nam tworzyć pamięci podręczne dla rzeczy takich jak $HOME/.cache/pip/.

requirements.txtJako przykładu użyjemy następującego pliku:

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

Typowy przykład Pythona Dockerfilemoże wyglądać następująco:

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

Po włączeniu BuildKit przy użyciu DOCKER_BUILDKITzmiennej środowiskowej możemy zbudować niezbuforowany pipkrok w około 65 sekund:

$ 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

Teraz dodajmy eksperymentalny nagłówek i zmodyfikujmy RUNkrok, aby buforować pakiety Pythona:

# 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

Śmiało i zrób teraz kolejną kompilację. Powinno to zająć tyle samo czasu. Ale tym razem buforuje pakiety Pythona w naszym nowym montowaniu pamięci podręcznej:

$ 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

Około 60 sekund. Podobnie jak w naszej pierwszej kompilacji.

Wprowadź niewielką zmianę w requirements.txt(na przykład dodanie nowej linii między dwoma pakietami), aby wymusić unieważnienie pamięci podręcznej i uruchomić ponownie:

$ 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

Tylko około 16 sekund!

Otrzymujemy to przyspieszenie, ponieważ nie pobieramy już wszystkich pakietów Pythona. Zostały buforowane przez menedżera pakietów ( pipw tym przypadku) i przechowywane w woluminie pamięci podręcznej. Montaż woluminu jest dostarczany do kroku uruchamiania, dzięki czemu pipmożna ponownie wykorzystać nasze już pobrane pakiety. Dzieje się to poza buforowaniem warstwy Dockera .

Zyski powinny być znacznie lepsze na większych requirements.txt.

Uwagi:

  • To jest eksperymentalna składnia Dockerfile i tak powinna być traktowana. W tej chwili możesz nie chcieć tworzyć tego w wersji produkcyjnej.
  • Kompilacja BuildKit nie działa w Docker Compose ani w innych narzędziach, które w tej chwili bezpośrednio używają API Dockera. Jest to teraz obsługiwane w Docker Compose od 1.25.0. Zobacz, jak włączyć BuildKit za pomocą docker-compose?
  • W tej chwili nie ma bezpośredniego interfejsu do zarządzania pamięcią podręczną. Jest czyszczony, gdy wykonujesz docker system prune -a.

Miejmy nadzieję, że te funkcje zostaną wprowadzone do Dockera do budowania, a BuildKit stanie się domyślnym. Jeśli tak się stanie, spróbuję zaktualizować tę odpowiedź.

Andy Shinn
źródło
Mogę potwierdzić, że to rozwiązanie działa bardzo dobrze. Moja kompilacja spadła z ponad minuty do zaledwie 2,2 sekundy. Dzięki @ andy-shinn.
Kwuite
2
Teraz także Docker-Compose: stackoverflow.com/questions/58592259/ ...
Rexcirus,
Uwaga: jeśli używasz SUDO do uruchomienia dockera, prawdopodobnie musisz to zrobić: sudo DOCKER_BUILDKIT = 1 ...
Vinícius M
Otrzymuję ten błąd: - nie udało się rozwiązać z frontend dockerfile.v0: nie udało się utworzyć definicji LLB: Dockerfile parse Error line 10: Unknown flag: mount
Mayur Dangar
To brzmi tak, jakbyś przegapił komentarz u góry Dockerfilelub wersja Dockera jest za stara. Utworzyłbym nowe pytanie ze wszystkimi informacjami dotyczącymi debugowania.
Andy Shinn
-10

Zauważyłem, że lepszym sposobem jest po prostu dodanie katalogu pakietów witryn Pythona jako woluminu.

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

W ten sposób mogę po prostu zainstalować nowe biblioteki za pomocą pip bez konieczności wykonywania pełnej przebudowy.

EDYCJA : Zignoruj tę odpowiedź, odpowiedź jkukul powyżej działała dla mnie. Moim zamiarem było buforowanie folderu pakietów witryn . To wyglądałoby bardziej jak:

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

Buforowanie folderu pobierania jest jednak znacznie czystsze. To również buforuje koła, więc poprawnie wykonuje zadanie.

jaywhy13
źródło
2
A co się stanie, gdy spróbujesz zbudować ten plik dockerfile na innym komputerze. To nie jest trwałe rozwiązanie.
Aaron McMillin
Naprawdę zdezorientowany, okazało się, że to buggy i nie byłem pewien dlaczego. Czy mógłbyś podać więcej szczegółów.
jaywhy13
3
Obraz dockera zależy od stanu systemu hosta. To unieważnia większość użyteczności dockera. Wszystko, czego potrzebuje obraz, powinno być w nim zainstalowane. użyj pliku Dockerfile, aby zainstalować wszystkie zależności. Jeśli chcesz uniknąć ponownego pobierania pakietów za każdym razem, gdy tworzysz odpowiedź z jkukul, aby zamontować pamięć podręczną pip, najlepszym rozwiązaniem jest.
Aaron McMillin
2
Żarówka właśnie się włączyła, dzięki. W rzeczywistości próbowałem zamontować katalog pakietów witryn z maszyny wirtualnej, a nie z hosta. Całkiem przeoczenie. Myślę, że w duchu starałem się zrobić to samo, co sugerował jkulkul. Dzięki za jasność!
jaywhy13
@AaronMcMillin W rzeczywistości nie jest zależny od ścieżki na hoście. Montuje pakiety witryn w kontenerze do anonimowego tomu. Wciąż jednak zły pomysł
ruohola