Docker

В данном практическом занятии вспомним основные возможности docker клиента.

Vagrant

Для работы с докером в независимости от платформы можно воспользоваться следующим Vagrantfile:

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/lunar64"
  config.vm.provision "docker"
end

Container lifecycle

Run

Для простого запуска команды в контейнере достаточно запустить команду docker run указав имя образа и команду. В данном примере используется образ python:3.11.5 и команда для просмотра версии. В некоторых образах уже имеется команда для запуска по-умолчанию, так что нет необходимости ее указывать.

$ docker run python:3.11.5-alpine python --version
Unable to find image 'python:3.11.5-alpine' locally
3.11.5-alpine: Pulling from library/python
7264a8db6415: Pull complete
66e1d5e70e42: Pull complete
0448660c92fc: Pull complete
3ce23f846e31: Pull complete
efebc2e683d2: Pull complete
Digest: sha256:5d769f990397afbb2aca24b0655e404c0f2806d268f454b052e81e39d87abf42
Status: Downloaded newer image for python:3.11.5-alpine
Python 3.11.5

Как видно при отсутствии образа docker скачает его.

Список запущенных контейнеров можно увидеть командой docker ps:

$ docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

Так как наш контейнер после вывода строки с информацией о версии завершил свою работу, то мы не увидим запущенных контейнеров. Чтобы увидеть список остановленных контейнеров нужно добавить опцию -a:

$ docker ps -a
CONTAINER ID   IMAGE                  COMMAND              CREATED          STATUS                      PORTS     NAMES
6df80e8b97b3   python:3.11.5-alpine   "python --version"   12 seconds ago   Exited (0) 12 seconds ago             modest_spence

Чтобы очистить завершенные контейнеры можно выполнить команду docker container prune:

$ docker container prune
WARNING! This will remove all stopped containers.
Are you sure you want to continue? [y/N] y
Deleted Containers:
6df80e8b97b3bd430f588337dd06cddd2550b82519c8f5c67ecd47fe85913134

Total reclaimed space: 0B

Для того, чтобы высвобождать ресурсы контейнера сразу после его завершения можно добавить опцию --rm в команде docker run:

$ docker run --rm python:3.11.5-alpine python --version
Python 3.11.5
$ docker ps -a
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

Для работы в интерактивном режиме с командой, запускаемой в контейнере, необходимо использовать две опции - -i(interactive) и -t(tty):

$ docker run --rm -it python:3.11.5-alpine python
Python 3.11.5 (main, Aug 26 2023, 00:26:34) [GCC 12.2.1 20220924] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> os.name
'posix'
>>> exit()

С помощью опции -d можно запустить контейнер в фоновом режиме, а с помощью опции --name задать имя с которым в дальнейшем удобно будет работать:

$ docker run --name test -d python:3.11.5-alpine python -m http.server 8888
1a3e73023693d79e0ea13a54d3825887e52dcafdf09a7e91a336e590ccce5462
$ docker ps
CONTAINER ID   IMAGE                  COMMAND                  CREATED          STATUS          PORTS     NAMES
1a3e73023693   python:3.11.5-alpine   "python -m http.serv…"   26 seconds ago   Up 26 seconds             test

В ответ мы получим id контейнера, а сам контейнер продолжит работу в фоновом режиме. Данная команда в контейнере запустит http сервер, который работает на порту 8888.

Exec

С помощью команды docker exec мы можем выполнить команду внутри контейнера, запустим утилиту wget, которая выполнит http запрос внутри контейнера:

$ docker exec test wget -qO- localhost:8888
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Directory listing for /</title>
</head>
<body>
...

Stop

Остановим контейнер командой docker stop:

$ docker stop test
test
$ docker ps -a
CONTAINER ID   IMAGE                  COMMAND                  CREATED          STATUS                       PORTS     NAMES
4504eaafb180   python:3.11.5-alpine   "python -m http.serv…"   46 seconds ago   Exited (137) 3 seconds ago             test

Но после остановки данные контейнера все еще остаются в системе, для их удаления можно воспользоваться командой docker rm:

$ docker rm test
test
$ docker ps -a
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

Run Options

Опишем простой http сервер на python, расположим его в директории с Vagrantfile, так что внутри виртуальной машины он будет располагаться по пути /vagrant/test.py:

from http.server import HTTPServer, BaseHTTPRequestHandler
from os import getenv

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.end_headers()
        file = getenv("FILE", "none")
        self.wfile.write(f"file: {file}\n".encode())
        if file != "none":
            self.wfile.write(open(file, "rb").read())

HTTPServer(('', 8888), Handler).serve_forever()

Данный http сервер будет слушать порт 8888 и при наличии переменной среды FILE выводить содержимое файла в ответе на GET запрос.

С помощью опции -v команды docker run можно смонтировать директорию внутрь контейнера:

