Съдържание на курса

Урок 2 — Контейнерите в ежедневната работа

От · основател и главен изпълнителен директор на Hábil ·

Остават дванайсет минути до демонстрацията, а услугата не стартира. Въвеждаш същата команда, която вчера работеше, и Docker ти отговаря с ред на английски, който завършва с port is already allocated. Опитваш отново и сега съобщението е друго: The container name ... is already in use. Някой до теб предлага да рестартираш компютъра. Преди да го направиш, пускаш docker ps -a и екранът се изпълва: контейнери от миналата седмица, едни спрени, други все още работещи, един от тях публикуван на порт 9000 за всеки, който е в същата безжична мрежа на кафенето, където работи във вторник. Никой не го е направил със зъл умисъл. Просто никой не се е научил да стартира, наблюдава, спира и изтрива контейнери с ред. Остават ти десет минути, а този урок е онова, което би трябвало да знаеш.

Време: 2 × 45 мин

Какво изграждаш: тестовата услуга на курса, работеща в контейнер, с порт, публикуван само за твоя компютър, и с ограничения за памет, CPU и процеси; питаш я в трите ѝ режима (здрав, бавен и неработещ), разчиташ лога ѝ, инспектираш я, спираш я и я изтриваш. Накрая на компютъра ти не остава нито един контейнер на курса; образът curso-docker-prueba:1.0.0 обаче остава, защото следващите уроци го използват отново.

Какво научаваш: командите за ежедневната работа — run, ps, logs, exec, inspect, stats, stop, start и rm; какво означава да публикуваш порт и защо в този курс винаги се публикува на 127.0.0.1; как да зададеш ограничения за памет, CPU и процеси и как да провериш, че са приложени; жизнения цикъл на един контейнер (създаден, работещ, спрян, изтрит); и как да почистиш само своето, без да пипаш чуждото.

Откъде идваш. От Урок 0 носиш картата: контейнерът е изолиран процес, образът е шаблонът, а контейнерът — живото копие, и клиентът docker иска всичко от демона през един socket. От Урок 1 носиш Docker, инсталиран на твоя Mint, и потребителя си в групата docker, като знаеш, че това е равносилно на root. Трябва ти и хранилището на курса на компютъра ти: от него ще използваш папката programas/servicio-prueba/, която съдържа вече написаната тестова услуга с готов Dockerfile. Не е нужно да знаеш Go, нито още да можеш да четеш този Dockerfile.

Какво не прави този урок. Не пише никакъв Dockerfile: този на тестовата услуга се използва такъв, какъвто е, а да се научиш да го четеш е работа на Уроци 3 до 5. Все още не пипа revisor, главния проект на курса, защото, за да го стартираш, първо трябва да го пакетираш, а това започва в Урок 3. Не обяснява и томове, нито мрежи между контейнери; тук всеки контейнер живее сам и се говори с него от твоя компютър.

За изпълненията. Изходите в този урок са получени на 21 октомври 2026 г. на Mac с Colima, която стартира виртуална машина с Linux с Ubuntu 24.04.4 (същата основа като Linux Mint 22), с 64-битов ARM процесор. Скриптовете бяха пуснати вътре в тази виртуална машина, с Docker 29.2.1 (клиент и демон, API 1.53). Клиентът на Mac беше 29.8.2, версията, с която е планиран курсът, а най-новата публикувана в този ден беше 29.9.2, от 20 октомври 2026 г., според официалните бележки към версиите. Четири бележки за това как е изпълнено. Първо: тази машина се използваше едновременно от други уроци на курса, затова, за да не се сблъскат, скриптовете бяха пуснати с друг префикс на имената и с други портове: те имат променливите PREFIJO (по подразбиране curso-docker-) и PUERTO (по подразбиране 9000) и бяха изпълнени с PREFIJO=curso-docker-l02- и PUERTO=18020; fig02_05.sh използва освен това следващия порт, 18021 вместо 9001. Там, където изходите показват имена и портове, урокът показва стойностите по подразбиране, които ще видиш и ти: curso-docker-prueba вместо curso-docker-l02-prueba и 9000 и 9001 вместо 18020 и 18021; таблиците са пренаредени според ширината на по-краткото име, защото ширината на всяка колона зависи от името; това е единствената промяна, а файловете .salida.txt пазят изпълнението такова, каквото е. Онова, което не се променя с променливите, защото се случва вътре в контейнера, излиза такова, каквото е: портът 9000 на проверките на състоянието. trap на всеки скрипт изтрива по точно име, така че с друг префикс не пипа нищо от друг урок. В твоя Mint не е нужна никаква променлива: скриптовете работят така, както са написани. Второ: изходът на fig02_01.sh е от изграждане на студено, пуснато със SIN_CACHE=1, за да не използва слоеве от предишни изграждания — това ще видиш и ти първия път. Машината вече имаше изтеглени базовите образи, затова не се виждат изтегляния; първия път ще видиш най-напред изтеглянето на три образа: този на Go, минималния и един малък, docker/dockerfile:1, който е интерпретаторът на рецептата (Урок 3 обяснява този първи ред), и ще отнеме повече време. Стъпката, която компилира, отне 236.8 секунди при това изпълнение, защото машината беше споделена и силно натоварена; при едно по-ранно изпълнение на същия скрипт, с по-малко натоварване, бяха 32.9 (този изход не е запазен): времето зависи от компютъра ти. Ако повториш скрипта с вече изграден образ, изходът е друг (2.1.3); това изпълнение е в fig02_01.salida-repetida.txt. Трето: в твоя Mint се променят архитектурата (amd64 вместо arm64, ако процесорът ти е Intel или AMD), идентификаторите, времената и общата памет, която отчита docker stats. Четвърто: упражненията и решенията им също бяха изпълнени, със същите префикси, във fig02_08.sh (упражнение 1), fig02_09.sh (упражнения 2 и 3) и fig02_10.sh (docker update, 2.10.4); всяко със запазения си изход. Програмите са в programas/02-contenedores-dia-a-dia/, всяка със запазения си изход до нея.

След урока ще можеш да

  • Изградиш образа на тестовата услуга с една-единствена команда и да го намериш с docker image ls.
  • Стартираш контейнер във фонов режим с име, порт, публикуван на 127.0.0.1, и ограничения за памет, CPU и процеси, и да обясниш всяка опция на командата.
  • Изброиш контейнерите си с docker ps, да ги филтрираш по име и да избереш кои колони да се показват.
  • Разчетеш лога на контейнер с docker logs, целия или само последните му редове, и да изпълниш програма в него с docker exec.
  • Провериш с docker inspect и docker stats, че ограниченията са приложени, и със ss — на кои интерфейси слуша компютърът ти.
  • Разчетеш и решиш двете ежедневни грешки: повторено име и зает порт.
  • Спреш, стартираш отново и изтриеш контейнери и да оставиш компютъра си без нито един контейнер curso-docker- накрая.

Защо, преди как

В Урок 0 видя контейнерите отвътре, за да разбереш какво са. Сега е ред на обратното: да ги използваш всеки ден, без да мислиш за пространства от имена, нито за контролни групи. И тук се появява проблем, който няма нищо техническо и който вреди повече от много грешки на ядрото: безпорядъкът.

Контейнер се създава много лесно. Достатъчен е един docker run. И тъй като не вдига шум, много лесно се забравя. След седмица тестове имаш спрени контейнери, които заемат диск, работещи контейнери, които заемат памет и портове, и имена, които вече не можеш да използваш отново. Най-лошото не е дискът: а че някой от тях е останал да слуша на всички мрежови интерфейси на компютъра ти, а ти не знаеш. Сцената от началото не е причинена от рядка грешка; причинили са я три навика, които никой не е поправил: да публикуваш портове, без да казваш къде, да стартираш без ограничения и да не чистиш.

Този урок ти дава пълния набор от команди, за да управляваш един контейнер от началото до края, а заедно с всяка команда — навика, който предотвратява безпорядъка. Ще го направиш с тестовата услуга на курса, много малък HTTP сървър, който се проваля нарочно. Има три режима: в режим здрав (sano) отговаря с 200 веднага; в режим бавен (lento) чака три секунди и след това отговаря с 200; в режим неработещ (caido) отговаря с 503, HTTP кода за „услугата не е достъпна“. А режимът се сменя в движение, без рестартиране, със заявка POST. През целия курс тази услуга ще бъде целта, която revisor наблюдава; тук ти служи за безопасна практика и всичко, което направиш с нея, ще повториш с revisor: когато го пакетираш в Урок 3, ще го стартираш с команда от същото семейство: сменят се името, образът и портът 8080, а адресът 127.0.0.1 и трите ограничения се запазват. И ти служи, за да видиш една идея, която курсът повтаря: това, че контейнерът казва „работи“, не означава, че услугата е здрава.

Урокът върви в този ред:

  1. Изграждане на образа на тестовата услуга (2.1), като първо сгрешиш.
  2. Стартиране на контейнер и разбиране на всяка част от командата (2.2).
  3. Наблюдение: ps (2.3), запитване (2.4) и разчитане на лога с logs (2.5).
  4. Влизане, или опит за влизане, с exec (2.6).
  5. Инспектиране с inspect и stats (2.7).
  6. Портовете: 127.0.0.1 срещу всички интерфейси (2.8), и ограниченията (2.9).
  7. Жизненият цикъл: спиране, повторно стартиране и изтриване (2.10), и да не оставяш остатъци (2.11).

Това е командата, до която ще стигнеш. В края на урока всяка дума в нея ще има причина:

docker run -d --name curso-docker-prueba -p 127.0.0.1:9000:9000 \
  --memory 64m --cpus 0.5 --pids-limit 50 curso-docker-prueba:1.0.0

Понятията

2.1 Изграждане на образа на тестовата услуга

2.1.1 Първо — грешката

За да стартираш контейнер, ти трябва образ. Преди да го изградиш, струва си да видиш какво става, ако прескочиш тази стъпка, защото това е грешка, която ще виждаш много пъти в живота си: някой ти дава команда, поставяш я и образът не съществува на компютъра ти. fig02_01.sh прави точно това, а след това изгражда образа:

#!/usr/bin/env bash
# fig02_01.sh — Correr una imagen que todavía no existe, y después construirla.
# Primero el error (la imagen no está en tu equipo ni en Docker Hub); luego el
# único comando que necesitas para construir el servicio de prueba.
set -uo pipefail
cd "$(dirname "$0")/.."          # la carpeta programas/, donde está servicio-prueba/
# Nombres por omisión: los del curso. Se pueden cambiar para no chocar con otra
# corrida en el mismo equipo (PREFIJO=... ./fig02_01.sh).
PREFIJO="${PREFIJO:-curso-docker-}"
# SIN_CACHE=1 ./fig02_01.sh construye sin reusar capas, como la primera vez en un equipo nuevo.
NOMBRE="${PREFIJO}prueba"
IMAGEN="${PREFIJO}prueba:1.0.0"
# Si la imagen ya existía, el bloque 1 no falla y SÍ crea un contenedor: se borra al salir.
trap 'docker rm -f "$NOMBRE" >/dev/null 2>&1 || true' EXIT

echo "== 1. Correr la imagen antes de construirla"
docker run -d --name "$NOMBRE" "$IMAGEN"
echo "código de salida: $?"

echo
echo "== 2. Construirla: un solo comando"
docker build ${SIN_CACHE:+--no-cache} -t "$IMAGEN" servicio-prueba
echo "código de salida: $?"

echo
echo "== 3. La imagen ya está en tu equipo"
docker image ls "$IMAGEN"

echo
echo "== 4. Y el intento fallido no dejó contenedores"
docker ps -a --filter name="$NOMBRE" --format '{{.Names}} {{.Status}}' | wc -l

echo
echo "== 5. El tamaño, con un formato propio"
docker image ls "$IMAGEN" --format '{{.Repository}}:{{.Tag}} {{.Size}}'

Скриптът използва set -uo pipefail, а не set -euo pipefail като тези от Урок 0: махнахме -e нарочно, защото първата команда трябва да се провали, а искаме скриптът да продължи. cd "$(dirname "$0")/.." преминава в папката programas/, където е servicio-prueba/, независимо откъде пускаш скрипта. А trap ... EXIT програмира изтриването на контейнера при излизане: първия път няма какво да се изтрие, но ако пуснеш скрипта с вече изграден образ, блок 1 вече не се проваля, създава контейнера и trap го премахва. Ето първата част от изхода:

$ ./fig02_01.sh
== 1. Correr la imagen antes de construirla
Unable to find image 'curso-docker-prueba:1.0.0' locally
docker: Error response from daemon: pull access denied for curso-docker-prueba, repository does not exist or may require 'docker login'

Run 'docker run --help' for more information
código de salida: 125

