Для кого статья: для тех, кто не знаком с Docker. Мы разберемся, что он из себя представляет, какие имеет возможности и как его использовать.

Если Docker вам знаком и подробности не хочется читать — забирайте cheat sheet и краткое описание отсюда:

Сегодня коснемся только темы запуска контейнера из командной строки.

На прочтение понадобится около 15 минут, на чтение и выполнение 30–45 минут. Так что, если есть условно час времени ☕️, предлагаю разобраться с таким важным современным инструментом, как Docker.как docker.

Contents

Что такое docker?

Если кратко, то это движок для запуска "виртуальных машин" (как VMWare/VirtualBox).

Чтобы быть точным

Он, правда, не эмулирует железо, как VMWare или VirtualBox, и поэтому не является по определению "виртуальной машиной", но я буду в тексте его называть так, просто чтобы вы представляли на что это похоже.
Контейнер в Docker — это что-то вроде отдельной песочницы внутри вашей системы, где может работать сервер, приложение или инструмент, не мешая другим процессам.

Под ним обычно не запускают OS и приложения с UI (хотя, наверное, можно, не пробовал), но для запуска серверов и разных готовых систем разработки под Linux — самое то. Особенно если серверов много, и они должны быть легкими и изолированными.

Начнем с установки.

Установка:

Установка детально описана на официальном сайте. Устанавливайте Docker Desktop для своей системы (Mac, Windows, Linux). В простейшем случае необходимо просто скачать программу установки и установить. Проверьте, что он выполняется в фоне и работает, запустив простой контейнер:

docker run -it --rm alpine

Вы увидите подсказку командной строки Linux. Всё работает! Вы запустили виртуальную машину с Alpine Linux (очень маленькая версия Linux, всего 3.48 МБ в сжатом виде). На базе Alpine можно построить различные серверы и сетевые службы или использовать как рабочую машину Linux. Если захотите что-то установить в этой виртуальной машине, используйте apk.

apk install пакет

Когда "наиграетесь" с Alpine Linux, введите exit (всё установленное внутри потеряется, но это можно изменить).

Итак, если всё готово, давайте детальнее разберёмся, что же такое Docker. А это:

Виртуальная машина с набором предустановленных образов...

С его помощью создаётся виртуальная машина (контейнер, container) и устанавливается операционная система с уже установленным набором программ (образ, image, "имидж"). Образ — это что-то вроде бэкапа уже настроенной системы.

container 4 png

Причём существует целый каталог (hub.docker.com) этих образов/имиджей, где можно найти всё что угодно, и часто от самих разработчиков систем. А можно создать свой образ — взять за базу готовый или чистую операционную систему (ту же Alpine) и установить в него свои приложения. Можно сохранить этот предустановленный "слепок" системы в свой репозиторий на hub.docker.com как свой новый образ. И если он будет открытым для всех, то даже бесплатно.

container connect 2 png

Операции с контейнером

Попробуйте все эти операции с образом Alpine. Запустите его в одном окне терминала, а операции выполняйте в другом окне:

docker run -it --rm alpine

Практически все команды имеют формат: docker (ему даётся команда), затем сама команда (run, exec, attach, ps и другие), затем ключи, относящиеся к команде (уточняющие работу команды), и самый последний — образ или команда, которую нужно выполнить в контейнере.

Команда может быть "двухэтажной" (т.е. как бы две команды + ключи), если мы обращаемся не к контейнеру (например, docker image команда ключи или docker network команда ключи). Это потому, что с контейнером мы работаем в 90% случаев, и поэтому для упрощения docker container не пишут, а пишут сразу, что делать с контейнером (docker ps, docker rm...).

Запустить контейнер

docker run .... образ

run - это команда docker, запускающая контейнер

Интерактивный/терминальный доступ к контейнеру

К виртуальной машине (контейнеру) можно подключиться терминалом, выполнять там команды, устанавливать или удалять программы. Точно так же, как если бы подключились по SSH к серверу.

docker ... -it ...

Ключ -it делает так, чтобы контейнер выводил stdout в терминал, из которого запущен (ключ -i), даже если не указан ключ -t, и делает псевдотерминальный ввод (tty, ключ -t). Обычно используются совместно -it (можно запомнить как "Inter-acTive" или "Interactive Terminal").

Посмотреть список контейнеров

docker ps -a

Команду ps легко запомнить тем, кто работал с Linux. Там это "показать список процессов, запущенных в фоновом режиме".

