Лабораторна робота № 3. Два контейнери разом і ізольована мережа
Мета роботи
Навчитися піднімати кілька взаємодіючих контейнерів у власній ізольованій мережі Docker з DNS-резолвінгом за іменем, обмінюватися даними через спільний том (volume), незалежний від життєвого циклу контейнерів, і безпечно передавати секретні ключі між контейнерами через змінні середовища, мінімізуючи слід секрету в місцях, доступних стороннім.
Теоретичні відомості
1. Мережева ізоляція контейнерів і потреба у спільній мережі
Кожен контейнер за замовчуванням отримує власний NET-простір імен (лабораторна робота
№ 1), тому два довільні контейнери не бачать мереж один одного, доки не приєднані до
спільної мережі Docker. Дефолтна мережа bridge, у яку контейнери потрапляють без
явного --network, не веде записів DNS для контейнерів за іменем — доступ можливий лише
за внутрішньою IP-адресою, яка змінюється при кожному перезапуску контейнера.
2. User-defined bridge мережа і вбудований DNS
Користувацька (user-defined) bridge-мережа, створена командою docker network create,
дає контейнерам, приєднаним до неї з іменем (--name), вбудований DNS-резолвінг: звернення
до імені контейнера всередині мережі автоматично резолвиться у поточну IP-адресу того
контейнера, без ручного оновлення /etc/hosts чи будь-якого зовнішнього DNS-сервера. Це
принципова відмінність від дефолтної мережі bridge, де такого сервісного DNS немає.
3. Том (volume) як незалежна одиниця зберігання
Файлова система контейнера — тимчасова: видалення контейнера (docker rm) знищує все, що
контейнер записав понад базові шари образу. Том (volume), створений командою docker volume create, — окрема сутність, керована Docker і не прив'язана до життєвого циклу
конкретного контейнера. Кілька контейнерів можуть монтувати той самий том одночасно
(-v <ім'я-тому>:<шлях>) і обмінюватися файлами через файлову систему, не відкриваючи для
цього мережевий порт. На відміну від bind mount (монтування конкретної теки хоста), том
керується самим Docker і не залежить від структури файлів на конкретному хості, що робить
його переносним.
4. Секрети: змінні середовища, --env-file, межі приховування
Секрети (ключі доступу, паролі) не повинні лишати слідів у місцях, доступних стороннім:
у команді запуску контейнера (видима через docker inspect, поле Config.Cmd), в історії
процесів термінала, у логах CI/CD чи безпосередньо в Dockerfile. Передача через змінні
середовища при запуску контейнера можлива двома способами:
docker run -e KEY=value— значення потрапляє і вConfig.Envконтейнера, і дублюється в команді запуску, видимій в історії шелла та журналах.docker run --env-file secrets.env— значення потрапляє вConfig.Env, але не з'являється в команді запуску й не лишається в історії шелла; сам файл секретів не комітиться в репозиторій (.gitignore).
Важливо розуміти межу цього підходу: значення все одно залишається доступним через
docker inspect у полі Config.Env — повністю приховати його від будь-якого доступу цей
механізм не може. Мета навчального завдання — не лишити слід у Dockerfile, історії команд
збірки/запуску й репозиторії, а не досягти криптографічно строгого приховування (для
цього існують docker secret у режимі Swarm або секрети, змонтовані як файли в
Kubernetes/Compose — тема розширення поза межами базового завдання).
Обладнання та програмне забезпечення
- GitHub Codespaces або локальний devcontainer із Docker Engine.
docker network,docker volume,docker run --env-file.- Два прості образи/сервіси: умовний «сервіс» (клієнт, що звертається до сховища за обраним протоколом) і умовне «сховище» (сервер або процес, що надає доступ до даних).
Порядок виконання
- Створити user-defined bridge-мережу:
docker network create mission-net. Очікуваний результат: мережаmission-netз'являється у спискуdocker network ls. - Підняти контейнер-сховище в цій мережі з іменем (
--name storage --network mission-net). Очікуваний результат: стан контейнераrunning. - Підняти контейнер-сервіс у тій самій мережі (
--name agent --network mission-net) і перевірити резолвінг іменіstorage(ping storageабоgetent hosts storage). Очікуваний результат: успішна відповідь за іменем, без явного вказання IP. - Створити том:
docker volume create mission-logsі змонтувати його в обидва контейнери (-v mission-logs:/data). Очікуваний результат: файл, записаний у/dataодним контейнером, видно в іншому. - Записати тестовий «зашифрований лог» у спільний том із контейнера-сервісу і прочитати його з контейнера-сховища. Очікуваний результат: вміст файлу ідентичний у обох контейнерах.
- Видалити обидва контейнери й підняти їх заново з тим самим томом. Очікуваний результат: дані в томі збереглися, незважаючи на видалення контейнерів.
- Передати секретний ключ у контейнер-сервіс через
--env-file secrets.env(файл не комітиться в репозиторій), перевірити доступність змінної всередині контейнера й переконатися, що ключ не видно вdocker inspect agentяк частина команди запуску. Очікуваний результат: усередині контейнераecho "$STORAGE_ACCESS_KEY"виводить значення;docker inspect agent --format '{{.Config.Cmd}}'значення не містить.
Завдання за рівнями
Рівень 1 (оцінка 3, автотести). Підняти два контейнери (сервіс і сховище) й налаштувати ізольовану user-defined мережу між ними з резолвінгом за іменем. Відповідає крокам 1–3 порядку виконання.
Рівень 2 (оцінка 4, захист). Підключити спільний том (volume) для обміну даними між контейнерами; на захисті продемонструвати запис і читання даних обома контейнерами. Відповідає крокам 4–6 порядку виконання.
Рівень 3 (оцінка 5, розширення). Налаштувати передачу секретних ключів між контейнерами через змінні середовища так, щоб значення не з'являлося в команді запуску контейнера. Відповідає кроку 7 порядку виконання.
Розібраний приклад
Приклад 1. DNS-резолвінг у user-defined мережі
$ docker network create mission-net
$ docker run -d --name storage --network mission-net nginx:1.27-alpine
$ docker run -it --rm --network mission-net alpine:3.20 sh
/ # getent hosts storage
172.19.0.2 storage
Резолвінг storage → 172.19.0.2 відбувається автоматично через вбудований DNS-сервер
Docker у межах мережі mission-net, без попереднього вказання IP. У дефолтній мережі
bridge (без явного docker network create) та сама команда завершилася б помилкою —
дефолтна мережа не веде записів DNS для контейнерів за іменем.
Приклад 2. -e проти --env-file
$ docker run -d --name bad-agent -e STORAGE_ACCESS_KEY=0xC1D-secret alpine sleep 1000
$ docker inspect bad-agent --format '{{.Config.Env}}'
[PATH=/usr/local/sbin:... STORAGE_ACCESS_KEY=0xC1D-secret]
$ echo "STORAGE_ACCESS_KEY=0xC1D-secret" > secrets.env
$ docker run -d --name agent --env-file secrets.env alpine sleep 1000
$ docker inspect agent --format '{{.Config.Cmd}}'
[sleep 1000]
В обох випадках значення технічно опиняється в Config.Env контейнера — це властивість
самого механізму змінних середовища. Різниця в тому, де ще значення лишає слід: -e
дублює його в команді запуску (видно в Config.Cmd/аргументах та в історії шелла), тоді
як --env-file тримає значення виключно у виключеному з репозиторію файлі.
Контрольні питання
- Чому два контейнери в дефолтній bridge-мережі не резолвлять один одного за іменем, а в user-defined bridge — резолвлять?
- Що станеться з даними в томі (volume) після видалення обох контейнерів, які його монтували?
- Чим том (volume) відрізняється від bind mount з погляду керування Docker?
- Чому передавати секрет як аргумент команди в Dockerfile (
ARG/ENVіз хардкодом) небезпечніше, ніж через--env-fileпід час запуску? - Як мережева ізоляція (
--network mission-net) захищає обмін даними між сервісом і сховищем від зовнішнього доступу? - Що покаже
docker inspect agentщодо переданих через-eзмінних середовища? - Чому DNS-резолвінг за іменем контейнера важливіший, ніж здається, якщо контейнер може бути перезапущений з новою IP-адресою?
- У чому принципова межа приховування секрету через
--env-fileпорівняно зdocker secretу режимі Swarm?
Парні теми СРС
- Рівень 2: srs05 — «Мережі Docker (bridge/none/host) і службове DNS між контейнерами».
- Рівень 3: srs06 — «Керування секретами: env проти файлів проти docker secrets».
Критерії оцінювання та форма звіту
Здача відбувається тегом submit/lab03. Звіт REPORT.md містить: вивід резолвінгу
agent → storage за іменем, вміст файлу зі спільного тому, записаного одним
контейнером і прочитаного іншим, фрагмент docker inspect agent (зі значенням секрету,
видаленим уручну зі звіту, якщо воно там присутнє в незашифрованому вигляді), і відповіді
на контрольні питання.
Автотести (рівень 1) перевіряють: обидва контейнери піднято, приєднано до спільної
user-defined мережі, і контейнер-сервіс успішно резолвить та опитує контейнер-сховище за
іменем (без явного IP). Захист (рівень 2) — усне пояснення механізму DNS-резолвінгу в
мережі Docker і демонстрація запису/читання даних через спільний том обома контейнерами.
Розширення (рівень 3) перевіряється автотестами на відсутність секретного значення у
виводі docker inspect (у полі команди запуску чи аргументів) і коректну доступність
значення змінної всередині контейнера під час виконання.