Лабораторна робота № 1. Контейнер розробника й огляд просторів імен
Мета роботи
Навчитися запускати ізольований Docker-контейнер розробника, оглядати простори імен процесів усередині нього, свідомо прокидати порти для доступу з хоста та автоматизувати розгортання середовища ідемпотентним bash-скриптом. Робота формує базове розуміння механізму контейнерної ізоляції в Linux, на якому будуються всі наступні лабораторні першого модуля.
Теоретичні відомості
1. Контейнер як процес, а не як віртуальна машина
Контейнер Docker принципово відрізняється від віртуальної машини. Гіпервізор (VMware, KVM, VirtualBox) виконує окрему копію ядра гостьової ОС поверх емульованого або паравіртуалізованого обладнання — це важка, але повна ізоляція на рівні заліза. Контейнер натомість — це звичайний процес хостового ядра Linux, якому надано підмінену видимість системних ресурсів через простори імен (namespaces). Ядро одне на всіх контейнерів хоста; ізолюється не апаратне середовище, а те, що процес бачить про систему. Наслідок — контейнери запускаються за мілісекунди й мають накладні витрати, близькі до нуля, тоді як віртуальна машина завантажується секундами-хвилинами і тримає постійні витрати на емуляцію.
2. Простори імен Linux
Простір імен — це механізм ядра, що дозволяє групі процесів мати власну, ізольовану копію певного глобального системного ресурсу. Docker за замовчуванням створює для контейнера шість просторів імен:
| Простір | Що ізолює | Наслідок для контейнера |
|---|---|---|
| PID | Дерево процесів | Перший процес контейнера має PID 1; процеси хоста невидимі зсередини |
| NET | Мережевий стек | Власні інтерфейси, таблиця маршрутизації, localhost; порти хоста й контейнера не перетинаються без явного проброшування |
| MNT | Дерево монтувань | Контейнер бачить лише свою файлову систему (шари образу + томи), а не файлову систему хоста |
| UTS | Хостнейм і доменне ім'я NIS | hostname усередині контейнера відрізняється від хоста |
| IPC | Об'єкти міжпроцесної взаємодії (семафори, черги повідомлень, спільна пам'ять) | Контейнер не бачить і не втручається в IPC-об'єкти хоста |
| USER | Відображення UID/GID | UID 0 усередині контейнера може відповідати непривілейованому UID на хості |
Технічно кожен простір імен створюється системним викликом clone(2) з відповідним
прапорцем (CLONE_NEWPID, CLONE_NEWNET, CLONE_NEWNS, CLONE_NEWUTS, CLONE_NEWIPC,
CLONE_NEWUSER) або пізніше приєднується процесом через unshare(2)/setns(2). Це
документується в namespaces(7). Docker (через containerd і runc) виконує ці виклики
за розробника автоматично при docker run.
Важливо для розуміння асиметрії ізоляції. Простори імен ховають хост від контейнера і
контейнер від сторонніх процесів (та інших контейнерів), але не ховають контейнер від
хоста: із хоста звичайний ps aux покаже процес контейнера як рядок зі своїм, хостовим
PID — просто це інший номер, ніж той, що бачить сам процес зсередини свого PID-простору.
Ізоляція — не невидимість ззовні, а підміна картини світу зсередини.
3. Проброс портів (-p) як правило NAT
Оскільки контейнер має власний NET-простір імен, служба, що слухає порт усередині нього,
за замовчуванням недосяжна ні з хоста, ні з зовнішньої мережі. Прапорець
docker run -p <host_port>:<container_port> створює правило трансляції мережевих адрес
(NAT) на рівні мережевого простору імен, у якому працює Docker-демон: вхідний трафік на
<host_port> хоста перенаправляється на <container_port> усередині мережевого простору
контейнера. Без цього правила порт лишається закритим ззовні незалежно від того, що
відбувається всередині контейнера.
4. Автоматизація розгортання
Ручний набір docker run з десятком прапорців щоразу — джерело помилок і невідтворюваних
середовищ. Фіксація точного набору команд у bash-скрипті — мінімальна практична форма
принципу «інфраструктура як код»: середовище описується і відтворюється однаково на будь-
якій машині. Ключова вимога до такого скрипту — ідемпотентність: повторний запуск без
попереднього ручного прибирання не повинен завершуватися помилкою (типовий кандидат —
docker: Error response from daemon: Conflict. The container name "..." is already in use). Ідемпотентність досягається перевіркою наявності ресурсу перед створенням або
примусовим видаленням старого екземпляра (docker rm -f) на початку скрипту.
Обладнання та програмне забезпечення
- GitHub Codespaces або локальний devcontainer із попередньо встановленим Docker Engine.
- Термінал bash.
- Утиліти для огляду процесів і мереж:
ps,lsns,iproute2(ip), доступ до/proc. - Базовий образ
ubuntu:24.04(дозволено інший дистрибутив Ubuntu/Debian, якщо не вказано інше вstarter/README.md).
Порядок виконання
- Перевірити доступність Docker Engine:
docker version. Очікуваний результат: блокиClient:іServer:виводяться без помилок з'єднання з демоном. - Запустити базовий контейнер розробника в інтерактивному режимі:
docker run -it --name devbox ubuntu:24.04 bash. Очікуваний результат: інтерактивне запрошення командного рядка всередині контейнера. - Усередині контейнера виконати
ps auxтаecho $$. Очікуваний результат: видно лише кілька процесів самого контейнера; PID оболонки —1. - У другому терміналі на хості виконати
ps aux | wc -lі порівняти з кількістю, побаченою в контейнері. Очікуваний результат: на хості кількість процесів суттєво більша. - Зупинити й прибрати перший контейнер (
docker rm -f devbox), запустити новий із проброшеним портом:docker run -it -p 8080:80 --name devbox2 ubuntu:24.04 bash, усередині встановити й запустити простий HTTP-сервер (apt-get install -y python3 && python3 -m http.server 80). Очікуваний результат: з хостаcurl -i http://localhost:8080/повертає200 OK. - Повторити крок 5 без прапорця
-pна новому контейнері (інше ім'я) і переконатися, що звернення з хоста завершується помилкою з'єднання. Очікуваний результат:curlповертаєConnection refused— доказ, що порт контейнера недоступний без явного проброшування. - Оформити всі кроки 2–6 у вигляді єдиного bash-скрипту
scripts/deploy.sh(#!/usr/bin/env bash,set -euo pipefail), який розгортає середовище без ручного втручання і коректно поводиться при повторному запуску (ідемпотентність). Очікуваний результат: два послідовні запуски скрипту завершуються кодом0, без помилок про зайняте ім'я контейнера.
Завдання за рівнями
Рівень 1 (оцінка 3, автотести). Запустити базовий контейнер розробника й оглянути
простори імен — переконатися, що процеси хоста приховані від контейнера, а PID
оболонки всередині дорівнює 1. Відповідає крокам 1–4 порядку виконання.
Рівень 2 (оцінка 4, захист). Прокинути порт для доступу з хоста до сервісу всередині контейнера. На захисті — усно пояснити механізм NAT-переадресації та різницю мережевих просторів контейнера й хоста, наживо продемонструвати робочий проброшений порт. Відповідає крокам 5–6 порядку виконання.
Рівень 3 (оцінка 5, розширення). Написати автоматизований, ідемпотентний
bash-скрипт scripts/deploy.sh, що розгортає середовище з усіма потрібними утилітами
без ручного втручання. Відповідає кроку 7 порядку виконання.
Розібраний приклад
Приклад 1. Огляд PID-простору зсередини й ззовні
# Хост, термінал 1
$ ps aux | wc -l
187
$ docker run -it --name probe ubuntu:24.04 bash
root@9f1c2e7b0a4d:/# echo $$
1
root@9f1c2e7b0a4d:/# ps aux
USER PID %CPU %MEM COMMAND
root 1 0.0 0.1 bash
root 9 0.0 0.1 ps aux
Усередині контейнера видно лише два процеси — власний PID-простір починається з чистого
аркуша. З іншого терміналу хоста той самий процес контейнера видно як звичайний рядок
ps aux під іншим, реальним PID хоста (наприклад, 4821). Це ілюструє асиметрію
ізоляції, описану в теоретичних відомостях: підміна видимості діє лише зсередини простору
імен.
Приклад 2. Проброс порту та перевірка ззовні
$ docker run -d -p 8080:80 --name web python:3.12-slim python3 -m http.server 80
$ curl -i http://localhost:8080/
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.12.4
...
-d тримає контейнер у фоновому режимі; -p 8080:80 створює правило NAT хост:8080 →
контейнер:80. Якщо повторити запуск без -p, ідентичний curl поверне помилку з'єднання —
переконливий доказ того, що проброс порту є явною, окремою дією, а не побічним ефектом
запуску контейнера.
Контрольні питання
- Чому процес усередині контейнера бачить себе як
PID 1, хоча на хості в нього інший, реальний PID? - Який системний виклик і які прапорці лежать в основі створення нового простору імен для процесу?
- Що станеться, якщо звернутися до
http://localhost:8080з хоста, а контейнер запущено без прапорця-p? - Чим прапорець
-p 8080:80відрізняється від-p 80:80з погляду безпеки локальної машини? - Які саме простори імен активуються за замовчуванням у
docker runбез додаткових прапорців — перелічити всі шість і пояснити, що кожен ізолює? - Чому автоматизація розгортання bash-скриптом надійніша за ручний набір команд у терміналі щоразу?
- Що покаже
ps auxна хості для процесу, запущеного всередині контейнера? - Чому для ідемпотентності скрипту потрібно прибирати або перевикористовувати
контейнер із заданим іменем перед повторним
docker run?
Парні теми СРС
- Рівень 2: srs01 — «Простори імен Linux (PID, NET, MNT, UTS, IPC, USER): що ізолює кожен».
- Рівень 3: srs02 — «Групи керування cgroups v2: обмеження CPU та пам'яті контейнера».
Критерії оцінювання та форма звіту
Здача відбувається тегом submit/lab01 у навчальному репозиторії, разом зі скрипт-файлом
scripts/deploy.sh і звітом REPORT.md, що містить вивід команд кожного кроку та
відповіді на контрольні питання.
Автотести (рівень 1) перевіряють: контейнер успішно запускається; усередині PID
оболонки дорівнює 1; у виводі ps aux контейнера відсутні процеси хоста; контейнер
коректно прибирається після перевірки.
Захист (рівень 2) — усне пояснення різниці між мережевим простором контейнера і хоста та жива демонстрація робочого проброшеного порту (значення порту може відрізнятися від заданого у звіті — важливо продемонструвати розуміння, а не завчений результат).
Розширення (рівень 3) перевіряється автотестами на ідемпотентність і повноту
bash-скрипту: два послідовні запуски scripts/deploy.sh не повинні завершуватися
помилкою через уже наявний контейнер, а сам скрипт має встановлювати всі утиліти,
перелічені в starter/README.md (curl, git, procps, iproute2).