Чете се на два етапа. Първият ред казва, че образът не е на твоя компютър (locally). Това още не е грешка: когато docker run не намери образ, търси го в регистър, който по подразбиране е Docker Hub. Документацията го обяснява с опцията --pull, чиято стойност по подразбиране е missing: изтегля образа само ако вече не е на компютъра ти (docker container run). Вторият ред е истинската грешка: Docker Hub няма публично хранилище с име curso-docker-prueba, а ти не си дал идентификационни данни, затова е отказал изтеглянето. Съобщението не може да знае кое от двете се случва — дали не съществува, или е частно, — затова казва и двете.

Кодът на изход 125 също има значение. Документацията на Docker запазва три кода: 125 означава, че грешката е на самия демон на Docker; 126 — че командата на контейнера не може да бъде извикана; и 127 — че командата на контейнера не е намерена. Всяко друго число е кодът, с който е приключила програмата вътре (Docker, „Running containers“). Тук контейнерът дори не е започнал да съществува: провалил се е Docker, а не услугата. Ще видиш 127 в раздел 2.6.

2.1.2 Една-единствена команда за изграждането

Тестовата услуга идва с написана рецепта: файлът programas/servicio-prueba/Dockerfile. Изграждането ѝ е една-единствена команда:

docker build -t curso-docker-prueba:1.0.0 servicio-prueba

Има две части. -t curso-docker-prueba:1.0.0 дава име и таг на образа, който се получи: името е curso-docker-prueba, а тагът, онова след двоеточието, е 1.0.0. А servicio-prueba накрая е папката, която Docker използва като контекст: там търси Dockerfile и оттам взема файловете, които рецептата иска да се копират. Уроци 3 и 4 обясняват контекста в дълбочина; тук е достатъчно да знаеш, че е папка.

Изходът от изграждането е дълъг; ето централната част (целият файл е в fig02_01.salida.txt):

== 2. Construirla: un solo comando
#0 building with "default" instance using docker driver

#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 836B 0.1s done
#1 DONE 0.1s
[…]
#9 [compilacion 1/4] FROM docker.io/library/golang:1.27.3-trixie@sha256:604106102d33b1084e37ca08adf98ac0813624ae8595bd0d7f3965ecebded09c
#9 resolve docker.io/library/golang:1.27.3-trixie@sha256:604106102d33b1084e37ca08adf98ac0813624ae8595bd0d7f3965ecebded09c 0.2s done
#9 DONE 0.3s
[…]
#12 [compilacion 4/4] RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/servicio-prueba .
#12 DONE 236.8s

#13 [stage-1 2/2] COPY --from=compilacion /out/servicio-prueba /servicio-prueba
#13 DONE 2.4s

#14 exporting to image
[…]
#14 naming to docker.io/library/curso-docker-prueba:1.0.0 0.2s done
#14 unpacking to docker.io/library/curso-docker-prueba:1.0.0 0.3s done
#14 DONE 6.1s
código de salida: 0

Не е нужно днес да разбереш всеки ред, но три неща — да. Изграждането върви на номерирани стъпки (#1, #2…) и всяка казва колко е отнела: най-дългата, 236.8 секунди при това изпълнение (машината беше силно натоварена; виж „За изпълненията“), е тази, която компилира програмата на Go вътре в образ, съдържащ компилатора. Накрая се появява naming to docker.io/library/curso-docker-prueba:1.0.0: краткото име, което даде, е, в пълния си вид, това на образ от Docker Hub в официалната библиотека (library), макар никога да няма да бъде качен никъде; така Docker именува всичко, което няма изписан регистър. А кодът на изход е 0: всичко е минало добре. Уроци 3, 4 и 5 те учат да четеш този файл ред по ред.

2.1.3 Намиране на образа

== 3. La imagen ya está en tu equipo
WARNING: This output is designed for human readability. For machine-readable output, please use --format.
IMAGE                       ID             DISK USAGE   CONTENT SIZE   EXTRA
curso-docker-prueba:1.0.0   56406ca1d761       15.1MB         3.37MB        

== 4. Y el intento fallido no dejó contenedores
0

== 5. El tamaño, con un formato propio
curso-docker-prueba:1.0.0 15.1MB

Ако идваш от стари ръководства, тази таблица ще ти се стори различна. От версия 29.0 docker image ls показва нов изглед по подразбиране, „като --tree, но свит“, и скрива образите без таг, освен ако не поискаш --all (бележки към версиите на Docker Engine 29). Справочната страница на командата все още показва предишните колони (REPOSITORY, TAG, IMAGE ID, CREATED, SIZE) (docker image ls), така че не се учудвай, ако твоят изход и някое ръководство не съвпадат. Предупреждението WARNING на първия ред ти го казва самият Docker: този изглед е за четене от човек; ако ще обработваш изхода със скрипт, използвай --format, както прави останалата част от урока.

Колоната, която този курс използва, за да говори за размера на образ, е DISK USAGE: 15.1 MB е онова, което образът заема на диска ти. Колоната CONTENT SIZE е по-малка; всичко сочи, че отчита съдържанието на образа в компресиран вид, както пътува по мрежата, но справочната документация все още не я описва, затова в този курс не я използваме за нищо. Със собствен формат се получава направо размерът:

docker image ls curso-docker-prueba:1.0.0 --format '{{.Repository}}:{{.Tag}} {{.Size}}'

Това е блок 5 на fig02_01.sh и при изпълнението той отпечата curso-docker-prueba:1.0.0 15.1MB.

Блок 4 потвърждава нещо, което е важно за почистването: неуспешният docker run от раздел 2.1.1 не остави контейнер. Тъй като нямаше образ, нямаше от какво да се създаде. В „Грешката, която ще видиш“ предизвикваш такава, която оставя контейнер.

Едно предупреждение, ако повториш скрипта: с вече изграден образ блок 1 не се проваля. Създава контейнера curso-docker-prueba, отпечатва идентификатора му и завършва с код 0, а блок 4 брои 1, а не 0, защото този контейнер продължава да работи, докато trap не го изтрие при излизане. Това не е грешка: доказателство е, че грешката в блок 1 зависеше от това образът да не съществува. Изпълнението е в fig02_01.salida-repetida.txt.

2.2 Стартиране на контейнер

2.2.1 Командата, опция по опция

С готовия образ — командата от този урок:

docker run -d --name curso-docker-prueba -p 127.0.0.1:9000:9000 \
  --memory 64m --cpus 0.5 --pids-limit 50 curso-docker-prueba:1.0.0

Чети я отляво надясно. Всяка опция отговаря на въпрос, на който, ако не отговориш ти, Docker отговаря вместо теб, и почти никога така, както ти е удобно:

Опция Въпрос, на който отговаря Какво става, ако не я сложиш
-d ще ми върнеш ли терминала? контейнерът остава закачен за терминала ти; ако го затвориш или натиснеш Ctrl+C, сигналът стига до него
--name curso-docker-prueba как се казва? Docker измисля случайно име, като busy_goldwasser, което няма да запомниш
-p 127.0.0.1:9000:9000 кой може да говори с него и през кой порт? никоя друга машина, а ти — само през вътрешния адрес на контейнера, не през 127.0.0.1; а ако сложиш -p 9000:9000 без адрес — всеки в мрежата ти
--memory 64m колко памет може да използва? цялата памет на компютъра ти
--cpus 0.5 колко CPU? целия
--pids-limit 50 колко процеса (и нишки) може да създаде? колкото поиска
curso-docker-prueba:1.0.0 от кой образ? (задължително)

-d е кратката форма на --detach: според документацията стартира контейнера във фонов режим и отпечатва идентификатора му (docker container run). Без -d изходът на услугата се появява в терминала ти и терминалът остава зает, докато контейнерът не приключи. Понякога това е полезно за бърза проба; за сървър — почти никога.

--name му дава име. Документацията казва, че ако не го дадеш, демонът генерира случайно (docker container run). Собственото име служи за две неща: да напишеш docker logs curso-docker-prueba, вместо да копираш идентификатор от 64 знака, и името да ти напомня чий е контейнерът. Но има последствие: в един и същ демон не може да има два контейнера с едно и също име, дори единият да е спрян. Това е първата грешка от сцената и я предизвикваш в „Грешката, която ще видиш“.

-p е кратката форма на --publish и има три части, разделени с двоеточие: адрес:порт-на-хоста:порт-на-контейнера. 127.0.0.1:9000:9000 означава „на моя компютър, само на адрес 127.0.0.1 и на порт 9000, приемай връзки и ги предавай на порт 9000 на контейнера“. Двете числа не е задължително да са еднакви: с 127.0.0.1:18000:9000 ти би говорил с него през 18000, а услугата би продължила да слуша на своя 9000. Спомни си от Урок 0, че всеки контейнер има собствена мрежа. Без -p неговият порт 9000 е достъпен само от вътрешната мрежа на Docker: от другите контейнери в тази мрежа и от собствения ти компютър, ако използваш вътрешния адрес на контейнера; документацията го казва така: портовете на контейнерите в мрежите от тип мост (bridge) са достъпни от хоста, дори да не са публикувани (Docker, „Port publishing and mapping“). Онова, което без -p не е достъпно, е портът през 127.0.0.1 на компютъра ти, нито от която и да е друга машина. Раздел 2.8 е посветен на адреса, който е най-важната част.

--memory, --cpus и --pids-limit са бюджетът, който Урок 0 те научи да разчиташ в /sys/fs/cgroup. Раздел 2.9 обяснява защо са тези числа.

2.2.2 Стартиране и изчакване да отговори

fig02_02.sh стартира контейнера и прави с него онова, което би правил в обикновен ден. Ето целия скрипт; изходът му е разпределен в раздели 2.2 до 2.5:

#!/usr/bin/env bash
# fig02_02.sh — El servicio de prueba corriendo: arrancar, listar, consultar y leer su bitácora.
# Puerto publicado solo en 127.0.0.1 y con límites de memoria, CPU y procesos.
set -euo pipefail
# Nombres y puerto por omisión: los del curso. Se pueden cambiar para no chocar
# con otra corrida en el mismo equipo (PREFIJO=... PUERTO=... ./figNN.sh).
PREFIJO="${PREFIJO:-curso-docker-}"
PUERTO="${PUERTO:-9000}"
NOMBRE="${PREFIJO}prueba"
IMAGEN="${PREFIJO}prueba:1.0.0"
trap 'docker rm -f "$NOMBRE" >/dev/null 2>&1 || true' EXIT

echo "== 1. Arrancar en segundo plano"
docker run -d --name "$NOMBRE" -p "127.0.0.1:${PUERTO}:9000" \
  --memory 64m --cpus 0.5 --pids-limit 50 "$IMAGEN" | cut -c1-12
# Esperar a que conteste (máximo 10 s), en vez de dormir un número fijo.
for _ in $(seq 20); do curl -fs -o /dev/null "http://127.0.0.1:${PUERTO}/" && break; sleep 0.5; done

echo
echo "== 2. Lo que está corriendo"
docker ps --filter name="$NOMBRE" --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}'

echo
echo "== 3. Consultarlo en sus tres modos"
curl -s "http://127.0.0.1:${PUERTO}/"
curl -s -X POST "http://127.0.0.1:${PUERTO}/modo/caido"
curl -s -w 'código HTTP: %{http_code}\n' "http://127.0.0.1:${PUERTO}/"
curl -s -X POST "http://127.0.0.1:${PUERTO}/modo/lento"
curl -s -w 'código HTTP: %{http_code} en %{time_total} s\n' "http://127.0.0.1:${PUERTO}/"
curl -s -X POST "http://127.0.0.1:${PUERTO}/modo/sano"
curl -s "http://127.0.0.1:${PUERTO}/modo"

echo
echo "== 4. Su bitácora completa"
docker logs "$NOMBRE"

echo
echo "== 5. Solo los dos últimos renglones"
docker logs --tail 2 "$NOMBRE"
$ ./fig02_02.sh
== 1. Arrancar en segundo plano
7f01c45325dd

Единственото, което отпечатва docker run -d, е идентификаторът на контейнера, шестнайсетично число от 64 знака; скриптът го съкращава до първите 12 с cut -c1-12, както Docker го показва почти навсякъде. Променя се при всяко изпълнение. Появата му означава, че контейнерът е създаден и е поискано стартирането му; не означава, че услугата вътре вече обслужва.

Затова следващият ред на скрипта е цикъл: опитва заявка с curl на всеки половин секунда, до двайсет пъти, и излиза веднага щом една отговори успешно. curl -f кара curl да се провали, ако сървърът отговори с грешка, -s го заглушава, а -o /dev/null изхвърля отговора. Това е по-добре от фиксиран sleep 3 по две причини: на бърз компютър не чакаш излишно, а на бавен не продължаваш преждевременно. Ще виждаш този модел във всички скриптове на курса, които говорят с услугата; там, където не се говори с нея, не е нужно да се чака.