$ docker run -v /vagrant:/vagrant -d python:3.11.5-alpine python /vagrant/test.py
b1b891ee04ca0556acbbd7fd6a2669176f4d5920fbb3633f6bbde7eda2322258

Запустить команду внутри работающего контейнера также можно в интерактивном режиме с опциями -it команды docker exec, например можно запустить командную оболочку:

$ docker exec -it test /bin/sh
/ # wget -qO- localhost:8888
file: none
/ # ls /vagrant/
Vagrantfile  test.py
/ # env
HOSTNAME=8dcddea20192
PYTHON_PIP_VERSION=23.2.1
SHLVL=1
HOME=/root
GPG_KEY=A035C8C19219BA821ECEA86B64E628F8D684696D
PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/9af82b715db434abb94a0a6f3569f43e72157346/public/get-pip.py
TERM=xterm
PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
LANG=C.UTF-8
PYTHON_VERSION=3.11.5
PYTHON_SETUPTOOLS_VERSION=65.5.1
PWD=/
PYTHON_GET_PIP_SHA256=45a2bb8bf2bb5eff16fdd00faef6f29731831c7c59bd9fc2bf1f3bed511ff1fe
/ # exit

Работающий контейнер также можно принудительно завершить(используя сигнал SIGKILL) и высвободить ресурсы командой docker rm -f:

$ docker rm -f test
test

Указать переменные среды для контейнера можно с помощью опции -e команды docker run, а также есть возможность проброса портов опцией -p:

$ docker run -p 8888:8888 -v /vagrant:/vagrant -e FILE=/vagrant/Vagrantfile -d --name test python:3.11.5-alpine python /vagrant/test.py
3868ed2ca1d4dde97cdac888cf595bf76293f5e81693192db8a6eb4ffda9228f

Убедимся, что все настройки сработали сделав запрос к приложению в контейнере с хоста:

$ curl localhost:8888
file: /vagrant/Vagrantfile
Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/lunar64"
  config.vm.provision "docker"
end

Logs

Логи самого приложения можно посмотреть с помощью команды docker logs:

$ docker logs test
172.17.0.1 - - [19/Sep/2023 21:59:56] "GET / HTTP/1.1" 200 -

Ports

А конфигурацию портов с помощью docker port:

$ docker port test
8888/tcp -> 0.0.0.0:8888
8888/tcp -> [::]:8888

Stats

Посмотреть запущенные процессы в контейнере позволяет команда docker top:

$ docker top test
UID   PID    PPID   C  STIME  TTY  TIME      CMD
root  11609  11587  0  21:59  ?    00:00:00  python  /vagrant/test.py

А статистику потребления всех контейнеров можно наблюдать командой docker stats:

CONTAINER ID NAME CPU % MEM USAGE / LIMIT   MEM % NET I/O       BLOCK I/O   PIDS
3868ed2ca1d4 test 0.01% 11.07MiB / 952.6MiB 1.16% 1.79kB / 778B 0B / 1.98MB 1

Images

При указании образов контейнеров в командах docker используются локально загруженные, а при их отсутствии загружаются из общедоступного реджестри hub.docker.com. Список загруженных образов можно увидеть командой docker images:

$ docker images
REPOSITORY   TAG             IMAGE ID       CREATED       SIZE
python       3.11.5-alpine   b9b301ab01c2   3 weeks ago   52.1MB

Pull

Скачивать образа в локальное хранилище можно командой docker pull, скачаем образ registry с помощью которого можно развернуть свой локальный реджестри в докере:

$ docker pull registry:2
2: Pulling from library/registry
7264a8db6415: Already exists
c4d48a809fc2: Pull complete
88b450dec42e: Pull complete
121f958bea53: Pull complete
7417fa3c6d92: Pull complete
Digest: sha256:d5f2fb0940fe9371b6b026b9b66ad08d8ab7b0d56b6ee8d5c71cb9b45a374307
Status: Downloaded newer image for registry:2
docker.io/library/registry:2
$ docker images
REPOSITORY   TAG             IMAGE ID       CREATED       SIZE
python       3.11.5-alpine   b9b301ab01c2   3 weeks ago   52.1MB
registry     2               0030ba3d620c   6 weeks ago   24.1MB

Запустим собственный реджестри:

$ docker run -d -p 5000:5000 --name registry registry:2
83abcb32fb48828890d5f89b6bd8eb18bdcca95928e3cebf83310a182ca83d2f

Push

Для того чтобы отправить образ в наш реджестри необходимо, чтобы в имени образа было указание на конкретный реджестри. Задать имя и тег можно командой docker tag, после чего отправить командой docker push. Так как наш реджестри запущен локально на порту 5000, то в качестве имени может использоваться localhost:5000:

$ docker tag python:3.11.5-alpine localhost:5000/python:3.11.5-alpine
$ docker push localhost:5000/python:3.11.5-alpine
The push refers to repository [localhost:5000/python]
08928985481f: Pushed
7acf52b2a13c: Pushed
ce0f4c80e9b7: Pushed
9ad60c84bfbe: Pushed
4693057ce236: Pushed
3.11.5-alpine: digest: sha256:e5d592c422d6e527cb946ae6abb1886c511a5e163d3543865f5a5b9b61c01584 size: 1368