Ключ -a покажет всё, включая остановленные контейнеры:

image 9

Самые главные поля — image (образ контейнера), container_id и names (идентификаторы контейнера).

Везде, где требуется указать контейнер, можно указать либо container_id, либо names. Если явно не указать контейнеру имя при запуске (docker run --name имя), Docker придумает его самостоятельно (иногда даже смешно получается). В этом случае имя можно посмотреть с помощью команды docker ps.

Подлючние к запущенному контейнеру

Кроме запуска (run) к уже работающему контейнеру можно подключиться (attach).

docker attach .... идентификатор_контейнера

Открепить контейнер от терминала (запустить в фоновом не интерактивном режиме)

Чтобы контейнер при запуске "открепился" от терминала, в котором его запустили, можно использовать ключ -d (detach) при запуске контейнера.

Попробуйте запустить такой контейнер с ключом -d и без него:

docker run -d -p 8080:80 -v ./:/usr/share/nginx/html/ nginx

Если не использовать -d, то после запуска в терминал будет бесконечно выводиться лог (так настроен образ).

Выполнить команду внутри запущенного контейнера

docker exec идентификатор_контейнера команда

Контейнер должен быть запущен (посмотрите с помощью docker ps, заодно посмотрите имя контейнера или id). Это одна из наиболее часто встречающихся команд.

В качестве примера повыполняйте команды на запущенном нами ранее контейнере с Alpine. Попробуйте что-то простое: ls (список файлов), cat файл (вывести содержимое файла из полученного ls списка)...

Командой может быть и командная оболочка (bash, sh). Так можно вернуться к "интерактивному" режиму работы с контейнером.

Удалить контейнер

docker rm идентификатор_контейнера

Удалить можно только остановленный контейнер (не открепленный -d, а остановленный)

Логи

Также можно посмотреть логи внутри контейнера

docker logs идентификатор_контейнера

Логи можно посмотреть за какой-то определённый промежуток времени. Время задаётся в кавычках и может быть относительным (в минутах — "10m", часах — "10h", днях — "10d") или абсолютным в формате ISO (yyyy-mm-ddThh:mm:ssZ, в формате UTC+0).

  • от начала до указанного времени --until "время"
  • от времени и до конца --sinсe "время"

Операции с образами

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

docker image команда

Смотреть их список

Локальных скачанных образов. Если нам необходимо создать ещё один контейнер, то Docker сначала ищет образ в локально скачанных образах, а если не находит, то скачивает с hub.docker.com.

docker image ls

опция -a (или --all) покажет все

Удалять (remove)

docker image rm имя_образа

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

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

Ключ -a (all) — для удаления всех неиспользуемых образов, а не только без тегов, а ключ -f (force) удаляет без вопросов.

docker prune -a -f

... c сетью...

У каждого контейнера есть IP-адрес, как если бы он был настоящим компьютером и был подключён к вашему компьютеру по локальной сети.

Можно запустить несколько контейнеров и соединить их в сеть. Можно даже сделать несколько сетей, создавая своего рода виртуальные роутеры. Для этого можно использовать ключ --network=роутер (тут "роутер" — это имя сети/виртуального роутера, например my_network или int_network).

docker ... --network=роутер ...

Но предварительно необходимо создать этот "роутер"

docker network create имя_роутера

Сеть может быть достаточно простой (можно даже не настраивать, docker сделает все самостоятельно, но контейнеры не смогут обращаться друг к другу по имени)

network1 png

А может быть и сложной, например с reverse proxy server (расскажу об этом как-нибудь позже)

network2 png

Внутри контейнера доступен ваш интернет, так что можно устанавливать программы прямо через apt-get install (Debian), apk add (Alpine) и т.д.

... настриваемыми портами, которые можно "пробрасывать"...

Некоторые приложения внутри контейнера "слушают" обращения к ним через определённый порт (веб-серверы, базы данных, FTP, почтовые серверы и т.д.). Чтобы "достучаться" до этого "внутреннего" порта из внешнего мира, нужно, чтобы контейнер тоже слушал и принимал обращения из внешнего мира и передавал их внутрь контейнера.

ports 1 png

Причём он может слушать на одном порту снаружи, а передавать его внутрь уже на другой номер порта. Это и есть "проброс портов".

Этот параметр указывается ключом -p, причём сначала всегда указывается порт "снаружи".