trap ... EXIT отгоре е същият навик от Урок 0: каквото и да стане, при излизане скриптът изтрива контейнера си. В скриптовете на този урок ще го виждаш винаги, защото раздел 2.11 го превръща в правило.

2.3 Какво работи: docker ps

== 2. Lo que está corriendo
NAMES                 IMAGE                       STATUS                            PORTS
curso-docker-prueba   curso-docker-prueba:1.0.0   Up 2 seconds (health: starting)   127.0.0.1:9000->9000/tcp

docker ps изброява контейнерите. По подразбиране показва само работещите; с -a (--all) показва и спрените (docker container ls). Тази разлика е онова, което крие остатъците: спрян контейнер не се появява в docker ps, но продължава да съществува, заема диск и задържа името си.

Скриптът използва две опции, които си струва да научиш още отсега. --filter name=curso-docker-prueba оставя само контейнерите, чието име съдържа този текст; документацията казва, че филтърът name съвпада с „цялото или част от“ името (docker container ls). А --format 'table ...' избира колоните: {{.Names}}, {{.Image}}, {{.Status}} и {{.Ports}} са полета, а думата table в началото иска заглавия и подравнени колони. Онова между двойните фигурни скоби е шаблон на езика Go; не е нужно да знаеш Go, за да го използваш, само имената на полетата, които страницата на командата изброява. Има и други полезни филтри; този за състояние, например, приема created, restarting, running, removing, paused, exited и dead (docker container ls).

Колоните, една по една:

  • NAMES е името, което му даде.
  • IMAGE — образът, от който е роден.
  • STATUS казва Up 2 seconds: работи от две секунди. И има още нещо, (health: starting), което заслужава отделен абзац.
  • PORTS казва 127.0.0.1:9000->9000/tcp: порт 9000 на компютъра ти, само на 127.0.0.1, води към 9000 на контейнера, по TCP.

(health: starting) се появява, защото Dockerfile на тестовата услуга съдържа инструкция HEALTHCHECK: тя казва на Docker на всеки пет секунди да изпълнява вътре в контейнера подкомандата servicio-prueba healthcheck, която пита самата услуга дали отговаря правилно. Докато нито една проверка не е минала, състоянието е starting; става healthy с първата, която отговори правилно, и unhealthy, когато се провалят поред толкова, колкото казва retries (в този образ — две) (справочник на Dockerfile, HEALTHCHECK). Днес няма да го използваш за друго, освен да го разчетеш; как се пише проверка на състоянието (health check) и какво прави Docker с нея е тема на Урок 7. Но забележи още отсега, че Up и състоянието са две различни неща: Up казва, че процесът съществува; проверката на състоянието казва дали обслужва. Следващият раздел го показва.

2.4 Разговор с услугата

== 3. Consultarlo en sus tres modos
sano
modo: caido
caido
código HTTP: 503
modo: lento
lento
código HTTP: 200 en 3.002647 s
modo: sano
modo: sano

Чети всяка двойка редове като въпрос и отговор:

  1. GET / в режим здрав отговаря sano.
  2. POST /modo/caido сменя режима и потвърждава modo: caido. От този момент GET / отговаря с текста caido и кода 503 (Service Unavailable, RFC 9110, §15.6.4). Опцията -w на curl отпечатва данни за заявката след отговора; тук — HTTP кода.
  3. POST /modo/lento, и следващият GET / отнема 3.00 секунди, за да отговори lento с 200. Трите секунди са забавянето на бавния режим; останалото (тук — около две хилядни) е пътят напред и назад. Това число се променя малко при всяко изпълнение.
  4. POST /modo/sano го връща към нормалното, а GET /modo го потвърждава.

Сега въпросът, който има значение. Докато услугата отговаряше с 503, какво казваше docker ps? Казваше Up. За Docker контейнерът беше перфектен: процесът съществуваше и не беше приключил. За всеки, който го използваше, той не работеше. Docker не знае какво отговаря твоята услуга, освен ако не го научиш с проверка на състоянието. Това е принципът, който курсът повтаря във всеки урок: работещ контейнер не е здрава услуга. revisor, проектът, който ще пакетираш от Урок 3 нататък, съществува точно за да задава този въпрос отвън.

2.5 Логът: docker logs

== 4. Su bitácora completa
servicio-prueba escuchando en :9000 en modo sano
servicio-prueba: GET / en modo sano
servicio-prueba: GET / en modo sano
servicio-prueba: modo cambiado a caido
servicio-prueba: GET / en modo caido
servicio-prueba: modo cambiado a lento
servicio-prueba: GET / en modo lento
servicio-prueba: modo cambiado a sano

== 5. Solo los dos últimos renglones
servicio-prueba: GET / en modo lento
servicio-prueba: modo cambiado a sano

Всичко, което главната програма на контейнера пише в своя стандартен изход и в своя изход за грешки — онова, което в терминала би видял с printf или с echo, — Docker го пази, а docker logs ти го връща. Документацията го описва така: командата предава записите, които съществуват в момента на изпълнението ѝ, а с --follow продължава да предава новото, което излиза от STDOUT и STDERR на контейнера (docker container logs).

Забележи, че логът разказва цялата история от предишния раздел, поред: стартирането, първите две заявки (тази от цикъла на изчакване и твоята), всяка смяна на режима и всяка заявка с режима ѝ. Заявките към /modo не оставят следа, защото услугата не ги записва; една програма оставя в лога само онова, което авторът ѝ е решил да пише. И едно предупреждение: проверката на състоянието на образа (2.3) също прави GET / на всеки пет секунди и всяка оставя своя ред. Първата се изпълнява пет секунди след стартирането (справочник на Dockerfile, HEALTHCHECK). При това изпълнение не се появява нито една, и това не е защото е траяло по-малко от пет секунди: блок 2 вече казваше Up 2 seconds, а бавната заявка от блок 3 отне още три. Най-вероятно първата проверка е попаднала, докато услугата беше в бавен режим: проверката се отказва след две секунди, а услугата, като види, че питащият вече си е тръгнал, се връща, без да запише недовършената заявка (в кода ѝ бавният режим приключва, без да пише, ако заявката бъде отменена). С други думи, проверката се е случила и не е оставила следа. В контейнер, който работи от известно време, ще видиш редове GET / en modo …, които не си поискал ти, а на твоя компютър, според скоростта му, може да се появи един повече в края на този блок и тогава твоят блок 5 ще е различен. Поискал ги е Docker, не ти.

Опциите, които ще използваш всеки ден, всички от страницата на командата:

Опция Какво прави
--tail N (или -n N) само последните N реда; по подразбиране — всички
--follow (или -f) остава да слуша и ти показва новото, щом пристигне; излизаш с Ctrl+C
--since 10m само последните десет минути; приема и дата и час
--timestamps (или -t) поставя пред всеки ред часа, в който Docker го е получил

--tail има по-голямо значение, отколкото изглежда. Услуга, която работи от седмици, може да има милиони редове, а docker logs без ограничения ти задръства терминала. Добрият навик е първо docker logs --tail 50 <име>, а -f, ако искаш да продължиш да гледаш.

Две предупреждения, които ще ти спестят стряскания. Логът оцелява след docker stop, но не и след docker rm: живее с контейнера, така че можеш да го прочетеш, след като го спреш (ще го видиш в 2.10), и изчезва, когато го изтриеш. Ако контейнер е умрял по странен начин, прочети лога му, преди да го изтриеш. И docker logs вижда само онова, което главната програма пише в двата си изхода: ако програмата пише лога си във файл вътре в контейнера, docker logs не го вижда. Затова програмите, замислени за контейнери, като тестовата услуга и revisor, пишат всичко в стандартния изход.

2.6 Влизане в контейнера: docker exec

2.6.1 Образът няма shell

В ръководствата често ще виждаш тази команда: docker exec -it <име> sh, „за да влезеш в контейнера“. docker exec изпълнява програма вътре в контейнер, който вече работи; -i държи отворен стандартния вход, а -t му зачислява терминал, за да можеш да пишеш команди (docker container exec). Пробвай я с тестовата услуга — това прави fig02_03.sh (без -it, защото в скрипта няма кой да пише):

#!/usr/bin/env bash
# fig02_03.sh — exec: ejecutar un programa dentro de un contenedor que ya corre.
# La imagen del servicio de prueba no trae shell: se ve el error y lo que sí funciona.
set -uo pipefail
# Nombres y puerto por omisión: los del curso. Se pueden cambiar para no chocar
# con otra corrida en el mismo equipo (PREFIJO=... PUERTO=... ./figNN.sh).
PREFIJO="${PREFIJO:-curso-docker-}"
PUERTO="${PUERTO:-9000}"
NOMBRE="${PREFIJO}prueba"
IMAGEN="${PREFIJO}prueba:1.0.0"
trap 'docker rm -f "$NOMBRE" >/dev/null 2>&1 || true' EXIT

docker run -d --name "$NOMBRE" -p "127.0.0.1:${PUERTO}:9000" \
  --memory 64m --cpus 0.5 --pids-limit 50 "$IMAGEN" >/dev/null
for _ in $(seq 20); do curl -fs -o /dev/null "http://127.0.0.1:${PUERTO}/" && break; sleep 0.5; done

echo "== 1. Pedir un shell"
docker exec "$NOMBRE" sh
echo "código de salida: $?"

echo
echo "== 2. Ejecutar un programa que la imagen SÍ trae"
docker exec "$NOMBRE" /servicio-prueba healthcheck
echo "código de salida: $?"

echo
echo "== 3. Lo mismo, con el servicio en modo caído"
curl -s -X POST "http://127.0.0.1:${PUERTO}/modo/caido" >/dev/null
docker exec "$NOMBRE" /servicio-prueba healthcheck
echo "código de salida: $?"

echo
echo "== 4. exec en un contenedor detenido"
docker stop "$NOMBRE" >/dev/null
docker exec "$NOMBRE" /servicio-prueba healthcheck
echo "código de salida: $?"
$ ./fig02_03.sh
== 1. Pedir un shell
OCI runtime exec failed: exec failed: unable to start container process: exec: "sh": executable file not found in $PATH
código de salida: 127

== 2. Ejecutar un programa que la imagen SÍ trae
sano
código de salida: 0

== 3. Lo mismo, con el servicio en modo caído
no sano: GET / respondió 503
código de salida: 1

== 4. exec en un contenedor detenido
Error response from daemon: container 9601e9716cd5e38f75a59d4cfb0b59706907808aaefba701a04fb9244a931c4a is not running
código de salida: 1

Първият блок е грешката. Чети я отдясно наляво — така се четат почти всички грешки на Docker: причината е в края. exec: "sh": executable file not found in $PATH казва, че няма програма с име sh в папките, където се търсят програмите. Предходното, OCI runtime exec failed, казва кой го е открил: runc, програмата, която създава процесите на контейнерите (запозна се с нея в Урок 0). А кодът 127 е същият, който документацията на docker run запазва за „командата не е намерена“ (2.1.1); при exec го връща средата за изпълнение (runtime), със същото значение.

Това не е повреда на контейнера: това е дизайнерско решение. Образът на тестовата услуга е изграден върху минимална основа, от т.нар. distroless, която съдържа само програмата и най-необходимото, за да работи: няма sh, нито ls, нито cat, нито пакетен мениджър. Това има цена, която току-що плати: не можеш да „влезеш“ да разглеждаш. И има печалба, която Урок 5 използва за revisor: ако някой успее да изпълни нещо вътре в този контейнер, няма да намери инструменти, с които да продължи. Образ без shell е образ с по-малко неща, с които да се атакува.

2.6.2 Какво все пак може да се направи

Вторият блок показва, че exec работи перфектно с програма, която образът наистина има: самата /servicio-prueba, с подкомандата ѝ healthcheck. Програмата пита услугата, която работи до нея, дали отговаря, отпечатва sano и излиза с 0. Третият блок повтаря въпроса с услугата в неработещ режим: no sano: GET / respondió 503, и излиза с 1. Тази подкоманда е същата, която инструкцията HEALTHCHECK изпълнява на всеки пет секунди; току-що я изпълни ръчно. Забележи, че тук кодът не е 127, а 1: програмата е намерена и е изпълнена; просто е отговорила „не“.

Две правила за exec, които идват от документацията му (docker container exec):

  • Работи само докато главният процес на контейнера работи. Това е четвъртият блок: при спрян контейнер демонът отговаря container ... is not running. За да видиш какво се е случило със спрян контейнер, не се използва exec, а logs и inspect.
  • Командата трябва да е изпълним файл, а не shell ред. docker exec <име> "echo hola && ls" не работи; ако образът има shell, се пише docker exec <име> sh -c 'echo hola && ls'.

Когато образът има shell, като alpine:3.24.2 от Урок 0, прочутата команда работи и те оставя да пишеш вътре:

