Съдържание на курса
Урок 0 — Какво е контейнер и какво НЕ е
От Dorian Chávez · основател и главен изпълнителен директор на Hábil ·
Два и четиринайсет през нощта е и алармата звъни: тестовият сървър е спрял да отговаря. Някак успяваш да влезеш и първото, което виждаш, е, че базата данни, която живееше на същата машина, вече не работи. Системният журнал казва, че ядрото я е убило заради липса на памет. Някой от екипа пише в чата, убеден: „Не може да е revisor, той работи в контейнер; изолиран е, като отделна машина“. Десет минути по-късно се появява виновникът: контейнерът на revisor, без никакво ограничение, беше изял паметта на цялата машина, докато ядрото трябваше да избира кого да убие, и избра базата данни. Никой не излъга. Просто този човек имаше погрешна представа какво е контейнер и тя струваше една нощ. Този урок съществува, за да нямаш ти такава.
Време: 90 мин
Какво изграждаш: засега нищо. Това е урок за четене: тук се изгражда мисловната карта, с която останалата част от курса става предвидима. Съдържа девет кратки програми, за да видиш със собствените си очи онова, което урокът твърди.
Какво научаваш: че контейнерът е изолиран процес, а не виртуална машина; с кои части на ядрото на Linux се изгражда тази изолация (пространства от имена и контролни групи); разликата между образ и контейнер; че Docker са две програми, клиент и демон; и какво споделя един контейнер с хоста и защо от това зависи сигурността.
Откъде идваш. Не носиш нищо от този курс: още не си инсталирал Docker. Носиш обаче онова, което курсът приема за известно: работиш уверено с терминал, знаеш какво е процес (програма, която работи в момента) и какво е HTTP сървър. Може би вече си писал revisor в курса по Go, Rust или TypeScript на Hábil. Ако не, достатъчно е това: програма, която пита наведнъж списък от услуги и връща отчет кои работят, кои са бавни и кои са недостъпни. Което вероятно не носиш, е точна представа какво е контейнерът отвътре, а тъкмо тя ще ти трябва всеки път, когато нещо се счупи.
Какво не прави този урок. Не инсталира нищо (това е Урок 1) и не преподава командите на Docker една по една (това започва в Урок 2). Програмите в този урок са скриптове на bash от десет до тридесет реда, които извикват docker и обикновени инструменти на Linux като ps и uname. Не е нужно още сега да разбираш всяка опция, макар че всяка е обяснена: важното е какво отпечатват. Изпълняват се след Урок 1, когато вече имаш Docker.
За изпълненията. Изходите в този урок са получени на 21 октомври 2026 г. на Mac с Colima, която стартира виртуална машина с Linux, за да могат да се пускат контейнери. Тази виртуална машина е с Ubuntu 24.04.4, същата основа като Linux Mint 22, така че скриптовете са изпълнени вътре в нея и именно там „хостът“ се появява в изходите: в твоя Mint хостът е самият ти компютър и същите команди се пишат по същия начин, след като минеш Урок 1; променят се стойностите, присъщи на твоя компютър, които са изброени по-долу. Версията на Docker във виртуалната машина беше 29.2.1 (клиент и демон). Курсът беше планиран с 29.8.2, а в деня на това изпълнение най-новата публикувана беше 29.9.2, от 20 октомври 2026 г., според официалните бележки към версиите. Бележките към версиите от 29.2.1 до 29.9.2 не отбелязват промени в capabilities по подразбиране, във формата на docker version, нито в docker context inspect, а това са командите и стойностите, от които зависи този урок, така че това, което виждаш тук, би трябвало да съвпада с твоето; ако някоя по-късна версия промени нещо, важи казаното в документацията на твоята версия. Урок 1 те учи как да видиш коя е. Което се променя от едно изпълнение до друго, и се казва там, където се случва: номерата на процесите (PID), номерата на пространствата от имена, времената и, ако компютърът ти е с процесор Intel или AMD, думата arm64, която при теб ще гласи amd64. Програмите са в programas/00-que-es-un-contenedor/, в хранилището на курса, всяка със запазения си изход до нея.
След урока ще можеш да
- Покажеш с
ps, че контейнер, който работи, е още един процес на хоста, и да намериш номера на процеса му отвън. - Обясниш с пример защо контейнерът не е виртуална машина, като използваш думата ядро.
- Назовеш пространствата от имена, които Docker изолира, и двете, които по подразбиране не изолира.
- Прочетеш в
/sys/fs/cgroupограниченията на контейнера за памет, процесор и процеси и да кажеш какво става, когато няма никакво. - Различаваш образ от контейнер и да предвидиш какво се случва с файл, записан вътре в контейнер, когато този контейнер бъде изтрит.
- Обясниш какво правят клиентът
dockerи демонътdockerdи как си говорят. - Кажеш какво споделя един контейнер с хоста и защо това превръща „root вътре в контейнера“ във въпрос на сигурност.
Защо, преди как
Представи си, че си довършил revisor и някой те моли да го пуснеш на сървър. Най-прякото е да копираш изпълнимия файл и да го стартираш. Работи, докато не спре да работи: сървърът има друга версия на някаква библиотека, или порт 8080 вече се ползва от друга програма, или revisor има грешка, която го кара да трупа памет без край и повлича със себе си всичко останало, което работи там. А когато накрая откриеш, че на твоята машина е работило, фразата „на моята машина работи“ вече не върши работа никому.
Контейнерите се родиха, за да решат точно това: да доставят програмата заедно с всичко, от което се нуждае, за да работи, и да я пускат отделно от останалите, с ограничения. Това е вярно и много полезно. Проблемът започва, когато това описание се превърне в погрешна мисловна картина: „контейнерът е малък компютър вътре в моя“. С тази картина сцената от началото е невъзможна: как би могъл един малък компютър да убие програма на големия? С правилната картина сцената е очевидна.
Правилната картина е тази и тя е същата, която използва самата документация на Docker: контейнерът е изолиран процес (Docker, „What is a container?“). Не е друг компютър. Той е програма, която работи на твоя компютър, върху твоето ядро на Linux, на която ядрото слага стени (за да не вижда онова, което не бива) и бюджет (за да не харчи онова, което не бива). Ако стените са добре поставени, процесът вярва, че е сам. Ако няма бюджет, може да изхарчи всичко.
Урокът има седем части, в този ред:
- Контейнерът е процес (0.1): гледаш го отвътре и отвън.
- Не е виртуална машина (0.2): споделеното ядро и какво означава това.
- Стените: пространства от имена (0.3).
- Бюджетът: контролни групи (0.4), с грешката на този урок.
- Образ срещу контейнер (0.5).
- Клиентът и демонът (0.6).
- Онова, което се споделя, и сигурността (0.7), и малко история, за да се ориентираш (0.8).
С тази цел пред очи, ето какво ще видиш в раздел 0.4, когато един контейнер с ограничение на паметта се опита да го надхвърли. Това е онова, което е липсвало на контейнера от сцената:
voy a comer memoria...
código de salida: 137
== Lo que Docker anotó
OOMKilled=true ExitCode=137
Понятията
0.1 Контейнерът е процес
0.1.1 Да го погледнеш отвътре и отвън
Най-бързият начин да се отървеш от идеята за „малкия компютър“ е да стартираш контейнер и да го потърсиш с обичайните инструменти. Програмата fig00_01.sh стартира контейнер, който не прави нищо друго, освен да спи пет минути (sleep 300), гледа го отвътре, гледа го отвън и го изтрива накрая:
#!/usr/bin/env bash
# fig00_01.sh — Un contenedor es un proceso del equipo anfitrión.
# Arranca un contenedor que solo duerme, lo mira desde dentro y desde fuera,
# y lo borra al final.
set -euo pipefail
IMAGEN=alpine:3.24.2
NOMBRE=curso-docker-dormilon
trap 'docker rm -f "$NOMBRE" >/dev/null 2>&1 || true' EXIT
docker run -d --name "$NOMBRE" "$IMAGEN" sleep 300 >/dev/null
echo "== 1. Desde DENTRO del contenedor: todos sus procesos"
docker exec "$NOMBRE" ps -o pid,user,args
echo
echo "== 2. Desde FUERA: el mismo proceso, visto por Docker"
PID_ANFITRION=$(docker inspect --format '{{.State.Pid}}' "$NOMBRE")
echo "PID en el anfitrión: $PID_ANFITRION"
echo
echo "== 3. Desde FUERA: el mismo proceso, visto por el sistema operativo"
ps -o pid,ppid,user,args -p "$PID_ANFITRION"
echo
echo "== 4. Quién es su padre en el anfitrión"
PPID_ANFITRION=$(ps -o ppid= -p "$PID_ANFITRION" | tr -d ' ')
ps -o pid,user,comm -p "$PPID_ANFITRION"
$ ./fig00_01.sh
== 1. Desde DENTRO del contenedor: todos sus procesos
PID USER COMMAND
1 root sleep 300
7 root ps -o pid,user,args
== 2. Desde FUERA: el mismo proceso, visto por Docker
PID en el anfitrión: 836426
== 3. Desde FUERA: el mismo proceso, visto por el sistema operativo
PID PPID USER COMMAND
836426 836402 root sleep 300
== 4. Quién es su padre en el anfitrión
PID USER COMMAND
836402 root containerd-shim
Числата 836426 и 836402 се менят при всяко изпълнение; всичко останало излиза същото. Преди да прочетеш изхода, редовете от скрипта, които имат значение. set -euo pipefail кара скрипта да спре при първата грешка, вместо да продължава, сякаш нищо не е станало. trap ... EXIT гарантира, че при излизане, каквото и да е станало, контейнерът ще бъде изтрит: така не остава боклук на машината ти, навик, който Урок 2 превръща в правило. docker run -d стартира контейнера „на заден план“ (detached) и му дава име с --name; всички имена в този курс започват с curso-docker-, за да знаеш с един поглед кое е твое. docker exec изпълнява команда вътре в контейнер, който вече работи, а docker inspect пита за данни на един контейнер; тук, за номера на процеса му в хоста.
Сега изходът, блок по блок:
- Отвътре контейнерът вижда само два процеса:
sleep 300, който има номер 1, иps, който току-що стартира. Не вижда нито браузъра ти, нито терминала ти, нищо от онова, което работи на машината ти. От негова гледна точка той е първият процес в системата. - Отвън Docker ти казва, че същият този процес има номер 836426 в хоста.
- И операционната система на хоста го потвърждава с обикновен
ps, същия, който би използвал за всяка друга програма: ето гоsleep 300, работи катоroot. Няма друг компютър. Има още един процес в списъка на машината ти. - Родителят му е
containerd-shim, малка програма, която Docker слага да се грижи за всеки контейнер, така че контейнерът да не зависи от това демонът да продължава да работи, за да работи и той. (Пълното име еcontainerd-shim-runc-v2;psго съкращава, защото полетоcommпази само 15 знака.containerdиruncса разгледани в 0.6 и 0.8.)
🔑 Прочети това още веднъж: един и същ процес има два номера. Отвътре е 1; отвън е 836426. Не са два процеса: един е, гледан от две места. Това, че има два номера, е първата следа как работи изолацията, а раздел 0.3 я обяснява.
0.1.2 Какво означава, че е процес
Това, че контейнерът е процес, не е технически детайл; има последствия, които ще усещаш през целия курс:
- Стартира за милисекунди, защото стартирането на процес е бързо. Няма операционна система, която да се пуска.
- Умира като процес. Ако основната програма приключи, контейнерът приключва. Ако ядрото я убие, контейнерът умира. В Уроци 3 и 7 ще видиш, че това включва сигналите:
docker stopизпраща на този процес с номер 1 същия сигнал, който би изпратил сkill. - Харчи като процес. Използва паметта и процесора на машината ти, от общия запас заедно с всички останали програми. Ако никой не му сложи ограничение, няма ограничение.
- Виждаш го с обичайните си инструменти.
ps,topи системният монитор го показват. Това е полезно за диагностика и е предупреждение: който има власт над процесите на хоста, има власт над контейнерите.
0.2 Не е виртуална машина
0.2.1 Две дистрибуции, едно ядро
Виртуалната машина е програма, която имитира цял компютър: симулирани процесор, памет, диск и мрежова карта. Вътре в този симулиран компютър стартира цяла операционна система, със собствено ядро. Затова във виртуална машина можеш да пуснеш Windows върху Linux или Linux върху Mac.
Ядрото (kernel) е частта от операционната система, която командва хардуера: разпределя паметта, решава кой процес ползва процесора във всеки миг, говори с диска и мрежата. Всичко останало —shell, ls, пакетният мениджър, библиотеките— работи върху него и се обръща към него със заявки. Онова отгоре обикновено се нарича потребителско пространство (user space).
Контейнерът не носи ядро. Носи само потребителско пространство. Доказва го fig00_02.sh, който пита за дистрибуцията и версията на ядрото в хоста и в два контейнера от различни дистрибуции:
#!/usr/bin/env bash
# fig00_02.sh — Dos imágenes de dos distribuciones distintas, un solo núcleo.
set -euo pipefail
ALPINE=alpine:3.24.2
DEBIAN=debian:13.7-slim
distro() { grep '^PRETTY_NAME=' "$1" | cut -d= -f2 | tr -d '"'; }
echo "== El anfitrión"
echo "distribución: $(distro /etc/os-release)"
echo "núcleo: $(uname -r)"
for IMAGEN in "$ALPINE" "$DEBIAN"; do
echo
echo "== Un contenedor de $IMAGEN"
docker run --rm --name curso-docker-distro "$IMAGEN" sh -c '
echo "distribución: $(grep "^PRETTY_NAME=" /etc/os-release | cut -d= -f2 | tr -d "\"")"
echo "núcleo: $(uname -r)"'
done
$ ./fig00_02.sh
== El anfitrión
distribución: Ubuntu 24.04.4 LTS
núcleo: 6.8.0-100-generic
== Un contenedor de alpine:3.24.2
distribución: Alpine Linux v3.24
núcleo: 6.8.0-100-generic
== Un contenedor de debian:13.7-slim
distribución: Debian GNU/Linux 13 (trixie)
núcleo: 6.8.0-100-generic
При теб в Mint първият ред ще гласи Linux Mint 22..., а ядрото ще е това на твоята машина; важното е, че трите номера на ядро ще са еднакви помежду си. Файлът /etc/os-release е мястото, където всяка дистрибуция записва името си, а uname -r пита ядрото за версията му. Опцията --rm казва на Docker да изтрие контейнера веднага щом приключи.
Виж какво стана. Вътре в контейнера на Alpine файлът казва, че си в Alpine 3.24. Вътре в този на Debian — че си в Debian 13. Но когато питаш ядрото, и трите отговарят същото: 6.8.0-100-generic, това на хоста. Файловете са на Alpine или на Debian; ядрото е само едно, споделено. Точно това казва документацията: „ако пуснеш няколко контейнера, всички споделят едно и също ядро“ (Docker, „What is a container?“).
От тук идва правило, което си струва да запомниш: „образът на Debian“ не е Debian. Той е потребителското пространство на Debian —неговите програми, библиотеки, пакетен мениджър— което работи върху ядрото, което ти имаш. Почти винаги това стига, защото интерфейсът на ядрото на Linux се променя много малко между версиите. Но ако една програма се нуждае от функция на по-ново ядро от твоето, образът няма да ѝ я донесе.
0.2.2 Доказателството наобратно: защо Mac има нужда от виртуална машина
Ако един контейнер на Linux има нужда от ядро на Linux, какво става на компютър, който няма такова? Става каквото видя в „За изпълненията“: трябва да му се осигури. В macOS Colima стартира виртуална машина с Linux и вътре в нея работи Docker. Docker Desktop, настолното приложение на Docker, прави същото със своя виртуална машина: според документацията му „демонът на Docker и контейнерите работят във лека виртуална машина с Linux, управлявана от Docker“ (Docker Desktop, разрешения в Mac).
Тази виртуална машина не е случайност; тя е потвърждение на всичко дотук. Ако контейнерите носеха собствено ядро, нямаше да им трябва. Твоят Mint не я иска: ядрото на Linux вече е там и контейнерите работят направо върху него. Това е една от причините курсът да използва Linux като референтна система: това, което виждаш, е това, което има, без междинен слой.
0.2.3 Пълното сравнение
| Виртуална машина | Контейнер | |
|---|---|---|
| Какво е | компютър, имитиран от софтуер | процес със стени и бюджет |
| Ядро | собствено | на хоста, споделено |
| Какво стартира | цяла операционна система | една програма |
| Време за стартиране | секунди до минути | милисекунди |
| Може ли да пуска друга операционна система | да (например Windows върху Linux) | не: контейнер на Linux има нужда от ядро на Linux |
| Какво я отделя от хоста | хипервайзорът, който имитира хардуер | ядрото, което поставя стените |
| Ако нещо излезе от изолацията | стига до имитираната машина | стига до ядрото, което споделят всички |
Последният ред е този, който има значение за сигурността, и ще го поемем отново в 0.7.
За времето за стартиране не е нужно да вярваш: измерва се. fig00_09.sh създава, пуска и изтрива един контейнер, който не прави нищо (true е програма, която приключва веднага), пет пъти поред:
#!/usr/bin/env bash
# fig00_09.sh — Cuánto tarda en nacer, trabajar y morir un contenedor.
# Cinco veces seguidas, con una imagen que ya está descargada.
set -euo pipefail
IMAGEN=alpine:3.24.2
for i in 1 2 3 4 5; do
inicio=$(date +%s%N)
docker run --rm --name curso-docker-rapido "$IMAGEN" true
fin=$(date +%s%N)
echo "corrida $i: $(( (fin - inicio) / 1000000 )) ms"
done
$ ./fig00_09.sh
corrida 1: 136 ms
corrida 2: 154 ms
corrida 3: 431 ms
corrida 4: 347 ms
corrida 5: 507 ms
Тези числа винаги се менят: зависят от машината ти и от това какво друго прави в този момент. При друго изпълнение на същата машина излязоха между 135 и 194 ms. Стабилен е редът на големината: десети от секундата за създаването на контейнера, стартирането на процеса, изчакването му и изтриването на всичко. Една виртуална машина губи толкова само докато започне да зарежда ядрото си.
0.3 Стените: пространства от имена
0.3.1 Какво е пространство от имена
Да се върнем към следата от 0.1: един и същ процес е номер 1 отвътре и 836426 отвън. Как може един процес да има два номера? Защото ядрото на Linux има функция, наречена пространства от имена (namespaces). Пространството от имена е частен „изглед“ на някакъв ресурс на системата. Процесите, които са вътре в едно пространство от имена, виждат своя собствена версия на този ресурс и не виждат тази на останалите.
Docker го казва без заобикалки: използва „технология, наречена namespaces, за да даде изолираното работно пространство, наречено контейнер“ (Docker, „What is Docker?“). Има осем вида и всеки изолира различно нещо. Списъкът идва от наръчника на Linux (namespaces(7)):
| Вид | Име в /proc |
Какво изолира | Какво се вижда в контейнер |
|---|---|---|---|
| PID | pid |
номерата на процесите | програмата му е номер 1 и не вижда процесите на хоста |
| Mount | mnt |
точките на монтиране | вижда собственото си дърво от файлове, това на образа, а не твоето |
| Network | net |
интерфейси, адреси, портове | има собствена мрежа и собствен порт 8080 |
| UTS | uts |
името на машината | има собствено hostname |
| IPC | ipc |
комуникацията между процеси чрез споделена памет | не се пресича с тази на други програми |
| Cgroup | cgroup |
корена на контролните групи | вижда собствената си група, сякаш е коренът (0.4) |
| User | user |
номерата на потребители и групи | по подразбиране не се изолира; може да се включи (userns-remap, rootless; виж по-долу) |
| Time | time |
някои системни часовници | по подразбиране не се изолира; справочникът на docker run не изброява никаква опция за изолирането му |
Редът за Network обяснява нещо, което ще използваш от Урок 2: два контейнера могат да слушат на порт 8080 едновременно, без да си пречат, защото всеки има собствена мрежа. Редът за Mount обяснява защо контейнерът на Debian виждаше файлове на Debian, макар машината ти да е Mint.
0.3.2 Да го видиш със собствените си очи
Всеки процес на Linux има папка /proc/<номер>/ns/ с по една връзка за всеки вид пространство от имена. Онова, което съдържа тази връзка, е видът и едно число, например pid:[4026531836]. Наръчникът го обяснява така: ако два процеса са в едно и също пространство от имена, това число е същото (namespaces(7)). Значи е достатъчно да сравниш числата. Това прави fig00_03.sh, който чете осемте връзки на хоста и на един контейнер и ги поставя една до друга:
#!/usr/bin/env bash
# fig00_03.sh — Los espacios de nombres (namespaces) del anfitrión y de un contenedor.
# Si el número entre corchetes coincide, los dos procesos comparten ese espacio.
set -euo pipefail
IMAGEN=alpine:3.24.2
TIPOS="cgroup ipc mnt net pid time user uts"
LISTA='for n in '"$TIPOS"'; do echo "$n $(readlink /proc/self/ns/$n)"; done'
ANFITRION=$(sh -c "$LISTA")
CONTENEDOR=$(docker run --rm --name curso-docker-ns "$IMAGEN" sh -c "$LISTA")
printf '%-7s %-20s %-20s %s\n' TIPO ANFITRION CONTENEDOR ¿IGUAL?
for n in $TIPOS; do
a=$(echo "$ANFITRION" | awk -v t="$n" '$1==t {print $2}')
c=$(echo "$CONTENEDOR" | awk -v t="$n" '$1==t {print $2}')
if [ "$a" = "$c" ]; then igual="sí, compartido"; else igual="no, aislado"; fi
printf '%-7s %-20s %-20s %s\n' "$n" "${a#*:}" "${c#*:}" "$igual"
done
$ ./fig00_03.sh
TIPO ANFITRION CONTENEDOR ¿IGUAL?
cgroup [4026531835] [4026532382] no, aislado
ipc [4026531839] [4026532379] no, aislado
mnt [4026531841] [4026532377] no, aislado
net [4026531840] [4026532383] no, aislado
pid [4026531836] [4026532380] no, aislado
time [4026531834] [4026531834] sí, compartido
user [4026531837] [4026531837] sí, compartido
uts [4026531838] [4026532378] no, aislado
Числата в квадратните скоби са различни на всяка машина, а тези на контейнера се менят при всяко изпълнение; колоната ¿IGUAL? излиза винаги същата, стига да използваш конфигурацията на Docker по подразбиране. /proc/self е пряк път, който винаги сочи към процеса, който го чете, а readlink показва накъде сочи една връзка.
Шест стени и две дупки. Контейнерът има собствени процеси, собствено дърво от файлове, собствена мрежа, собствено име на машината, собствена споделена памет и собствен изглед на контролните групи. Но споделя с хоста часовниците (time) и, преди всичко, потребителите (user). Това, че пространството на потребителите е същото, означава, че потребителски номер 0, root, е същият root вътре и отвън. Вече го видя в 0.1: sleep 300 се появяваше в хоста като на root. Docker поддържа и изолиране на потребителите, но не го прави по подразбиране (Docker Engine security). Всичко това важи за Docker в обичайния му режим, този, който инсталира Урок 1: демонът работи като root и без пренасочване на потребителите. Има два режима, които променят историята. С userns-remap root отвътре е в хоста висок потребителски номер без привилегии; в режим rootless демонът и контейнерите му работят вътре в потребителско пространство без привилегии. И в двата редът user от таблицата би гласил „не, изолиран“. Машината на тези изпълнения използва обичайния режим: docker info --format '{{.SecurityOptions}}' отпечата [name=apparmor name=seccomp,profile=builtin name=cgroupns], без rootless и без userns, и така можеш да провериш своя. Това е половината от историята за сигурността от 0.7.
0.4 Бюджетът: контролни групи
0.4.1 Какво е контролна група
Контролните групи (control groups, почти винаги съкращавани cgroups) са другата половина от работата. Те са механизъм на ядрото да подрежда процесите в групи и да разпределя между тези групи ресурсите на системата —памет, процесор, брой процеси, четене и писане на диск— по контролиран начин (документация на ядрото, cgroup v2). Ръководството за сигурност на Docker го обобщава в едно изречение: контролните групи „осъществяват отчитането и ограничаването на ресурсите“ (Docker Engine security).
Всеки контейнер живее в собствена група. Ядрото води сметка колко харчи тази група и, ако има ограничение, не я оставя да го надхвърли. Ограниченията могат да се четат като текстови файлове в /sys/fs/cgroup. Три от тях ще се появяват през целия курс:
memory.max: колко памет може да използва групата. Ако стигне това ограничение и не може да освободи нищо, ядрото вика „убиеца при липса на памет“ (OOM killer) вътре в тази група (cgroup v2).cpu.max: две числа, „колко време“ и „на какъв период“. По подразбиране еmax 100000, което значи без таван във всеки период от 100 000 микросекунди (една десета от секундата).pids.max: колко процеса най-много може да има групата.
0.4.2 Без ограничение няма ограничение
Най-важното в този раздел е онова, което Docker прави, ако не му поискаш нищо. Документацията е изрична: „по подразбиране контейнерът няма ограничения на ресурсите“ и може да използва всичко, което планировчикът на ядрото му позволи (Docker, „Resource constraints“). fig00_04.sh чете трите файла в контейнер без ограничения и в друг с ограничения:
#!/usr/bin/env bash
# fig00_04.sh — Los grupos de control (cgroups): lo que el núcleo deja gastar.
# Primero sin límites, después con límites de memoria, CPU y procesos.
set -euo pipefail
IMAGEN=alpine:3.24.2
LEER='for f in memory.max cpu.max pids.max; do printf "%-11s %s\n" "$f" "$(cat /sys/fs/cgroup/$f)"; done'
echo "== Sin límites (lo que Docker hace si no le pides nada)"
docker run --rm --name curso-docker-sin-limites "$IMAGEN" sh -c "$LEER"
echo
echo "== Con --memory 64m --cpus 0.5 --pids-limit 50"
docker run --rm --name curso-docker-con-limites \
--memory 64m --cpus 0.5 --pids-limit 50 \
"$IMAGEN" sh -c "$LEER"
$ ./fig00_04.sh
== Sin límites (lo que Docker hace si no le pides nada)
memory.max max
cpu.max max 100000
pids.max max
== Con --memory 64m --cpus 0.5 --pids-limit 50
memory.max 67108864
cpu.max 50000 100000
pids.max 50
Този изход е стабилен, при две условия. Първото е версия 2 на контролните групи, която има Ubuntu 24.04, основата на Mint 22 (машината на тези изпълнения я отчита: stat -fc %T /sys/fs/cgroup отпечатва cgroup2fs). Второто е да са включени контролерите memory, cpu и pids, които са частите на ядрото, водещи всяка сметка: във версия 2 се включват един по един и файл като memory.max се появява в една група само ако контролерът му е включен (документация на ядрото, cgroup v2). В обикновена инсталация на Mint са включени; за да провериш, cat /sys/fs/cgroup/cgroup.controllers трябва да съдържа тези три думи (на машината на тези изпълнения отпечата cpuset cpu io memory hugetlb pids rdma misc). Ако липсва някой, Docker може да го съобщи с WARNING в края на docker info (Docker, ограничения на ресурсите), съответният файл не съществува вътре в контейнера и изходът ти няма да прилича на този. Без ограничения трите файла казват max: контейнерът може да използва цялата памет на машината, целия процесор и да създаде колкото процеси иска. Такъв беше контейнерът от сцената.
С ограничения всяка опция на docker run се превръща в число, което ядрото налага:
--memory 64mстана67108864, което е 64 × 1024 × 1024 байта: точно 64 MiB.--cpus 0.5стана50000 100000: във всяка десета от секундата групата може да ползва процесора половината време, 50 000 микросекунди. Това е половин процесор.--pids-limit 50стана50.
Обърни внимание на веригата от отговорности. Ти пишеш опция в docker run; Docker я превежда и я записва във файл на ядрото; ядрото я прилага. Docker не наблюдава контейнера: наблюдава ядрото. Затова ограничението се спазва, дори програмата вътре да има грешки и да харчи памет или процеси без спирачка. Внимавай с това, което ограничението не обещава: то ограничава колко харчи групата, но не защитава нито нея, нито теб от атака, насочена към споделеното ядро; с това се занимават слоевете от 0.7.
0.4.3 Какво става, когато се достигне ограничението
Ограничение, което никога не се докосва, не учи на нищо. fig00_05.sh стартира контейнер с 32 MiB памет и без място за размяна (swap) и му иска нещо, което не може да изпълни: tail /dev/zero чете безкраен файл от нули в търсене на края на последния ред и, тъй като нова линия така и не идва, трупа всичко в паметта без спиране:
#!/usr/bin/env bash
# fig00_05.sh — El error de esta lección: el núcleo mata al proceso que pasa su límite.
# El contenedor intenta llenar la memoria; tiene un tope de 32 MiB y sin swap.
set -uo pipefail
IMAGEN=alpine:3.24.2
NOMBRE=curso-docker-gloton
trap 'docker rm -f "$NOMBRE" >/dev/null 2>&1 || true' EXIT
docker run --name "$NOMBRE" --memory 32m --memory-swap 32m \
"$IMAGEN" sh -c 'echo "voy a comer memoria..."; tail /dev/zero'
echo "código de salida: $?"
echo
echo "== Lo que Docker anotó"
docker inspect --format 'OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}}' "$NOMBRE"
$ ./fig00_05.sh
voy a comer memoria...
código de salida: 137
== Lo que Docker anotó
OOMKilled=true ExitCode=137
Този изход е стабилен. Тук не се използва --rm, нарочно: ако Docker изтрие контейнера при приключване, няма да можеш после да го попиташ какво му се е случило. --memory-swap 32m определя сбора от памет и размяна на същата стойност като паметта, тоест нула размяна; без това Docker позволява на контейнера да използва толкова размяна, колкото и памет (с --memory 32m — още 32 MiB, стига хостът да има конфигурирана размяна), така че процесът би оцелял по-дълго и OOM би дошъл по-късно (Docker, „Resource constraints“).
Контейнерът отпечата съобщението си, започна да яде памет и, щом стигна 32 MiB, ядрото го уби вътре в собствената му група. Ядрото не трябваше да избира друг: базата данни от сцената, ако беше там, нямаше да бъде избраната. Числото 137 е конвенцията за „този процес умря от сигнал“: това е 128 плюс номера на сигнала, а сигнал 9 е KILL, който не може да бъде уловен или пренебрегнат. Конвенцията не е измислена от Docker: това е тази на shell, чийто наръчник казва, че когато команда приключи от сигнал N, bash използва 128+N като код на изход (наръчник на Bash); можеш да го провериш във всеки bash: bash -c 'kill -9 $$'; echo $? отпечатва 137. Docker записва същото число в ExitCode, а docker run го връща такова, каквото е, защото всеки код, различен от 125, 126 и 127, е този на командата на контейнера (Docker, кодове на изход). А OOMKilled=true е Docker, който потвърждава, че е било от липса на памет. Разделът „Грешката, която ще видиш“ ти казва как да разчиташ това, когато се появи, без да си го предизвикал.
Сега можеш да разкажеш сцената отново, този път точно. Контейнерът на revisor беше в собствените си пространства от имена —не виждаше базата данни—, но в контролна група без ограничение —харчеше от общия запас—. Когато запасът се изчерпа, ядрото трябваше да убие някого в цялата машина, не само в групата на контейнера, и избра друг. С --memory убиецът би действал вътре в групата на revisor и би умрял само revisor. Урок 2 слага ограничения още от първия контейнер по тази причина.
0.5 Образ срещу контейнер
0.5.1 Шаблонът и живото копие
Досега използва две думи, сякаш са взаимозаменяеми, а не са. Документацията на Docker ги определя така: образът е „шаблон само за четене с инструкции за създаване на контейнер“, а контейнерът е „изпълним екземпляр на образ“ (Docker, „What is Docker?“).
Сравнение, което помага, ако идваш от програмирането: образът е като изпълним файл на диска, а контейнерът е като процеса, който работи от него. От един и същ изпълним файл можеш да пуснеш десет процеса; от един и същ образ можеш да създадеш десет контейнера. Ако процесът запише нещо в паметта си, изпълнимият файл на диска не се променя.
Частта, която липсва, е къде пише един контейнер, когато създава файл. Отговорът според документацията за съхранението на Docker: образът е направен от слоеве само за четене и когато се създава контейнер, върху тях се добавя тънък слой, в който може да се пише, наречен слой на контейнера. Всичко, което контейнерът променя, отива в този слой. Няколко контейнера могат да споделят един и същ образ, всеки със свой собствен слой. А когато контейнерът бъде изтрит, слоят му се изтрива с него, докато образът отдолу „остава непроменен“ (Docker, „Storage drivers“).
0.5.2 Да го видиш със собствените си очи
fig00_06.sh създава два контейнера от един и същ образ, записва файл в единия, проверява дали другият го вижда, пита какво се е променило във всеки и накрая изтрива първия:
#!/usr/bin/env bash
# fig00_06.sh — Imagen contra contenedor: una plantilla, dos copias vivas.
set -euo pipefail
IMAGEN=alpine:3.24.2
A=curso-docker-uno
B=curso-docker-dos
trap 'docker rm -f "$A" "$B" >/dev/null 2>&1 || true' EXIT
echo "== La imagen antes de empezar"
docker image inspect --format 'id={{.Id}}' "$IMAGEN" | cut -c1-22
docker run -d --name "$A" "$IMAGEN" sleep 300 >/dev/null
docker run -d --name "$B" "$IMAGEN" sleep 300 >/dev/null
echo
echo "== Escribo un archivo en el contenedor uno"
docker exec "$A" sh -c 'echo "hola desde uno" > /tmp/nota.txt && cat /tmp/nota.txt'
echo
echo "== ¿Lo ve el contenedor dos?"
docker exec "$B" cat /tmp/nota.txt || echo "(el contenedor dos no tiene ese archivo)"
echo
echo "== Lo que cambió en cada contenedor respecto de su imagen (docker diff)"
echo "-- uno:"; docker diff "$A"
echo "-- dos:"; docker diff "$B"; echo "(nada)"
echo
echo "== Borro el contenedor uno y miro la imagen otra vez"
docker rm -f "$A" >/dev/null
docker image inspect --format 'id={{.Id}}' "$IMAGEN" | cut -c1-22
docker run --rm --name curso-docker-tres "$IMAGEN" sh -c 'cat /tmp/nota.txt 2>&1 || true'
$ ./fig00_06.sh
== La imagen antes de empezar
id=sha256:294b683cb724
== Escribo un archivo en el contenedor uno
hola desde uno
== ¿Lo ve el contenedor dos?
cat: can't open '/tmp/nota.txt': No such file or directory
(el contenedor dos no tiene ese archivo)
== Lo que cambió en cada contenedor respecto de su imagen (docker diff)
-- uno:
C /tmp
A /tmp/nota.txt
-- dos:
(nada)
== Borro el contenedor uno y miro la imagen otra vez
id=sha256:294b683cb724
cat: can't open '/tmp/nota.txt': No such file or directory
Идентификаторът 294b683cb724 е този на образа alpine:3.24.2 за 64-битови ARM процесори; на машина с Intel или AMD ще е друг, но ще е същият в началото и в края на изпълнението, което е важното. docker diff показва какво е променил един контейнер спрямо образа си: A значи добавено (added), а C — променено (changed); папката /tmp се появява като променена, защото сега съдържа нещо ново.
Чети изхода като история от четири момента:
- Образът има идентификатор, отпечатък на съдържанието си.
- Контейнер едно записва
/tmp/nota.txtи го чете без проблем. Контейнер две, направен от същия образ, го няма: всеки пише в собствения си слой. docker diffго потвърждава: първият е добавил файл; вторият не е променил нищо.- Контейнер едно се изтрива. Образът има същия идентификатор: нищо от това, което контейнерът е направил, не го е докоснало. И нов контейнер от този образ също няма бележката. Файлът си отиде с контейнера.
Последствията за останалата част от курса са две и и двете са важни. Първото: онова, което се записва вътре в контейнер, е за изхвърляне. Ако revisor запази отчета си вътре в контейнера и някой го изтрие, отчетът си отива. Затова съществува Урок 8, с томовете. Второто: образът не се поправя, като влезеш в контейнера. Ако влезеш с docker exec да поправиш нещо, поправяш това живо копие, не шаблона; следващият контейнер се ражда без твоята поправка. Образите се променят, като се изграждат наново от рецептата им, Dockerfile, което учиш в Урок 3.
0.6 Клиентът и демонът
0.6.1 Две програми, не една
Когато напишеш docker run, изглежда, че една програма върши всичко. Не е така. Docker използва архитектура клиент-сървър: програмата docker, която пишеш, е само клиент, а истинската работа я върши един демон на име dockerd, програма, която работи на заден план през цялото време. Според документацията демонът „слуша заявките към API на Docker и управлява обектите на Docker“ —образи, контейнери, мрежи и томове—, а клиентът и демонът си говорят „чрез REST API, по сокети на UNIX или мрежов интерфейс“ (Docker, „What is Docker?“).
Сокетът на UNIX е специален файл, който служи на две програми на една и съща машина да си разменят съобщения. Този на демона на Docker е по подразбиране в /var/run/docker.sock. А вече видя на кого се пада да създава контейнерите: containerd-shim, който се появи като родител на sleep в 0.1, е част от веригата, която демонът задвижва. Ти питаш клиента; клиентът пита демона; демонът, с помощта на други части, иска от ядрото пространствата от имена и контролните групи.
fig00_07.sh показва двете половини и какво става, когато клиентът не намира демона:
#!/usr/bin/env bash
# fig00_07.sh — El cliente y el daemon son dos programas distintos.
set -uo pipefail
echo "== Las dos versiones: la del cliente y la del daemon"
docker version --format 'cliente: {{.Client.Version}} ({{.Client.Os}}/{{.Client.Arch}})
daemon: {{.Server.Version}} ({{.Server.Os}}/{{.Server.Arch}})'
echo
echo "== Por dónde habla el cliente con el daemon"
docker context inspect --format '{{.Endpoints.docker.Host}}'
echo
echo "== El cliente apuntando a un daemon que no existe"
DOCKER_HOST=unix:///tmp/curso-docker-no-hay-daemon.sock docker ps
echo "código de salida: $?"
$ ./fig00_07.sh
== Las dos versiones: la del cliente y la del daemon
cliente: 29.2.1 (linux/arm64)
daemon: 29.2.1 (linux/arm64)
== Por dónde habla el cliente con el daemon
unix:///var/run/docker.sock
== El cliente apuntando a un daemon que no existe
failed to connect to the docker API at unix:///tmp/curso-docker-no-hay-daemon.sock; check if the path is correct and if the daemon is running: dial unix /tmp/curso-docker-no-hay-daemon.sock: connect: no such file or directory
código de salida: 1
При теб се менят версиите и, ако е Intel или AMD, ще гласи amd64. docker context inspect показва към кой демон сочи клиентът, а променливата на средата DOCKER_HOST служи да го насочиш към друг; тук, нарочно, към такъв, който не съществува.
Три неща, които се научават от този изход:
docker versionотчита две версии, защото са две програми. Почти винаги съвпадат, но не е задължително: на Mac от тези изпълнения клиентът на macOS беше 29.8.2, а демонът на виртуалната машина — 29.2.1. Когато някоя команда се държи странно, първият въпрос е коя от двете версии я причинява.- Клиентът и демонът си говорят през сокета. Той е врата, а който може да я отвори, командва демона.
- Без демон клиентът не може да поиска нищо от Docker. Командите, които нямат нужда от демона, като
docker --versionилиdocker context ls, отговарят. Съобщението казва точно това: не е успял да се свърже с API на Docker по този път; провери дали пътят е верен и дали демонът работи. В твоя Mint ще видиш това съобщение с пътя/var/run/docker.sockв деня, когато услугата на Docker е спряна.
0.6.2 Защо е важно да го знаеш
Изглежда като архитектурна подробност, но не е. Демонът работи като root: ръководството за сигурност казва, че „изисква привилегии на root“, освен ако не включиш режим rootless, и че затова само „доверени потребители“ трябва да могат да го управляват (Docker Engine security). Ако можеш да искаш неща от демона, можеш да му поискаш да създаде контейнер с кореновата папка на машината ти монтирана вътре и той ще го направи като root. Затова документацията за инсталиране предупреждава, че групата docker „дава на потребителя привилегии, еквивалентни на тези на root“ (Docker, стъпки след инсталиране в Linux). Урок 1 те слага в тази група и те кара да погледнеш последствието в очите. А Урок 2 ти забранява, със същия аргумент, да монтираш сокета на демона вътре в контейнер: това би дало на контейнера ключа от машината ти.
0.7 Онова, което се споделя, и защо от него зависи сигурността
0.7.1 Root вътре е root отвън
Вече събра всички части за най-сериозния въпрос на урока: колко отделен е контейнерът от хоста? fig00_08.sh пита кой е потребителят вътре, чий е процесът, гледан отвън, и какви правомощия има:
#!/usr/bin/env bash
# fig00_08.sh — Lo que el contenedor comparte con el anfitrión: el usuario y el núcleo.
set -euo pipefail
IMAGEN=alpine:3.24.2
NOMBRE=curso-docker-quien
trap 'docker rm -f "$NOMBRE" >/dev/null 2>&1 || true' EXIT
echo "== Dentro del contenedor, ¿quién soy?"
docker run --rm --name curso-docker-id "$IMAGEN" id
echo
echo "== Y desde el anfitrión, ¿de quién es ese proceso?"
docker run -d --name "$NOMBRE" "$IMAGEN" sleep 300 >/dev/null
PID=$(docker inspect --format '{{.State.Pid}}' "$NOMBRE")
ps -o uid,user,args -p "$PID"
echo
echo "== Las capacidades efectivas de ese root (máscara en hexadecimal)"
echo "$(docker run --rm --name curso-docker-cap1 "$IMAGEN" grep CapEff /proc/self/status | cut -f2) contenedor, por omisión"
echo "$(docker run --rm --name curso-docker-cap2 --cap-drop ALL "$IMAGEN" grep CapEff /proc/self/status | cut -f2) contenedor, con --cap-drop ALL"
echo "$(grep CapEff /proc/1/status | cut -f2) proceso 1 del anfitrión (root)"
$ ./fig00_08.sh
== Dentro del contenedor, ¿quién soy?
uid=0(root) gid=0(root) groups=0(root),0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)
== Y desde el anfitrión, ¿de quién es ese proceso?
UID USER COMMAND
0 root sleep 300
== Las capacidades efectivas de ese root (máscara en hexadecimal)
00000000a80425fb contenedor, por omisión
0000000000000000 contenedor, con --cap-drop ALL
000001ffffffffff proceso 1 del anfitrión (root)
Този изход е стабилен при конфигурацията на Docker по подразбиране. Последният ред зависи от това колко възможности познава ядрото ти; с ядро от серия 6.8 излиза същият.
Първите два блока казват същото от двете страни: вътре в контейнера си потребител 0, root, а отвън този процес също е на потребител 0. Не е фалшив root със същото име: тъй като пространството от имена на потребителите не е изолирано (0.3), това е същият потребителски номер за същото ядро. Ако този процес успее да излезе от стените си —заради грешка в ядрото, заради папка на хоста, монтирана небрежно, заради сокета на демона—, би стигнал до хоста като root.
0.7.2 Подрязан root: възможностите
Значи ли това, че контейнерът е толкова опасен, колкото програма, която работи като root на машината ти? Не съвсем и третият блок обяснява защо. Linux разделя правомощията на root на отделни единици, наречени възможности (capabilities), които могат да се включват и изключват една по една (capabilities(7)). Да смениш собственика на файл е една; да отвориш порт под 1024 е друга; да заредиш модули в ядрото е трета.
Редът CapEff от /proc/<pid>/status казва кои възможности има включени един процес, като битова маска в шестнайсетичен запис. Процес 1 на хоста, който е истински root, има 000001ffffffffff: 41-те възможности, които това ядро познава, всички включени. root на контейнера има 00000000a80425fb, което са 14. Като се декодира тази маска с инструмента capsh на хоста, излизат точно четиринайсетте, които документацията на Docker изброява като разрешени по подразбиране: CHOWN, DAC_OVERRIDE, FOWNER, FSETID, KILL, SETGID, SETUID, SETPCAP, NET_BIND_SERVICE, NET_RAW, SYS_CHROOT, MKNOD, AUDIT_WRITE и SETFCAP (Docker, „Running containers“). Docker използва списък на разрешеното, а не на забраненото: маха всичко и оставя само това (Docker Engine security). А с --cap-drop ALL можеш да махнеш дори тези четиринайсет, както показва вторият ред; Урок 7 те учи да махаш онези, които revisor не ползва.
Обратната опция съществува и си струва да я познаваш, за да не я ползваш: --privileged „дава на контейнера всички възможности“ и освен това достъп до всички устройства на хоста (Docker, „Running containers“). С нея почти не остава стена.
0.7.3 Картата на споделеното
| Онова, което контейнерът има свое, по подразбиране | Онова, което споделя с хоста, по подразбиране |
|---|---|
| своите процеси и своите номера на процеси | ядрото, същото за всички контейнери |
| своето дърво от файлове (това на образа му и на слоя му) | потребителите: root вътре е потребител 0 отвън |
| своята мрежа, своите портове, своето име на машината | системните часовници |
| своя изглед на контролните групи | паметта, процесорът и дискът, ако не му сложиш ограничения |
| онова, което му монтираш или публикуваш: папки, портове, сокетът |
Дясната колона е списъкът с онова, за което трябва да се грижиш. Всеки урок от курса затваря по една част: 2 слага ограничения и публикува портове само на 127.0.0.1; 3 спира да ползва root с USER; 7 маха възможности и поставя файловата система само за четене; 8 се грижи за онова, което се монтира. Не е параноя: това е разликата между процес със стени и процес със стени от хартия.
0.8 Откъде дойде всичко това
Нищо от гореизброеното не е измислено от Docker. Частите са на ядрото на Linux и са идвали постепенно. Ръководството за сигурност на Docker го разказва: пространствата от имена влизат в ядрото между версиите 2.6.15 и 2.6.26, последната от юли 2008 г., а контролните групи започват през 2006 г. и се интегрират във версия 2.6.24 (Docker Engine security). Години наред имаше инструменти, които ги използваха, но бяха трудни за боравене.
Онова, което се промени през 2013 г., беше начинът на използване. На 15 март същата година, в петминутна светкавична презентация (lightning talk) на конференцията PyCon, Solomon Hykes разказа, че във фирмата му използват контейнери на Linux, че е сложно и затова не са говорили за това, „до сега: Docker“ (PyCon US 2013, „The future of Linux Containers“). Docker събра частите на ядрото с две свои идеи: формат на образ, който може да се споделя, и команди, които всеки разбира.
Две години по-късно, на 22 юни 2015 г., Docker, CoreOS и други фирми основаха Open Container Initiative (OCI) и Docker ѝ дари формата си и програмата си за създаване на контейнери, runc (OCI, „About“). Днес OCI поддържа три спецификации: тази на средата за изпълнение, тази на формата на образа и тази на разпространението. За теб това означава нещо практично: образът, който изградиш в този курс, не е „образ на Docker“, заключен в Docker, а е образ на OCI и затова клъстерът на Kubernetes от следващия курс може да го пусне без Docker. Между другото, runc е програмата, която се появява в края на името containerd-shim-runc-v2, което видя в 0.1.
0.9 Какво предстои в останалата част от курса
С картата от този урок всеки следващ урок е част, за която вече знаеш къде отива:
- Урок 1 инсталира клиента, демона и междинните части в твоя Mint.
- Урок 2 борави с изолирани процеси: стартиране, наблюдение, бюджет (0.4) и изтриване.
- Уроци 3, 4 и 5 изграждат образа на
revisor(0.5): рецептата му, слоевете му и как да стане малък. - Урок 6 отделя конфигурацията и тайните от образа.
- Урок 3 мери какво става със сигнала на
docker stopспоред това как е написанCMD; Урок 7 се занимава с процес номер 1 (0.1) и с подрязването на правомощията му (0.7). - Урок 8 решава проблема, че слоят на контейнера е за изхвърляне (0.5), и свързва контейнери по мрежа (0.3).
- Урок 9 описва всичко това във файл, а 10 предава образа на OCI (0.8), готов за Kubernetes.
Грешката, която ще видиш
Този урок има една основна грешка, тази на контейнера, който ядрото убива, и една второстепенна, тази на клиента без демон. И двете идват от предишни раздели.
Основната грешка: кодът 137 и OOMKilled=true. Тя е от fig00_05.sh:
código de salida: 137
== Lo que Docker anotó
OOMKilled=true ExitCode=137
Чете се така: процесът не е приключил сам; убит е от сигнал 9 (137 = 128 + 9) и Docker е отбелязал, че причината е липса на памет. Коварното е, че в реалния живот контейнерът не те предупреждава: не отпечатва нищо, просто изчезва, а ако има политика за рестартиране, стартира и умира отново и отново. Когато видиш 137, първият въпрос е docker inspect --format '{{.State.OOMKilled}}' <контейнер>. Ако казва true, има две възможни поправки според случая: да увеличиш ограничението на паметта, ако програмата наистина се нуждае от повече; или да поправиш програмата, ако харчи излишно. Което не се прави, е да махнеш ограничението, „за да не умира“: това не го поправя, само променя кой умира, както в сцената.
Второстепенната грешка: клиентът, който не намира демона.
failed to connect to the docker API at unix:///tmp/curso-docker-no-hay-daemon.sock; check if the path is correct and if the daemon is running: dial unix /tmp/curso-docker-no-hay-daemon.sock: connect: no such file or directory
Чете се отляво надясно: клиентът не е могъл да се свърже с API на Docker по този път; съветва те да провериш пътя и дали демонът работи; а накрая идва техническата причина: файлът на сокета не съществува. В твоя Mint, с пътя /var/run/docker.sock, почти винаги значи, че услугата на Docker е спряна. Ако вместо no such file or directory пише permission denied, демонът е там, но потребителят ти няма право да говори с него: това е работа на Урок 1.
Навикът за сигурност на урока
Да знаеш какво споделя един контейнер с машината и да не разчиташ на онова, което не изолира. Контейнерът споделя ядрото, потребителите и, ако не му сложиш ограничения, паметта и процесора. Този навик предотвратява два конкретни инцидента. Първият е този от сцената: контейнер без ограничение, който събаря други програми на същата машина. Вторият е по-сериозен: да третираш контейнера като отделна машина и да му даваш неща от хоста —сокета на демона, монтираната коренова папка, --privileged—, мислейки, че „вътре нищо не става“. Вътре работи процес на root на твоето собствено ядро. Всичко, което му дадеш, го даваш на този процес.
Какво се прави погрешно
Да казваш „като лека виртуална машина е“. Това е най-повтаряната фраза и тази, която прави най-много вреда, защото води до очакване за изолация, която не съществува. Контейнерът е изолиран процес; виртуалната машина е имитиран компютър. Приличат си в онова, което се вижда отвън, и се различават в онова, което има значение, когато нещо се провали.
Да пускаш контейнери без ограничения „защото на моята машина памет колкото щеш“. По подразбиране няма ограничение и онова, което днес стига, не стига в деня, когато програмата има грешка. Ограничението не е за нормалния случай; то е за лошия ден.
Да влизаш в контейнера да „поправиш“ нещо. Каквото промениш с docker exec, живее в слоя на контейнера и си отива с него. Поправката се прави в рецептата на образа и се изгражда наново.
Да пазиш важни данни вътре в контейнера. По същата причина: слоят на контейнера е за изхвърляне. Данните, които трябва да оцелеят, отиват в томове (Урок 8).
Да предполагаш, че root вътре е безобиден. Той е потребител 0 на същото ядро, с 14 включени възможности. По-малко от root на хоста, но много повече от обикновен потребител. От Урок 3 revisor не работи като root.
Да вярваш, че образ на Ubuntu или Debian носи цялата тази операционна система. Носи потребителското ѝ пространство. Ядрото е твоето. Ако ядрото ти не поддържа нещо, образът също.
Да бъркаш клиента с демона. Когато една команда се проваля, да знаеш дали проблемът е в клиента (версия, конфигурация, DOCKER_HOST), или в демона (спрян, без права, друга версия) спестява часове търсене на грешното място.
Упражнения
Упражненията в този урок не изискват Docker, за да се отговорят: първото и четвъртото са за размисъл, а второто и третото се отговарят преди да пуснеш каквото и да е и се проверяват след Урок 1. Запиши предположенията си в дневника, преди да видиш решенията; сравняването им е половината от ученето.
Упражнение 1 — Нощта на revisor (проект)
Върни се към сцената от началото. Отговори със свои думи, като използваш наученото:
- Защо контейнерът на
revisorне „виждаше“ базата данни, но можа да я убие? - Коя опция на
docker runби предотвратила смъртта на базата данни и кой би умрял на нейно място? - Ако в онзи момент беше пуснал
docker inspectвърху контейнера наrevisor, какво би очаквал да видиш вOOMKilled? Помисли добре: кой кого е убил?
Упражнение 2 — Същото или различно?
За всяка двойка предскажи дали стойността излиза същата или различна. Приеми конфигурацията на Docker по подразбиране.
uname -rв твоя Mint и вътре в контейнер отdebian:13.7-slim.cat /etc/os-releaseв твоя Mint и вътре в същия този контейнер.readlink /proc/self/ns/netв твоя Mint и вътре в контейнер.readlink /proc/self/ns/userв твоя Mint и вътре в контейнер.- Номерът на процеса на основната програма, гледан отвътре в контейнера и гледан от хоста.
- Идентификаторът на един образ преди и след изтриването на контейнер, който е записвал файлове.
Упражнение 3 — Предскажи memory.max
Предскажи какво число се появява в /sys/fs/cgroup/memory.max вътре във всеки от тези контейнери, а после го провери, като промениш опциите на fig00_04.sh:
docker run --rm alpine:3.24.2 cat /sys/fs/cgroup/memory.maxdocker run --rm --memory 128m alpine:3.24.2 cat /sys/fs/cgroup/memory.maxdocker run --rm --memory 1g alpine:3.24.2 cat /sys/fs/cgroup/memory.max
И един за процесора: какво ще каже cpu.max с --cpus 1.5?
Упражнение 4 — Обясни го с пет изречения
Колега ти казва: „Инсталирах Docker, за да имам виртуална машина с Ubuntu в моя Mint“. Обясни му с най-много пет изречения какво има в действителност, като използваш думите процес, ядро, пространство от имена, контролна група и образ. Без да гледаш урока.
Решения
Решение 1
- Защото контейнерът беше в собствените си пространства от имена: с това на процесите (
pid) не виждаше процесите на хоста, сред тях и базата данни. Но да виждаш и да харчиш са различни неща: без ограничение в контролната си група той харчеше от същата памет като цялата машина. Когато тя свърши, ядрото трябваше да убие някого в цялата система и избра базата данни. --memory(например--memory 256m; числото зависи от това колко наистина е нужно наrevisor). С това ограничение, при достигане на тавана ядрото би действало вътре в групата на контейнера и умрелият щеше да е самиятrevisor, с код 137 иOOMKilled=true. Базата данни дори нямаше да разбере.- Най-вероятно
false. Без собствено ограничение контейнерът не е стигнал до таван на своята група: изчерпана е била паметта на цялата машина и ядрото е убило базата данни, която не е контейнер.revisorе останал жив. Тъкмо това прави сцената подвеждаща: виновникът няма отметката, има я жертвата (и то в журнала на системата, не в Docker).
Решение 2
- Същото. Ядрото е само едно, споделено (fig00_02).
- Различно. Файлът идва от образа, който носи потребителското пространство на Debian (fig00_02).
- Различно. Мрежата е изолирана: всеки контейнер има свое пространство от имена
net(fig00_03). - Същото. По подразбиране Docker не изолира потребителите (fig00_03). Затова
rootвътре е потребител 0 отвън (fig00_08). - Различно. Вътре е 1; отвън — произволно число, като 836426 от fig00_01. Това е един и същ процес, гледан от две пространства от имена на процеси.
- Същото. Онова, което контейнерът е записал, е живеело в собствения му слой и си е отишло с него; образът не се е променил (fig00_06).
Ако си сгрешил в 4, то е най-честото и най-важното: то е причината за целия блок за сигурност на курса.
Решение 3
Стойности, проверени със същата машина и същата версия на Docker като в урока:
max: без--memoryняма ограничение.134217728, което е 128 × 1024 × 1024.1073741824, което е 1024 × 1024 × 1024. Docker тълкуваgкато гибибайти (степени на 1024), кактоmкато мебибайти.
С --cpus 1.5, cpu.max казва 150000 100000: във всеки период от 100 000 микросекунди групата може да използва 150 000 микросекунди процесорно време, разпределени между ядрата на процесора. Един процесор и половина.
Решение 4
Един възможен отговор: „Нямаш виртуална машина; имаш програма, която създава изолирани процеси. Тези процеси работят върху ядрото на твоя Mint, едно и също за всички, и затова стартират за десети от секундата. Пространствата от имена им слагат стени, за да виждат собствените си процеси, файлове и мрежа, а контролните групи им слагат бюджет от памет и процесор, ако им го поискаш. „Ubuntu“ идва от образа: носи програмите и библиотеките на Ubuntu, но не и неговото ядро. А тъй като root отвътре е същият потребителски номер като този отвън, с него трябва да се внимава“.
Как разбирам, че съм успял
В този урок няма какво да се изгради, така че списъкът смесва разбиране и, след Урок 1, проверка. Отбелязвай всяка точка само ако можеш да я направиш без да гледаш отново текста:
- Обясняваш за една минута защо контейнерът не е виртуална машина и думата ядро присъства в обяснението ти.
- Назоваваш шестте пространства от имена, които Docker изолира по подразбиране, и двете, които не изолира.
- Казваш какво съдържат
memory.maxиcpu.maxбез ограничения и какво значи50000 100000. - Обясняваш какво значат кодът 137 и
OOMKilled=trueи какво не трябва да се прави, когато се появят. - Обясняваш какво се случва с файл, записан вътре в контейнер, когато този контейнер бъде изтрит, и защо образът не се променя.
- Чертаеш пътя на
docker run: клиент → сокет → демон → ядро. - Казваш защо групата
dockerе равна наroot. - След Урок 1 пускаш деветте скрипта от
programas/00-que-es-un-contenedor/и изходите ти съвпадат с тези от този урок в онова, което всеки раздел отбелязва като стабилно. Което не се сравнява, защото се променя от машина на машина или от изпълнение на изпълнение: номерата на процесите, номерата на пространствата от имена, времената, версиите и архитектурата; а ако твоят Docker работи в режим rootless или с пренасочване на потребителите, или му липсва някой контролер на контролните групи, разликите, за които 0.3 и 0.4.2 ти казаха да очакваш. И накраяdocker ps -a --filter name=curso-docker-не показва нищо: не си оставил боклук. - Записа в дневника си какво си мислел, че е контейнерът, преди този урок.
Обобщение
Отговори, без да гледаш урока:
- Какво е контейнерът, с три думи? Защо се появява в
psна хоста? - Какво има една виртуална машина, което контейнерът няма? Защо Mac има нужда от виртуална машина, за да пуска контейнери на Linux?
- Какво прави едно пространство от имена? Дай три примера за онова, което изолира.
- Какво прави една контролна група? Какво ограничение има един контейнер, ако не му поискаш никакво?
- Какво значат 137 и
OOMKilled=true? - Какво е образ и какво е контейнер? Къде пише един контейнер файловете си и какво им става, когато го изтриеш?
- Какво правят клиентът
dockerи демонътdockerdи как си говорят? - Защо
rootвътре в контейнер е въпрос на сигурност и какво го подрязва?
Източници
Всички са посетени на 21 октомври 2026 г.
- Docker, „What is a container?“: определението на контейнера като изолиран процес и сравнението с виртуалните машини.
- Docker, „What is Docker?“: архитектурата клиент-сървър, демонът, определенията на образ и контейнер и използването на пространствата от имена.
- Docker Engine security: пространства от имена, контролни групи, повърхността за атака на демона, възможностите и пространствата от имена на потребителите; също датите от 0.8.
- Docker, „Resource constraints“: че контейнерът няма ограничения по подразбиране и опциите
--memory,--memory-swapи--cpus. - Docker, „Running containers“: четиринайсетте възможности, разрешени по подразбиране, и какво прави
--privileged. - Docker, „Storage drivers“: слоевете само за четене и слоят на контейнера.
- Docker, стъпки след инсталиране в Linux: предупреждението за групата
docker. - Docker Desktop, разрешения в Mac: виртуалната машина с Linux на Docker Desktop.
- Docker Engine 29, бележки към версиите: версиите и датите от „За изпълненията“.
- namespaces(7), наръчник на Linux: осемте вида пространства от имена и връзките в
/proc/<pid>/ns. - capabilities(7), наръчник на Linux: привилегировани процеси и възможности.
- Control Group v2, документация на ядрото на Linux:
memory.max,cpu.maxиpids.max. - Open Container Initiative, „About“: основаването на OCI и трите ѝ спецификации.
- PyCon US 2013, „The future of Linux Containers“: презентацията, в която беше представен Docker.
- Colima: инструментът, с който бяха пуснати програмите в macOS.
Термини от този урок
| хост (host) | машината, на която работят контейнерите; в курса — твоят Linux Mint |
| слой (layer) | всяка от частите само за четене на един образ; контейнерът добавя собствен слой, в който да пише |
| възможност (capability) | всяко от правомощията, на които Linux разделя потребителя root |
| клиент | програмата docker, която пишеш; изпраща заявки на демона |
| контейнер | изолиран процес, създаден от образ |
| демон | програма, която работи на заден план; този на Docker се казва dockerd |
| пространство от имена (namespace) | частен изглед на ресурс на системата (процеси, мрежа, файлове...) |
| потребителско пространство | всичко, което работи върху ядрото: програми, библиотеки, shell |
| контролна група (cgroup) | механизъм на ядрото, който отчита и ограничава ресурсите на група процеси |
| образ | шаблон само за четене, от който се създават контейнери |
| виртуална машина | компютър, имитиран от софтуер, със собствено ядро |
| ядро (kernel) | частта от операционната система, която командва хардуера |
| OCI | Open Container Initiative, отвореният стандарт за образи и контейнери |
| OOM killer | частта от ядрото, която убива процеси, когато паметта свърши |
| сокет на UNIX | специален файл, чрез който две програми на една и съща машина си разменят съобщения |
Предпочитате имейл? Пишете ни на hola@habil.mx