Вообще, во всех ключах, где необходимо указать "пробросы" между контейнером и "внешним миром"/локальным компьютером, формат всегда снаружи:внутри (контейнера).

docker ...  -p снаружи:внутри_контейнера

Пробрасывать порты приходится часто, потому что:

  1. Во-первых, без явного проброса контейнер вообще не будет общаться с внешним миром.
  2. Во-вторых, контейнеры обычно работают на стандартных портах (веб-сервер на 80, MySQL на 3306). Но что делать, если у вас эти порты уже кем-то заняты? Пробросить на любой другой порт!

Если в контейнере вы захотите, чтобы какая-то служба слушала порт (но это не было предусмотрено первоначальной конфигурацией образа), то при запуске контейнера нужно указать, какие порты предоставляет/экспозирует контейнер. Делается это ключом --expose порт.

docker run -it --expose 80 -p 80:80 образ

Обратите внимание, что --expose недостаточно. Так мы открываем порт только внутри контейнера, но его необходимо ещё пробросить, чтобы "снаружи" он тоже слушал порт и передавал его "внутрь".

... папками, которые можно "подменить" папками хоста..

Ещё один "фокус", который можно делать с контейнерами, — это примонтировать к контейнеру папку нашего хоста (говоря про виртуальные машины, "хост" — это ваш физический компьютер). И эту папку контейнер будет видеть как свою.

Причём, как и с портами, папка на хосте может иметь один путь и имя, а внутри контейнера — другой. Например, на хосте это будет папка /home/me/Documents/myproj, а в контейнере — папка /usr/share/nginx/html (в этой папке обычно nginx хранит файлы веб-страницы).

volumes 1 png

И важно! Папка не будет копироваться в контейнер, просто открывая свою папку /var/www, контейнер фактически будет видеть её содержимое на хосте.

Для этого используется ключ -v в формате: путь снаружи:внутри (контейнера)

docker ... -v снаружи:внутри_контейнера

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

Остаётся один вопрос: если имидж — это уже предустановленная и настроенная система, то как её кастомизировать? Как передать в неё пароль администратора или имя базы данных? Ответ простой — через системные переменные (переменные окружения).

Если вы не знаете, что такое системные переменные, обязательно приходите на мой курс Manual QA (он уже доступен) или SDET:Base (но если вы не знаете, что такое системные переменные, то лучше всё-таки начать с Manual QA).

docker .... -e переменная=значение

Как узнать какие системные переменные поддерживаются? Почитать описание имиджа на hub.docker.com

Все команды в одном cheat sheet

docker cli cheat sheet

Это небольшая "шпаргалка" по самым важным командам. Вообще, в Docker очень удобная справка. Вы вводите docker --help и получаете список команд.

image 10

Вводите docker команда --help и получаете список ключей этой команды

image 11

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


Зачем может понадобится docker?

Сразу примеры из жизни:

Легковесность (как запустить несколько контейнеров)

Для курса GetManualQA для "опытов" студентов мне нужно было развернуть 7 серверов. Конечно, можно было запустить их на любимом VMWare, но одновременный запуск 7 операционных систем на 7 виртуальных машинах был бы очень тяжёлым. А вот под Docker я их разрабатывал локально, и они так и остались выполняться на моём MacBook M1 (хотя курс уже почти год не идёт). Только недавно я про них вспомнил и остановил...

Для запуска 7 виртуальных серверов можно их просто запустить по отдельности. Я же использовал docker-compose, но о нём как-нибудь в другой раз (а ещё мы его разбираем даже на курсе Manual QA).

Изолированность (запуск web-сервера из текущей папки)

Я пишу фронт (Vue.js/TypeScript/Quasar/Vite). На продуктовом web-сервере будет лежать уже собранный (преобразованный в JavaScript-код), упакованный (без переноса строк и пробелов) и преобразованный в необходимую версию JavaScript для совместимости со всеми браузерами (Babel). Именно так это работают современные фронты: ваш код на любом языке преобразуется в html+js+css и выкладывается как статический на сервер, а интерактивный контент выполняется запросами к API из JavaScript на клиенте и обновлением html (DOM) опять же на клиенте (все это мы разбираем даже на курсе Manual QA, приходите). Так вот, код, который лежит на сервере, — это уже совсем другой код. И иногда бывает, что код локально работает, а на продакшене — нет.

