Привет, Хабр! Мы перевели для вас свежую статью Джея Шмидта из блога Docker. Надеемся, что вам будет полезен этот материал. Приятного чтения!
RUN
Инструкция RUN
используется в Dockerfile для выполнения команд, которые создают и конфигурируют образ контейнера Docker. Эти команды выполняются в процессе сборки образа, и каждая инструкция RUN
создает новый слой в образе. Например, если вы создаете образ, для которого требуется установить определенные ПО или библиотеки, вы должны использовать RUN для выполнения необходимых команд установки.
В следующем примере мы показали, как процессу сборки Docker задать команду обновить apt cache и установить Apache во время создания образа:
RUN apt update && apt -y install apache2
Инструкцию RUN
следует использовать с умом. Важно сводить количество слоев к минимуму, по возможности объединяя связанные команды в одну инструкцию RUN
, чтобы уменьшить размер образа.
CMD
В инструкции CMD
указывается команда, которая будет выполняться при запуске контейнера из образа Docker по умолчанию. То есть если при запуске контейнера (т. е. в команде docker run
) команда не будет указана, то запустится та, которую мы установили в качестве команды по умолчанию. CMD
можно переопределить, указав аргументы командной строки для docker run
.
CMD
полезен для настройки команд по умолчанию и легко переопределяемых параметров. Он часто используется в образах как способ определения параметров запуска по умолчанию и может быть переопределен из командной строки при запуске контейнера.
Например, по умолчанию может запускаться веб-сервер, но пользователи могут указать, чтобы вместо него запускалась оболочка:
CMD ["apache2ctl", "-DFOREGROUND"]
Пользователи могут запустить контейнер с помощью команды docker run -it <image> /bin/bash
, чтобы получить оболочку Bash вместо запуска Apache.
ENTRYPOINT
Инструкция ENTRYPOINT
задает исполняемый файл для контейнера в качестве файла по умолчанию. Она похожа на CMD
, но между ними есть различие: если CMD
переопределяется аргументами командной строки, переданными в команду docker run
, то для переопределения ENTRYPOINT
все аргументы командной строки добавляются к самой инструкции ENTRYPOINT
.
Примечание: используйте инструкцию ENTRYPOINT
в том случае, когда вам нужно, чтобы контейнер всегда выполнял одну и ту же функцию, но так, чтобы пользователи имели возможность добавлять дополнительные команды в конце.
ENTRYPOINT
особенно полезна для преобразования контейнера в отдельный исполняемый файл. Например, предположим, что вы упаковываете пользовательский скрипт, для которого требуются аргументы (к примеру, “my_script extra_args”
). В таком случае вы можете использовать ENTRYPOINT, чтобы всегда запускать процесс скрипта (“my_script”
), а затем разрешить пользователям образа указывать “extra_args”
в командной строке docker run
. Как это будет выглядеть:
ENTRYPOINT ["my_script"]
Объединяем CMD и ENTRYPOINT
Инструкция CMD
в формате exeс может использоваться для предоставления аргументов по умолчанию для ENTRYPOINT
. Эта настройка позволяет использовать точку входа в качестве основного исполняемого файла, а CMD
— указывать дополнительные аргументы, которые могут быть переопределены пользователем.
Например, у вас может быть контейнер, запускающий приложение на Python, в котором вы хотите всегда использовать один и тот же файл приложения, но с возможностью указывать пользователями разные аргументы командной строки:
ENTRYPOINT ["python", "/app/my_script.py"]
CMD ["--default-arg"]
Выполнение команды docker run myimage --user-arg
приводит к выполнению другой команды python /app/my_script.py --user-arg
.
Что такое PID 1 и почему это важно?
В контексте Unix и Unix-подобных систем, включая контейнеры Docker, PID 1 относится к первому процессу, запущенному при загрузке системы. Затем все остальные процессы запускаются с помощью PID 1, который в модели дерева процессов является родительским для каждого процесса в системе.
В контейнерах Docker процесс, выполняющийся под PID 1, имеет решающее значение, поскольку он отвечает за управление всеми остальными процессами внутри контейнера. Кроме того, PID 1 — это процесс, который просматривает и обрабатывает сигналы от хоста Docker. Например, сигнал SIGTERM
, поступивший в контейнер, будет перехвачен и обработан PID 1, после чего работа контейнера будет успешно остановлена.
Когда команды выполняются в Docker с использованием формата shell, процессу shell (/bin/sh -c
) обычно присваивается значение PID 1. Тем не менее в этом формате он обрабатывает эти сигналы не совсем правильно, что может привести к некорректному завершению работы контейнера. При использовании же формата exec команда выполняется непосредственно как PID 1 без участия оболочки, что позволяет ей напрямую получать и обрабатывать сигналы. Такое поведение гарантирует, что контейнер сможет корректно останавливать, перезапускать или обрабатывать прерывания.
Shell и exec форматы
В предыдущих примерах мы использовали два способа передачи аргументов в инструкции RUN
, CMD
и ENTRYPOINT
. Эти способы называются shell form и exec form.
Важно: ключевое визуальное различие заключается в том, что форма exec передается в виде массива команд и аргументов, разделенных запятыми, с одним аргументом/командой на элемент. И наоборот, форма shell выражается в виде строки, объединяющей команды и аргументы.
Каждый формат по-своему влияет на выполнение команд внутри контейнеров, воздействуя на всё — от обработки сигналов до расширения переменных окружения.
В формате shell команда выполняется в подоболочке, обычно /bin/sh -c
в системах Linux. Этот формат позволяет обрабатывать оболочку (например, расширять переменные, использовать подстановочные знаки и т. д.), таким образом делая её более гибкой для определённых типов команд. Однако это также означает, что процесс, выполняющий вашу команду, не является PID 1 контейнера, что может привести к проблемам с обработкой сигналов, поскольку сигналы, посылаемые Docker (например, SIGTERM
для плавного завершения работы), получает оболочка, а не предполагаемый процесс.
Формат exec не вызывает командную оболочку. Это значит, что указанная вами команда будет выполняться непосредственно как PID 1 контейнера, что очень важно для корректной обработки сигналов, посылаемых контейнеру. Кроме того, эта форма не выполняет расширения оболочки, поэтому она более безопасна и прогнозируема, особенно при указании аргументов или команд из внешних источников.
Собираем всё вместе
Давайте рассмотрим несколько примеров, иллюстрирующих практическое применение и нюансы инструкций RUN
, CMD
и ENTRYPOINT
в Docker, а также выбор между форматами shell и exec. Эти примеры покажут, как каждая инструкция может быть эффективно использована в реальных сценариях работы с файлами Dockerfile, и подчеркнут различия между shell и exec.
Благодаря этим примерам вы лучше поймете, когда и как использовать каждую инструкцию, чтобы вы смогли адаптировать поведение контейнера к вашим потребностям, обеспечив надлежащую конфигурацию, безопасность и производительность ваших контейнеров. Этот подход поможет объединить теоретические знания, которые мы обсуждали выше, в практические идеи, что могут быть непосредственно применены к вашим проектам в Docker
Инструкция RUN
Для инструкции RUN
, используемой в процессе сборки Docker для установки пакетов или изменения файлов, выбор между форматами shell и exec может зависеть от необходимости обработки оболочки. Форма shell необходима для команд, требующих функциональности оболочки, таких как пайплайны или глобализация файлов. Однако форма exec предпочтительнее для простых команд без функций оболочки, поскольку она снижает сложность и вероятность ошибок.
# Shell form, useful for complex scripting
RUN apt-get update && apt-get install -y nginx
# Exec form, for direct command execution
RUN ["apt-get", "update"]
RUN ["apt-get", "install", "-y", "nginx"]
Инструкции CMD и ENTRYPOINT
Эти инструкции управляют поведением контейнера во время выполнения. Использование формата exec с ENTRYPOINT
гарантирует, что основное приложение контейнера будет обрабатывать сигналы напрямую, что очень важно для правильного поведения при запуске и завершении работы. CMD
может предоставлять параметры по умолчанию для ENTRYPOINT
в exec-формате.
# ENTRYPOINT with exec form for direct process control
ENTRYPOINT ["httpd"]
# CMD provides default parameters, can be overridden at runtime
CMD ["-D", "FOREGROUND"]
Гибкость и обработка сигналов
Использование ENTRYPOINT
в формате exec и CMD
для указания параметров гарантирует, что контейнеры смогут корректно обрабатывать сигналы операционной системы, оперативно реагировать на вводимые пользователем данные и поддерживать безопасную и предсказуемую работу.
Такая настройка особенно полезна для контейнеров, в которых запущены критически важные приложения, требующие корректного поведения при выключении и настройке.
Ниже — таблицы, обобщающие всё вышесказанное.
Примеры
В следующем разделе будут рассмотрены различия на высоком уровне между CMD
и ENTRYPOINT
.
В этих примерах инструкция RUN
не рассматривается, поскольку вы можете легко принять решение, сравнив два разных формата.
Тестовый Dockerfile
# Use syntax version 1.3-labs for Dockerfile
# syntax=docker/dockerfile:1.3-labs
# Use the Ubuntu 20.04 image as the base image
FROM ubuntu:20.04
# Run the following commands inside the container:
# 1. Update the package lists for upgrades and new package installations
# 2. Install the apache2-utils package (which includes the 'ab' tool)
# 3. Remove the package lists to reduce the image size
#
# This is all run in a HEREDOC; see
# https://www.docker.com/blog/introduction-to-heredocs-in-dockerfiles/
# for more details.
#
RUN <<EOF
apt-get update;
apt-get install -y apache2-utils;
rm -rf /var/lib/apt/lists/*;
EOF
# Set the default command
CMD ab
Первая сборка
Мы создадим этот образ и пометим его как ab
.
$ docker build -t ab .
[+] Building 7.0s (6/6) FINISHED docker:desktop-linux
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 730B 0.0s
=> [internal] load metadata for docker.io/library/ubuntu:20.04 0.4s
=> CACHED [1/2] FROM docker.io/library/ubuntu:20.04@sha256:33a5cc25d22c45900796a1aca487ad7a7cb09f09ea00b779e 0.0s
=> [2/2] RUN <<EOF (apt-get update;...) 6.5s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:99ca34fac6a38b79aefd859540f88e309ca759aad0d7ad066c4931356881e518 0.0s
=> => naming to docker.io/library/ab
Запуск с помощью CMD ab
Без каких-либо аргументов мы получим блок использования.
$ docker run ab
ab: wrong number of arguments
Usage: ab [options] [http[s]://]hostname[:port]/path
Options are:
-n requests Number of requests to perform
-c concurrency Number of multiple requests to make at a time
-t timelimit Seconds to max. to spend on benchmarking
This implies -n 50000
-s timeout Seconds to max. wait for each response
Default is 30 seconds
<-- SNIP -->
Но если мы запускаем ab
и включаем URL для проверки, то сначала получаем ошибку:
$ docker run --rm ab https://jayschmidt.us
docker: Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: exec: "https://jayschmidt.us": stat https://jayschmidt.us: no such file or directory: unknown.
Проблема заключается в том, что строка, передаваемая в командной строке — https://jayschmidt.us
— переопределяет инструкцию CMD
. Это недопустимая команда, которая приводит к возникновению ошибки. Поэтому нам нужно указать команду для запуска:
$ docker run --rm ab ab https://jayschmidt.us/
This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking jayschmidt.us (be patient).....done
Server Software: nginx
Server Hostname: jayschmidt.us
Server Port: 443
SSL/TLS Protocol: TLSv1.2,ECDHE-ECDSA-AES256-GCM-SHA384,256,256
Server Temp Key: X25519 253 bits
TLS Server Name: jayschmidt.us
Document Path: /
Document Length: 12992 bytes
Concurrency Level: 1
Time taken for tests: 0.132 seconds
Complete requests: 1
Failed requests: 0
Total transferred: 13236 bytes
HTML transferred: 12992 bytes
Requests per second: 7.56 [#/sec] (mean)
Time per request: 132.270 [ms] (mean)
Time per request: 132.270 [ms] (mean, across all concurrent requests)
Transfer rate: 97.72 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 90 90 0.0 90 90
Processing: 43 43 0.0 43 43
Waiting: 43 43 0.0 43 43
Total: 132 132 0.0 132 132
Запуск с помощью ENTRYPOINT
В этом запуске мы удаляем инструкцию CMD ab
из Dockerfile, заменяем её на ENTRYPOINT ["ab"]
и пересобираем образ.
Это похоже на команду CMD
, но есть отличие: когда вы используете ENTRYPOINT
, вы не можете переопределить эту команду (если только не используете флаг –entrypoint
в команде docker run
). Вместо этого все аргументы, переданные команде docker run
, рассматриваются как аргументы ENTRYPOINT
.
$ docker run --rm ab "https://jayschmidt.us/"
This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking jayschmidt.us (be patient).....done
Server Software: nginx
Server Hostname: jayschmidt.us
Server Port: 443
SSL/TLS Protocol: TLSv1.2,ECDHE-ECDSA-AES256-GCM-SHA384,256,256
Server Temp Key: X25519 253 bits
TLS Server Name: jayschmidt.us
Document Path: /
Document Length: 12992 bytes
Concurrency Level: 1
Time taken for tests: 0.122 seconds
Complete requests: 1
Failed requests: 0
Total transferred: 13236 bytes
HTML transferred: 12992 bytes
Requests per second: 8.22 [#/sec] (mean)
Time per request: 121.709 [ms] (mean)
Time per request: 121.709 [ms] (mean, across all concurrent requests)
Transfer rate: 106.20 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 91 91 0.0 91 91
Processing: 31 31 0.0 31 31
Waiting: 31 31 0.0 31 31
Total: 122 122 0.0 122 122
Что насчёт синтаксиса?
В приведённом выше примере мы использовали синтаксис ENTRYPOINT ["ab"]
. Но ведь ещё можно ввести ENTRYPOINT ab
(без кавычек и скобок). Давайте посмотрим, что случится, если мы напишем именно так:
$ docker run --rm ab "https://jayschmidt.us/"
ab: wrong number of arguments
Usage: ab [options] [http[s]://]hostname[:port]/path
Options are:
-n requests Number of requests to perform
-c concurrency Number of multiple requests to make at a time
-t timelimit Seconds to max. to spend on benchmarking
This implies -n 50000
-s timeout Seconds to max. wait for each response
Default is 30 seconds
<-- SNIP -->
Скорее всего, у вас возникнет идея повторно запустить команду docker run
, как мы ранее сделали для CMD ab
, которая предоставляет как исполняемый файл, так и аргумент:
$ docker run --rm ab ab "https://jayschmidt.us/"
ab: wrong number of arguments
Usage: ab [options] [http[s]://]hostname[:port]/path
Options are:
-n requests Number of requests to perform
-c concurrency Number of multiple requests to make at a time
-t timelimit Seconds to max. to spend on benchmarking
This implies -n 50000
-s timeout Seconds to max. wait for each response
Default is 30 seconds
<-- SNIP -->
Это связано с тем, что ENTRYPOINT
можно переопределить, если вы добавите аргумент –entrypoint
в docker run
. Вывод: всегда используйте ENTRYPOINT
когда вы хотите принудительно использовать указанный исполняемый файл в контейнере при его запуске.
Подведём итоги: основные выводы и best practices
Процесс принятия решений в выборе между RUN
, CMD
и ENTRYPOINT
, а также между форматами shell и exec, демонстрирует сложную природу Docker. Каждая команда служит определенной цели в экосистеме Docker, влияя на то, как контейнеры создаются, работают и взаимодействуют со своими средами.
Выбирая правильные инструкции и форматы для каждого конкретного сценария, разработчики могут создавать более надёжные и безопасные образы.
Внедрение этой передовой практики гарантирует, что приложения, развёрнутые в контейнерах Docker, достигают максимальной производительности в различных настройках, улучшая рабочие процессы разработки и развёртывания в производстве.
Спасибо, что дочитали до конца! Хотим предложить вам подписаться на наш блог Хабр, TG‑канал DevOps FM, VC.ru и познакомиться с YouTube. Везде выходит разный, но интересный и полезный контент.