Эта публикация - текстовый вариант и сценарий для видео на YouTube (оно удобно разбито на эпизоды).

Привет, сегодня я расскажу о том что такое Dockerfile, из чего он состоит и как его написать.

С помощью Dockerfile можно создавать image. Docker автоматически создает image читая инструкции из этого файла. С помощью Dockerfile вы описываете то как ваше приложение будет работать внутри контейнера. Это основная задача Dockerfile.

Image можно представить как слоеный пирог, где некоторые инструкции добавляет новый слой. Каждый слой занимает какой-то объем памяти, поэтому когда вы пишите Dockerfile, необходимо использовать инструкции FROM, RUN, COPY, ADD рационально. Именно эти инструкции и создают слои в итоговом image.

Build Context

Давайте сначала разберемся в том что такое Build context.

Когда мы вызываем команды docker build, Docker создает image на основе Dockerfile и build context.

Build context - это набор файлов, к которым есть доступ во время построения image.

При вызове команды docker build, можно передать локальную директорию, в которой находится Dockerfile, tar-архив, удаленный git-репозиторий или же сам текст Dockerfile переданный прямо в консоль.

В этом случае build context является локальной директорией, удаленным Git репозиторием или tar архивом, к которым можно получить доступ во время сборки. COPY и ADD инструкции могут обратиться к любому из файлов и директорий внутри этого контекста.

Все поддиректории включаются в контекст тоже.

Если вы передаете Dockerfile текстом в команду build, то тогда он интерпретируется как Dockerfile и Docker не использует никакие другие файлы из контекста.

Как и с Git, можно указать .dockerignore файл и указанные файлы или директории из контекста не будут доступны Docker для копирования в image.

Вы можете сделать несколько Dockerfile, назвать их по разному и они будут работать. Соответствующе назвав .dockerignore Docker учтет и его.

Инструкции Dockerfile

Dockerfile - набор комментариевинструкций и аргументов к ним.

Инструкция не чувствительна к регистру, но принято писать ее в верхнем регистре, как и SQL запросы для того, чтобы было проще отличить от аргументов.

Docker выполняет инструкции из Dockerfile по порядку.

Dockerfile должен начинаться с инструкции FROM. Эта инструкция определяет родительский image, на основе которого строится данный image. Все image должны начинаться с какого-нибудь базового image. Чтобы начать вообще с минимума, используйте базовый image alpine - всего 5мб и работающий линукс. В других случаях вам понадобится использовать уже существующий image.

Например, когда мы хотим контейнеризовать Spring Boot приложение, нам необходимо с помощью Maven установить зависимости, собрать исполняемый jar файл, а потом уже запустить его с помощью jdk.

Так будет выглядеть этот image.
Мы воспользовались уже существующими image maven и openjdk. И для контейнеризации приложения понадобилось лишь несколько собственных инструкций.

В этом примере вообще две инструкции FROM, их может быть несколько, если сборка image - это многошаговый процесс.

Перед первой инструкцией FROM могут находиться только комментарии, директивы парсера и инструкция ARG с описанием аргументов.

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

FROM [--platform=<platform>] <image> [AS <name>]
FROM [--platform=<platform>] <image>[:<tag>] [AS <name>]
FROM [--platform=<platform>] <image>[@<digest>] [AS <name>]

Image должен быть валидным и он может спулиться с DockerHub или другого публичного репозитория во время сборки.

FROM может использоваться несколько раз, если сборка представляет многошаговый процесс, тогда каждому шагу можно дать имя с помощью AS name в инструкции FROM.

Можно воспользоваться COPY –from=<name> и обратиться к предыдущему собранному image.

Tag или digest необязательны, если вы не указываете их, тогда Docker пытается найти тег latest и выбрасывает ошибку если предоставленный тег не найден.

Может быть применен дополнительный тег –platform, чтобы указать платформу image, например linux/amd64 или windows/amd64. По умолчанию используется платформа, на которой собирается image.

Комментарии

Docker считает строки, которые начинаются с # как комментарии.

В другие местах # считается аргументом. То есть вы не можете написать комментарий с середины строки. Комментарии однострочные, и если хотите продолжить, то нужно указать решетку еще раз.