docker run -d --name curso-docker-dormilon --memory 16m --cpus 0.25 --pids-limit 20 \
  alpine:3.24.2 sleep 300
docker exec -it curso-docker-dormilon sh

Използвай я, за да гледаш, никога, за да поправяш. Научи го в Урок 0: онова, което промениш там вътре, живее в слоя на контейнера и си отива с него. И изтрий контейнера накрая: docker rm -f curso-docker-dormilon.

2.7 Какво знае Docker за един контейнер: inspect и stats

2.7.1 docker inspect с шаблон

docker inspect връща „информация на ниско ниво“ за обект на Docker (docker inspect): всичко, което демонът знае за него, в JSON. Това е много. fig02_04.sh го използва с шаблони, за да извади само важното, и сравнява контейнера с ограничения с един същия без ограничения:

#!/usr/bin/env bash
# fig02_04.sh — inspect y stats: lo que Docker sabe de un contenedor, con y sin límites.
set -euo pipefail
# Nombres y puerto por omisión: los del curso. Se pueden cambiar para no chocar
# con otra corrida en el mismo equipo (PREFIJO=... PUERTO=... ./figNN.sh).
PREFIJO="${PREFIJO:-curso-docker-}"
PUERTO="${PUERTO:-9000}"
CON="${PREFIJO}prueba"
SIN="${PREFIJO}sin-limites"
IMAGEN="${PREFIJO}prueba:1.0.0"
trap 'docker rm -f "$CON" "$SIN" >/dev/null 2>&1 || true' EXIT

docker run -d --name "$CON" -p "127.0.0.1:${PUERTO}:9000" \
  --memory 64m --cpus 0.5 --pids-limit 50 "$IMAGEN" >/dev/null
docker run -d --name "$SIN" "$IMAGEN" >/dev/null
for _ in $(seq 20); do curl -fs -o /dev/null "http://127.0.0.1:${PUERTO}/" && break; sleep 0.5; done

FORMATO='{{.Name}}: memoria={{.HostConfig.Memory}} cpus(nano)={{.HostConfig.NanoCpus}} pids={{.HostConfig.PidsLimit}} usuario={{.Config.User}}'

echo "== 1. inspect, con una plantilla: solo los campos que me importan"
docker inspect --format "$FORMATO" "$CON" "$SIN"

echo
echo "== 2. inspect: estado, puerto publicado y comando principal"
docker inspect --format 'estado={{.State.Status}} pid1={{.Config.Entrypoint}} puertos={{json .HostConfig.PortBindings}}' "$CON"

echo
echo "== 3. stats: lo que gastan ahora mismo, contra su límite"
docker stats --no-stream --format 'table {{.Name}}\t{{.MemUsage}}\t{{.PIDs}}' "$CON" "$SIN"

echo
echo "== 4. Cuántos renglones trae inspect completo"
docker inspect "$CON" | wc -l
$ ./fig02_04.sh
== 1. inspect, con una plantilla: solo los campos que me importan
/curso-docker-prueba: memoria=67108864 cpus(nano)=500000000 pids=50 usuario=65532:65532
/curso-docker-sin-limites: memoria=0 cpus(nano)=0 pids=<no value> usuario=65532:65532

== 2. inspect: estado, puerto publicado y comando principal
estado=running pid1=[/servicio-prueba] puertos={"9000/tcp":[{"HostIp":"127.0.0.1","HostPort":"9000"}]}

== 3. stats: lo que gastan ahora mismo, contra su límite
NAME                       MEM USAGE / LIMIT    PIDS
curso-docker-prueba        1.484MiB / 64MiB     7
curso-docker-sin-limites   1.57MiB / 15.59GiB   8

== 4. Cuántos renglones trae inspect completo
224

Целият първи блок е стабилен: тези числа излизат същите в твоя Mint. Разчети ги:

  • memoria=67108864 са 64 × 1024 × 1024 байта: 64-те MiB, които поиска, същото число, което Урок 0 прочете в memory.max.
  • cpus(nano)=500000000: Docker пази CPU в милиардни части от CPU; 500 милиона са половин CPU.
  • pids=50 — таванът на процесите.
  • usuario=65532:65532: услугата не работи като root. Решил го е образът, с инструкция USER, която Урок 3 те учи да пишеш. Сравни го със sleep от Урок 0, който работеше като потребител 0.

А редът на контейнера без ограничения казва memoria=0, cpus(nano)=0 и pids=<no value>. В това поле нулата не означава „никаква памет“: означава „без ограничение“. Така Docker пази „нищо не си ми поискал“. <no value> е онова, което шаблонът отпечатва, когато полето е празно: в пълния JSON на docker inspect то се появява като "PidsLimit": null. Когато преглеждаш чужд контейнер, нула тук е сигнал за тревога, а не за спокойствие.

Името излиза с наклонена черта отпред, /curso-docker-prueba. Това не е твоя грешка: така Docker пази имената вътрешно и затова съобщението за повторено име от „Грешката, която ще видиш“ също я съдържа.

Вторият блок показва други три полета. .State.Status е състоянието (running). .Config.Entrypoint е програмата, която образът стартира като главен процес. А {{json .HostConfig.PortBindings}} използва функцията json на шаблоните, за да отпечата съставно поле такова, каквото е: порт 9000 на контейнера е вързан към адрес 127.0.0.1, порт 9000, на компютъра ти.

Четвъртият блок обяснява защо се използват шаблони: пълният inspect на този контейнер, който е от простите, има 224 реда. Да го прочетеш целия е полезно веднъж, за да знаеш какво има. За конкретен въпрос — шаблон.

2.7.2 docker stats: какво харчи точно сега

inspect казва какви са ограниченията; docker stats казва колко се харчи. По подразбиране е екран, който се обновява сам, като top; с --no-stream прави едно-единствено отчитане и приключва — това е нужно на един скрипт (docker container stats). Колоната MEM USAGE / LIMIT показва паметта, която контейнерът използва, срещу тази, която му е позволена, а PIDS — колко процеса и нишки е създал (docker container stats).

Тестовата услуга използва между 1 и 2 MiB и има няколко нишки (7 и 8 при това изпълнение; числото варира от изпълнение до изпълнение). Важна е колоната на ограничението. С ограничения казва 64MiB. Без ограничения казва 15.59GiB, което е цялата памет на машината, на която е изпълнено (виртуалната машина на Colima). В твоя Mint това число ще бъде паметта на компютъра ти. Това е сцената от Урок 0, видяна от Docker: контейнер без ограничение има за ограничение целия компютър.

2.8 Портовете: 127.0.0.1 срещу всички интерфейси

2.8.1 Какво означава адресът

Компютърът ти има няколко мрежови интерфейса, които са като врати, през които влизат връзки: този на безжичната мрежа, може би този на кабела, и един специален, който се казва loopback, с адрес 127.0.0.1. Loopback не води никъде извън компютъра ти: онова, което влиза оттам, може да идва само от собствения ти компютър.

Когато публикуваш порт без адрес, с -p 9000:9000, Docker го публикува на всички интерфейси. Документацията го казва без заобикалки: ако не дадеш адрес, демонът публикува портовете на всички адреси на хоста, 0.0.0.0 и [::], и предупреждава в карето, че „публикуването на портове на контейнер е несигурно по подразбиране“, защото портът става достъпен не само за хоста, а и „за външния свят“ (Docker, „Port publishing and mapping“). С 127.0.0.1 отпред „само хостът има достъп“ до публикувания порт (същият източник). Това важи от Docker 28 нататък: същата страница предупреждава, че във версии преди 28.0.0 компютрите от същия мрежов сегмент можеха да стигнат до портовете, публикувани на 127.0.0.1. Това е още една причина да инсталираш Docker от официалното хранилище, както в Урок 1, а не стара версия.

fig02_05.sh стартира две копия на услугата, по едно от всеки вид, и пита Docker и операционната система къде са останали:

#!/usr/bin/env bash
# fig02_05.sh — Publicar en 127.0.0.1 contra publicar en todas las interfaces.
# Se corre en el anfitrión (tu Mint): ss lista los puertos en los que escucha el equipo.
set -euo pipefail
# Nombres y puerto por omisión: los del curso. Se pueden cambiar para no chocar
# con otra corrida en el mismo equipo (PREFIJO=... PUERTO=... ./figNN.sh).
PREFIJO="${PREFIJO:-curso-docker-}"
PUERTO="${PUERTO:-9000}"
ABIERTO=$((PUERTO + 1))        # el segundo puerto, el que se abre a propósito
IMAGEN="${PREFIJO}prueba:1.0.0"
trap 'docker rm -f "${PREFIJO}prueba" "${PREFIJO}abierto" >/dev/null 2>&1 || true' EXIT

docker run -d --name "${PREFIJO}prueba" -p "127.0.0.1:${PUERTO}:9000" \
  --memory 64m --cpus 0.5 --pids-limit 50 "$IMAGEN" >/dev/null
docker run -d --name "${PREFIJO}abierto" -p "${ABIERTO}:9000" \
  --memory 64m --cpus 0.5 --pids-limit 50 "$IMAGEN" >/dev/null
for p in "$PUERTO" "$ABIERTO"; do
  for _ in $(seq 20); do curl -fs -o /dev/null "http://127.0.0.1:$p/" && break; sleep 0.5; done
done

echo "== 1. Lo que dice docker port"
echo "-- ${PREFIJO}prueba:"; docker port "${PREFIJO}prueba"
echo "-- ${PREFIJO}abierto:"; docker port "${PREFIJO}abierto"

echo
echo "== 2. Lo que dice el sistema operativo: dónde escucha el equipo"
ss -ltn | grep -E ":(${PUERTO}|${ABIERTO}) " | awk '{print $4}' | sort
$ ./fig02_05.sh
== 1. Lo que dice docker port
-- curso-docker-prueba:
9000/tcp -> 127.0.0.1:9000
-- curso-docker-abierto:
9000/tcp -> 0.0.0.0:9001
9000/tcp -> [::]:9001

== 2. Lo que dice el sistema operativo: dónde escucha el equipo
0.0.0.0:9001
127.0.0.1:9000
[::]:9001

docker port показва за даден контейнер кой негов порт към кой адрес и порт на компютъра ти води. ss -ltn е обикновен инструмент на Linux, който изброява портовете, на които компютърът слуша за TCP връзки (-l слушащи, -t TCP, -n с числа вместо имена); grep оставя само редовете на портове 9000 и 9001, а awk запазва колоната с адреса.

Двата източника казват едно и също. Правилно публикуваният контейнер слуша само на 127.0.0.1:9000. Другият слуша на 0.0.0.0:9001, което на езика на мрежите означава „на всички IPv4 адреси на този компютър“, и на [::]:9001 — същото за IPv6. Ако твоят Mint е свързан към мрежата на кафене, университет или офис, всеки в тази мрежа може да напише адреса на компютъра ти и порт 9001 и да говори с контейнера ти. Тестовата услуга е безобидна; тестова база данни с примерна парола — не.

2.8.2 Защитната стена не те спасява

Може би си мислиш, че затова имаш защитна стена. В Mint защитната стена на работния плот е ufw. Ето капана: портовете, които Docker публикува, минават над правилата на ufw. Справочникът на docker run предупреждава за това: портовете, публикувани на всички интерфейси, са достъпни отвън, дори ufw да е конфигуриран да ги блокира, защото Docker управлява собствените си правила за филтриране на пакети (docker container run). Ръководството на Docker за защитните стени обяснява механизма: трафикът на контейнер „се отклонява, преди да мине през конфигурацията на ufw“, а пакетите се маршрутизират „преди правилата на защитната стена да могат да бъдат приложени“ (Docker, „Packet filtering and firewalls“).

Не го измервай в мрежата си без разрешение. Достатъчно е да го знаеш и да стигнеш до извода: защитата е в -p, а не в защитната стена. Който наистина трябва да филтрира трафика към контейнерите, има веригата DOCKER-USER (1.3), но това е работа с правила на iptables, писани ръчно, а не с ufw такъв, какъвто идва, и не променя извода за обикновен работен плот. Затова в този курс всички портове се публикуват с 127.0.0.1: отпред, а единственият, който няма този префикс, е 9001 от този скрипт, който съществува, за да го видиш, и се изтрива накрая.

2.9 Ограниченията: памет, CPU и процеси

Урок 0 ти показа какво правят ограниченията отвътре: Docker записва число във файл на ядрото и ядрото го налага. Тук въпросът е практически: какви числа да сложиш.

Документацията на Docker е ясна за това какво става, ако не сложиш никакви: контейнерът „няма ограничения на ресурсите“ и използва всичко, което планировчикът на ядрото му позволи; а когато компютърът остане без памет, ядрото започва да убива процеси, който и да е процес, включително самия Docker, и това „може да срине цялата система, ако бъде убит грешният процес“ (Docker, „Resource constraints“). Трите ограничения в командата от този урок:

