Docker и Docker Compose — професионално ръководство

Част 4: Docker Containers

Container е runtime инстанцията на Docker Image. Тук разглеждам жизнения му цикъл, процесите, network, volumes, logs, диагностика и начина, по който управлявам работещи containers.

Docker Image е шаблонът. Container е живият обект. Тук вече излизам от света на build процеса и влизам в runtime. Container има собствен жизнен цикъл, собствен writable layer, процеси, мрежова конфигурация и връзки към volumes. Именно тук Docker започва да прилича не на „архив с приложение“, а на реална работеща система.

1. Какво всъщност е Docker Container

Docker Container е runtime инстанция на Docker Image. Това определение е кратко, но зад него стои доста повече. Container има собствен process namespace, filesystem view, network namespace и runtime state, а Docker Engine управлява неговия жизнен цикъл.

Важно е да не смесвам трите различни неща:

  • Image – неизменяемият шаблон;
  • Container – стартираната runtime инстанция;
  • Volume – мястото, където мога да държа постоянни данни.
Docker Image
шаблон
docker create
създаване
Container
runtime обект
start
stop
restart

2. Жизнен цикъл на Container

Един от най-честите източници на объркване е, че docker stop, docker start, docker restart, docker rm и docker run не са взаимозаменяеми. Всеки от тях работи с различно състояние на container.

Created
създаден, но не е стартиран
start
Running
процесът работи
stop
Exited
container съществува, но процесът е спрян
rm
Removed
container вече не съществува

Това разграничение е особено важно при диагностика. Stopped container не е изтрит container. Мога да го стартирам отново, да разгледам logs и да проверя неговата конфигурация.

3. docker run – създаване и стартиране

Най-познатата команда е:

TERMINAL
docker run nginx:alpine

Това не е просто „стартирай image“. Ако няма подходящ container, Docker създава нов, конфигурира го и след това го стартира.

Например:

TERMINAL
docker run -d \
  --name web \
  -p 8080:80 \
  nginx:alpine

Тук имам няколко отделни действия в една команда:

  • -d – стартира container в background;
  • --name web – задава име;
  • -p 8080:80 – публикува host порт 8080 към container порт 80;
  • nginx:alpine – image, от който се създава container.
Важно: -p 8080:80 не означава, че приложението „слуша на 8080“ вътре в container-а. То означава host порт 8080 → container порт 80.

4. docker create и docker start

Ако искам да разделя създаването от стартирането, използвам:

СЪЗДАВАНЕ
docker create \
  --name web \
  -p 8080:80 \
  nginx:alpine

Сега container съществува, но не работи. Мога да проверя това:

ПРОВЕРКА
docker ps
docker ps -a

След това:

СТАРТИРАНЕ
docker start web

На практика docker run е удобният shortcut за типичния сценарий, докато create и start ми дават по-ясен контрол върху отделните стъпки.

5. docker ps – какво реално работи

Първата команда, която използвам при проблем с containers, е:

TERMINAL
docker ps

Тя показва само работещите containers. Ако искам и спряните:

TERMINAL
docker ps -a

Това е дребна, но важна разлика. Когато някой каже „container-ът го няма“, първо проверявам с docker ps -a. Може просто да е stopped.

6. stop, kill и restart – не са едно и също

6.1. docker stop

docker stop изпраща сигнал към основния процес и му дава възможност да приключи нормално.

TERMINAL
docker stop web

Това е нормалният ми избор за спиране на container.

6.2. docker kill

docker kill е по-грубо средство. Използвам го, когато процесът не реагира нормално или имам конкретна причина да прекратя container-а веднага.

TERMINAL
docker kill web

6.3. docker restart

Когато искам да спра и веднага да стартирам отново:

TERMINAL
docker restart web
Не използвам restart като универсално лекарство. Ако не знам защо container-ът пада, рестартирането може само да прикрие проблема. Първо гледам status, logs и configuration.

7. docker rm – кога container-ът наистина изчезва

docker rm премахва container-а.

TERMINAL
docker rm web

Ако container-ът все още работи, Docker по подразбиране няма да го премахне. Мога да използвам:

ВНИМАНИЕ
docker rm -f web

Това вече е принудително действие. Не го използвам механично, защото при production системи предпочитам първо да разбера какво ще изгубя.

8. docker logs – първата линия на диагностика

Ако container не се държи както очаквам, гледам logs:

TERMINAL
docker logs web

За следене в реално време:

TERMINAL
docker logs -f web

За последните 100 реда:

TERMINAL
docker logs --tail 100 web

А когато искам timestamp:

