Если не вдаваться в технические детали Docker ближе всего к VirtualBox, VMware или другим средствам виртуализации. Технические отличия заключаются в других способах изоляции запускаемой гостевой (guest) операционной системы и разделения ресурсов основной (host) операционной системы. Как правило каждый работающий в Docker экземпляр гостевой операционной системы предназначен для запуска одного единственного приложения. При этом задействуется меньше ресурсов, чем при запуске в виртуальной машине. Для этого создается своя файловая система, свои виртуальные сетевые интерфейсы - как бы контейнер внутри которого приложение работает. Docker это программное обеспечение для контейнеризации приложений.
Основные понятия, которые надо различать, это образ (image) и контейнер (container). Если совсем просто - образ это готовый для запуска в Docker пакет, контейнер это запущенный в Docker образ. Образ Docker можно представить как iso-образ дистрибутива операционной системы, а контейнер - запущенный экземпляр операционной системы в виртуальной машине. Из одного образа можно запустить несколько контейнеров.
Образ создается из конфигурационного текстового файла. Новый образ можно создать на основе уже существующего. Как правило конфигурационный файл начинается с команды, которая указывает на существующий образ, взятый за основу и дополняется командами для установки дополнительного программного обеспечения или изменения конфигурации.
Для опубликования своих образов существуют репозитории, самый популярный из которых Docker Hub.
Начнем с самого простого примера. Выполним команду
$ docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
2db29710123e: Pull complete
Digest: sha256:cc15c5b292d8525effc0f89cb299f1804f3a725c8d05e158653a563f15e4f685
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
This message shows that your installation appears to be working correctly.
To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
(amd64)
3. The Docker daemon created a new container from that image which runs the
executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which sent it
to your terminal.
To try something more ambitious, you can run an Ubuntu container with:
$ docker run -it ubuntu bash
Share images, automate workflows, and more with a free Docker ID:
https://hub.docker.com/
For more examples and ideas, visit:
https://docs.docker.com/get-started/
Команда run
запускает контейнер. Разберем каждую строку
Unable to find image 'hello-world:latest' locally
Сначала Docker ищет запрашиваемый образ на локальной машине, если не находит - продолжает поиск в репозитории. По умолчанию это Docker Hub. Название образа состоит из двух частей: название и версия. Docker сам добавляет номер версии latest
к названию, если пользователь не указал что-то другое.
latest: Pulling from library/hello-world
Docker нашел подходящий образ в репозитории и готов его скачать.
2db29710123e: Pull complete
Digest: sha256:cc15c5b292d8525effc0f89cb299f1804f3a725c8d05e158653a563f15e4f685
Status: Downloaded newer image for hello-world:latest
Образ скачан, в выводе указан результат вычисления хэш-функции, подсчитанный по алгоритму sha256 для проверки образа. Всё остальное это результат выполнения приложения в запущенном контейнере. Результат работы этого примера сам по себе интересен - в выводе информация об этапах запуска контейнера. Там же еще одна интересная команда
$ docker run -it ubuntu bash
По этой команде запускается контейнер из образа ubuntu:latest
и управление передается выполняемой в контейнере командной оболочке bash.
Опции -i
или --interactive
и -t
или --tty
(или вместе -it
) позволяют запустить в контейнере интерактивные (взаимодействующие с пользователем) приложение. Они указывают Docker оставить стандартный вход (stdin) в открытом состоянии и выделить псевдо-терминал, который соединяет используемый терминал с потоками stdin и stdout контейнера. Если это сложно, то достаточно запомнить, что опции -it
позволяют перейти в контейнер и запустить команду, ожидающую ввода пользователя.
Например команда
$ docker run -it --rm python
Запустит REPL python последней версии. Опция --rm
автоматически удаляет контейнер после того, как его выполнение завершится.
Теперь создадим свой первый образ для запуска контейнера, который будет выполнять скрипт на python. Скрипт самый простой
print("Hello, world!")
сохраним его в файл hello.py
. Конфигурационный файл docker-образа называется Dockerfile
. Создадим Dockerfile такого содержания:
FROM python
COPY hello.py /
ENTRYPOINT [ "python", "./example.py" ]
Каждая строчка файла начинается с команды. Команды принято записывать заглавными буквами, но это не обязательно. Команды выполняются последовательно, в том порядке, в каком они указаны в Dockerfile.
FROM python
- указываем, что будем делать свой образ на основе базового образа python последней версии (latest) из репозитория Docker Hub.
COPY hello.py /
- скопируем наш скрипт hello.py
из текущего каталога (тот же каталог, где находится и Dockerfile) в корневой каталог файловой системы образа.
ENTRYPOINT [ "python", "/hello.py" ]
- запустим наш скрипт командой python /hello.py
Теперь соберем наш образ командой build
$ docker build -t hello-py:1.0 .
Точка в конце команды указывает, что Dockerfile для сборки образа находится в текущем каталоге.
Опция -t
(tag) команды build
позволяет задать имя и версию нашему образу. Если версию не указывать, Docker автоматически добавить latest. Если не указывать имя, к образу можно будет обратиться по значению IMAGE ID
. Узнать, какие образы у нас уже есть можно командой
$ docker images
или
$ docker image ls
Это идентичные команды, но лучше пользоваться вторым вариантом. Он введен для приведения команд Docker к некоторому унифицированному виду. Все команды, которые управляют образами находятся в разделе docker image
, контейнерами - docker container
.
REPOSITORY TAG IMAGE ID CREATED SIZE
<none> <none> 6af91789bf03 20 minutes ago 917MB
python latest a42e2a4f3833 5 days ago 917MB
Здесь видно, что при создании образа не было указано имя. Контейнер в таком случае можно запустить командой
$ docker run 6af91789bf03
Если указано имя, команда для запуска контейнера может быть более удобочитаемой
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
hello-py 1.0 6af91789bf03 22 minutes ago 917MB
python latest a42e2a4f3833 5 days ago 917MB
$ docker run hello-py:1.0
Контейнер мы запустили без опции --rm
, значит после завершения выполнения, он не удалился. Посмотреть существующие контейнеры можно командой
$ docker ps -a
или
$ docker container ls -a
Команды идентичны, как в случает с docker images
и docker image ls
. Опция -a
указывает выводить все контейнеры, запущенные и уже остановленные
$ docker container ls -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
9839a6c57209 hello-py:1.0 "python /hello.py" 5 seconds ago Exited (0) 4 seconds ago upbeat_noyce
Каждому контейнеру присваивается случайное имя. В этом случае это upbeat_noyce
. Но при создании контейнера можно указать и свое значение имени используя опцию --name
.
$ docker run --name hello-world hello-py:1.0
в этом случае список контейнеров будет такой
$ docker container ls -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
11794828bf57 hello-py:1.0 "python /hello.py" 29 seconds ago Exited (0) 29 seconds ago hello-world
9839a6c57209 hello-py:1.0 "python /hello.py" 3 minutes ago Exited (0) 3 minutes ago upbeat_noyce
Удалить остановленные контейнеры можно командой docker container rm <имя_контейнера>
$ docker container rm hello-world
Удалить локальные образы можно командой docker image rm <имя образа>
$ docker image rm hello-py:1.0
Усложним наш скрипт. Запустим в контейнере web-приложение выводящее на главную страницу Hello, world!
В качестве сервера будем использовать Flask.
Наш новый файл hello.py
from flask import Flask
app = Flask(__name__)
@app.route("/")
def index():
return "Hello, world!"
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
Изменим Dockerfile, добавим только одну команду, с помощью которой установим flask.
FROM python
RUN python -m pip install Flask
COPY hello.py /
ENTRYPOINT ["python", "/hello.py"]
Немного о порядке команд в Dockerfile. Docker выполняет команды по очереди, но, если команда не меняет состояние образа, она пропускается. Выполняется первая и последующие команды, которые приведут образ в состояние отличное от прошлой сборки. Для этого в результате выполнения каждой команды создается новый слой (layer), для которого вычисляется значение хэш-функции. Это значение будет сравниваться с новым значением при каждой последующей сборке, и, если хэши совпадают, текущий шаг сборки будет пропущен. Поэтому имеет смысл в каждом слое объединять как можно больше команд, которые будут редко меняться. Например установку дополнительных пакетов лучше объединить в одну команду RUN
. А те команды, которые будут приводить к изменению образа, лучше переносить в конец Dockerfile насколько это возможно. В нашем случае предполагается, что мы можем часто менять файл hello.py
, поэтому копирование нашего скрипта в контейнер выполняется после остальных команд, непосредственно перед запуском.
Если интересует, почему использую команду python -m pip install Flask
вместо pip install Flask
советую прочитать Зачем использовать python -m pip.
Соберем новый образ
$ docker build -t hello-py .
Запустим контейнер с именем hello-flask
из образа hello-py:latest
$ docker run -d -p 80:5000 --name hello-flask hello-py
Опция -d
(или --detach
) запускает контейнер в фоновом режиме. Это позволяет использовать терминал, из которого запущен контейнер, для выполнения других команд во время работы контейнера.
Опция -p
(или --port
) открывает порт контейнера для взаимодействия с внешним миром. В нашем случае запросы на порт с номером 80 (дефолтный порт для http-сервера) на локальном компьютере будут перенаправлены на порт 5000 запущенного контейнера.
Теперь мы можем открыть в браузере адрес http://localhost и увидеть наше приветствие Hello, world!
Убедимся, что наш контейнер продолжает работать
$ docker container ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3371c56c3c3a hello-py "python /hello.py" 3 minutes ago Up 3 minutes 0.0.0.0:80->5000/tcp hello-flask
В колонке STATUS
видим, что контейнер запущен, а в колонке PORTS
запись о перенаправлении запросов на порт с номером 5000.
Прежде чем удалить работающий контейнер, его надо остановить
$ docker container stop hello-flask
Бывает, что команда stop
не срабатывает, тогда можно воспользоваться командой kill
$ docker container kill hello-flask
Теперь можно контейнер удалять.
Продолжим изменять Dockerfile
FROM python
WORKDIR /app
RUN python -m pip install Flask
COPY hello.py .
CMD ["python", "./hello.py"]
Команда WORKDIR
устанавливает рабочий каталог. Если такого каталога нет, он создается. Теперь команда COPY
копирует скрипт в текущий каталог, которым стал /app
и скрипт выполняет по относительному пути ./hello.py
.
Вместо команды ENTRYPOINT
мы используем команду CMD
. Разница в том, что параметры команды CMD
можно подменить непосредственно из команды запуска контейнера. Например, следующая команда запустит наш контейнер, но http-сервер в нем запущен не будет, а вместо него выведется информация об установленных пакетах python
$ docker run -it --rm --name hello-flask hello-py pip freeze
click==8.0.3
Flask==2.0.2
itsdangerous==2.0.1
Jinja2==3.0.3
MarkupSafe==2.0.1
Werkzeug==2.0.2
При отладке удобно использовать команду CMD
, но в конечном Dockerfile лучше использовать ENTRYPOINT
, чтобы никто не смог сломать наш контейнер, передав из командной строки на выполнение незапланированную команду.
Давайте создадим файл requirement.txt
с нашими зависимостями. Пока нам нужен только Flask
Flask==2.0.2
Изменим Dockerfile
FROM python
WORKDIR /app
COPY requirement.txt .
RUN python -m pip install -r requirement.txt
COPY . .
ENTRYPOINT ["python"]
CMD ["./hello.py"]
Файл requirement.txt
с перечисленными зависимостями будет меняться не так часто, как скрипт, поэтому его копирование вынесли в отдельную команду. Так же я разделил запуск скрипта на две части. Позже я покажу зачем, а пока проверим, что все по-прежнему работает.
Собираем образ
$ docker build -t hello-py .
Запускаем контейнер
$ docker run -d --rm -p 80:5000 --name hello-flask hello-py
Проверяем результат в браузере. Если все нормально контейнер можно остановить
$ docker container stop hello-flask
У нас указана опция --rm
, поэтому после остановки контейнер сам удалится.
Создадим новый файл hello-cmd.py
print("Hello from command line")
Пересоберем образ. На этот раз образ собрался намного быстрее, чем в прошлый. Это связано с тем, что изменился только один слой, в котором мы выполняем операцию копирования всех файлов нашего каталога в текущий каталог контейнера COPY . .
Запустим контейнер, но в качестве дополнительного параметра укажем название нашего нового скрипта. Этот параметр заменит тот, что указан в команде CMD
.
$ docker run --rm --name hello-flask hello-py hello-cmd.py
Hello from command line
Проверим, что контейнер с web-сервером так же запускается.
Таким образом, можно писать новые программы и передавать их как параметры для запуска нашему контейнеру. Только для этого придется каждый раз пересобирать образ, иначе новые программы не попадут в рабочий каталог контейнера. Это удобно, когда мы хотим распространять образ с готовой программной для запуска на других компьютерах. Но для разработки или тестирования наших программ это решение не удобно. Для этого можно смонтировать локальный каталог в качестве тома (volume) контейнера Docker. Наш локальный каталог будет доступен в файловой системе контейнера.
Удалим команду COPY
из Dockerfile
FROM python
WORKDIR /app
COPY requirement.txt .
RUN python -m pip install -r requirement.txt
ENTRYPOINT ["python"]
CMD ["./hello.py"]
Пересоберем образ
$ docker build -t hello-volume .
Запустим контейнер с опцией -v
. Параметром этой опции служит полный путь к каталогу, который мы ходим сделать доступным из контейнера и после :
каталог в файловой системе контейнера, обращаясь к которому мы будем получать доступ к нашему локальному каталогу. В моем случае это /home/alex/projects/docker-projects/starting-docker:/app
у вас путь до локального каталога, где лежат наши программы скорее всего другой.
$ docker run -d --rm -p 80:5000 -v /home/alex/projects/docker-projects/starting-docker:/app --name hello-flask hello-volume
Запустим еще один контейнер, но передадим для выполнения скрипт hello-cmd.py
$ docker run -v /home/alex/projects/docker-projects/starting-docker:/app --name hello-cmd hello-volume hello-cmd.py
Hello from command line
Проверим что наши контейнеры существуют
$ docker container ls -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
de7a891e4e81 hello-volume "python hello-cmd.py" 5 seconds ago Exited (0) 4 seconds ago hello-cmd
c012bc603b88 hello-volume "python ./hello.py" About a minute ago Up About a minute 0.0.0.0:80->5000/tcp hello-flask
Напишем еще одну программу new-hello.py
for i in range(10):
print("Hello!", end=" === ")
И запустим ее в третьем контейнере
$ docker run -v $(pwd):/app --name new-hello hello-volume
new-hello.py
Hello! === Hello! === Hello! === Hello! === Hello! === Hello! === Hello! === Hello! === Hello! === Hello! ===
Я подключаю к контейнеру текущий каталог, поэтому вместо ввода полного пути я воспользовался командой подстановка $(pwd)
. Эта запись означает, что результат выполнения команды pwd
- вывод полного пути к текущему каталогу, подставится в строку нашей команды на запуск контейнера.
Еще раз убедимся, что все три контейнера существуют
$ docker container ls -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c4c103819fb0 hello-volume "python new-hello.py" 16 seconds ago Exited (0) 15 seconds ago new-hello
de7a891e4e81 hello-volume "python hello-cmd.py" 12 minutes ago Exited (0) 12 minutes ago hello-cmd
c012bc603b88 hello-volume "python ./hello.py" 14 minutes ago Up 14 minutes 0.0.0.0:80->5000/tcp hello-flask
Как видите, на этот раз пересобирать образ не пришлось.
В заключении расскажу как использовать в docker-контейнере виртуальное окружение python. Хотя Docker сам по себе изолирует среду выполнения иногда возникает необходимость воспользоваться модулем python venv
. Изменим наш Dockerfile
FROM python
WORKDIR /app
RUN python -m venv /venv
ENV PATH="/venv/bin:$PATH"
COPY requirement.txt .
RUN python -m pip install -r requirement.txt
ENTRYPOINT ["python"]
CMD ["./hello.py"]
В добавленных строках мы создаем в каталоге /venv
виртуальное окружение и добавляем путь к каталогу, откуда будет запускаться python в самое начало значения переменной окружения PATH
.
Для проверки того, что наша программа действительно выполнится в виртуальном окружении, напишем небольшой скрипт check-venv.py
import sys
print(sys.prefix)
print(sys.executable)
Соберем образ и запустим контейнер
$docker build -t hello-volume .
$docker run --rm -v $(pwd):/app --name venv hello-volume check-venv.py
/venv
/venv/bin/python
Всё работает.