--memory 64m. Тестовата услуга използва между 1 и 2 MiB (измери го в 2.7.2). 64 MiB ѝ дават над трийсет пъти резерв. Общото правило е такова: първо мери, а после сложи щедро ограничение над измереното, а не под него. Ако програмата надхвърли ограничението, ядрото я убива вътре в собствената ѝ група, както видя в Урок 0 (код 137 и OOMKilled=true), и никой друг на компютъра ти не разбира. Документацията определя минимум от 6 MB за тази опция (Docker, „Resource constraints“).

--cpus 0.5. Половин CPU. Услуга, която само отговаря на малки заявки, няма нужда от повече, а ако по грешка влезе в безкраен цикъл, изяжда половин CPU, а не целия. Документацията го обяснява със собствен пример: на компютър с два CPU --cpus="1.5" гарантира на контейнера най-много един и половина CPU (Docker, „Resource constraints“). За разлика от паметта, надхвърлянето на CPU не убива никого: ядрото просто му дава по-малко време (казва се, че го задушава — throttling) и програмата работи по-бавно. Внимавай с едно косвено последствие: твърде задушена услуга може да отговаря по-бавно, отколкото търпи проверката ѝ на състоянието (2.3), и тогава Docker ще я отбележи като unhealthy, без нищо да я е убило.

--pids-limit 50. Задава тавана на процесите на контейнера; с -1 се премахва (docker container run). Това ограничение защитава от нещо много конкретно: програма, която по грешка или при атака създава процеси без спиране. Без таван тя може да напълни таблицата на процесите на компютъра и да те остави без възможност да отвориш дори терминал. Услугата имаше 7 или 8 (2.7.2); 50 ѝ стигат с излишък.

Забележи, че трите ограничения се слагат още от първия контейнер, а не „когато услугата е в продукция“. Урок 0 го каза със сцената с базата данни: ограничението не е за нормалния ден, а за лошия, а лошият ден не предупреждава.

2.10 Жизненият цикъл: спиране, повторно стартиране и изтриване

2.10.1 Състоянията

Контейнерът минава през състояния и всяка команда го премества от едно в друго:

Състояние Как стига дотам Какво се вижда в docker ps -a
създаден (created) docker create или docker run, който не е успял да стартира Created
работещ (running) docker run или docker start Up …
спрян (exited) docker stop или главната програма е приключила сама Exited (код) …
изтрит docker rm вече не се появява

Най-важното в таблицата е разликата между спрян и изтрит. Спрелият контейнер не работи, но съществува: пази слоя си за запис, лога си, конфигурацията си и името си. Може да бъде стартиран отново. Изтритият контейнер вече не съществува и с него си отиват слоят и логът му.

fig02_07.sh минава през целия цикъл и завършва с проверката за почистване:

#!/usr/bin/env bash
# fig02_07.sh — El ciclo de vida completo y la limpieza: stop, start, rm y --rm.
set -uo pipefail
# Nombres y puerto por omisión: los del curso. Se pueden cambiar para no chocar
# con otra corrida en el mismo equipo (PREFIJO=... PUERTO=... ./figNN.sh).
PREFIJO="${PREFIJO:-curso-docker-}"
PUERTO="${PUERTO:-9000}"
NOMBRE="${PREFIJO}prueba"
IMAGEN="${PREFIJO}prueba:1.0.0"
trap 'docker rm -f "$NOMBRE" "${PREFIJO}efimero" >/dev/null 2>&1 || true' EXIT
ms() { date +%s%3N; }

docker run -d --name "$NOMBRE" -p "127.0.0.1:${PUERTO}:9000" \
  --memory 64m --cpus 0.5 --pids-limit 50 "$IMAGEN" >/dev/null
for _ in $(seq 20); do curl -fs -o /dev/null "http://127.0.0.1:${PUERTO}/" && break; sleep 0.5; done

echo "== 1. Borrar un contenedor que está corriendo, sin -f"
docker rm "$NOMBRE"
echo "código de salida: $?"

echo
echo "== 2. Detenerlo, midiendo cuánto tarda"
inicio=$(ms); docker stop "$NOMBRE" >/dev/null; fin=$(ms)
echo "docker stop tardó $(( fin - inicio )) ms"
docker ps -a --filter name="$NOMBRE" --format '{{.Names}} {{.Status}}'
docker logs --tail 2 "$NOMBRE"

echo
echo "== 3. Detenido no es borrado: vuelve a arrancar con start"
docker start "$NOMBRE" >/dev/null
docker ps --filter name="$NOMBRE" --format '{{.Names}} {{.Status}}'

echo
echo "== 4. Ahora sí: borrarlo"
docker stop "$NOMBRE" >/dev/null
docker rm "$NOMBRE"

echo
echo "== 5. Un contenedor con --rm se borra solo al terminar"
# Lo que va después de la imagen reemplaza al comando por omisión: aquí, en vez
# de servir, el programa corre su subcomando healthcheck y termina.
docker run --rm --name "${PREFIJO}efimero" "$IMAGEN" healthcheck
echo "código de salida: $?"
docker ps -a --filter name="${PREFIJO}efimero" --format '{{.Names}}' | wc -l

echo
echo "== 6. La revisión final: ningún contenedor del curso"
docker ps -a --filter name="^${PREFIJO}" --format '{{.Names}}' | wc -l
$ ./fig02_07.sh
== 1. Borrar un contenedor que está corriendo, sin -f
Error response from daemon: cannot remove container "curso-docker-prueba": container is running: stop the container before removing or force remove
código de salida: 1

== 2. Detenerlo, midiendo cuánto tarda
docker stop tardó 414 ms
curso-docker-prueba Exited (0) Less than a second ago
servicio-prueba: GET / en modo sano
servicio-prueba: recibí terminated, apagando

== 3. Detenido no es borrado: vuelve a arrancar con start
curso-docker-prueba Up Less than a second (health: starting)

== 4. Ahora sí: borrarlo
curso-docker-prueba

== 5. Un contenedor con --rm se borra solo al terminar
no sano: Get "http://127.0.0.1:9000/": dial tcp 127.0.0.1:9000: connect: connection refused
código de salida: 1
0

== 6. La revisión final: ningún contenedor del curso
0

Времената се променят при всяко изпълнение; всичко останало излиза същото.

2.10.2 docker rm не изтрива онова, което работи

Първият блок е защита: Docker не те оставя да изтриеш контейнер, който работи. Съобщението ти дава двата изхода: да го спреш преди това (stop the container before removing) или да принудиш изтриването (force remove). Принуждаването е docker rm -f, а документацията казва какво прави: изпраща SIGKILL на главния процес и след това изтрива контейнера (docker container rm). SIGKILL е сигнал 9, който Урок 0 те научи да разпознаваш в код 137: не може да бъде уловен, така че програмата няма възможност да затвори нищо. За тестов контейнер, който ще изхвърлиш, това е наред и trap-овете на тези скриптове го използват. За услуга с данни — първо stop.

2.10.3 docker stop: сигналът и срокът

docker stop е учтивият начин. Документацията казва, че изпраща на главния процес сигнала SIGTERM (освен ако образът или контейнерът не са конфигурирали друг), чака определен срок — по подразбиране 10 секунди в Linux — и ако контейнерът все още е жив, го убива със SIGKILL. Срокът се променя с --timeout (docker container stop).

Вторият блок показва, че тестовата услуга спира правилно. docker stop отне 414 ms при това изпълнение (машината беше натоварена), много по-малко от 10-те секунди на срока. Състоянието е Exited (0): програмата е приключила сама и с код 0, което в Linux означава „всичко е наред“. А логът, който остава и след спирането, показва защо: recibí terminated, apagando. Услугата е написана да обработва SIGTERM (terminated е начинът, по който Go нарича този сигнал): получава го, спира да приема заявки и излиза. Ако програмата пренебрегваше сигнала, щеше да видиш, че docker stop отнема 10 секунди, и състоянието Exited (137). Защо една програма понякога не получава сигнала, макар да е написана да го обработва, е реален проблем със собствено име — този на процес номер 1 — и Уроци 3 и 7 се занимават с него.

2.10.4 docker start: спрян не значи изтрит

Третият блок стартира отново същия контейнер с docker start. Не е нужно да повтаряш опциите: името, портът и ограниченията са запазени в контейнера. docker start служи, за да продължиш нещо, което си спрял. Ограниченията за памет, CPU и процеси наистина могат да се променят, без контейнерът да се пресъздава, с docker update (docker container update), който този курс не използва в скриптовете си, но fig02_10.sh го изпробва: с работещ контейнер docker update --memory 128m --memory-swap 128m --cpus 1 --pids-limit 100 остави memoria=134217728 cpus(nano)=1000000000 pids=100, контейнерът продължи да работи, без рестартирания, а -p не съществува като опция на update. (--memory-swap върви заедно с --memory, защото иначе docker update се оплаква, когато вече зададеният таван на swap остане под новата памет.) Онова, което не може да се промени, е публикуваният порт, нито образът: за това трябва да изтриеш контейнера и да създадеш нов с docker run. Ето fig02_10.sh:

#!/usr/bin/env bash
# fig02_10.sh — docker update: cambiar los límites de un contenedor sin recrearlo.
# El puerto publicado y la imagen, en cambio, sí exigen un contenedor nuevo.
set -uo pipefail
# Nombres y puerto por omisión: los del curso. Se pueden cambiar para no chocar
# con otra corrida en el mismo equipo (PREFIJO=... PUERTO=... ./figNN.sh).
PREFIJO="${PREFIJO:-curso-docker-}"
PUERTO="${PUERTO:-9000}"
IMAGEN="${PREFIJO}prueba:1.0.0"
NOMBRE="${PREFIJO}prueba"
trap 'docker rm -f "$NOMBRE" >/dev/null 2>&1 || true' EXIT
FORMATO='memoria={{.HostConfig.Memory}} cpus(nano)={{.HostConfig.NanoCpus}} pids={{.HostConfig.PidsLimit}} puertos={{json .HostConfig.PortBindings}}'

docker run -d --name "$NOMBRE" -p "127.0.0.1:${PUERTO}:9000" \
  --memory 64m --cpus 0.5 --pids-limit 50 "$IMAGEN" >/dev/null

echo "== 1. Antes"
docker inspect --format "$FORMATO" "$NOMBRE"

echo
echo "== 2. docker update de los tres límites, con el contenedor corriendo"
docker update --memory 128m --memory-swap 128m --cpus 1 --pids-limit 100 "$NOMBRE"
echo "código de salida: $?"

echo
echo "== 3. Después (mismo contenedor, sin reiniciar)"
docker inspect --format "$FORMATO" "$NOMBRE"
docker inspect --format 'sigue corriendo: {{.State.Running}}, reinicios: {{.RestartCount}}' "$NOMBRE"

echo
echo "== 4. El puerto publicado no tiene opción en update"
docker update -p 127.0.0.1:9002:9000 "$NOMBRE" 2>&1 | head -2

Реално изпълнение:

$ ./fig02_10.sh
== 1. Antes
memoria=67108864 cpus(nano)=500000000 pids=50 puertos={"9000/tcp":[{"HostIp":"127.0.0.1","HostPort":"9000"}]}

== 2. docker update de los tres límites, con el contenedor corriendo
curso-docker-prueba
código de salida: 0

== 3. Después (mismo contenedor, sin reiniciar)
memoria=134217728 cpus(nano)=1000000000 pids=100 puertos={"9000/tcp":[{"HostIp":"127.0.0.1","HostPort":"9000"}]}
sigue corriendo: true, reinicios: 0

== 4. El puerto publicado no tiene opción en update
unknown shorthand flag: 'p' in -p

Четвъртият блок почиства по ред: stop и после rm, който отпечатва името на това, което е изтрито.

2.10.5 --rm: контейнери, които се изтриват сами

За контейнерите, които само изпълняват една задача и приключват, съществува --rm: според документацията изтрива контейнера и анонимните му томове автоматично, когато приключи (томовете са тема на Урок 8) (docker container run). Петият блок го използва с нов детайл: след името на образа стои думата healthcheck. Онова, което пишеш след образа, се подава на главната програма, и тук превръща услугата в неин собствен клиент: вместо да обслужва, изпълнява проверката на състоянието и приключва. Тъй като вътре в този контейнер никой не обслужва на порт 9000, проверката се проваля с connection refused и код 1. А 0 след това казва, че контейнерът вече не съществува: --rm го е изтрил.

