Docker образ используется для создания контейнеров в контейнеризации, включает в себя легковесную ОС и необходимые файлы для работы конкретного приложения. Каждый созданный кем-либо контейнер использует тот или иной Docker образ. По сути Docker образ — это снимок ОС с вшитыми библиотеками, необходимыми для работы конкретного приложения. Например, вы хотите развернуть приложение, работающее на python. Соответственно вы просто используете Docker образ с python, не тратя особого времени на его установку в основную ОС.

Образы можно хранить и перемещать локально или же использовать репозитории, которые могут быть как публичными, так и приватными. Самый распространённый публичный репозиторий - docker hub.

docker image pull

Структура Docker образа

Давайте разберём из чего состоит Docker образ (Dockerfile):

  1. Самое первое это базовый образ (Base Image), т.е. по сути это ОС, но только урезанная. Например, alpine, ubuntu, python:3.9-slim.
  2. Каждый Docker образ состоит из слоёв. Каждый слой представляет из себя изменения, которые применяются поверх базового образа (Base Image). Т.е. если в образ включить установку нового пакета, которого нет в базовом образе, то установка этого пакета создаст новый слой.
  3. Если мы говорим о контейнеризации в разработке, то конечно же контейнер будет содержать код приложения, написанный разработчиком.
  4. Почти все приложения имеют свои файлы конфигураций, которые также содержатся в самом Docker образе.
  5. Ну и куда же без информации о самом образе, т.е. метаданные образа.

Пример слоев в Docker образе

Как я уже сказал любое изменение в создаваемом образе приведёт к созданию нового слоя. В итоге после создания образа у вас получится большой слоённый бутерброд. Кстати для просмотра слоёв и вообще информации о существующем образе можно воспользоваться утилитой dive.

enter image description here

Когда вы выполняете команду docker build в выводе можно просмотреть сколько в итоге создалось слоёв в вашем образе.

docker build . -t myapp:1.7
=> [1/4] FROM docker.io/library/python:3.9-slim@sha256:2851c06da1fdc3c451784beef8aa31d1a313d8e3fc122e4a1891085a104b7cfb 
CACHED [2/4] WORKDIR /app 
CACHED [3/4] COPY osapp/. .  
CACHED [4/4] RUN pip install -r requirements.txt

Также рекомендую посмотреть слои какого-нибудь готового образа на dockerhub, например nginx:1.27.1-alpine-perl.

Как создать свой собственный Docker образ

Конечно это хорошо просто скачивать уже созданные кем-то Docker образы с публичных репозиториев, но рано или поздно придётся создать свой собственный. Рассмотрим пример создания Docker образа для приложения, написанного на python. Это простенькое приложение, которое пытается получить доступ к сайту и выводит http responce. Скачать все необходимые файлы для воссоздания можно тут.

Для этого я создам файл Dockerfile и впишу следующее:

  1. Как я описывал выше каждый образ начинается с базового образа (Base Image). Также рекомендуется указывать тэг образа, т.е. конкретную версию образа и использовать максимально облегченный образ используя инструкцию FROM.
    FROM python:3.9-slim
    
  2. Понятное дело, что запускать файлы из корня контейнера не стоит, поэтому мы будем использовать директорию /app внутри образа. Используя инструкцию WORKDIR укажем текущую директорию /app. Т.е. всё что, мы будем выполнять при создании образа дальше будет выполняться в директории /app самого образа.
    WORKDIR /app
    
  3. Как несложно догадаться если есть файл приложения, то его нужно скопировать в наш новый Docker образ. Поэтому скопируем все файлы с директории osapp в самой хостовой ОС в текущую директорию образа, используя инструкцию COPY. Кстати так как наш базовый образ с добавлением файлов по сути изменился отсюда добавился новый слой.
    COPY osapp/. .
    
  4. Наш python скрипт использует библиотеку requests, которая по умолчанию не стоит в базовом образе python:3.9-slim. Соответственно нам надо её установить и для этого при создании нового образа должна запуститься команда установки зависимостей pip install -r requirements.txt. В шаге выше мы скопировали все файлы с osapp в корневую директорию /app образа, в том числе requirements.txt.
    RUN pip install -r /app/requirements.txt
    
  5. Ну и последний шаг это уже запуск нашего приложения, используя инструкцию CMD. Но эта команда уже выполнится, когда мы запустим сам контейнер.
    CMD ["python","pyfile.py"]
    
  6. После того как файл Dockerfile готов мы можем запускать процесс создания нашего собственного Docker образа.
    docker build . -t myapp:1.0
    
  7. Для проверки того что у нас получилось пробуем запустить сам контейнер.
    docker run myapp:1.0
    

