Для кого статья: для тех, кто не знаком с Docker. Мы разберемся, что он из себя представляет, какие имеет возможности и как его использовать.
Если Docker вам знаком и подробности не хочется читать — забирайте cheat sheet и краткое описание отсюда:
Сегодня коснемся только темы запуска контейнера из командной строки.
На прочтение понадобится около 15 минут, на чтение и выполнение 30–45 минут. Так что, если есть условно час времени ☕️, предлагаю разобраться с таким важным современным инструментом, как Docker.как docker.
Contents
- 1 Что такое docker?
- 1.1 Виртуальная машина с набором предустановленных образов...
- 1.2 Операции с контейнером
- 1.2.1 Запустить контейнер
- 1.2.2 Интерактивный/терминальный доступ к контейнеру
- 1.2.3 Посмотреть список контейнеров
- 1.2.4 Подлючние к запущенному контейнеру
- 1.2.5 Открепить контейнер от терминала (запустить в фоновом не интерактивном режиме)
- 1.2.6 Выполнить команду внутри запущенного контейнера
- 1.2.7 Удалить контейнер
- 1.2.8 Логи
- 1.2.9 Операции с образами
- 1.3 ... c сетью...
- 1.4 ... настриваемыми портами, которые можно "пробрасывать"...
- 1.5 ... папками, которые можно "подменить" папками хоста..
- 1.6 ... и управлять настройкой приложений через переменные окружения
- 2 Все команды в одном cheat sheet
- 3 Зачем может понадобится docker?
Что такое 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, "имидж"). Образ — это что-то вроде бэкапа уже настроенной системы.
Причём существует целый каталог (hub.docker.com) этих образов/имиджей, где можно найти всё что угодно, и часто от самих разработчиков систем. А можно создать свой образ — взять за базу готовый или чистую операционную систему (ту же Alpine) и установить в него свои приложения. Можно сохранить этот предустановленный "слепок" системы в свой репозиторий на hub.docker.com как свой новый образ. И если он будет открытым для всех, то даже бесплатно.
Операции с контейнером
Попробуйте все эти операции с образом 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
(образ контейнера), 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 сделает все самостоятельно, но контейнеры не смогут обращаться друг к другу по имени)
А может быть и сложной, например с reverse proxy server (расскажу об этом как-нибудь позже)
Внутри контейнера доступен ваш интернет, так что можно устанавливать программы прямо через apt-get install
(Debian), apk add
(Alpine) и т.д.
... настриваемыми портами, которые можно "пробрасывать"...
Некоторые приложения внутри контейнера "слушают" обращения к ним через определённый порт (веб-серверы, базы данных, FTP, почтовые серверы и т.д.). Чтобы "достучаться" до этого "внутреннего" порта из внешнего мира, нужно, чтобы контейнер тоже слушал и принимал обращения из внешнего мира и передавал их внутрь контейнера.
Причём он может слушать на одном порту снаружи, а передавать его внутрь уже на другой номер порта. Это и есть "проброс портов".
Этот параметр указывается ключом -p
, причём сначала всегда указывается порт "снаружи".
Вообще, во всех ключах, где необходимо указать "пробросы" между контейнером и "внешним миром"/локальным компьютером, формат всегда снаружи:внутри (контейнера).
docker ... -p снаружи:внутри_контейнера
Пробрасывать порты приходится часто, потому что:
- Во-первых, без явного проброса контейнер вообще не будет общаться с внешним миром.
- Во-вторых, контейнеры обычно работают на стандартных портах (веб-сервер на 80, MySQL на 3306). Но что делать, если у вас эти порты уже кем-то заняты? Пробросить на любой другой порт!
Если в контейнере вы захотите, чтобы какая-то служба слушала порт (но это не было предусмотрено первоначальной конфигурацией образа), то при запуске контейнера нужно указать, какие порты предоставляет/экспозирует контейнер. Делается это ключом --expose порт
.
docker run -it --expose 80 -p 80:80 образ
Обратите внимание, что --expose
недостаточно. Так мы открываем порт только внутри контейнера, но его необходимо ещё пробросить, чтобы "снаружи" он тоже слушал порт и передавал его "внутрь".
... папками, которые можно "подменить" папками хоста..
Ещё один "фокус", который можно делать с контейнерами, — это примонтировать к контейнеру папку нашего хоста (говоря про виртуальные машины, "хост" — это ваш физический компьютер). И эту папку контейнер будет видеть как свою.
Причём, как и с портами, папка на хосте может иметь один путь и имя, а внутри контейнера — другой. Например, на хосте это будет папка /home/me/Documents/myproj
, а в контейнере — папка /usr/share/nginx/html
(в этой папке обычно nginx хранит файлы веб-страницы).
И важно! Папка не будет копироваться в контейнер, просто открывая свою папку
/var/www
, контейнер фактически будет видеть её содержимое на хосте.
Для этого используется ключ -v в формате: путь снаружи:внутри (контейнера)
docker ... -v снаружи:внутри_контейнера
... и управлять настройкой приложений через переменные окружения
Остаётся один вопрос: если имидж — это уже предустановленная и настроенная система, то как её кастомизировать? Как передать в неё пароль администратора или имя базы данных? Ответ простой — через системные переменные (переменные окружения).
Если вы не знаете, что такое системные переменные, обязательно приходите на мой курс Manual QA (он уже доступен) или SDET:Base (но если вы не знаете, что такое системные переменные, то лучше всё-таки начать с Manual QA).
docker .... -e переменная=значение
Как узнать какие системные переменные поддерживаются? Почитать описание имиджа на hub.docker.com
Все команды в одном cheat sheet
Это небольшая "шпаргалка" по самым важным командам. Вообще, в Docker очень удобная справка. Вы вводите docker --help
и получаете список команд.
Вводите docker команда --help и получаете список ключей этой команды
Единственное, вы получите под сотню ключей, многие из которых никогда не будете использовать. Я выбрал для вас самые важные.
Зачем может понадобится 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 для компиляции:
- Примонтировать текущую папку в какую-нибудь папку внутри контейнера (ключ
-v снаружи:внутри
). - Сделать в контейнере папку (ту, в которую примонтировали текущую) рабочей (ключ
-w папка_внутри
). - Выполнить
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).
Отлично! Samba работает, можно тестировать её на безопасность.
Как — читайте в одном из следующих постов и следите за проектом SDETedu™ (qacedu.com), где я готовлю такие же доходчивые курсы простым языком на темы тестирования в разных его проявлениях (от мануального до автоматизации тестирования UI, unit-тестирования, тестирования в CI/CD, тестирования производительности и до тестирования безопасности).
А пока он в процессе разработки, есть готовый курс мануального тестирования, который я провожу прямо сейчас. Детали курса можно найти на qacedu.com. Главное отличие моих курсов — я обучаю не по времени, а на результат. Мы будем разбираться с темой, пока вы её не поймёте, а за дополнительное время платить, как репетитору, не придётся.