Перед исполнением Dockerfile комментарии удаляются, поэтому они никак не влияют на построение image.

Табуляция и пробелы перед инструкциями и комментариями допускаются, но не рекомендуются.

Эти примеры будет работать одинаково.

        # это комментарий
    RUN echo hello
RUN echo world
# это комментарий
RUN echo hello
RUN echo world

Директивы парсера

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

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

Директивы из этих примеров не будут работать из-за нарушения правил.

Она не может быть разделена на две строки, не может повторяться, должна находиться до первой инструкции и комментария. Неизвестная директива воспринимается как комментарий.

Вот примеры невалидных директив.

В этом случае строка разрывается, что делает директиву невалидной.

#direc \
tive=value

В этом случае одна и та же директива дублируется.

#directive=value1
#directive=value2
FROM SomeImageName

В этом случае директива воспринимается как комментарий, потому что идет после инструкции FROM.

FROM SomeImageName
#directive=value

А эта директива невалидная, потому что идет после комментария.

# какой-то коментарий
#directive=value
FROM SomeImageName

Если директива неизвестна, то она воспринимается как комментарий, что делает все остальные директивы после нее невалидными.

#unknowndirective=value
#knowndirective=value

Есть две директивы парсера - syntax и escape .

Syntax используется при сборке с помощью BuildKit, это не обычный клиент, поэтому я не буду рассказывать про нее.

Escape обозначает символ конца строки в Dockerfile. По умолчанию это \.