С такой проблемой столкнулся и я — на фронте, развернутом на продакшене, не были видны системные переменные. В общем-то, это нормально, ведь, как я написал выше, фронт — это набор статики и JavaScript, которые пользователь через браузер по сути скачивает локально и выполняет. Поэтому пользователь видит системные переменные своей локальной машины, а не сервера. Vite решает эту проблему, но его нужно правильно настроить (как — расскажу в отдельном посте, тут не об этом).

На тот момент я только изучал Vue3+Quasar и мне нужно было сэмулировать работу фронта в среде продакшена (т.е. на изолированной от моего окружения системе), чтобы разобраться, как это работает. Это нужно было сделать разово, только чтобы разобраться, и желательно иметь возможность быстро пересобрать и задеплоить проект. Разворачивать какие-то сервера локально или в VMWare/VirtualBox/UTM и потом туда копировать статику не хотелось (долго), да и выкладывать для тестов на рабочий сервер — тоже не вариант. Можно было, конечно, воспользоваться LiveServer в VSCode, но там тоже нужно было бы делать настройки конфигураций, потом менять их обратно. К тому же это не был бы изолированный сервер без моих системных переменных.

С Docker всё решается одной командной строкой: текущая папка становится корневой для Nginx web-сервера (/usr/share/nginx/html/), который моментально становится доступным как внешний. И он полностью изолированный и полностью повторяет то, как это будет на реальном продакшене!

Вот эта строка:

docker run --name test -it -p 8080:80 -v ./:/usr/share/nginx/html/ --rm nginx

Я захожу на локальной машине в папку dist (куда идёт сборка), выполняю эту команду. Она сама загружает Nginx, разворачивает его в контейнере, подменяет в нём папку, где должны храниться файлы страницы на веб-сервере, на мою текущую (dist), запускает в нём Nginx, который начинает слушать порт 80, а контейнер начинает слушать порт 8080. Теперь я могу обратиться к нему по адресу http://127.0.0.1:8080, и откроется мой собранный проект! Полностью изолированный и точно такой, каким он будет на продуктовом сервере!

Компиляция кода, если нет установленного компилятора

Компиляция с++ кода для Linux

Представим, что вы не часто программируете на C++, и у вас нет установленных компиляторов, но какая-то очень нужная утилита поставляется в виде исходного кода. Как решить этот вопрос, не устанавливая компилятор?

Создаём файл comp или comp.bat с командной строкой:

docker run --rm -v ./:/usr/src/test/ -w /usr/src/test/ gcc c++ $*
  • run — создаём и "запускаем" контейнер.
  • --rm — удалить контейнер после запуска (каждое выполнение будет создавать контейнер, выполнять команду и удалять контейнер). Обратите внимание, что образ будет загружаться только первый раз.
  • -v — текущую папку "подставляет" в папку контейнера /usr/src/test.gcc — название образа.
  • c++ $* — командная строка C++ и параметры, с которыми запускали скрипт ($ — это параметры, $0 — сама команда, $1 — первый параметр... $* — все параметры).
  • $* — передача параметров скрипту.

Для Linux и macOS даём права на запуск

chmod +x comp

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

./comp test.cpp -o test.o

в итоге получим команду внутри контейнера:

c++ test.cpp -o test.o

и результат (test.o) в текущей папке

Комиляция c++ кода для Windows (mingw)

Другой пример — компиляция C++ кода для Windows (MinGW).

Всё так же, но образ mdashnet/mingw (если больше недоступен, поищите образ на hub.docker.com по слову mingw). Создаём для удобства скрипт для запуска компилятора (например, comp1).

docker run --rm -v ./:/usr/src/test -w /usr/src/test mdashnet/mingw x86_64-w64-mingw32-g++ $*

Единственное, что тут стоит прокомментировать, — это x86_64-w64-mingw32-g++. Это командная строка компиляции MinGW.

Запуск будет выглядеть так:

./comp1 test.cpp -o test.exe

В целом, один из алгоритмов использования Docker для компиляции:

  1. Примонтировать текущую папку в какую-нибудь папку внутри контейнера (ключ -v снаружи:внутри).
  2. Сделать в контейнере папку (ту, в которую примонтировали текущую) рабочей (ключ -w папка_внутри).
  3. Выполнить docker run образ команда параметры (указать --rm, иначе каждая компиляция оставит копию контейнера).
docker run --rm -v откуда:куда -w куда образ команда параметры

Использование exec вместо run