Вот так выглядит простенький Dockerfile, но этими инструкциями он не ограничивается поэтому рекомендую прочитать про все инструкции ниже.

Чтобы просмотреть какие вообще образы присутствуют в вашей ОС можно воспользоваться командой docker images

Метаданные Docker образа

Для того, чтобы прописать какую-то важную информацию для пользователей или же просто указать версию образа или имя создателя можно добавить в контейнер метаданные, используя инструкцию LABEL.

Для этого в файл Dockerfile добавляем, например, такую строку:

LABEL maintainer="main@example.com" \ version="1.0" \ description="This is a Python application that try to access an url and return responce code." \ license="MIT"

Для того чтобы просмотреть метаданные того или иного образа можно воспользоваться командой docker image inspect myapp:1.0 | jq .[0].Config.Labels.

Переменные в Docker образе

Для указания переменных в образе используется 2 инструкции: ARG и ENV. Конечно же если их несколько значит есть отличия.

Инструкция ENV создаёт переменную, которая доступна как во время создания образа, так и при использовании самого контейнера, созданного с этого образа. Т.е. приложение, которое будет работать в будущем внутри контейнера сможет использовать эту переменную и её значение.

ENV APP_PORT=8080

Инструкция ARG в свою очередь объявляет переменную, которую вы можете использовать только внутри образа, во время его создания.

ARG APP_ARG=production
ENV APP_ENV=$APP_ARG

Если нужно присвоить значение со знаками пробелами, то используем один из следующих вариантов:

ENV APP_ARG="Must be production"
ENV APP_ARG=Must\ be\ production

Команда сборки Docker образа

Как мы уже поняли из текста выше сборка образа осуществляется командой docker build. Через опцию -t мы присваиваем тэг нашему образу, если просто, то это имя и версия нашего образа. Например, чтобы собрать образ с именем nginx и версией 1.0 мы выполняем команду docker build . -t nginx:1.0.

Для того, чтобы выполнить сборку образа в текущей директории должен присутствовать файл Dockerfile, с описанием нашего образа. Если же вы по каким-то причинам не хотите использовать стандартное имя для файла Dockerfile, можете назвать его иначе. Но тогда команда сборки образа будет выглядеть так: docker build --tag myapp:1.1 --file './filename' .

Обратите внимание на версионность, дело в том, что если вы вдруг обновили сам Dockerfile и хотите пересоздать образ, не меняя его версию, то старый образ всё также остаётся у вас в системе, просто становиться без имени и тэга. Поэтому либо удаляйте после пересоздания образы без имени или увеличивайте версию образа.

docker build . -t myapp:1.1
docker build . -t myapp:1.1
docker images
myapp    1.1       d2daa12adee5   13 minutes ago   125MB
<none>     <none>    f082ea0fb51f   16 minutes ago   125MB

Чтобы удалить все безымянные образы можно воспользоваться командой docker rmi $(docker images -f "dangling=true" -q).

Copy или Add

Для копирования файлов при сборке контейнера можно воспользоваться двумя инструкциями: ADD и COPY, ну и конечно есть в них различия.