Save/Load

Образ можно выгрузить из локального хранилища в файл командой docker save:

$ docker save python:3.11.5-alpine -o python.tar
$ ls
python.tar

И если потребуется, то можно перенести на другую машину и загрузить в локальное хранилище командой docker load:

$ docker load -i python.tar
Loaded image: python:3.11.5-alpine

Build

Сборка же самих образов производится командой docker build. Для сборки образа опишем простой Dockerfile, который добавит файл test.py и укажет команду запуска:

FROM python:3.11.5-alpine

ADD test.py /test.py

CMD ["python", "test.py"]

Расположим его рядом с Vagrantfile, чтобы внутри виртуальной машины он находился по пути /vagrant/Dockerfile. В команде docker build передадим опцию -t для указания имени образа(если не указать тег будет использоваться latest), а также путь до директории с контекстом(там где будет производиться сборка):

$ docker build -t test /vagrant/
[+] Building 0.1s (7/7) FINISHED                                                docker:default
 => [internal] load .dockerignore                                                         0.0s
 => => transferring context: 2B                                                           0.0s
 => [internal] load build definition from Dockerfile                                      0.0s
 => => transferring dockerfile: 112B                                                      0.0s
 => [internal] load metadata for docker.io/library/python:3.11.5-alpine                   0.0s
 => [internal] load build context                                                         0.0s
 => => transferring context: 459B                                                         0.0s
 => [1/2] FROM docker.io/library/python:3.11.5-alpine                                     0.0s
 => [2/2] ADD test.py /test.py                                                            0.0s
 => exporting to image                                                                    0.0s
 => => exporting layers                                                                   0.0s
 => => writing image sha256:41a4beea6cab781be7403620f5edd2f35f242c471dc22f4a2d7820f5235b  0.0s
 => => naming to docker.io/library/test                                                   0.0s
$ docker images
REPOSITORY              TAG             IMAGE ID       CREATED          SIZE
test                    latest          41a4beea6cab   15 seconds ago   52.1MB
localhost:5000/python   3.11.5-alpine   b9b301ab01c2   3 weeks ago      52.1MB
python                  3.11.5-alpine   b9b301ab01c2   3 weeks ago      52.1MB
registry                2               0030ba3d620c   6 weeks ago      24.1MB

Запустим контейнер из нашего образа, предварительно завершив контейнер test, если он запущен, и заодно проверим его работу:

$ docker rm -f test
test
$ docker run -p 8888:8888 -d --name test test
90f3da847ba319893fa5906b9ca7ab3988dfe04824da029f1c935bdef095aa18
$ curl localhost:8888
file: none

Remote

Docker имеет клиент-серверную архитектуру, утилита docker является клиентом, который по умолчанию общается с docker демоном используя unix socket. Взаимодействие может осуществляться и по другим протоколам, самый простой - это tcp. С помощью него вы можете взаимодействовать с удаленным docker демоном точно также как и локально утилитой docker. Подготовим новую виртуальную машину в vagrant изменив конфигурацию docker демона для взаимодействия по сети через tcp, для этого можно воспользоваться Vagrantfile:

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/lunar64"
  config.vm.network "forwarded_port", guest: 2375, host: 2375
  config.vm.provision "docker" do |d|
    d.post_install_provision "shell", inline: <<-SHELL
      systemctl cat docker.service > /etc/systemd/system/docker.service
      sed -i '/ExecStart/s#$# -H tcp://0.0.0.0:2375#' /etc/systemd/system/docker.service
      systemctl daemon-reload
      systemctl restart docker.service
    SHELL
  end
end

После запуска вм с данной конфигурацией на хост будет проброшен tcp порт 2375, через который можно подключиться не заходя в виртуальную машину с помощью docker клиента. Для этого достаточно установить переменную среды DOCKER_HOST:

$ export DOCKER_HOST=localhost:2375
$ docker run -d registry:2
Unable to find image 'registry:2' locally
2: Pulling from library/registry
7264a8db6415: Pull complete
c4d48a809fc2: Pull complete
88b450dec42e: Pull complete
121f958bea53: Pull complete
7417fa3c6d92: Pull complete
Digest: sha256:d5f2fb0940fe9371b6b026b9b66ad08d8ab7b0d56b6ee8d5c71cb9b45a374307
Status: Downloaded newer image for registry:2
12e58496092d71819605aef4fb8d0ebf9cf251e5ff78bb6fd43fcb18cf77e967
$ docker ps
CONTAINER ID   IMAGE        COMMAND                  CREATED         STATUS         PORTS      NAMES
12e58496092d   registry:2   "/entrypoint.sh /etc…"   6 seconds ago   Up 5 seconds   5000/tcp   fervent_varahamihira