# Docker
В данном практическом занятии вспомним основные возможности docker клиента.
## Vagrant
Для работы с докером в независимости от платформы можно воспользоваться
следующим `Vagrantfile`:
```ruby
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/lunar64"
config.vm.provision "docker"
end
```
## Container lifecycle
### Run
Для простого запуска команды в контейнере достаточно запустить команду `docker run`
указав имя образа и команду. В данном примере используется образ `python:3.11.5` и
команда для просмотра версии. В некоторых образах уже имеется команда для запуска
по-умолчанию, так что нет необходимости ее указывать.
```console
$ 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`:
```console
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
```
Так как наш контейнер после вывода строки с информацией о версии завершил
свою работу, то мы не увидим запущенных контейнеров. Чтобы увидеть
список остановленных контейнеров нужно добавить опцию `-a`:
```console
$ 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`:
```console
$ 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`:
```console
$ 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)`:
```console
$ 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` задать имя с которым в дальнейшем удобно будет работать:
```console
$ 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 запрос внутри контейнера:
```console
$ docker exec test wget -qO- localhost:8888
Directory listing for /
...
```
### Stop
Остановим контейнер командой `docker stop`:
```console
$ 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`:
```console
$ docker rm test
test
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
```
### Run Options
Опишем простой http сервер на python, расположим его в директории с `Vagrantfile`, так
что внутри виртуальной машины он будет располагаться по пути `/vagrant/test.py`:
```python
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` можно смонтировать директорию внутрь контейнера:
```console
$ docker run -v /vagrant:/vagrant -d python:3.11.5-alpine python /vagrant/test.py
b1b891ee04ca0556acbbd7fd6a2669176f4d5920fbb3633f6bbde7eda2322258
```
Запустить команду внутри работающего контейнера также можно в интерактивном режиме
с опциями `-it` команды `docker exec`, например можно запустить командную оболочку:
```console
$ 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`:
```console
$ docker rm -f test
test
```
Указать переменные среды для контейнера можно с помощью опции `-e` команды `docker run`,
а также есть возможность проброса портов опцией `-p`:
```console
$ 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
```
Убедимся, что все настройки сработали сделав запрос к приложению в контейнере с хоста:
```console
$ curl localhost:8888
file: /vagrant/Vagrantfile
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/lunar64"
config.vm.provision "docker"
end
```
### Logs
Логи самого приложения можно посмотреть с помощью команды `docker logs`:
```console
$ docker logs test
172.17.0.1 - - [19/Sep/2023 21:59:56] "GET / HTTP/1.1" 200 -
```
### Ports
А конфигурацию портов с помощью `docker port`:
```console
$ docker port test
8888/tcp -> 0.0.0.0:8888
8888/tcp -> [::]:8888
```
### Stats
Посмотреть запущенные процессы в контейнере позволяет команда `docker top`:
```console
$ 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`:
```console
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-hub].
Список загруженных образов можно увидеть командой `docker images`:
```console
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
python 3.11.5-alpine b9b301ab01c2 3 weeks ago 52.1MB
```
### Pull
Скачивать образа в локальное хранилище можно командой `docker pull`, скачаем образ
`registry` с помощью которого можно развернуть свой локальный реджестри в докере:
```console
$ 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
```
Запустим собственный реджестри:
```console
$ docker run -d -p 5000:5000 --name registry registry:2
83abcb32fb48828890d5f89b6bd8eb18bdcca95928e3cebf83310a182ca83d2f
```
### Push
Для того чтобы отправить образ в наш реджестри необходимо, чтобы в имени образа было
указание на конкретный реджестри. Задать имя и тег можно командой `docker tag`,
после чего отправить командой `docker push`. Так как наш реджестри запущен локально на
порту `5000`, то в качестве имени может использоваться `localhost:5000`:
```console
$ 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`:
```console
$ docker save python:3.11.5-alpine -o python.tar
$ ls
python.tar
```
И если потребуется, то можно перенести на другую машину и загрузить в локальное хранилище
командой `docker load`:
```console
$ docker load -i python.tar
Loaded image: python:3.11.5-alpine
```
### Build
Сборка же самих образов производится командой `docker build`. Для сборки образа
опишем простой `Dockerfile`, который добавит файл `test.py` и укажет команду запуска:
```dockerfile
FROM python:3.11.5-alpine
ADD test.py /test.py
CMD ["python", "test.py"]
```
Расположим его рядом с `Vagrantfile`, чтобы внутри виртуальной машины он находился по
пути `/vagrant/Dockerfile`. В команде `docker build` передадим опцию `-t` для указания
имени образа(если не указать тег будет использоваться latest), а также путь до
директории с контекстом(там где будет производиться сборка):
```console
$ 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`, если он
запущен, и заодно проверим его работу:
```console
$ 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`:
```ruby
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`:
```console
$ 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
```
[docker-hub]:https://hub.docker.com/