Инструкцию ADD рекомендуется использовать если вы хотите скопировать в контейнер файлы по HTTPS или с Github. Также если вы копируете архив, он автоматически будет разархивирован.

В остальных же случаях просто используем инструкцию COPY.

Инструкция для запуска приложения ENTRYPOINT, CMD

В идеале инструкция для запуска приложения помещается в последнюю строку файла Dockerfile. CMD устанавливает команду, которая будет выполняться при запуске контейнера из образа и имеет следующий синтаксис CMD ["executable","param1","param2"]. Если вы вдруг решите использовать несколько CMD в одном Dockerfile, эффект даст только последняя команда.

Инструкция ENTRYPOINT делает по сути тоже самое, но в неё рекомендуется помещать только, те команды, которые не должны меняться, например, контейнер в любом случае должен запустить определённый файл, соответственно можно поместить команду в ENTRYPOINT.

ENTRYPOINT ["python"]
CMD ["--help"]

Используя ENTRYPOINT, вы указываете какой процесс должен запускаться при старте контейнера. CMD же нужен для того, чтобы передать параметры по умолчанию для процесса. Также, когда вы запускаете вы можете передать свои значения для CMD, т.е. заменить параметры по умолчанию, которые прописаны в Dockerfile.

docker run myapp:1.2
docker run myapp:1.2 --version

Инструкция EXPOSE

Конечно если мы говорим об образе для веб-приложения, то нам нужно порт, через который мы сможем получить доступ к приложение из вне. Для того, чтобы указать какой порт будет использоваться в образе используется EXPOSE.

EXPOSE 80/tcp

Инструкция RUN

Мы используем RUN для того, чтобы выполнять команды при сборке образа. Например, установить обновления на базовый образ, или установить новые пакеты в новый образ.

RUN apt-get update && apt-get install -y curl

Инструкция USER

Вы как пользователь Linux уже ни раз слышали, что не стоит запускать сервисы от root, для контейнеров используется тоже правило. Для того чтобы запустить сервис в контейнере от другого пользователя используется инструкция USER.

USER patrick

Многоэтапная сборка Docker образа

Мы уже просмотрели как создать один образ используя один файл Dockerfile, но что если нам для работы одного приложения понадобиться собрать несколько образов? Самый простой пример для чего это нужно - это когда в одном образе вы просто делаете сборку приложения, а во втором образе уже запускаете само приложение.

FROM golang:1.22-alpine3.20 as build
WORKDIR /app
RUN echo "print('Hello')" >> mypython.py

FROM python:3.9-slim
WORKDIR /myapp
COPY --from=build /app/mypython.py .
CMD ["python", "mypython.py"]
docker build . -t mymulti:1.0
docker run --rm mymulti:1.0
Hello

Обновление Docker образа и кэш

Каждый раз, когда мы запускаем build впервые все изменения слоёв кэшируются, т.е. если вы повторно запускаете build и не поменяли в Dockerfile ничего, то build полностью пройдёт с использованием кэша, что ускорит процесс сборки.

Если же вы изменили какую-то строку, а соответственно и слой, то при сборке этот слой уже не будет использовать кэш, и все последующие слои тоже.

Так что если вы хотите не использовать кэш? Например, если вдруг у вас в сборке есть команды для обновления пакетов и вы хотите, чтобы образ был свежим. Тогда просто добавляем опцию --no-cache к команде docker build.

docker build . -t mymulti:1.1
CACHED [stage-1 4/4] RUN apt-get update
docker build --no-cache . -t mymulti:1.1
[stage-1 4/4] RUN apt-get update

В итоге

  • Все инструкции (конфигурации) для будущего образа задаются в файле Dockerfile
  • Dockerfile начинается с базового образа (from)
  • Всегда указывайте определённые версии базового образа, пакетов, зависимостей
  • Старайтесь максимально уменьшить размер вашего образа (повысить время выкладки приложения)
  • Используйте COPY вместо ADD
  • Используйте ENTRYPOINT в связке с CMD