Един детайл, измерен при писането на този урок: с docker run -d --rm изтриването става миг след като контейнерът спре, а не преди docker stop да се върне. В една първа версия на този скрипт, която правеше docker stop на контейнер -d --rm и веднага броеше, броенето даде 1: спрелият контейнер оставаше там няколко милисекунди. Затова скриптът използва формата без -d, при която docker run не се връща, докато контейнерът не приключи и не бъде изтрит. Ако някога твой скрипт брои контейнери веднага след спирането на такъв с --rm, вече знаеш откъде идва излишното число.

2.11 Да не оставяш остатъци

2.11.1 Само твоето

Шестият блок на fig02_07.sh е проверката, която ще правиш в края на всеки урок от курса:

docker ps -a --filter name='^curso-docker-' --format '{{.Names}}' | wc -l

-a включва спрените, които са онези, които се забравят. Филтърът name=^curso-docker- оставя само контейнерите, чието име започва с curso-docker-: ^ отбелязва началото на името. Документацията казва само, че филтърът съвпада с цялото или част от името, затова беше проверено при писането на този урок, с блок 4 на fig02_06.sh: с три контейнера (curso-docker-prueba, curso-docker-otro и един viejo-curso-docker-x) --filter name=prueba намери един, но --filter name='^prueba' не намери нито един, макар името да съдържа prueba в средата: ^ наистина закотвя в началото. А --filter name=curso-docker- намери трите, докато --filter name='^curso-docker-' — само двата, които започват така. Без ^ филтър по curso-docker- хваща и viejo-curso-docker-x, който не е твой. А wc -l брои редове. Ако излезе 0, не си оставил нищо.

За да изтриеш всичко свое наведнъж, същият списък служи като вход за docker rm -f:

docker ps -aq --filter name='^curso-docker-' | xargs -r docker rm -f

-q иска само идентификаторите (docker container ls), xargs ги подава като аргументи на следващата команда, а -r кара xargs да не изпълнява нищо, ако списъкът е празен.

Това е дълбоката причина за правилото на курса всичко, което създаваш, да започва с curso-docker-: позволява ти да чистиш само своето, без да пипаш нищо друго на компютъра.

2.11.2 Командата, която не бива да използваш на сляпо

Рано или късно ще прочетеш в някое ръководство docker container prune. Прави точно онова, което казва документацията му: „изтрива всички спрени контейнери“ (docker container prune). Всички: твоите, тези на други проекти, които имаш на същия компютър, и тези на всеки инструмент, който използва Docker отдолу. Спрян контейнер може да е базата данни на друг проект, която си оставил изключена нарочно, с конфигурацията ѝ, запазена в контейнера. prune не пита чий е; пита един-единствен път „сигурен ли си?“, а с --force — дори и това не. Приема филтри по label и по възраст, но не и по име (docker container prune). Самата документация на docker rm го препоръчва за изтриване на всички спрени (docker container rm); това е наред на машина, посветена на едно-единствено нещо. На твоята предпочитай филтъра по име: изтрива онова, за което знаеш, че е твое. Същото важи за роднините му docker system prune и docker volume prune, които изтриват още повече.

Грешката, която ще видиш

Те са две, тези от сцената в началото, и двете ги предизвиква fig02_06.sh:

#!/usr/bin/env bash
# fig02_06.sh — Los dos errores de todos los días: nombre repetido y puerto ocupado.
# Ojo con el segundo: el contenedor que falla SÍ queda creado.
set -uo pipefail
# Nombres y puerto por omisión: los del curso. Se pueden cambiar para no chocar
# con otra corrida en el mismo equipo (PREFIJO=... PUERTO=... ./figNN.sh).
PREFIJO="${PREFIJO:-curso-docker-}"
PUERTO="${PUERTO:-9000}"
IMAGEN="${PREFIJO}prueba:1.0.0"
trap 'docker rm -f "${PREFIJO}prueba" "${PREFIJO}otro" "viejo-${PREFIJO}x" >/dev/null 2>&1 || true' EXIT

docker run -d --name "${PREFIJO}prueba" -p "127.0.0.1:${PUERTO}:9000" \
  --memory 64m --cpus 0.5 --pids-limit 50 "$IMAGEN" >/dev/null

echo "== 1. Otro contenedor con el MISMO nombre"
docker run -d --name "${PREFIJO}prueba" "$IMAGEN"
echo "código de salida: $?"

echo
echo "== 2. Otro contenedor en el MISMO puerto"
docker run -d --name "${PREFIJO}otro" -p "127.0.0.1:${PUERTO}:9000" \
  --memory 64m --cpus 0.5 --pids-limit 50 "$IMAGEN" | cut -c1-12
echo "código de salida: ${PIPESTATUS[0]}"

echo
echo "== 3. ¿Qué quedó?"
docker ps -a --filter name="^${PREFIJO}" --format 'table {{.Names}}\t{{.Status}}'

echo
echo "== 4. El filtro name y el ancla ^ (cuántos contenedores encuentra cada uno)"
docker create --name "viejo-${PREFIJO}x" "$IMAGEN" >/dev/null     # un nombre que lleva el prefijo en medio
echo "name=prueba           : $(docker ps -aq --filter name=prueba | wc -l)"
echo "name='^prueba'        : $(docker ps -aq --filter name='^prueba' | wc -l)"
echo "name=${PREFIJO}      : $(docker ps -aq --filter name="${PREFIJO}" | wc -l)"
echo "name='^${PREFIJO}'   : $(docker ps -aq --filter name="^${PREFIJO}" | wc -l)"
$ ./fig02_06.sh
== 1. Otro contenedor con el MISMO nombre
docker: Error response from daemon: Conflict. The container name "/curso-docker-prueba" is already in use by container "61860733f6c9d97f75ca78c406e3c29d849500f981d1c8a506de77775199b7c6". You have to remove (or rename) that container to be able to reuse that name.

Run 'docker run --help' for more information
código de salida: 125

== 2. Otro contenedor en el MISMO puerto
docker: Error response from daemon: failed to set up container networking: driver failed programming external connectivity on endpoint curso-docker-otro (1de3263e30d85ae5c235769e50302b3a7bba937a5a2d7b88698413e7b62995d3): Bind for 127.0.0.1:9000 failed: port is already allocated

Run 'docker run --help' for more information
ab4acba42a64
código de salida: 125

== 3. ¿Qué quedó?
NAMES                 STATUS
curso-docker-otro     Created
curso-docker-prueba   Up 5 seconds (health: starting)

== 4. El filtro name y el ancla ^ (cuántos contenedores encuentra cada uno)
name=prueba           : 1
name='^prueba'        : 0
name=curso-docker-      : 3
name='^curso-docker-'   : 2

Дългите идентификатори се променят при всяко изпълнение; съобщенията и кодовете излизат същите. ${PIPESTATUS[0]} е променлива на bash, която пази кода на изход на първата команда в конвейер (pipeline); нужна е, защото $? би дал този на cut.

Повтореното име. Съобщението казва всичко: има конфликт (Conflict), името /curso-docker-prueba вече се използва от контейнера с еди-кой си идентификатор и за да използваш името отново, трябва да изтриеш или преименуваш този контейнер. Код 125: провалил се е Docker, не услугата. Коварното е, че контейнерът, който държи името, може да е спрян: не се появява в docker ps, а само в docker ps -a. Поправката, по ред на предпочитание: ако старият вече не е нужен, docker rm (или docker rm -f, ако работи); ако искаш да го запазиш, docker rename <стар> <нов>; или използвай друго име за новия.

Заетият порт. Чете се отдясно наляво: причината е port is already allocated, портът вече е зает; опитът е бил Bind for 127.0.0.1:9000, да се върже към този адрес и този порт; а онова, което се е опитвало, е мрежовата конфигурация на контейнера curso-docker-otro. Отново 125. За да разбереш кой държи порта — docker ps в колоната PORTS; ако не е контейнер, ss -ltnp ти казва коя програма на компютъра го използва (p иска процеса; за да видиш процеси на други потребители, е нужен sudo). Поправката: спри онзи, който го държи, или публикувай своя на друг порт на хоста (-p 127.0.0.1:9002:9000), което не променя нищо вътре в контейнера.

И детайлът, който никой не ти казва. Обърни внимание на 1dad19130213, който се появява след грешката за порта: това е идентификатор на контейнер. docker run -d го е отпечатал, защото контейнерът е бил създаден; провалило се е стартирането му. Третият блок го потвърждава: curso-docker-otro е останал в състояние Created. Не работи, не харчи памет, но задържа името си. Ако поправиш порта и повториш командата със същото име, следващата грешка, която ще видиш, е тази за повтореното име, и ще си помислиш, че Docker е полудял. Не: това са две верижни грешки. След „зает порт“ изтрий останалия контейнер: docker rm curso-docker-otro. И не се опитвай да го спасиш с docker start, когато портът се освободи: упражнение 1 ти показва какво става.

Грешката за повтореното име със спрян контейнер и тази за порта с неговия контейнер призрак са, заедно, сцената от началото. С docker ps -a и филтъра от 2.11 излизат за десет секунди.

Навикът за сигурност на урока

Портове, публикувани само на 127.0.0.1, ограничения още от първия контейнер и никога --privileged, нито socket-ът на Docker вътре в контейнер.

Първите два вече ги упражни. Публикуването на 127.0.0.1 не позволява тестов контейнер да остане достъпен за всеки в мрежата ти, а защитната стена не те покрива (2.8). Ограниченията не позволяват грешка в един контейнер да срине останалите програми на компютъра ти (2.9).

Третият се казва тук, защото в ежедневните ръководства ще срещаш команди, които го нарушават, и е добре да ги разпознаваш, преди да ги поставиш. Урок 0 те научи, че --privileged дава на контейнера всички capabilities и достъп до устройствата на хоста (Docker, „Running containers“): от стената почти нищо не остава. И те научи, че демонът работи като root и че само доверени потребители трябва да могат да го управляват (Docker Engine security), дотолкова, че групата docker е равносилна на root (Docker, стъпки след инсталирането в Linux). Монтирането на socket-а вътре в контейнер — в ръководствата изглежда като -v /var/run/docker.sock:/var/run/docker.sock — дава на този контейнер точно тази власт: може да поиска от демона да създаде друг контейнер с монтирана коренова папка на компютъра ти, като root. Ако команда, която ще копираш, съдържа което и да е от двете, спри се и се запитай дали наистина ти трябва. В този курс — никога.

Какво се прави погрешно

Да публикуваш портове без адрес, „защото така е в примера“. -p 9000:9000 публикува на всички интерфейси, а ufw не го спира. Пиши винаги -p 127.0.0.1:9000:9000 и когато наистина ти трябва друга машина да стига до контейнера, реши го съзнателно.

Да стартираш без ограничения, „защото е само тест“. Тестовете са точно мястото, където програмата има грешки. --memory, --cpus и --pids-limit струват един ред.

Да четеш Up като „работи“. Up казва, че процесът съществува. Видя го в 2.4: услугата отговаряше с 503, а Docker продължаваше да казва Up. За да разбереш дали обслужва, питай услугата.

Да гледаш само docker ps. Без -a не виждаш спрените, а те са онези, които задържат имена и диск. За да прегледаш компютъра си — docker ps -a.

Да използваш docker exec, за да поправяш. Онова, което промениш, си отива с контейнера. Освен това в минималните образи, като този на тестовата услуга, дори няма shell, и е добре, че няма.

docker rm -f като рефлекс. Убива със SIGKILL, без да остави програмата да затвори онова, което има отворено. За онова, което има данни — първо docker stop.

Да изтриеш контейнер, който се е провалил, без да прочетеш лога му. Логът си отива с него. Първо docker logs --tail 50, после rm.

Да чистиш с prune на споделен компютър. Изтрива на всички. Филтрирай по своя префикс.

Да оставиш контейнера призрак от заетия порт. Остава в Created със заето име; изтрий го, преди да опиташ отново.

Упражнения

Упражнения 1 и 4 са за разсъждение; 2 и 3 се правят на твоя Mint. Запиши предвижданията си в дневника, преди да видиш решенията.

Упражнение 1 — Предскажи docker ps -a

Тръгваш от компютър без контейнери curso-docker-. Пускаш това, поред:

docker run -d --name curso-docker-a -p 127.0.0.1:9000:9000 curso-docker-prueba:1.0.0
docker run -d --name curso-docker-b -p 127.0.0.1:9000:9000 curso-docker-prueba:1.0.0
docker stop curso-docker-a
docker run --rm --name curso-docker-c curso-docker-prueba:1.0.0 healthcheck

От тези команди са махнати ограниченията, за да се поберат на страницата и последователността да се чете по-лесно; на компютъра си им добави --memory 64m --cpus 0.5 --pids-limit 50, което не променя отговора.

  1. Кои от четирите команди завършват с грешка и с какъв код?
  2. Кои контейнери се появяват в docker ps -a --filter name=^curso-docker- и в какво състояние?
  3. Ако сега пуснеш docker start curso-docker-b, ще стартира ли? Защо?
  4. Кои две команди оставят компютъра чист?

