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