Как вариант, можно первый раз выполнить docker run, а потом использовать docker exec (тогда во всех случаях --rm нужно убрать, а второй и последующие разы -v, -w не нужны, но нужно дать имя контейнеру).

Первый раз

docker run --name имя_конт -v откуда:куда -w куда образ команда параметры

Второй и последующий разы

docker exec имя_конт команда параметры

В этом случае первый раз мы создаём контейнер и можем выполнить в нём команду (если не нужно выполнять, уберите "команду" и "параметры"). А второй и последующие разы подключаемся к созданному контейнеру и выполняем в нём команду.

Этот вариант не лучше и не хуже первого, так как папка с рабочими файлами в обоих случаях остаётся на локальном компьютере. И в первом, и во втором случае образ скачивается только при первом запуске.

Работа прямо в контейнере

Ещё один способ (как по мне, самый лучший) — это работать прямо в контейнере. Всё, что нужно, — это добавить к варианту с docker run ключ -it и убрать команду с параметрами (в некоторых случаях в качестве команды нужно указать бинарник командной оболочки: bash, sh, /bin/bash).

docker run -it --rm -v откуда:куда -w куда образ

После этого переходим в папку "куда"

cd куда

И там выполнять команды.

Плюс этого метода: можно запустить и проверить собранный файл в контейнере, а также отладить его (собрав с ключом -g и используя gdb).

Тестировние безопасности

Тестирование Web

Запускаем специально созданный тестовый сайт (с множеством ошибок безопасности).

docker run --name buggy_web -p 3000:3000 bkimminich/juice-shop

Открываем в браузере http://localhost:3000 и можем оттачивать умения и проверять OWASP.

Тестирование узвимостей серверов

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

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

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

docker network create sm_net

И теперь запустим контейнер с нужным образом в этой сети.

docker run -it --rm --name smb_victim --network sm_net -p 445:445 -v ./:/storage -e "USER=samba" -e "PASS=secret" dockurr/samba:4.18.10

Тут мы запустили контейнер в интерактивном режиме (-it), удалим его при выходе (--rm), контейнер назовём smb_victim (--name), подключим к сети sm_net (--network), пробросим порт 445 (-p), примонтируем текущую папку (из которой выполняем команду) к пути /storage (-v) и передаём через переменные окружения настройки (так настроен образ, переменные окружения и пути монтирования можно посмотреть в документации на hub.docker.com, поискав образ dockurr/samba). Настройки такие: пользователь — samba, пароль — secret, зашаренная папка — Data.

Теперь можно безопасно проверять уязвимости SAMBA локально, а не напрямую на реальном сайте.

Нужную версию сервера не всегда можно найти, но можно создать свой образ. Как — расскажу в отдельной статье про создание образов.

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

docker run -it --rm -p 4444:4444 --network sm_net metasploitframework/metasploit-framework

А ещё может понадобиться всеми любимый Kali Linux с базовыми функциями и возможностью доустановить всё необходимое. Запускаем в третьем терминале и в той же сети.

docker run -it --rm --network sm_net kalilinux/kali-rolling

В нём давайте доустановим необходимые для тестирования SAMBA утилиты.

Обновим список пакетов (Kali Linux запустится сразу под root, поэтому sudo можно не вводить).

apt-get update

Установим удобную программу для установки утилит Kali Linux.

apt-get install kali-tweaks

Запустим и выберем метапакет "Kali's top 10 tools" (или какой вам нужен) и установим.

kali-tweaks

Также может понадобиться какая-то отдельная утилита, например Samba Client (smbclient).

apt-get install smbclient

И попробуем подключиться к контейнеру Samba. Пользователем samba с паролем secret, к зашаренной папке Data (мы это настроили при запуске контейнера с Samba).

image 12

Отлично! Samba работает, можно тестировать её на безопасность.

Как — читайте в одном из следующих постов и следите за проектом SDETedu™ (qacedu.com), где я готовлю такие же доходчивые курсы простым языком на темы тестирования в разных его проявлениях (от мануального до автоматизации тестирования UI, unit-тестирования, тестирования в CI/CD, тестирования производительности и до тестирования безопасности).

А пока он в процессе разработки, есть готовый курс мануального тестирования, который я провожу прямо сейчас. Детали курса можно найти на qacedu.com. Главное отличие моих курсов — я обучаю не по времени, а на результат. Мы будем разбираться с темой, пока вы её не поймёте, а за дополнительное время платить, как репетитору, не придётся.