# Docker Images В данном практическом занятии рассматриваются операции с docker образами: директивы Dockerfile, сборка, работа с registry. ## Vagrant Для работы с докером в независимости от платформы можно воспользоваться следующим `Vagrantfile`: ```ruby Vagrant.configure("2") do |config| config.vm.box = "ubuntu/lunar64" config.vm.provision "docker" end ``` ## Dockerfile [Рассмотрим различные директивы, которые можно использовать в Dockerfile][dockerfile]. Во всех примерах `Dockerfile` будет находиться в директории проекта вместе с `Vagrantfile` и внутри виртуальной машины будет доступен по пути `/vagrant/Dockerfile`. ### Hello Создадим простой [Dockerfile][], который содержит две директивы - [`FROM`][from] для указания базового образа и [`CMD`][cmd] для команды, которая запустится при старте контейнера: ```dockerfile FROM python:3.11.5-alpine CMD ["python", "-c", "print('hello')"] ``` Соберем образ с именем `hello` из данного [Dockerfile][], при простом запуске увидим как отработала команда в директиве [`CMD`][cmd]: ```console $ docker build -t hello /vagrant/ # без указания тега образ будет иметь тег latest [+] Building 4.5s (5/5) FINISHED docker:default => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 102B 0.0s => [internal] load .dockerignore 0.0s => => transferring context: 2B 0.0s => [internal] load metadata for docker.io/library/python:3.11.5-alpine 1.9s => [1/1] FROM docker.io/library/python:3.11.5-alpine@sha256:5d769f990397afbb2aca24b0655 2.6s => => resolve docker.io/library/python:3.11.5-alpine@sha256:5d769f990397afbb2aca24b0655 0.0s => => sha256:66e1d5e70e420aa86a23bd8b4eebf2a6eb60b4aff9ee8a6ca52e27 622.31kB / 622.31kB 0.3s => => sha256:0448660c92fc1b6557cf48081d99028eeaba7809a1c2f23018e2331f 12.46MB / 12.46MB 1.8s => => sha256:5d769f990397afbb2aca24b0655e404c0f2806d268f454b052e81e39d8 1.65kB / 1.65kB 0.0s => => sha256:e5d592c422d6e527cb946ae6abb1886c511a5e163d3543865f5a5b9b61 1.37kB / 1.37kB 0.0s => => sha256:b9b301ab01c2af0b5f52069b97e1d89885433dc55b6623ad6aa4a43b2e 6.26kB / 6.26kB 0.0s => => sha256:7264a8db6415046d36d16ba98b79778e18accee6ffa71850405994cffa 3.40MB / 3.40MB 0.7s => => sha256:3ce23f846e315e35618c4604547bbe6aa2b48a0601c335c76b8fe02729bb5c 241B / 241B 0.8s => => extracting sha256:7264a8db6415046d36d16ba98b79778e18accee6ffa71850405994cffa9be7d 0.1s => => sha256:efebc2e683d297ae71acae9230d17af8b95684d9a4f9b7f601a8e1e9b1 3.11MB / 3.11MB 1.5s => => extracting sha256:66e1d5e70e420aa86a23bd8b4eebf2a6eb60b4aff9ee8a6ca52e27f51f57b1b 0.2s => => extracting sha256:0448660c92fc1b6557cf48081d99028eeaba7809a1c2f23018e2331f3cfe4b2 0.4s => => extracting sha256:3ce23f846e315e35618c4604547bbe6aa2b48a0601c335c76b8fe02729bb5c4 0.0s => => extracting sha256:efebc2e683d297ae71acae9230d17af8b95684d9a4f9b7f601a8e1e9b103bda 0.2s => exporting to image 0.0s => => exporting layers 0.0s => => writing image sha256:caaa3b6bf89788867da2a2d8fbd3396b3486c6bf7922b2177e6c2dabd8f6 0.0s => => naming to docker.io/library/hello 0.0s $ docker run --rm hello:latest hello ``` Для получения детальной информации об образе можно воспользоваться командой `docker inspect`: ```console $ docker image inspect hello:latest [ { "Id": "sha256:b82039dd53d96094d9e8a5f0e5698fc26590f029b239883d931ce8a753772b71", "RepoTags": [ "hello:latest" ], ... ``` ### Copy/Add Для копирования файлов внутрь образа из контекста сборки можно воспользоваться инструкциями [`COPY`][copy] и [`ADD`][add]. Контекст сборки указывается в команде `docker build` в виде пути(в нашем случае это путь `/vagrant/`. Возьмем скрипт на python, который будет отвечать на HTTP запросы по порту 8888 и расположим его рядом с [Dockerfile][] под именем `main.py`: ```python #!/usr/bin/env python from http.server import HTTPServer, BaseHTTPRequestHandler from os import getenv class Handler(BaseHTTPRequestHandler): def do_GET(self): self.send_response(200) self.end_headers() file = getenv("FILE", "none") self.wfile.write(f"file: {file}\n".encode()) if file != "none": self.wfile.write(open(file, "rb").read()) HTTPServer(('', 8888), Handler).serve_forever() ``` Опишем [Dockerfile][] следующим образом: ```dockerfile FROM python:3.11.5-alpine COPY main.py /main.py ADD https://example.com /example.html CMD ["python", "main.py"] ``` В данном примере инструкция `COPY` скопирует файл из контекста сборки(`/vagrant/`) внутрь образа, а `ADD` скачает файл пор заданному URL и сохранит внутри образа с именем `example.html`. После чего мы задаем команду запуска интерпретатора python, которому передаем на исполнение наш файл. ```{note} Основное отличие `COPY` от `ADD` - это возоможность `ADD` скачивать файлы по URL, а также автоматически разархивировать tar архивы. Но рекомендуется использовать `COPY`, так у данной директивы более очевидное поведение, а скачивание и разархивирование производить в директивах `RUN`. ``` Соберем образ с именем `main`: ```console $ docker build -t main /vagrant/ [+] Building 1.6s (9/9) FINISHED docker:default => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 151B 0.0s => [internal] load .dockerignore 0.0s => => transferring context: 2B 0.0s => [internal] load metadata for docker.io/library/python:3.11.5-alpine 1.0s => [internal] load build context 0.0s => => transferring context: 29B 0.0s => https://example.com 0.5s => [1/3] FROM docker.io/library/python:3.11.5-alpine@sha256:5d769f990397afbb2aca24b0655 0.0s => CACHED [2/3] COPY main.py /main.py 0.0s => CACHED [3/3] ADD https://example.com /example.html 0.0s => exporting to image 0.0s => => exporting layers 0.0s => => writing image sha256:b82039dd53d96094d9e8a5f0e5698fc26590f029b239883d931ce8a75377 0.0s => => naming to docker.io/library/main 0.0s ``` И проверим его работу: ```console $ docker run -d -p 8888:8888 --name main main:latest f74c1f74e7bc09a0797a7717c144ffa64c5412f9621c66dd7d822057375ddd65 $ curl localhost:8888 file: none ``` Как видно наше приложение запустилось и обрабатывает HTTP запросы. Также в инструкция [`COPY`][copy] и [`ADD`][add] есть возможность задать права файла внутри образа с помощью опции `--chmod` и владельца опцией `--chown`. Дадим файлу `main.py` права на запуск, так что можно будет в [`CMD`][cmd] указать не интерпретатор python, а наш исполняемый файл: ```dockerfile FROM python:3.11.5-alpine COPY --chmod=555 main.py /main.py ADD https://example.com /example.html CMD ["/main.py"] ``` ```console $ docker build -t main /vagrant/ [+] Building 1.5s (9/9) FINISHED docker:default => [internal] load .dockerignore 0.0s => => transferring context: 2B 0.0s => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 154B 0.0s => [internal] load metadata for docker.io/library/python:3.11.5-alpine 1.0s => [internal] load build context 0.0s => => transferring context: 29B 0.0s => https://example.com 0.5s => [1/3] FROM docker.io/library/python:3.11.5-alpine@sha256:5d769f990397afbb2aca24b0655 0.0s => CACHED [2/3] COPY --chmod=555 main.py /main.py 0.0s => CACHED [3/3] ADD https://example.com /example.html 0.0s => exporting to image 0.0s => => exporting layers 0.0s => => writing image sha256:09368b064a4bbde6e44a79f7743bbd361d1f82ddcdf8b239f504a085997a 0.0s => => naming to docker.io/library/main 0.0s ``` ### Env/Arg Для передачи параметров во время сборки служит инструкция [`ARG`][arg], с помощью нее можно задать переменные среды, которые будут существовать только на время сборки. Если же необходимо задать переменные среды, которые будут существовать также при запуске контейнера, то можно воспользоваться директивой [`ENV`][env]. В данном примере мы задаем переменную `FILE` как аргумент сборки, а также передаем во время сборки в инструкцию [`ENV`][env], чтобы данная переменная была доступна также и после сборки во время запуска контейнера: ```dockerfile FROM python:3.11.5-alpine ARG FILE ENV FILE="${FILE}" COPY --chmod=555 main.py /main.py ADD https://example.com /example.html CMD ["/main.py"] ``` Передача аргументов при запуске сборки осуществляется опцией `--build-arg`: ```console $ docker build -t main --build-arg FILE=/example.html /vagrant/ [+] Building 0.7s (9/9) FINISHED docker:default => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 185B 0.0s => [internal] load .dockerignore 0.0s => => transferring context: 2B 0.0s => [internal] load metadata for docker.io/library/python:3.11.5-alpine 0.5s => [internal] load build context 0.0s => => transferring context: 29B 0.0s => https://example.com 0.1s => [1/3] FROM docker.io/library/python:3.11.5-alpine@sha256:5d769f990397afbb2aca24b0655 0.0s => CACHED [2/3] COPY --chmod=555 main.py /main.py 0.0s => CACHED [3/3] ADD https://example.com /example.html 0.0s => exporting to image 0.0s => => exporting layers 0.0s => => writing image sha256:e90a9bec6dacd27430cd93793704b5f1c7abcd668232d19cdbf94ec60a05 0.0s => => naming to docker.io/library/main 0.0s $ docker rm -f main main $ docker run -d -p 8888:8888 --name main main:latest c5e2af35ebb1fa9c5df54a6c4e699a8e8944df3ae0b22b587d79e5a3deb78fcc $ curl -s localhost:8888 | grep title Example Domain ``` ### Run Для запуска команд внутри образа во время сборки есть инструкция [`RUN`][run]. С помощью нее вы можете подготовить образ используя утилиты, которые находятся в базовом образе. Перепишем файл `main.py` с использованием внешних зависимостей, которые необходимо будет установить во время сборки: ```python #!/usr/bin/env python from os import getenv from fastapi import FastAPI from fastapi.responses import HTMLResponse app = FastAPI() @app.get("/", response_class=HTMLResponse) def read_example(): file = getenv("FILE", "none") ret = f"file: {file}\n".encode() if file != "none": ret += open(file, "rb").read() return ret ``` Соответственно [Dockerfile][] может выглядеть так: ```dockerfile FROM python:3.11.5-alpine ENV FILE="/example.html" COPY main.py . ADD https://example.com ${FILE} RUN pip install fastapi "uvicorn[standard]" CMD ["uvicorn", "main:app", "--host=0.0.0.0", "--port=8888"] ``` Соберем и проверим: ```console $ docker build -t main /vagrant/ [+] Building 10.8s (10/10) FINISHED docker:default => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 245B 0.0s => [internal] load .dockerignore 0.0s => => transferring context: 2B 0.0s => [internal] load metadata for docker.io/library/python:3.11.5-alpine 0.9s => [internal] load build context 0.0s => => transferring context: 29B 0.0s => CACHED [1/4] FROM docker.io/library/python:3.11.5-alpine@sha256:5d769f990397afbb2aca 0.0s => CACHED https://example.com 0.5s => [2/4] COPY main.py . 0.0s => [3/4] ADD https://example.com /example.html 0.0s => [4/4] RUN pip install fastapi "uvicorn[standard]" 8.8s => exporting to image 0.6s => => exporting layers 0.6s => => writing image sha256:a872628e15170dc4f697d8c403387f4f29ae495f11365f7343c5c008b552 0.0s => => naming to docker.io/library/main 0.0s $ docker rm -f main main $ docker run -d -p 8888:8888 --name main main:latest 959fb49fe6eb777f76279ee9e779c09f3d01ff94a9a505ddc8186e559fc4ed2d $ curl -s localhost:8888 | head -5 file: /example.html Example Domain ``` ### Cache Так как docker в процессе сборки кэширует слои, то важна последовательность сборки. При изменении вышележащего слоя потребуется пересборка всех последующих. Если изменится файл `main.py`, то потребуется заново скачать и установить зависимости: ```console $ echo >> /vagrant/main.py $ docker build -t main /vagrant/ [+] Building 11.2s (10/10) FINISHED docker:default => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 245B 0.0s => [internal] load .dockerignore 0.0s => => transferring context: 2B 0.0s => [internal] load metadata for docker.io/library/python:3.11.5-alpine 0.4s => [internal] load build context 0.0s => => transferring context: 381B 0.0s => CACHED https://example.com 1.4s => CACHED [1/4] FROM docker.io/library/python:3.11.5-alpine@sha256:5d769f990397afbb2aca 0.0s => [2/4] COPY main.py . 0.0s => [3/4] ADD https://example.com /example.html 0.0s => [4/4] RUN pip install fastapi "uvicorn[standard]" 8.8s => exporting to image 0.5s => => exporting layers 0.5s => => writing image sha256:b0d51dd802f65df529cded6d35e920b2cd58e99d9248c132443b79c6777b 0.0s => => naming to docker.io/library/main 0.0s ``` Зададим установку зависимостей в нижележащем слое в [Dockerfile][]: ```dockerfile FROM python:3.11.5-alpine ENV FILE="/example.html" RUN pip install fastapi "uvicorn[standard]" COPY main.py . ADD https://example.com ${FILE} CMD ["uvicorn", "main:app", "--host=0.0.0.0", "--port=8888"] ``` Теперь последующие сборки не потребуют выполнять скачивание и установку зависимостей, что значительно ускорит их время выполнения: ```console $ docker build -t main /vagrant/ [+] Building 1.0s (10/10) FINISHED docker:default => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 245B 0.0s => [internal] load .dockerignore 0.0s => => transferring context: 2B 0.0s => [internal] load metadata for docker.io/library/python:3.11.5-alpine 0.5s => [1/4] FROM docker.io/library/python:3.11.5-alpine@sha256:5d769f990397afbb2aca24b0655 0.0s => CACHED https://example.com 0.5s => [internal] load build context 0.0s => => transferring context: 29B 0.0s => CACHED [2/4] RUN pip install fastapi "uvicorn[standard]" 0.0s => [3/4] COPY main.py . 0.0s => [4/4] ADD https://example.com /example.html 0.0s => exporting to image 0.0s => => exporting layers 0.0s => => writing image sha256:4cf84d3406df7701d0c143deb30c913e4d88f68353740e03684c3056e83d 0.0s => => naming to docker.io/library/main 0.0s $ echo >> /vagrant/main.py $ docker build -t main /vagrant/ [+] Building 0.6s (10/10) FINISHED docker:default => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 245B 0.0s => [internal] load .dockerignore 0.0s => => transferring context: 2B 0.0s => [internal] load metadata for docker.io/library/python:3.11.5-alpine 0.4s => [1/4] FROM docker.io/library/python:3.11.5-alpine@sha256:5d769f990397afbb2aca24b0655 0.0s => [internal] load build context 0.0s => => transferring context: 382B 0.0s => CACHED https://example.com 0.1s => CACHED [2/4] RUN pip install fastapi "uvicorn[standard]" 0.0s => [3/4] COPY main.py . 0.0s => [4/4] ADD https://example.com /example.html 0.0s => exporting to image 0.0s => => exporting layers 0.0s => => writing image sha256:0657e4aef1a267c34f131d0c3e26b82823abd18db063c24b623201edd8b0 0.0s => => naming to docker.io/library/main 0.0s ``` ### Expose Для указания портов, которые могут использоваться для подключения к приложению в контейнере используется инструкция [`EXPOSE`][expose]: ```dockerfile FROM python:3.11.5-alpine ENV FILE="/example.html" RUN pip install fastapi "uvicorn[standard]" COPY main.py . ADD https://example.com ${FILE} EXPOSE 8888 CMD ["uvicorn", "main:app", "--host=0.0.0.0", "--port=8888"] ``` После сборки образа можно наблюдать выставленные порты в команде `docker image inspect`, а также после запуска, если явно не задан проброс портов, в команде `docker ps`: ```console $ docker build -t main /vagrant/ [+] Building 1.4s (10/10) FINISHED docker:default => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 258B 0.0s => [internal] load .dockerignore 0.0s => => transferring context: 2B 0.0s => [internal] load metadata for docker.io/library/python:3.11.5-alpine 0.9s => [1/4] FROM docker.io/library/python:3.11.5-alpine@sha256:5d769f990397afbb2aca24b0655 0.0s => https://example.com 0.5s => [internal] load build context 0.0s => => transferring context: 29B 0.0s => CACHED [2/4] RUN pip install fastapi "uvicorn[standard]" 0.0s => CACHED [3/4] COPY main.py . 0.0s => CACHED [4/4] ADD https://example.com /example.html 0.0s => exporting to image 0.0s => => exporting layers 0.0s => => writing image sha256:c34234caa5431df50019ca07f4072639dcfa51a737f175c2654b228706a6 0.0s => => naming to docker.io/library/main 0.0s $ docker image inspect main:latest --format='{{json .Config.ExposedPorts}}' {"8888/tcp":{}} $ docker rm -f main main $ docker run -d --name main main:latest 91a4683a36bc635e4ea91531f5a9861a7d95572eb30b17883123c66c9836bd5e $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 91a4683a36bc main:latest "uvicorn main:app --…" 5 seconds ago Up 4 seconds 8888/tcp main ``` ### Label С помощью инструкции [`LABEL`][label] можно добавить метаданные в образ в виде `=`, которые могут служить как дополнительная информация об образе или использоваться каким-либо способом в сборочных конвейерах: ```dockerfile FROM python:3.11.5-alpine ENV FILE="/example.html" RUN pip install fastapi "uvicorn[standard]" COPY main.py . ADD https://example.com ${FILE} EXPOSE 8888 LABEL version="0.1" CMD ["uvicorn", "main:app", "--host=0.0.0.0", "--port=8888"] ``` После сборки значения директив [`LABEL`][label] можно также посмотреть командой `docker image inspect`: ```console $ docker build -t main /vagrant/ [+] Building 1.4s (10/10) FINISHED docker:default => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 279B 0.0s => [internal] load .dockerignore 0.0s => => transferring context: 2B 0.0s => [internal] load metadata for docker.io/library/python:3.11.5-alpine 0.9s => [1/4] FROM docker.io/library/python:3.11.5-alpine@sha256:5d769f990397afbb2aca24b0655 0.0s => https://example.com 0.5s => [internal] load build context 0.0s => => transferring context: 29B 0.0s => CACHED [2/4] RUN pip install fastapi "uvicorn[standard]" 0.0s => CACHED [3/4] COPY main.py . 0.0s => CACHED [4/4] ADD https://example.com /example.html 0.0s => exporting to image 0.0s => => exporting layers 0.0s => => writing image sha256:f08ac61ac1576278ff220148791ae1fe212c60aa2efd0cb70e725f708f2a 0.0s => => naming to docker.io/library/main 0.0s $ docker image inspect main:latest --format='{{json .Config.Labels}}' {"version":"0.1"} ``` ### Cmd/Entrypoint Для указания команды, которая будет запускаться при старте контейнера есть две директивы: [`CMD`][cmd] и [`ENTRYPOINT`][entrypoint]. Если в [Dockerfile][] указана только одна из них, то она и будет определять команду запуска. Если же указаны обе, то команда запуска будет строиться сначала из параметров в [`ENTRYPOINT`][entrypoint], затем из параметров в [`CMD`][cmd]. [Подробнее о их взаимодействии можно почитать в документации.][cmd-entry] Разделим команду запуска на две инструкции: ```dockerfile FROM python:3.11.5-alpine ENV FILE="/example.html" RUN pip install fastapi "uvicorn[standard]" COPY main.py . ADD https://example.com ${FILE} EXPOSE 8888 LABEL version="0.1" ENTRYPOINT ["uvicorn", "main:app"] CMD ["--host=0.0.0.0", "--port=8888"] ``` После сборки можно убедиться, что контейнер функционирует как обычно: ```console $ docker build -t main /vagrant/ [+] Building 1.6s (10/10) FINISHED docker:default => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 291B 0.0s => [internal] load .dockerignore 0.0s => => transferring context: 2B 0.0s => [internal] load metadata for docker.io/library/python:3.11.5-alpine 1.1s => [1/4] FROM docker.io/library/python:3.11.5-alpine@sha256:5d769f990397afbb2aca24b065 0.0s => https://example.com 0.5s => [internal] load build context 0.0s => => transferring context: 29B 0.0s => CACHED [2/4] RUN pip install fastapi "uvicorn[standard]" 0.0s => CACHED [3/4] COPY main.py . 0.0s => CACHED [4/4] ADD https://example.com /example.html 0.0s => exporting to image 0.0s => => exporting layers 0.0s => => writing image sha256:c5e6676b703a475f3a09b547771d7d8afb8df158f92bee7b2734dc717f6 0.0s => => naming to docker.io/library/main 0.0s $ docker rm -f main main $ docker run -d -p 8888:8888 --name main main:latest 9e7f7d23812ed19f077e9ac29eff54e57b4efbcfa67b8b311acced68254a2ce7 $ curl -s localhost:8888 | head -5 file: /example.html Example Domain ``` Теперь же мы можем переопределить параметры в [`CMD`][cmd], указав их при запуске контейнера в конце команды `docker run`, а также [`ENTRYPOINT`][entrypoint] опцией `--entrypoint`: ```console $ docker rm -f main main $ docker run -d -p 8889:8889 --name main main:latest --host=0.0.0.0 --port=8889 acbc469c530e7c7fcc2e7e0032e2e84000a6dac38c4a82014002e7f5fcaf2806 $ curl -s localhost:8889 | head -5 file: /example.html Example Domain $ docker rm -f main main $ docker run --rm --name main --entrypoint echo main:latest hello hello ``` ### Multi-Stage Docker позволяет производить [multi-stage][] сборки, которые позволяют оптимизировать результирующий образ, вложив в него только необходимое для запуска приложения, а зависимости, необходимые для сборки приложения, иметь в отдельном стейдже только на время сборки. Возьмем [Dockerfile][] со следующим содержимым: ```dockerfile FROM golang:1.21 as build WORKDIR /src COPY < [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 290B 0.0s => [internal] load .dockerignore 0.0s => => transferring context: 2B 0.0s => [internal] load metadata for docker.io/library/golang:1.21 0.4s => [build 1/4] FROM docker.io/library/golang:1.21@sha256:19600fdcae402165dcdab18cb964 32.1s => => resolve docker.io/library/golang:1.21@sha256:19600fdcae402165dcdab18cb9649540bde 0.0s => => sha256:19600fdcae402165dcdab18cb9649540bde6be7274dedb5d205b2f840 2.36kB / 2.36kB 0.0s => => sha256:b47a222d28fa95680198398973d0a29b82a968f03e7ef361cc8ded5 24.03MB / 24.03MB 8.0s => => sha256:debce5f9f3a9709885f7f2ad3cf41f036a3b57b406b27ba3a88392 64.11MB / 64.11MB 13.5s => => sha256:b17c35044f4062d83c815434615997eed97697daae8745c6dd39dc367 1.58kB / 1.58kB 0.0s => => sha256:2159148dcc081245165b2aa99fc5a94ca9818bece66839d8eb11c9335 7.22kB / 7.22kB 0.0s => => sha256:167b8a53ca4504bc6aa3182e336fa96f4ef76875d158c1933d3e2f 49.56MB / 49.56MB 12.2s => => sha256:91b457aaf04f424db4f223ea7aad4b196d4a62da58d6f45938233e 92.30MB / 92.30MB 25.0s => => extracting sha256:167b8a53ca4504bc6aa3182e336fa96f4ef76875d158c1933d3e2fa19c57e0 2.3s => => sha256:b0ed6cc9b50977796e8eb9b270ad9c62922003c0090aa3e5ec26a1 66.99MB / 66.99MB 24.6s => => sha256:92b30a24413a45c9744b79bafa4c7717eafff586a9332210abbd384f7778 155B / 155B 13.7s => => extracting sha256:b47a222d28fa95680198398973d0a29b82a968f03e7ef361cc8ded562e4d84 0.7s => => extracting sha256:debce5f9f3a9709885f7f2ad3cf41f036a3b57b406b27ba3a8839283157870 2.9s => => extracting sha256:91b457aaf04f424db4f223ea7aad4b196d4a62da58d6f45938233e0f54bd16 2.5s => => extracting sha256:b0ed6cc9b50977796e8eb9b270ad9c62922003c0090aa3e5ec26a165cfcb9c 4.3s => => extracting sha256:92b30a24413a45c9744b79bafa4c7717eafff586a9332210abbd384f77785d 0.0s => [internal] preparing inline document 0.0s => [build 2/4] WORKDIR /src 0.1s => [build 3/4] COPY < [build 4/4] RUN go build -o /bin/hello ./main.go 5.6s => [stage-1 1/1] COPY --from=build /bin/hello /bin/hello 0.0s => exporting to image 0.0s => => exporting layers 0.0s => => writing image sha256:643d97e6291d22876100fff8e6edc90eccd8b045011cd8a4aa95524d8d5 0.0s => => naming to docker.io/library/hello 0.0s $ docker run --rm hello:latest hello, world ``` После сборки можно увидеть, что итоговый образ состоит из одного слоя, а его размер менее двух мегабайт: ```console $ docker image inspect hello:latest --format='{{json .RootFS}}' {"Type":"layers","Layers":["sha256:ce0d1c9c6c2209b264099514fad8f2d036136f31628db20370847bc3fcd68393"]} $ docker images hello REPOSITORY TAG IMAGE ID CREATED SIZE hello latest 643d97e6291d About a minute ago 1.8MB ``` ## Registry Для хранения образов обычно используется специальный реестр(registry), с которым docker клиент может взаимодействовать по http протоколу и который имеет свое [API][]. По-умолчанию docker клиент использует публичный реестр [hub.docker.com][docker-hub]. ### Run Поднимем свой локальный реестр из образа `registry:2`, который по-умолчанию слушает порт 5000: ```console $ docker run -d -p 5000:5000 --name registry registry:2 Unable to find image 'registry:2' locally 2: Pulling from library/registry 7264a8db6415: Already exists c4d48a809fc2: Pull complete 88b450dec42e: Pull complete 121f958bea53: Pull complete 7417fa3c6d92: Pull complete Digest: sha256:d5f2fb0940fe9371b6b026b9b66ad08d8ab7b0d56b6ee8d5c71cb9b45a374307 Status: Downloaded newer image for registry:2 6d6ce64e7f7302e287fe4e8325f48adc4c939b6e91400318c52528ceceefad09 ``` Проверить его работоспособность можно командой `curl`: ```console $ curl localhost:5000/v2/ {} ``` ### Push Для отправки образа в registry необходимо задать ему имя, в котором будет указание на конкретный registry. Для этого можно воспользоваться командой `docker tag`: ```console $ docker tag hello:latest localhost:5000/hello # без указания тега будет использован latest $ docker tag hello:latest localhost:5000/hello:0.1 # можно указать тег явно $ docker tag main:latest localhost:5000/main $ docker tag main:latest localhost:5000/main:python3.11.5 ``` После чего можно отправить образы в локальный реестр командой `docker push`: ```console $ docker push localhost:5000/hello Using default tag: latest The push refers to repository [localhost:5000/hello] ce0d1c9c6c22: Pushed latest: digest: sha256:3f8d6a1d510560a70b02dcc9722501c15fc4a1a78e11333913b863017dde40d7 size: 527 $ docker push localhost:5000/hello:0.1 The push refers to repository [localhost:5000/hello] ce0d1c9c6c22: Layer already exists 0.1: digest: sha256:3f8d6a1d510560a70b02dcc9722501c15fc4a1a78e11333913b863017dde40d7 size: 527 $ docker push localhost:5000/main Using default tag: latest The push refers to repository [localhost:5000/main] 18e02cddb804: Pushed 3a5eb5dbf933: Pushed c4b567ddc865: Pushed 08928985481f: Pushed 7acf52b2a13c: Pushed ce0f4c80e9b7: Pushed 9ad60c84bfbe: Pushed 4693057ce236: Pushed latest: digest: sha256:4957a623702236127336a3e54a5dad314c14e411fbd3096e5f669919afa90d21 size: 1994 $ docker push localhost:5000/main: latest python3.11.5 $ docker push localhost:5000/main:python3.11.5 The push refers to repository [localhost:5000/main] 18e02cddb804: Layer already exists 3a5eb5dbf933: Layer already exists c4b567ddc865: Layer already exists 08928985481f: Layer already exists 7acf52b2a13c: Layer already exists ce0f4c80e9b7: Layer already exists 9ad60c84bfbe: Layer already exists 4693057ce236: Layer already exists python3.11.5: digest: sha256:4957a623702236127336a3e54a5dad314c14e411fbd3096e5f669919afa90d21 size: 1994 ``` Как видно при отправке образа, который отличается только тегом, слои не отправляются повторно. ### Catalog Список репозиториев в реестре можно получить через [api][], например отправив запрос командой `curl`: ```console $ curl localhost:5000/v2/_catalog {"repositories":["hello","main"]} ``` ### Tags/Manifests Список тегов в каждом репозитории можно получить следующим запросом: ```console $ curl localhost:5000/v2/hello/tags/list {"name":"hello","tags":["latest","0.1"]} $ curl localhost:5000/v2/main/tags/list {"name":"main","tags":["latest","python3.11.5"]} ``` По тегу же можно получить манифест образа - описание состава образа в `json` формате, конкретный манифест можно получить запросом: ```console $ curl localhost:5000/v2/hello/manifests/0.1 { "schemaVersion": 1, "name": "hello", "tag": "0.1", "architecture": "amd64", "fsLayers": [ { "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" }, { "blobSum": "sha256:e00c2c33f044a4c9b2ce7fd84a33c63bb82fdf88dcc17994a14ecbf0dd0be01d" } ], "history": [ ... ``` Также с помощью заголовка `Accept` можно выбрать версию манифеста: ```console $ curl -v localhost:5000/v2/hello/manifests/0.1 -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' * Trying 127.0.0.1:5000... * Connected to localhost (127.0.0.1) port 5000 (#0) > GET /v2/hello/manifests/0.1 HTTP/1.1 > Host: localhost:5000 > User-Agent: curl/7.88.1 > Accept: application/vnd.docker.distribution.manifest.v2+json > < HTTP/1.1 200 OK < Content-Length: 527 < Content-Type: application/vnd.docker.distribution.manifest.v2+json < Docker-Content-Digest: sha256:3f8d6a1d510560a70b02dcc9722501c15fc4a1a78e11333913b863017dde40d7 < Docker-Distribution-Api-Version: registry/2.0 < Etag: "sha256:3f8d6a1d510560a70b02dcc9722501c15fc4a1a78e11333913b863017dde40d7" < X-Content-Type-Options: nosniff < { "schemaVersion": 2, "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "config": { "mediaType": "application/vnd.docker.container.image.v1+json", "size": 633, "digest": "sha256:643d97e6291d22876100fff8e6edc90eccd8b045011cd8a4aa95524d8d5a711f" }, "layers": [ { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "size": 1113611, "digest": "sha256:e00c2c33f044a4c9b2ce7fd84a33c63bb82fdf88dcc17994a14ecbf0dd0be01d" } ] ``` [dockerfile]:https://docs.docker.com/engine/reference/builder/ [from]:https://docs.docker.com/engine/reference/builder/#from [cmd]:https://docs.docker.com/engine/reference/builder/#cmd [copy]:https://docs.docker.com/engine/reference/builder/#copy [add]:https://docs.docker.com/engine/reference/builder/#add [env]:https://docs.docker.com/engine/reference/builder/#env [arg]:https://docs.docker.com/engine/reference/builder/#arg [run]:https://docs.docker.com/engine/reference/builder/#run [expose]:https://docs.docker.com/engine/reference/builder/#expose [label]:https://docs.docker.com/engine/reference/builder/#label [entrypoint]:https://docs.docker.com/engine/reference/builder/#entrypoint [cmd-entry]:https://docs.docker.com/engine/reference/builder/#understand-how-cmd-and-entrypoint-interact [multi-stage]:https://docs.docker.com/build/building/multi-stage/ [api]:https://docs.docker.com/registry/spec/api/ [docker-hub]:https://hub.docker.com/