Упражнение 2 — Друг порт, друг бюджет

Стартирай тестовата услуга при тези условия: име curso-docker-ejercicio, ти да говориш с нея през порт 9002 на компютъра си (само от компютъра си), и с най-много 32 MiB памет, една четвърт CPU и 20 процеса. После:

  1. Провери с curl, че отговаря sano на порт 9002.
  2. Предскажи какво ще кажат memoria= и cpus(nano)= с шаблона от fig02_04.sh и го провери.
  3. Предскажи какво ще каже docker stats --no-stream в колоната с ограничението на паметта.
  4. Сложи я в неработещ режим и провери, че docker ps продължава да казва Up.
  5. Остави всичко чисто.

Упражнение 3 — Логът като доказателство (проект)

Представи си, че revisor е съобщил в 10:04, че тестовата услуга не работи, а ти искаш да го разбереш, без да зависиш от revisor. Стартирай тестовата услуга, направи ѝ три заявки в режим здрав, смени я на неработещ, направи ѝ още две заявки и я върни в здрав. После, като използваш само docker logs и опциите му:

  1. Покажи само последните три реда.
  2. Покажи лога с часа на всеки ред.
  3. Покажи само последната минута.
  4. Спри контейнера и докажи, че логът още е там. Изтрий го и докажи, че вече го няма.

Упражнение 4 — Обясни го на онзи, който го е направил

Колегата ти от сцената в началото ти казва: „Имам активен ufw, така че да публикувам с -p 9000:9000 няма нищо лошо“. С най-много пет изречения му обясни защо има, и какво е трябвало да напише. Използвай думите интерфейс, 127.0.0.1 и защитна стена.

Решения

Решение 1

  1. Проваля се втората: порт 9000 на 127.0.0.1 вече е зает от curso-docker-a; завършва с port is already allocated и код 125. Четвъртата не е грешка на Docker, но завършва с код 1: подкомандата healthcheck не намира никой да обслужва вътре в този контейнер (connection refused), точно както в блок 5 на fig02_07.sh.
  2. Два: curso-docker-a в Exited (0), защото го спря, а услугата спира правилно със SIGTERM; и curso-docker-b в Created, контейнерът призрак от заетия порт. curso-docker-c не се появява: имаше --rm, а docker run без -d не се връща, докато не го изтрие.
  3. Стартира, но без порта си, и това е отговорът, който почти никой не предсказва. Измерено при писането на урока, с Docker 29.2.1: docker start curso-docker-b завърши с код 0 и docker ps го показа Up, но колоната PORTS излезе празна, docker port не отпечата нищо, а curl към 127.0.0.1:9000 не получи отговор. docker inspect показваше разликата: в .HostConfig.PortBindings все още беше поискан 127.0.0.1:9000, но в .NetworkSettings.Ports, онова, което наистина е свързано, нямаше нищо. Ако си предсказал „стартира и използва 9000, който вече се е освободил“, разсъждението ти е било логичното; контейнерът призрак просто не се държи така. Това е поведение на измерената версия (29.2.1) и не мога да кажа дали по-късна версия го поправя; ако при теб стартира с порта, толкова по-добре: правилото не се променя — след „зает порт“ контейнерът се изтрива и се създава отново. Това е още една причина за правилото от „Грешката, която ще видиш“: след зает порт изтрий контейнера, останал в Created, и го създай наново с docker run, вместо да го спасяваш с docker start.
  4. Една за изтриване и друга за проверка: docker ps -aq --filter name='^curso-docker-' | xargs -r docker rm -f и след това docker ps -a --filter name='^curso-docker-' --format '{{.Names}}' | wc -l, която трябва да отпечата 0.

Точки 1, 2 и 3 бяха проверени, като същата последователност беше изпълнена на машината на изпълненията, с fig02_08.sh (с поставени ограничения, което не променя отговора):

#!/usr/bin/env bash
# fig02_08.sh — Ejercicio 1: la secuencia de cuatro comandos y qué queda en docker ps -a.
# Muestra el contenedor fantasma del puerto ocupado y qué pasa si lo rescatas con start.
set -uo pipefail
# Nombres y puerto por omisión: los del curso. Se pueden cambiar para no chocar
# con otra corrida en el mismo equipo (PREFIJO=... PUERTO=... ./figNN.sh).
PREFIJO="${PREFIJO:-curso-docker-}"
PUERTO="${PUERTO:-9000}"
IMAGEN="${PREFIJO}prueba:1.0.0"
trap 'docker rm -f "${PREFIJO}a" "${PREFIJO}b" "${PREFIJO}c" >/dev/null 2>&1 || true' EXIT
LIMITES="--memory 64m --cpus 0.5 --pids-limit 50"

echo "== 1. docker run -d --name a -p 127.0.0.1:PUERTO:9000"
docker run -d --name "${PREFIJO}a" -p "127.0.0.1:${PUERTO}:9000" $LIMITES "$IMAGEN" >/dev/null
echo "código de salida: $?"
for _ in $(seq 20); do curl -fs -o /dev/null "http://127.0.0.1:${PUERTO}/" && break; sleep 0.5; done

echo
echo "== 2. docker run -d --name b -p 127.0.0.1:PUERTO:9000 (el mismo puerto)"
docker run -d --name "${PREFIJO}b" -p "127.0.0.1:${PUERTO}:9000" $LIMITES "$IMAGEN" | cut -c1-12
echo "código de salida: ${PIPESTATUS[0]}"

echo
echo "== 3. docker stop a"
docker stop "${PREFIJO}a" >/dev/null
echo "código de salida: $?"

echo
echo "== 4. docker run --rm --name c … healthcheck"
docker run --rm --name "${PREFIJO}c" $LIMITES "$IMAGEN" healthcheck
echo "código de salida: $?"

echo
echo "== 5. ¿Qué quedó?"
docker ps -a --filter name="^${PREFIJO}" --format 'table {{.Names}}\t{{.Status}}'

echo
echo "== 6. Con el puerto ya libre, docker start b"
docker start "${PREFIJO}b"
echo "código de salida: $?"
sleep 1
docker ps --filter name="${PREFIJO}b" --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
echo "docker port: [$(docker port "${PREFIJO}b")]"
curl -s -o /dev/null --max-time 3 -w 'curl al puerto: código HTTP %{http_code}\n' "http://127.0.0.1:${PUERTO}/"
echo "pedido:   $(docker inspect --format '{{json .HostConfig.PortBindings}}' "${PREFIJO}b")"
echo "conectado: $(docker inspect --format '{{json .NetworkSettings.Ports}}' "${PREFIJO}b")"

echo
echo "== 7. Dejar el equipo limpio"
docker ps -aq --filter name="^${PREFIJO}" | xargs -r docker rm -f >/dev/null
docker ps -a --filter name="^${PREFIJO}" --format '{{.Names}}' | wc -l

Реално изпълнение:

$ ./fig02_08.sh
== 1. docker run -d --name a -p 127.0.0.1:PUERTO:9000
código de salida: 0

== 2. docker run -d --name b -p 127.0.0.1:PUERTO:9000 (el mismo puerto)
docker: Error response from daemon: failed to set up container networking: driver failed programming external connectivity on endpoint curso-docker-b (dc1814c6dd261d5f6ac915d4b103a3e985a4489040144a3ce76dc22e9f918cf0): Bind for 127.0.0.1:9000 failed: port is already allocated

Run 'docker run --help' for more information
e40dfd88f4b3
código de salida: 125

== 3. docker stop a
código de salida: 0

== 4. docker run --rm --name c … healthcheck
no sano: Get "http://127.0.0.1:9000/": dial tcp 127.0.0.1:9000: connect: connection refused
código de salida: 1

== 5. ¿Qué quedó?
NAMES            STATUS
curso-docker-b   Created
curso-docker-a   Exited (0) 4 seconds ago

== 6. Con el puerto ya libre, docker start b
curso-docker-b
código de salida: 0
NAMES            STATUS   PORTS
curso-docker-b   Up 1 second (health: starting)
docker port: []
curl al puerto: código HTTP 000
pedido:   {"9000/tcp":[{"HostIp":"127.0.0.1","HostPort":"9000"}]}
conectado: {}

== 7. Dejar el equipo limpio
0

Ключът е в блок 6: docker start завърши с код 0 и контейнерът е Up, но колоната PORTS е празна, docker port не отпечатва нищо, curl не получава отговор (000), PortBindings продължава да иска 127.0.0.1:9000, а NetworkSettings.Ports е празно ({}).

Решение 2

docker run -d --name curso-docker-ejercicio -p 127.0.0.1:9002:9000 \
  --memory 32m --cpus 0.25 --pids-limit 20 curso-docker-prueba:1.0.0
curl -s http://127.0.0.1:9002/
docker inspect --format 'memoria={{.HostConfig.Memory}} cpus(nano)={{.HostConfig.NanoCpus}} pids={{.HostConfig.PidsLimit}}' curso-docker-ejercicio
docker stats --no-stream --format 'table {{.Name}}\t{{.MemUsage}}' curso-docker-ejercicio
curl -s -X POST http://127.0.0.1:9002/modo/caido
docker ps --filter name=curso-docker-ejercicio --format '{{.Names}} {{.Status}}'
docker rm -f curso-docker-ejercicio
  1. Важното е -p 127.0.0.1:9002:9000: 9002 е твоят, а 9000 остава този на услугата вътре в контейнера. Ако си написал -p 127.0.0.1:9002:9002, curl не получава отговор, защото вътре в контейнера никой не слуша на 9002.
  2. memoria=33554432, което е 32 × 1024 × 1024, и cpus(nano)=250000000, една четвърт от милиард: една четвърт CPU. Това е същата сметка от 2.7.1 с други числа.
  3. Колоната ще казва … / 32MiB. Онова пред наклонената черта се променя; ограничението — не.
  4. Up, със или без отметката за състоянието. При писането на урока беше измерено: веднага след превключването в неработещ режим — Up 7 seconds (health: starting); дванайсет секунди по-късно — Up 19 seconds (unhealthy) (това е в fig02_09.salida.txt). Продължава да казва Up, макар да е отбелязан като нездрав. Docker научава за срива благодарение на проверката на състоянието, която образът съдържа, а не защото сам знае какво отговаря услугата.
  5. docker rm -f е наред тук: това е тренировъчен контейнер без данни.

Решение 3

docker run -d --name curso-docker-bitacora -p 127.0.0.1:9000:9000 \
  --memory 64m --cpus 0.5 --pids-limit 50 curso-docker-prueba:1.0.0
for _ in $(seq 20); do curl -fs -o /dev/null http://127.0.0.1:9000/ && break; sleep 0.5; done
for i in 1 2 3; do curl -s http://127.0.0.1:9000/; done
curl -s -X POST http://127.0.0.1:9000/modo/caido
for i in 1 2; do curl -s http://127.0.0.1:9000/; done
curl -s -X POST http://127.0.0.1:9000/modo/sano
docker logs --tail 3 curso-docker-bitacora
docker logs --timestamps curso-docker-bitacora
docker logs --since 1m curso-docker-bitacora
docker stop curso-docker-bitacora
docker logs --tail 3 curso-docker-bitacora
docker rm curso-docker-bitacora
docker logs curso-docker-bitacora
  1. Ако си поставил блока наведнъж, последните три реда са двете заявки в неработещ режим и смяната към здрав: GET / en modo caido два пъти и modo cambiado a sano. Ако си въвеждал командите една по една и ти е отнело повече от пет секунди, между тях се появяват редове GET / en modo … от проверката на състоянието (2.5): не си ги поискал ти, поискал ги е Docker, и твоят --tail 3 ще е различен.
  2. С --timestamps всеки ред започва с датата и часа, в които Docker го е получил, в UTC (Z в края го показва), с наносекунди: при проверочното изпълнение — 2026-10-21T15:30:46.762332653Z servicio-prueba: GET / en modo caido. С тази колона би могъл да отговориш в колко точно часа е започнал сривът; не забравяй да го превърнеш в своята часова зона (за град Мексико извади 6 часа).
  3. --since 1m показва всичко, ако си направил упражнението за по-малко от минута: стартирането, всяка заявка (включително тази от цикъла на изчакване) и всяка смяна на режима, плюс по един ред за всяка проверка на състоянието, станала междувременно, така че общият брой расте с времето, което ти е отнело. Това е инструментът за „какво се е случило от 10:00 насам“, като замениш 1m с час.
  4. След docker stop логът е още там, а последният му ред сега е servicio-prueba: recibí terminated, apagando: спирането също е записано. След docker rm docker logs се проваля с Error response from daemon: No such container: curso-docker-bitacora: контейнерът вече не съществува и с него си е отишъл логът му. Това е доказателството, което би изгубил, ако изтриеш, преди да прочетеш.