Обычно в качестве разделителя на Windows используется апостроф `, где \ является разделителем директорий.

Аргументы

Мы помним, что аргументы могут быть указаны до инструкции FROM, они также могут быть указаны и после инструкции FROM.

Инструкция аргумента выглядит как:

ARG <name>[=<default value>]

Значение по умолчанию можно опустить.

Эта инструкция определяет те аргументы, которые пользователь может передать в команду build, во время сборки image при помощи флага --build-arg <varname>=<value>. Если аргумент не передан, тогда используется дефолтный.

Например, вы можете передать версию базового image или название директории, в которой будет происходить какая-нибудь логика.

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

Например, если вы указываете ${argname:-word}, то если аргумент argname был передан, его значение будет использовано, иначе слово word.

Если вы указываете ${argname:+word}, тогда если аргумент был передан, будет использоваться слово word, иначе пустая строка.

Это же работает и с переменными. Переменные можно получать на основе других переменных и аргументов с помощью обращения по названию.

Аргумент доступен в инструкциях после его упоминания. То есть в данном примере во второй строке user будет иметь значение some_user, потому что аргумент не определен еще на тот момент, а в четвертой уже переданный username.

FROM busybox
USER ${username:-some_user}
ARG username
USER $username

Также если вы хотите использовать один и тот же аргумент в нескольких стадиях сборки, то необходимо в каждой из них определять аргумент.

FROM busybox
ARG SETTINGS
RUN ./run/setup $SETTINGS

FROM busybox
ARG SETTINGS
RUN ./run/other $SETTINGS

Не передавайте секреты и пароли с помощью аргументов, потому что они будут видны в docker history. Для этого нужно использовать инструкцию RUN –mount=type=secret. Об этом я расскажу позже.

Переменные

Инструкция выглядит следующим образом:

ENV <key>=<value> ...

При вызове docker build или docker run можно переопределить переменные с помощью флага –env.

По сути эта инструкция работает как и аргументы. Но есть несколько отличий.

  1. Вы можете указывать несколько переменных в одной строке. Если значение включает пробелы, то нужно обернуть его в двойные кавычки.

  2. В отличие от аргументов, переменные хранятся в image и доступны для просмотра с помощью docker inspect или в Docker Desktop.

  3. Переменные наследуются из родительского image.

Значения переменных доступны после конца инструкции, то есть в таком случае значение def будет равно hello, а ghi будет равно bye.

ENV abc=hello
ENV abc=bye def=$abc
ENV ghi=$abc

Если хотите, чтобы значение переменной использовалось лишь во время сборки, тогда установите значение переменной при вызове команды RUN. Либо же воспользуйтесь инструкцией ARG.

RUN DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -y ...

WORKDIR

Инструкция WORKDIR устанавливает рабочую директорию для выполнения следующих команд.

WORKDIR /path/to/workdir

По умолчанию рабочая директория это корень файловой системы. Вам может понадобиться конкретная папка, поэтому можно использовать WORKDIR.

ENV DIRPATH=/path
WORKDIR $DIRPATH/$DIRNAME
RUN pwd

Эта инструкция может быть вызвана несколько раз. Если в начале пути стоит /, тогда путь к рабочей директории будет абсолютный, а если / нет, то относительный текущего.

Тут работает подстановка аргументов и переменных, поэтому в данном случае рабочая директория будет path/$DIRNAME

Рабочая директория наследуется от базового image, поэтому рекомендуется устанавливать ее явно при описании своего Dockerfile.

RUN

Инструкция RUN имеет два вида:

RUN <command>, выполняется в консоли /bin/sh -c или cmd /S /C (shell form)
RUN [“executable”, “param1”, “param2”] (exec form)

Команда RUN создает новый слой в текущем image. Соответственно этот обновленный image будет использоваться во всех инструкциях далее.

С помощью exec формы возможно использовать другую консоль и выполнять команды в базовом image, а не измененном.

Exec форма принимает как аргумент массив JSON, следует использовать двойные кавычки, а не одиночные.

Exec форма не вызывает консоль напрямую, поэтому нужно передавать первым аргументом необходимую консоль. Также при использовании данной формы используются локальные переменные консоли, а не Docker.

RUN –mount=[type=<TYPE>]

Этот флаг позволяет создать файловую систему, которая будет доступна во время сборки image.

Это может понадобиться для доступа к файлам на хосте, обращению к секретам или использования кэша для ускорения сборки.

Типы mount:

  • Bind - default, readonly

  • Cache - временная директория для кэша для компиляторов и пакетных менеджеров

  • Secret - позволяет Docker получить доступ к секретным файлам без копирования их в image

  • Ssh - позволяет Docker получить доступ к SSH ключам через SSH агентов

CMD

У инструкции CMD есть три формы:

CMD ["executable", "param1", "param2"] - exec-форма, она предпочтительна
CMD ["param1", "param2"] - по умолчанию для ENTRYPOINT
CMD command param1 param2 - shell-форма

В Dockerfile может быть лишь одна инструкция CMD, иначе только последняя будет выполнена.

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

Как и для RUN, exec форма не вызывает командную строку, а shell форма вызывает. Поэтому если хотите использовать консоль, то либо используйте shell форму, либо передавайте явно консоль.

Если вы передадите аргументы в docker run, тогда они переопределят те аргументы из инструкции CMD.

Разница между RUN и CMD в том, что RUN выполняется во время сборки и создает новый слой в image, а CMD не выполняется во время сборки, но исполняется при запуске контейнера.

ENTRYPOINT

Давайте рассмотрим инструкцию ENTRYPOINT, которая используется в паре с CMD.

У нее есть две формы:

ENTRYPOINT [“executable”, “param1”, “param2”] - exec-форма предпочитаемая
ENTRYPOINT command param1 param2 - shell-форма 

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

Можно переопределить ENTRYPOINT, если вызвать docker run –entrypoint

Как и CMD только последняя инструкция ENTRYPOINT будет исполнена

Есть несколько правил работы CMD и ENTRYPOINT:

  1. В Dockerfile должен быть определен как минимум CMD или ENTRYPOINT или обе инструкции

  2. ENTRYPOINT должна быть использована когда контейнер используется как исполняемое приложение

  3. CMD должна быть использована для определения аругментов по умолчанию для ENTRYPOINT

  4. CMD будет перезаписан, когда контейнер будет запущен с другими аргументами

Эта публикация - текстовый вариант и сценарий для видео на YouTube (оно удобно разбито на эпизоды). Привет, сегодня я расскажу о том что такое Dockerfile, из чего он состоит и как его написать.-11
Изображение взято из документации Docker.

Если CMD был определен в базовом image, то он не наследуется, поэтому его надо будет переопределять в текущем image.

LABEL

LABEL <key>=<value> <key>=<value> <key>=<value> ...

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

Можно переносить аргументы на новые строки, либо же писать на одной либо же писать несколько инструкций.

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

LABEL example="foo-$ENV_VAR"

Лейблы с базовых image наследуются и переопределяются, если указан тот же ключ.

Чтобы увидеть все лейблы image, используйте docker image inspect.

EXPOSE

EXPOSE <port> [<port>/<protocol>...]

Инструкция EXPOSE информирует Docker о том, что контейнер слушает определенный порт, когда он запущен. Можно указать TCP либо UDP соединение, по умолчанию TCP.

EXPOSE не открывает порт наружу на самом деле. Он лишь информирует пользователя image о работающих портах. Чтобы получить доступ к порту контейнера нужно явно указать флаг -p при создании контейнера.

Если контейнеры работают в одной сети, то они могут обращаться друг к другу без раскрытия порта хосту, что делает работу сети контейнеров более безопасной.

Вы можете создать контейнер базы и контейнер приложения, разместить их в одной сети, и приложение сможет обращаться к базе, в то время как снаружи Docker вы не сможете попадать в базу без явного раскрытия портов.

ADD

ADD [--chown=<user>:<group>] [--chmod=<perms>] [--checksum=<checksum>] <src>... <dest>
ADD [--chown=<user>:<group>] [--chmod=<perms>] ["<src>",... "<dest>"]

У инструкции ADD есть две формы. Она копирует файлы, директории и удаленные URLs, и добавляет их в файловую систему image.

Можно использовать wildcard из Golang filepath. Match, чтобы копировать несколько файлов, подходящих под этот паттерн.

ADD hom* /mydir/
ADD hom?.txt /mydir/

Указав относительный путь, файл будет добавлен в WORKDIR/relativeDir .

ADD test.txt relativeDir/

Указав абсолютный путь, файл будет добавлен в корень.

ADD test.txt /absoluteDir/

Есть несколько правил:

  1. <src> должен быть внутри build context, нельзя добавить файлы из вне контекста

  2. Директория не копируется, копируется только содержимое

  3. Если <src> это директория, то все ее содержимое копируется

  4. Если <src> это архив, тогда он распаковывается

  5. Если <dest> не существует, то он создается со всеми нужными путями для него

С помощью этой инструкции можно провалидировать хэшсумму файла.

COPY

COPY [--chown=<user>:<group>] [--chmod=<perms>] <src>... <dest>
COPY [--chown=<user>:<group>] [--chmod=<perms>] ["<src>",... "<dest>"]

Инструкция COPY копирует файлы и директории из <src> и добавляет их в файловую систему image

Как и в ADD поддерживаются wildcard, логика абсолютных и относительных путей. Эти инструкции схожи между собой, но есть несколько отличий.

  1. ADD поддерживает URL

  2. ADD автоматически извлекает локальные tar-архивы

  3. COPY принимает только локальные файлы

Рекомендуется использовать COPY, так как ADD предоставляет дополнительные функции, которые следует использовать с осторожностью.

VOLUME

VOLUME ["/data"]

VOLUME используются для того, чтобы разделять одну директорию между хостом, на котором работает Docker и контейнером. Так, например, можно сохранять данные базы данных и они не будут очищаться при перезапуске контейнера.

С помощью этой инструкции можно сказать Docker о том, что необходимо сохранить некоторую директорию с вложенными файлами. И тогда при старте контейнера будет создан volume, соответствующий этой директории.

Несколько особенностей volumes:

  1. При работе на Windows, volume должен быть несуществующей или пустой директорией и это не может быть диск С

  2. Если на каких-нибудь шагах построения image содержимое volume меняется, то эти изменения не будут сохранены

  3. Нужно использовать двойные кавычки как и везде, так как аргумент инструкции - это массив JSON строк

  4. Директория на хосте, которая будет хранить данные volume зависит от самого хоста и определяется на момент создания и старта контейнера.

USER

USER <user>[:<group>]
USER UID[:GID]

Инструкция USER необходима для установки имени пользователя и группы или их id для использования по умолчанию. Этот user доступен в инструкциях RUN, ENTRYPOINT и CMD.

Если у юзера нет определенной группы, то он будет в группе root.

FROM microsoft/windowsservercore
# создается Windows пользователь внутри контейнера
RUN net user /add patrick
# этот пользователь устанавливается для выполнения следующих комманд
USER patrick

В этом примере создается новый пользователь Патрик и он в дальнейшем используется в ходе построения image.

STOPSIGNAL

STOPSIGNAL signal

Инструкция STOPSIGNAL определяет сигнал, который будет отправлен контейнеру для его остановки. Это может быть полезно, если ваше приложение должно получать другой сигнал. Стопсигнал может быть переопределен при создании или запуске контейнера.

ONBUILD

ONBUILD INSTRUCTION

Инструкция ONBUILD добавляет триггер, который будет вызван, когда этот image будет использоваться как базовый для другого image. Он будет исполнен при создании дочернего контейнера как будто бы инструкция вставлена после FROM.

Это полезно, когда ваш image должен быть построен поверх какого-то другого image.

Это работает в такой последовательноcти:

  1. Когда инструкция ONBUILD исполняется, она добавляется в metadata этого image, она не влияет на сам image, только на дочерние.

  2. После конца сборки этого image список всех триггеров доступен под ключом OnBuild и его можно посмотреть с помощью docker inspect .

  3. Когда этот image будет использован в FROM при построении другого image, docker находит эти триггеры и исполняет их в том же порядке, в каком они были добавлены. Если какой-нибудь триггер падает, то и сборка всего image падает.

  4. Триггеры очищаются после сборки дочернего image, таким образом они не наследуются дальше первого уровня.

ONBUILD ADD . /app/src
ONBUILD RUN /usr/local/bin/python-build --dir /app/src

Например, мы делаем свой python-builder, который обрабатывает исходный код пользователей, добавив это в наш image, мы создадим два триггера. И когда пользователь воспользуется нашим image как родительским, то все файлы из его контекста добавятся в папку /app/src и запустится скрипт python-build.

ONBUILD инструкции не могут включать инструкции ONBUILD, FROM и MAINTAINER.

HEALTHCHECK

Инструкция HEALTHCHECK нужна для проверки работоспособности контейнера. Благодаря ей, Docker может перезапускать упавшие контейнеры и управлять жизненным циклом контейнера. Например, если ваш сервер попал в бесконечный цикл и не отвечает, с помощью HEALTHCHECK это можно определить.

Эта инструкция имеет две формы:

HEALTHCHECK [OPTIONS] CMD command
HEALTHCHECK NONE

В качестве опций можно передать:

  • --interval=DURATION (default: 30s)

  • --timeout=DURATION (default: 30s)

  • --start-period=DURATION (default: 0s)

  • --start-interval=DURATION (default: 5s)

  • --retries=N (default: 3)

Первая проверка пройдет через время указанное в interval, и в дальнейшем через interval после конца предыдущей проверки. Если проверка заняла больше timeout, то тогда контейнер считается нездоровым. Если прошло больше N попыток, то также контейнер считается нездоровым.

В Dockerfile может быть лишь одна инструкция HEALTHCHECK, иначе только последняя будет выполняться. Это логично, так как Docker должен понимать какие проверки нужно проводить для определения состояния контейнера.

Есть два exit статуса команды - 0, если контейнер здоровый и готов к работе и 1 - если контейнер нездоров.

HEALTHCHECK --interval=5m --timeout=3s \
CMD curl -f http://localhost/ || exit 1

В этом примере каждые 5 минут проверяется ответ сервера в течение трех секунд.

SHELL

SHELL ["executable", "parameters"]

Инструкция SHELL используется для переопределения консоли по умолчанию для shell-формы инструкции CMD, ENTRYPOINT и RUN.

Эта инструкция полезная для Windows, где есть обычная консоль и powershell.

FROM microsoft/windowsservercore

# выполняется как cmd /S /C echo default
RUN echo default

# выполняется как cmd /S /C powershell -command Write-Host default
RUN powershell -command Write-Host default

# выполняется как powershell -command Write-Host hello
SHELL ["powershell", "-command"]
RUN Write-Host hello

# выполняется как cmd /S /C echo hello
SHELL ["cmd", "/S", "/C"]
RUN echo hello

SHELL инструкция может быть написана несколько раз, таким образом заменяя прошлую. Это полезно, если нужно выполнить несколько команд на другой консоли, а затем вернуться к стандартной.

Пример Dockerfile

Давайте рассмотрим Dockerfile и разберемся в том, что там происходит.

FROM maven:3.8.5-openjdk-17 AS build
COPY /src /src
COPY pom.xml /
RUN mvn -f /pom.xml clean package

FROM openjdk:17-jdk-slim
COPY --from=build /target/*.jar application.jar
EXPOSE 8081
ENTRYPOINT ["java", "-jar", "application.jar"]

В этом Dockerfile создается image в двух шагах. Первый шаг - на основе maven с openjdk-17 устанавливаются все зависимости и собирается Java-приложение.

Для этого в image:

  • копируется папка с исходным кодом приложения

  • копируется файл с конфигурацией Maven

  • выполняется команда mvn -f /pom.xml clean packageкоторая создает исполняемый jar-файл

На втором шаге на основе openjdk-17 создается image, который будет запускать Java-приложение.

Для этого в image:

  • из предыдущего шага копируется любой .jar файл в текущий image под именем application.jar

  • указывается информация о том, что приложение работает на порту 8081 (это только информация, не открывающая порты и не обязывающая приложение действительно работать на этом порту)

  • указывается точка входа в приложение - команда java -jar application.jar, запускающая Java-приложение из файла application.jar (когда запустится контейнер, а не во время сборки image)

Dockerfile best practice

Есть несколько рекомендаций к написанию красивых и оптимизированных Dockerfile.

  1. Старайтесь использовать официальные image в инструкции FROM. Официальные image - это те, у которых есть синяя галочка на DockerHub. Это безопасно.

  2. Используйте alpine-версии. У множества image есть alpine-версии, которые весят меньше обычных.

  3. Если указываете LABEL, тогда предпочтите это делать в одной команде в одной строке, чтобы избежать создания дополнительных слоев.

  4. Разделяйте большие и сложные RUN команды на несколько строк с помощью переноса строк. Так ваш Dockerfile будет более читаемым и поддерживаемым.

  5. Используйте exec форму CMD - это форма вида CMD ["executable", "param1", "param2"]

  6. Используйте принятые порты для своих приложений и указывайте их в EXPOSE инструкции. Так другие разработчики смогут понять какие порты контейнера можно использовать

  7. Используйте переменные ENV, чтобы сделать запуск контейнера проще и более гибким.

  8. RUN –mount=type=bind более эффективна для копирования, чем COPY. Но такие файлы добавляются только для выполнения инструкции. ADD стоит использовать, если вы хотите скачать файлы из удаленного пути или разархивировать архив.

  9. Используйте VOLUME, чтобы указать Docker на необходимость сохранения определенной директории. Рекомендуется использовать VOLUME для всех данных, которые создаются пользователем.

  10. Предпочтительно использовать инструкцию USER вместо sudo.

  11. Используйте WORKDIR всегда, чтобы быть уверенным в правильности пути исполнения. Не используйте cd в RUN, так как это признак плохого кода.

  12. Думайте об ONBUILD как об инструкции, которую дает родительский image дочернему. Если вы разрабатываете такие image, то используйте отдельный тег -onbuild, например ruby:1.9-onbuild

Комментарии (2)


  1. nochkin
    01.04.2024 02:27
    +4

    Изображение взято из документации Docker.

    Там нет таких изображений. В документации код, который можно копировать. Проблема с изображением в том, что сложно копировать и поэтому они получаются довольно бесполезными.
    Нет смысла конвертировать кусок кода в картинку по этой причине. Преимуществ нет, а вот недостатков полно.


  1. FireLynx
    01.04.2024 02:27

    я новичок и хочу разобраться в докере. Эта статья вообще не помогла. А если бы с моей стороны не было потрачено время уже на попытки разобраться, в частности на чтение официальной документации, то эта статья бы сильно навредила.