TERMINAL
docker logs -t --tail 100 web
Практическо правило: ако application-ът пише смислени logs към stdout/stderr, docker logs е един от най-бързите начини да разбера какво се случва вътре.

9. docker inspect – конфигурацията, а не само status

docker ps ми казва какво работи. docker inspect ми показва как е конфигуриран конкретният container.

TERMINAL
docker inspect web

Това връща JSON структура с много информация – например:

  • container ID;
  • image;
  • state;
  • environment;
  • mounts;
  • network settings;
  • published ports;
  • restart policy;
  • entrypoint и command.

При нужда мога да извлека само конкретно поле:

ПРИМЕР
docker inspect web \
  --format '{{.State.Status}}'

Или IP адреса:

ПРИМЕР
docker inspect web \
  --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'

10. docker exec – влизане в работещ Container

Когато трябва да проверя какво се случва вътре в работещ container, използвам docker exec.

SHELL
docker exec -it web /bin/sh

Ако image-ът съдържа Bash:

SHELL
docker exec -it web /bin/bash

Това не е SSH. Аз изпълнявам процес вътре в namespace-овете на съществуващия container. Ако container-ът няма shell, docker exec няма магически да му добави такъв.

И тук има капан: ръчната промяна вътре в container не променя Image-а. Ако изтрия container-а и създам нов от същия image, ръчните промени изчезват. Затова docker exec е чудесен за диагностика, но не е начин за изграждане на deployment.

11. Ports – как container-ът става достъпен отвън

Container има собствен network namespace. Това означава, че порт 80 вътре в container не означава автоматично порт 80 на host системата.

ПРИМЕР
docker run -d \
  --name web \
  -p 8080:80 \
  nginx:alpine

Сега заявка към:

BROWSER / HTTP
http://localhost:8080

достига до порт 80 на nginx вътре в container-а.

Запис Значение
-p 8080:80 Host 8080 → Container 80
-p 127.0.0.1:8080:80 Достъп само през localhost на host-а
-p 8080:80/udp UDP mapping
Не публикувам портове без причина. Ако два containers комуникират само през Docker network, няма смисъл техният вътрешен порт задължително да бъде публикуван към host системата.

12. Writable layer срещу Volume

Всеки container има writable layer, но това не го прави подходящо място за постоянни данни. При databases, uploads, документи и други важни файлове използвам volume или bind mount.

NAMED VOLUME
docker volume create app-data

docker run -d \
  --name app \
  -v app-data:/var/lib/app \
  myapp:1.0

При bind mount:

BIND MOUNT
docker run -d \
  --name app \
  -v /srv/app/data:/var/lib/app \
  myapp:1.0

Разликата между named volume и bind mount е важна за поддръжката, backup стратегията и начина, по който управлявам данните.

13. Environment variables

Конфигурацията на приложението често се подава чрез environment variables:

ПРИМЕР
docker run -d \
  --name app \
  -e APP_ENV=production \
  -e APP_PORT=8080 \
  myapp:1.0

Проверка:

TERMINAL
docker inspect app \
  --format '{{range .Config.Env}}{{println .}}{{end}}'
Environment variable не е автоматично secret storage. Не приемам -e DB_PASSWORD=... като универсален начин за работа със secrets. При чувствителни данни използвам подходящ механизъм за secrets според конкретната инфраструктура.

14. Restart Policy – кога Docker стартира Container отново

Това е особено важно при сървъри. Ако container трябва да се стартира отново след crash или reboot, задавам подходяща restart policy.

ПРИМЕР
docker run -d \
  --name app \
  --restart unless-stopped \
  myapp:1.0

Най-често срещаните политики са:

Policy Поведение
no Без автоматичен restart.
on-failure Restart при ненулев exit code.
always Docker се опитва да стартира container-а отново.
unless-stopped Restart, освен ако container-ът не е бил умишлено спрян.

Това е и причината при планирано спиране да обръщам внимание как е конфигуриран container-ът. „Спрях го“ и „няма да се стартира след reboot“ не са непременно едно и също нещо.

15. Exit Code – защо Container-ът е спрял

Container-ът живее, докато основният му процес живее. Когато този процес приключи, container-ът преминава в exited state.

ПРОВЕРКА
docker ps -a

docker inspect app \
  --format '{{.State.ExitCode}}'

Exit code 0 обикновено означава нормално приключване, докато ненулев код показва грешка. Самият exit code обаче не е достатъчна диагноза – следва да се видят logs и command/configuration.

16. HEALTHCHECK – работи ли приложението, а не само процесът

Running container не означава непременно healthy application. Процесът може да е жив, но приложението да не отговаря правилно.