Проверките на решения 2 и 3 са във fig02_09.sh, който използва същите имена като упражненията, с префикса и порта от променливите:

#!/usr/bin/env bash
# fig02_09.sh — Ejercicios 2 y 3: las comprobaciones de sus soluciones, con salida guardada.
set -uo pipefail
# Nombres y puerto por omisión: los del curso. Se pueden cambiar para no chocar
# con otra corrida en el mismo equipo (PREFIJO=... PUERTO=... ./figNN.sh).
PREFIJO="${PREFIJO:-curso-docker-}"
PUERTO="${PUERTO:-9000}"
IMAGEN="${PREFIJO}prueba:1.0.0"
EJ="${PREFIJO}ejercicio"
BIT="${PREFIJO}bitacora"
trap 'docker rm -f "$EJ" "$BIT" >/dev/null 2>&1 || true' EXIT
esperar() { for _ in $(seq 20); do curl -fs -o /dev/null "http://127.0.0.1:${PUERTO}/" && return; sleep 0.5; done; }

echo "== Ejercicio 2: 32 MiB, un cuarto de CPU, 20 procesos"
docker run -d --name "$EJ" -p "127.0.0.1:${PUERTO}:9000" \
  --memory 32m --cpus 0.25 --pids-limit 20 "$IMAGEN" >/dev/null
esperar
curl -s "http://127.0.0.1:${PUERTO}/"
docker inspect --format 'memoria={{.HostConfig.Memory}} cpus(nano)={{.HostConfig.NanoCpus}} pids={{.HostConfig.PidsLimit}}' "$EJ"
docker stats --no-stream --format 'table {{.Name}}\t{{.MemUsage}}' "$EJ"
curl -s -X POST "http://127.0.0.1:${PUERTO}/modo/caido"
docker ps --filter name="$EJ" --format '{{.Names}} {{.Status}}'
sleep 12
docker ps --filter name="$EJ" --format '{{.Names}} {{.Status}}'
docker rm -f "$EJ" >/dev/null

echo
echo "== Ejercicio 3: la bitácora como evidencia"
docker run -d --name "$BIT" -p "127.0.0.1:${PUERTO}:9000" \
  --memory 64m --cpus 0.5 --pids-limit 50 "$IMAGEN" >/dev/null
esperar
for i in 1 2 3; do curl -s "http://127.0.0.1:${PUERTO}/"; done
curl -s -X POST "http://127.0.0.1:${PUERTO}/modo/caido"
for i in 1 2; do curl -s "http://127.0.0.1:${PUERTO}/"; done
curl -s -X POST "http://127.0.0.1:${PUERTO}/modo/sano"
echo "-- 1. docker logs --tail 3"
docker logs --tail 3 "$BIT"
echo "-- 2. docker logs --timestamps"
docker logs --timestamps "$BIT"
echo "-- 3. docker logs --since 1m (renglones)"
docker logs --since 1m "$BIT" | wc -l
echo "-- 4. detenido: la bitácora sigue"
docker stop "$BIT" >/dev/null
docker logs --tail 3 "$BIT"
echo "-- 4. borrado: ya no"
docker rm "$BIT" >/dev/null
docker logs "$BIT"
echo "código de salida: $?"

Реално изпълнение (моментите на --timestamps и времето до unhealthy се променят при всяко изпълнение):

$ ./fig02_09.sh
== Ejercicio 2: 32 MiB, un cuarto de CPU, 20 procesos
sano
memoria=33554432 cpus(nano)=250000000 pids=20
NAME                     MEM USAGE / LIMIT
curso-docker-ejercicio   1.594MiB / 32MiB
modo: caido
curso-docker-ejercicio Up 7 seconds (health: starting)
curso-docker-ejercicio Up 19 seconds (unhealthy)

== Ejercicio 3: la bitácora como evidencia
sano
sano
sano
modo: caido
caido
caido
modo: sano
-- 1. docker logs --tail 3
servicio-prueba: GET / en modo caido
servicio-prueba: GET / en modo caido
servicio-prueba: modo cambiado a sano
-- 2. docker logs --timestamps
2026-10-21T15:30:46.687217878Z servicio-prueba escuchando en :9000 en modo sano
2026-10-21T15:30:46.712613659Z servicio-prueba: GET / en modo sano
2026-10-21T15:30:46.727375778Z servicio-prueba: GET / en modo sano
2026-10-21T15:30:46.734891393Z servicio-prueba: GET / en modo sano
2026-10-21T15:30:46.744749971Z servicio-prueba: GET / en modo sano
2026-10-21T15:30:46.751853815Z servicio-prueba: modo cambiado a caido
2026-10-21T15:30:46.762332653Z servicio-prueba: GET / en modo caido
2026-10-21T15:30:46.771142826Z servicio-prueba: GET / en modo caido
2026-10-21T15:30:46.779167498Z servicio-prueba: modo cambiado a sano
-- 3. docker logs --since 1m (renglones)
9
-- 4. detenido: la bitácora sigue
servicio-prueba: GET / en modo caido
servicio-prueba: modo cambiado a sano
servicio-prueba: recibí terminated, apagando
-- 4. borrado: ya no
Error response from daemon: No such container: curso-docker-bitacora
código de salida: 1

Решение 4

Един възможен отговор: „Без адрес -p 9000:9000 публикува порта на всички интерфейси на компютъра ти, включително този на безжичната мрежа, а не само на твоя собствен. А защитната стена ufw не го спира, защото Docker пише собствени мрежови правила и трафикът на контейнера минава, преди ufw да го прегледа; документацията на Docker предупреждава за това. Така че всеки в мрежата на кафенето е можел да говори с контейнера ти. Трябваше да напишеш -p 127.0.0.1:9000:9000: 127.0.0.1 е интерфейсът, който води само до собствения ти компютър. Защитата е в -p, а не в защитната стена“.

Как разбирам, че съм успял

Пусни скриптовете от fig02_01.sh до fig02_07.sh от programas/02-contenedores-dia-a-dia/ на своя Mint, поред (fig02_01.sh изгражда образа, който използват останалите; блок 1 показва грешката само ако образът още не съществува). fig02_08.sh до fig02_10.sh са тези на упражненията 1 до 3 и на раздел 2.10.4 (упражнение 4 няма скрипт). Сравни изходите си с тези от урока. Онова, което не се сравнява, защото се променя при всяко изпълнение: идентификаторите на контейнери и образи, времената (Up 2 seconds, 3.00 s, 414 ms, тези от изграждането), използваната памет и колоната PIDS, които отчита docker stats, общата памет на компютъра ти, броят на редовете на docker inspect (зависи от версията на демона), някой излишен ред GET / en modo … в лога, поискан от проверката на състоянието, и arm64 срещу amd64. Останалото трябва да излиза същото. Отбележи всяка точка:

  • docker image ls curso-docker-prueba:1.0.0 --format '{{.Repository}}:{{.Tag}} {{.Size}}' (блок 5 на fig02_01.sh) отпечатва curso-docker-prueba:1.0.0 15.1MB (или много близък размер при amd64).
  • С работещата услуга docker ps показва 127.0.0.1:9000->9000/tcp в колоната PORTS, без 0.0.0.0 и без [::].
  • curl -s http://127.0.0.1:9000/ отговаря sano; в неработещ режим curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:9000/ отговаря 503, а docker ps продължава да казва Up.
  • docker inspect --format 'memoria={{.HostConfig.Memory}} cpus(nano)={{.HostConfig.NanoCpus}} pids={{.HostConfig.PidsLimit}} usuario={{.Config.User}}' curso-docker-prueba отпечатва memoria=67108864 cpus(nano)=500000000 pids=50 usuario=65532:65532.
  • Предизвика и разчете двете грешки на fig02_06.sh и знаеш защо след втората трябва да се изтрие контейнер в състояние Created.
  • docker stop curso-docker-prueba (тази услуга обработва SIGTERM) отнема по-малко от секунда и оставя Exited (0); логът завършва с recibí terminated, apagando.
  • Обясняваш, без да гледаш, разликата между спрян и изтрит и какво се губи с всяко от тях.
  • Накрая docker ps -a --filter name='^curso-docker-' --format '{{.Names}}' | wc -l отпечатва 0, а образът curso-docker-prueba:1.0.0 остава в docker image ls: ще ти трябва.
  • Записа в дневника си какво се променя между -p 9000:9000 и -p 127.0.0.1:9000:9000, проверено със ss -ltn.

Обобщение

Отговори, без да гледаш урока:

  1. Какво означават кодовете на изход 125, 126 и 127 на docker run? Кой видя, когато стартира образ, който не съществуваше, и кой — когато поиска sh в тестовата услуга?
  2. На какъв въпрос отговаря всяка опция на командата docker run -d --name … -p 127.0.0.1:9000:9000 --memory 64m --cpus 0.5 --pids-limit 50 …?
  3. Каква е разликата между docker ps и docker ps -a и защо е важна за почистването?
  4. Защо docker ps може да казва Up за услуга, която отговаря с 503?
  5. Какво вижда docker logs и какво не вижда? Какво става с лога при stop и при rm?
  6. Защо docker exec … sh се проваля в тестовата услуга и защо това е добре?
  7. Какво означава memoria=0 в docker inspect?
  8. Каква е разликата между публикуването на 127.0.0.1 и липсата на адрес? Защо ufw не те защитава?
  9. Какво прави docker stop, стъпка по стъпка, и с какво се различава от docker rm -f?
  10. Как изтриваш само своите контейнери и защо не използваш docker container prune на споделен компютър?

Източници

Всички проверени на 21 октомври 2026 г.

  • docker container run: -d, --name, -p, --rm, --pids-limit, стойността по подразбиране на --pull и предупреждението за портовете, публикувани на всички интерфейси, и ufw.
  • Docker, „Running containers“: кодовете на изход 125, 126 и 127 и какво прави --privileged.
  • docker container ls: -a, -q, филтърът по име и по състояние и полетата на --format.
  • docker container logs: какво предава командата и опциите --follow, --tail, --since и --timestamps.
  • docker container exec: -i, -t, че работи само докато главният процес работи и че командата трябва да е изпълним файл.
  • docker inspect: информацията на ниско ниво и шаблоните с --format.
  • docker container stats: --no-stream и колоните за памет и процеси.
  • docker container stop: SIGTERM, срокът от 10 секунди, SIGKILL и --timeout.
  • docker container rm: --force със SIGKILL и препоръката за prune.
  • docker container prune: че изтрива всички спрени контейнери, и филтрите му.
  • docker container update: че ограниченията за памет, CPU и процеси се променят в движение.
  • Dockerfile reference, HEALTHCHECK: кога се изпълнява първата проверка и кога състоянието минава от starting в healthy или unhealthy.
  • RFC 9110, §15.6.4: кодът 503, Service Unavailable.
  • docker image ls: справочният изглед на командата.
  • Docker, „Port publishing and mapping“: публикуването на всички адреси по подразбиране, предупреждението за несигурност и публикуването на 127.0.0.1.
  • Docker, „Packet filtering and firewalls“: защо трафикът на контейнер не минава през правилата на ufw.
  • Docker, „Resource constraints“: че по подразбиране няма ограничения, рискът да свърши паметта, минимумът за --memory и примерът с --cpus.
  • Docker Engine security: демонът като root и защо само доверени потребители трябва да го управляват.
  • Docker, стъпки след инсталирането в Linux: групата docker и привилегиите ѝ, равносилни на root.
  • Docker Engine 29, бележки към версиите: версиите и датите от „За изпълненията“ и новият изглед на docker image ls от 29.0 нататък.
  • Colima: инструментът, с който програмите са изпълнени на macOS.

Термини от този урок

лог (logs) онова, което главната програма на контейнер пише в стандартния си изход и в изхода за грешки; пази го Docker и го показва docker logs
код на изход числото, с което приключва една програма; 0 означава „всичко е наред“
контекст на изграждане папката, която docker build използва, за да търси Dockerfile и файловете, които рецептата копира
distroless семейство минимални базови образи, които съдържат само най-необходимото, за да работи една програма, без shell
състояние на контейнер създаден, работещ, спрян; изтрит означава, че вече не съществува
таг (tag) онова след двоеточието в името на образ: 1.0.0 в curso-docker-prueba:1.0.0
мрежов интерфейс всяка мрежова „врата“ на един компютър; 127.0.0.1 е тази, която води само до самия компютър (loopback)
публикуване на порт свързване на порт на хоста с порт на контейнера, с -p
SIGTERM сигналът, с който се иска от програма да приключи; може да бъде обработен, за да спре подредено
SIGKILL сигнал 9, който убива програма, без да ѝ даде възможност да затвори каквото и да е
Да обсъдим вашия проект

Предпочитате имейл? Пишете ни на hola@habil.mx