Dockerfile
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD wget -q --spider http://127.0.0.1:8080/ || exit 1

След това мога да видя health състоянието:

TERMINAL
docker inspect app \
  --format '{{json .State.Health}}'

Това е важна граница между „процесът не е умрял“ и „услугата реално работи“.

17. Containers и Docker Network

Не е необходимо containers да комуникират през публикувани host портове. При една и съща Docker network те могат да използват собствената вътрешна мрежа.

СЪЗДАВАНЕ НА NETWORK
docker network create app-net
СВЪРЗВАНЕ НА CONTAINERS
docker run -d \
  --name db \
  --network app-net \
  postgres:17

docker run -d \
  --name app \
  --network app-net \
  myapp:1.0

В такъв сценарий приложението може да използва името db като hostname. Не е необходимо PostgreSQL портът да бъде публикуван към host-а само заради комуникацията между двата containers.

Това е добрата граница: публикувам към host само това, което трябва да бъде достъпно извън Docker network. Вътрешната комуникация оставям вътрешна.

18. docker cp – копиране на файлове

За бърза диагностика или извличане на файл мога да използвам:

CONTAINER → HOST
docker cp app:/var/log/app.log ./app.log

И обратно:

HOST → CONTAINER
docker cp ./config.yml app:/etc/app/config.yml

Това е удобно за временна работа, но не го използвам като заместител на правилно конфигуриран volume, bind mount или deployment процес.

19. docker commit – защо не го използвам като deployment метод

Docker може да създаде image от текущото състояние на container:

TERMINAL
docker commit app myapp:debug

Технически това работи. Но ако променя production container ръчно и след това направя commit, получавам image, чийто build история вече не е ясно описана в Dockerfile.

За възпроизводими deployment-и предпочитам:

ПРЕПОРЪЧИТЕЛЕН МОДЕЛ
Dockerfile
    ↓
docker build
    ↓
Image
    ↓
Registry
    ↓
docker pull
    ↓
Container

20. Моят ред за диагностика

Когато container не работи, не започвам с docker rm -f. Това е последното, не първото действие.

1. docker ps -a
Какво е състоянието?
2. docker logs
Какво казва приложението?
3. docker inspect
Как е конфигуриран container-ът?
4. docker exec
Какво се случва вътре?
5. Network / Volume / Port
Има ли външна причина?

След това вече решавам дали проблемът е в image, command, configuration, network, volume, permissions или самото приложение.

21. Типични грешки

  • Да се мисли, че stopped container е изтрит.
  • Да се използва docker rm -f преди диагностика.
  • Да се пазят важни данни само в writable layer.
  • Да се публикуват всички вътрешни портове към host-а.
  • Да се използва docker exec за постоянна конфигурация.
  • Да се приема, че running container означава healthy application.
  • Да се използва restart за прикриване на проблем, вместо за реално управление.
  • Да се смесва lifecycle на container с lifecycle на Image.
Особено опасно: „Изтривам container-а и го създавам наново“ не е универсална процедура. Ако данните не са отделени от container filesystem-а, тази операция може да има реални последствия.

22. Най-важното от тази част

  • Container е runtime инстанция на Image.
  • docker run обикновено създава и стартира нов container.
  • docker create създава, а docker start стартира вече съществуващ container.
  • docker stop спира нормално, докато docker kill прекратява по-грубо.
  • docker rm премахва container-а.
  • docker logs и docker inspect са основни диагностични инструменти.
  • docker exec е за работа вътре в работещ container, не за постоянна конфигурация.
  • Persistent data трябва да бъде отделена чрез volumes или bind mounts.
  • Container network позволява вътрешна комуникация без излишно публикуване на host портове.
  • Restart policy трябва да бъде избрана съзнателно.
  • Running не означава автоматично healthy.

Заключение

Docker Container е временна runtime среда, а не виртуална машина в малък пакет. Той споделя kernel-а на host системата и използва Linux namespaces, cgroups и filesystem механизми, за да изолира процесите и ресурсите.

Именно затова container-ите са толкова леки и бързи, но и затова трябва да се разбира какво реално се случва. Когато знам разликата между Image, Container, Volume и Network, командите на Docker престават да бъдат набор от магически параметри.

За мен това е и границата между „използвам Docker“ и „разбирам Docker“. В първия случай запомням команди. Във втория знам какво променям с всяка команда и какви последствия има това.

В следващата част ще премина към Docker Volumes и persistent data – къде реално живеят данните, какво е named volume, какво е bind mount, как се прави backup и защо database без правилно storage управление е покана за неприятности.