Модуль 1.2: Основи Docker
Складність:
[СЕРЕДНЯ]- Вимагає практичної роботиЧас на виконання: 50-60 хвилин
Передумови: Модуль 1.1: Що таке контейнери?
Середовище: Робоча станція з Docker Engine або Docker Desktop, термінал та дозвіл на запуск локальних контейнерів.
Що ви зможете зробити
Розділ «Що ви зможете зробити»Наприкінці цього модуля ви зможете виконати ці завдання в локальному терміналі та пояснити операційну причину кожного кроку:
- Зібрати образ Docker з Dockerfile та пояснити, як кожна інструкція впливає на кінцевий артефакт.
- Запустити контейнери з мапінгом портів, змінними середовища, визначеними іменами та змонтованими постійними томами.
- Діагностувати несправний контейнер за допомогою логів, перевірки процесів, сеансів exec та перевірки конфігурації.
- Оцінити порядок шарів образу, вибір базового образу та налаштування користувача під час виконання з огляду на швидкість і безпеку.
- Порівняти робочі процеси Docker Compose з ментальною моделлю Kubernetes 1.35+, яку ви використовуватимете далі у цьому треку.
Чому це важливо
Розділ «Чому це важливо»У 2017 році компанія Equifax повідомила про витік, що розкрив конфіденційні дані приблизно 143 мільйонів людей після того, як зловмисники скористалися невиправленим компонентом вебзастосунку. Цей інцидент не стосувався безпосередньо Docker, але це саме той тип операційного збою, який контейнери роблять болісно очевидним: застосунок запускається з надмірними привілеями, постачається з занадто великою поверхнею атаки, приховує свій точний стан залежностей і дає фахівцям з реагування занадто мало впевненості щодо того, що насправді розгорнуто. Коли команди пакують програмне забезпечення в образи, не розуміючи контексту збірки, історії шарів, тегів, користувачів під час виконання та журналів, вони відтворюють ті самі патерни збоїв у новітньому конвеєрі доставки.
Docker став популярним, оскільки надав розробникам портативний спосіб перетворити застосунок та його допущення щодо середовища виконання в єдиний образ. Цей образ може запускатися на ноутбуці, у CI або як вхідні дані для оркестратора, а супровідний інтерфейс командного рядка робить життєвий цикл достатньо простим для практики ще до того, як з’являється Kubernetes. Kubernetes більше не використовує Docker Engine як середовище виконання для своїх вузлів, але робочий процес Docker досі навчає тим самим концепціям образу, процесу, файлової системи, порту та середовища, на яких базується кожна контейнерна платформа.
Цей модуль зосереджений на практичній грамотності роботи з Docker, а не на дрібницях Docker. У цьому модулі вам належить встановити або перевірити інструментарій, запустити контейнери з реалістичними параметрами, створити образи з Dockerfiles, спостерігати за кешуванням шарів, використовувати Compose для локального багатоконтейнерного стеку та пов’язати ці звички з Kubernetes 1.35+. Мета полягає не в тому, щоб перетворити вас на спеціаліста з Docker; вона полягає в тому, щоб переконатися, що коли пізніше Pod зазнає збою, ви зможете мислити, відштовхуючись від базових принципів, замість того, щоб завчати команди.
Існує також економічна причина вивчати ці деталі на ранньому етапі. Кожен зайвий мегабайт в образі завантажується runners у CI, ноутбуками розробників, кластерами для staging та виробничими вузлами. Кожен нечіткий тег створює додаткову роботу під час відкочування, оскільки ніхто не може довести, які саме байти виконувалися. Кожен відсутній журнал або приховане допущення щодо середовища подовжує час реагування на інцидент. Основи Docker виглядають як локальні команди, але вони формують шлях доставки задовго до того, як контейнер потрапить у кластер.
Перш ніж торкатися команд Kubernetes у цьому курсі, переконайтеся, що повний клієнт kubectl працює на вашій машині. Багато інженерів використовують особисті псевдоніми командної оболонки інтерактивно, але навчальні матеріали та приклади для копіювання мають використовувати повну назву команди, щоб вони залишалися придатними для виконання у скриптах, терміналах та завданнях CI, не покладаючись на локальні налаштування оболонки. Kubernetes 1.35+ зберігає ту саму основну схему усунення несправностей, яку ви практикуватимете з Docker тут: перевіряти заявлену конфігурацію, читати вивід процесів, входити у запущене робоче навантаження лише за потреби та тримати стан середовища виконання окремо від образу, який його створив.
kubectl version --clientВстановлення Docker
Розділ «Встановлення Docker»Встановлення Docker виглядає простим зовні, але воно приховує одну важливу архітектурну деталь: Linux може запускати контейнери безпосередньо через примітиви ядра, які використовують контейнери, тоді як macOS та Windows потребують невеликої віртуальної машини Linux, оскільки їхні ядра не надають таких самих інтерфейсів для контейнерів. Docker Desktop пакує цю віртуальну машину, Docker Engine, CLI, допоміжні програми для облікових даних, мережеву інтеграцію та графічний інтерфейс користувача в один продукт. На робочій станції Linux Docker Engine та CLI можна встановити більш безпосередньо, і модель дозволів користувача стає більш видимою, оскільки сокет Docker може керувати контейнерами на хості.
У macOS Docker Desktop є звичайним шляхом, оскільки він надає вам підтримуване середовище Linux, не вимагаючи від вас самостійного керування віртуальною машиною. Команда Homebrew нижче встановлює десктопний застосунок, але вам все одно потрібно запустити його один раз, щоб стартував фоновий рушій. В оригінальному модулі було наведено посилання на сторінку продукту Docker Desktop, і вона залишається правильним джерелом для отримання деталей про ліцензування та платформи: https://docker.com/products/docker-desktop.
# Install Docker Desktop# Download from https://docker.com/products/docker-desktop# Or use Homebrew:brew install --cask dockerВ Ubuntu або Debian зручний скрипт корисний для навчальної робочої станції, оскільки він встановлює поточні пакети Docker і налаштовує для вас репозиторій. У виробничому парку зазвичай надається перевага фіксованому джерелу пакетів і інструменту керування конфігурацією, щоб кожна машина була відтворюваною. Зверніть увагу на рядок usermod: він додає ваш обліковий запис до групи docker, що усуває потребу додавати префікс sudo до кожної команди, але це також надає потужний доступ, оскільки будь-хто, хто може звертатися до сокета Docker, може фактично запускати привілейовані робочі навантаження.
# Official installationcurl -fsSL https://get.docker.com -o get-docker.shsudo sh get-docker.shsudo usermod -aG docker $USER# Log out and back in for group changesПеревірка повинна тестувати як клієнт, так і рушій. docker --version лише доводить, що бінарний файл CLI існує, тоді як docker run hello-world доводить, що CLI може зв’язатися з рушієм, завантажити образ, створити контейнер, запустити процес, приєднатися до його виводу та видалити короткострокове робоче навантаження після його завершення. Якщо ця команда зазнає невдачі, уважно прочитайте помилку, перш ніж будь-що перевстановлювати, оскільки типовими причинами є незапущена віртуальна машина Docker Desktop, проблема з дозволами на сокеті або заблоковане з’єднання з реєстром.
docker --version# Docker version 29.x.x, build xxxxx
docker run hello-world# Should show "Hello from Docker!" messageМентальна модель, якої ви прагнете, — це клієнт-серверна модель. Команда docker є клієнтом, який надсилає запити API до демона Docker, а демон координує зберігання образів, налаштування мережі, створення контейнерів і середовища виконання нижчого рівня, такі як containerd і runc. Це означає, що більшість команд не є операціями з локальними файлами, навіть коли вони здаються миттєвими; це запити до рушія, який володіє станом, кешує шари образів і керує процесами від вашого імені.
Саме через цей стан рушія два термінали можуть впливати на одні й ті самі контейнери. Якщо один термінал запускає my-nginx, інший термінал може зупинити його, оскільки обидва клієнти спілкуються з тим самим демоном. Це зручно на ноутбуці і небезпечно на спільній машині, де випадкова команда очищення може видалити чужий зупинений контейнер або образ, що виглядає невикористовуваним. Ставтеся до стану Docker як до спільної інфраструктури, навіть коли ця інфраструктура є лише вашою робочою станцією.
+----------------------+ API request +-----------------------+| Docker CLI | -------------------------> | Docker Engine || docker run nginx | | images, networks, || docker build -t app | <------------------------- | volumes, containers |+----------------------+ status/output +-----------+-----------+ | v +-----------------------+ | containerd + runc | | Linux namespaces, | | cgroups, mounts | +-----------------------+Зупиніться та подумайте: якщо бінарний файл CLI встановлено, але Docker Desktop не запущено, яка команда перевірки все одно спрацює, а яка завершиться невдало? Команда версії може виконатися успішно, оскільки вона читає лише локальні метадані клієнта, тоді як docker run hello-world потребує демона. Ця відмінність стане в пригоді пізніше, коли команда CLI Kubernetes працює локально, але не може досягти сервера API кластера.
Запуск вашого першого контейнера
Розділ «Запуск вашого першого контейнера»Образ — це упакована файлова система та шаблон метаданих; контейнер — це запущений екземпляр, створений із цього шаблону. Ця різниця схожа на клас і об’єкт у програмуванні або на рецепт і приготовану страву на кухні. Ви можете зберігати один образ і запускати з нього багато контейнерів, кожен із яких матиме окремий шар для запису, дерево процесів, середовище та мережеву кінцеву точку. Саме тому Docker робить локальні експерименти дешевими: знищення контейнера не призводить до знищення образу, який ви використали для його створення.
Приклад з nginx невеликий, але він містить майже кожну концепцію середовища виконання, яка вам потрібна. Docker завантажує образ nginx, якщо він відсутній, створює контейнер, запускає стандартний процес вебсервера, зіставляє порт хоста з портом контейнера, призначає ім’я my-nginx і запускає його у відокремленому режимі. Порт хоста — це адреса, на яку ви переходите зі свого ноутбука, тоді як порт контейнера — це порт, на якому застосунок очікує з’єднання всередині мережевого простору імен контейнера.
Зупиніться та подумайте: Коли ви запустите наведену нижче команду, що станеться, якщо порт 8080 на вашій хост-машині вже використовується іншим застосунком? Команда завершиться невдало, оскільки Docker не може здійснити прив’язку до вже зайнятого порту хоста. Вам потрібно буде вибрати інший порт хоста, наприклад
-p 8081:80, залишаючи при цьому порт на стороні контейнера рівним80, оскільки nginx продовжує слухати саме його.
# Run nginx (a web server)docker run -d --name my-nginx -p 8080:80 nginx
# What happened:# - Pulled nginx image from Docker Hub# - Created a container from the image# - Started the container in detached mode (-d)# - Named the container "my-nginx" (--name)# - Mapped port 8080 (host) to port 80 (container)Після запуску контейнера протестуйте його ззовні контейнера. curl http://localhost:8080 звертається до порту хоста, Docker пересилає з’єднання через свій мережевий шар, і nginx отримує запит на порт 80 всередині контейнера. Потім docker ps показує запущений контейнер і робить трансляцію портів явною, і це перше місце, куди ви повинні подивитися, коли сервіс працює, але недоступний з хоста.
# Test itcurl http://localhost:8080# Returns nginx welcome page HTML
# View running containersdocker ps# CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES# a1b2c3d4e5f6 nginx "/docker-entrypoint.…" Up 10 seconds 0.0.0.0:8080->80/tcp my-nginx
# Stop the containerdocker stop my-nginx
# Remove the containerdocker rm my-nginxЗупинка та видалення — це окремі операції, оскільки Docker зберігає зупинені контейнери, поки ви їх не видалите. Такий дизайн корисний під час зневадження, оскільки зупинений контейнер все ще має метадані, статус завершення, журнали та свій фінальний шар для запису. Це також пояснює, чому ноутбук розробника повільно заповнюється старими контейнерами та образами: Docker зберігає докази доти, доки ви не вирішите, що їх безпечно викинути.
Контейнери навмисно зроблені одноразовими, але не всі дані повинні бути одноразовими. Якщо застосунок записує файли, завантажені користувачами, файли бази даних або згенеровані звіти у файлову систему контейнера, ці дані зберігаються в шарі контейнера для запису та зникають під час видалення контейнера. Томи переносять довговічні дані за межі життєвого циклу контейнера, тоді як змінні середовища вводять конфігурацію під час виконання без її запікання в образ. Зіставлення портів, змінні середовища та томи — це три параметри середовища виконання, які ви постійно використовуватимете як у Docker, так і в Kubernetes.
Імена — це ще одна невелика опція з великою цінністю для зневадження. Docker може ідентифікувати контейнери за згенерованими ідентифікаторами, але осмислене ім’я, як-от my-nginx або db, полегшує роботу з журналами, очищення та ментальне зіставлення. Компроміс полягає в тому, що імена мають бути унікальними, тому застарілий зупинений контейнер може заблокувати новий запуск, доки ви його не видалите або не перейменуєте. Коли команда завершується помилкою через конфлікт імен, Docker повідомляє вам, що існує старий стан, який варто перевірити перед видаленням.
Основний набір команд життєвого циклу достатньо малий, щоб його запам’ятати, але цінність полягає в розумінні того, коли кожна команда просить Docker створити стан, перевірити стан або знищити стан. docker run поєднує пошук образу, створення контейнера та запуск процесу. docker stop надсилає сигнал м’якого завершення перед примусовим зупиненням процесу, якщо він не завершується сам. docker rm видаляє стан контейнера, тоді як видалення образу обробляється окремо за допомогою docker rmi, оскільки багато контейнерів можуть використовувати один і той самий образ.
# Run a containerdocker run [OPTIONS] IMAGE [COMMAND]
# Common options:docker run -d nginx # Detached (background)docker run -it ubuntu bash # Interactive terminaldocker run -p 8080:80 nginx # Port mappingdocker run -v /host/path:/container/path nginx # Volume mountdocker run --name myapp nginx # Named containerdocker run -e MY_VAR=value nginx # Environment variabledocker run --rm nginx # Remove when stopped
# Container managementdocker ps # List running containersdocker ps -a # List all containersdocker stop CONTAINER # Stop gracefullydocker kill CONTAINER # Force stopdocker rm CONTAINER # Remove stopped containerdocker rm -f CONTAINER # Force remove (stop + rm)У Kubernetes 1.35+ ті самі концепції з’являються під іншими назвами. Pod — це не контейнер Docker, але він має образи контейнерів, змінні середовища, порти, журнали, поведінку перезапуску та томи. Коли пізніше в цьому курсі використовуватимуться kubectl logs, kubectl exec та kubectl describe, ви маєте розпізнати їх як версії тих звичок перевірки Docker на рівні оркестрації, які ви формуєте зараз.
apiVersion: v1kind: Podmetadata: name: nginx-practicespec: containers: - name: nginx image: nginx:1.26 ports: - containerPort: 80kubectl apply -f nginx-pod.yamlkubectl logs nginx-practicekubectl exec -it nginx-practice -- shІнспектування та діагностика контейнерів
Розділ «Інспектування та діагностика контейнерів»Діагностика контейнера починається з дисципліни: спостерігайте, перш ніж змінювати. Журнали (logs) показують, що головний процес записав у стандартний вивід та стандартний потік помилок. Списки процесів показують, чи досі виконується очікувана команда. Вивід команди inspect показує конфігурацію, яку Docker використав під час створення контейнера. Інтерактивні сесії exec є потужним інструментом, але до них слід переходити після неінвазивних перевірок, оскільки введення команд усередині запущеного контейнера може змінити той самий стан, який ви намагаєтеся зрозуміти.
Зупиніться та подумайте: Коли контейнер працює неправильно, ці три команди є вашим набором інструментів для діагностики в такому порядку:
docker logsзапитує, що сталося,docker exec -it ... bashдозволяє зазирнути всередину, аdocker inspectпоказує повну конфігурацію. Цей самий патерн застосовується пізніше в Kubernetes:kubectl logs,kubectl execтаkubectl describeнадають вам еквівалентні рівні даних для аналізу.
# View logsdocker logs CONTAINERdocker logs -f CONTAINER # Follow (tail)docker logs --tail 100 CONTAINER # Last 100 lines
# Execute command in running containerdocker exec -it CONTAINER bash # Interactive shelldocker exec CONTAINER ls /app # Run command
# Inspect container detailsdocker inspect CONTAINER # Full JSON detailsdocker stats # Resource usagedocker top CONTAINER # Running processesПерша пастка — це припущення, що docker exec працює на кожному контейнері, який ви бачите. Вона лише запускає новий процес усередині запущеного контейнера, тому команда завершиться помилкою, якщо контейнер уже припинив роботу. Для посмертного аналізу зазвичай достатньо журналів та docker inspect, а docker cp може витягти файли із зупиненого контейнера, якщо доступний для запису шар усе ще існує. Це відрізняється від діагностики через SSH, оскільки контейнер — це не маленька віртуальна машина, яка чекає на віддалений вхід; це пісочниця процесу, життєвий цикл якої прив’язаний до її головної команди.
Друга пастка — це припущення, що кожен образ має командну оболонку. Повні образи ОС та slim-версії зазвичай надають sh або bash, Alpine надає sh, а distroless-образи навмисно не містять жодного з них. Це є перевагою для посилення безпеки в продакшені, але обмеженням для діагностики. Коли команда переходить від development-образу зі зручною оболонкою до distroless-образу для виконання, їй потрібні альтернативні звички інспектування, такі як журнали, метрики, health endpoints та debug sidecars у Kubernetes.
Третя пастка — довіряти браузеру на хості більше, ніж доказам із контейнера. Помилка браузера може бути спричинена DNS, конфліктом портів на хості, зворотним проксі-сервером, винятком у застосунку або внутрішнім слухачем, який прив’язаний до неправильної адреси. Діагностика контейнера працює найкраще, коли ви перетворюєте розпливчастий симптом на менші тести: чи запущено процес, чи прив’язався він до очікуваного порту, чи отримав він запит, чи записав виняток у журнал, і чи опублікував Docker той порт, який ви очікували?
Практична послідовність діагностики має звужувати область збою. Якщо docker ps -a показує Exited, перевірте код завершення та прочитайте журнали. Якщо контейнер має статус Up, але застосунок повертає помилки, спершу скористайтеся журналами, потім перевірте змінні середовища, змонтовані файли та внутрішню зв’язність за допомогою сесії exec. Якщо хост не може отримати доступ до сервісу, але процес усередині контейнера є справним, порівняйте порт, який слухає застосунок, із відображенням портів хоста на контейнер, що показує docker ps.
Перед запуском цього, який вивід ви очікуєте, якщо вебконтейнер внутрішньо слухає порт 8000, але ви помилково налаштували відображення -p 8080:80? Контейнер може залишатися справним, оскільки його власний процес працює, але запити з хоста на порт 8080 будуть переспрямовані на порт 80 контейнера, де нічого не слухає. Такий збій виглядає як проблема з мережею, проте першопричиною є невідповідність між конфігурацією застосунку та параметрами запуску Docker.
Управління образами має таку саму логіку, орієнтовану на докази. Теги — це зрозумілі для людини назви, тоді як дайджест образу — це ідентифікатор, що базується на вмісті. Завантаження nginx:1.26 є більш відтворюваним, ніж завантаження nginx:latest, але навіть змінний тег може бути переміщений видавцем. Продакшен-конвеєри мають зрештою фіксувати дайджести або використовувати admission controls, але на цьому етапі головна звичка полягає в тому, щоб уникати “плаваючих” тегів, коли вам потрібна відтворюваність.
Реєстри доповнюють історію з образами. Ваш ноутбук може зібрати образ, але CI-runner або вузол Kubernetes потребують локації в реєстрі, звідки вони можуть його завантажити. Публічні приклади часто використовують Docker Hub, оскільки він є знайомим, тоді як організації зазвичай використовують приватні реєстри, такі як Amazon ECR, Google Artifact Registry, Azure Container Registry, GitHub Container Registry або внутрішній реєстр. Важливий принцип скрізь залишається незмінним: теги є вказівниками, дайджести ідентифікують вміст, а контроль доступу вирішує, хто може завантажувати або витягувати образи.
# Pull imagesdocker pull nginxdocker pull nginx:1.26docker pull gcr.io/project/image:tag
# Note: Docker images are content-addressable. The SHA256 hash of an image is its true identifier. Tags are just human-readable aliases.
# List imagesdocker images
# Remove imagesdocker rmi nginxdocker image prune # Remove unused images
# Build images (we'll cover this next)docker build -t myapp:v1 .Гіпотетичний сценарій: локальний тестовий сервіс починає повертати connection refused після того, як розробник внутрішньо змінив порт застосунку з 8000 на 8080, але команда запуску все ще відображає хост на старий порт контейнера. Виправленням може бути однорядкове коригування порту, проте діагностичний урок є набагато більшим: пошук несправностей у контейнерах просувається найшвидше, коли ви відокремлюєте стан здоров’я процесу, внутрішню адресу прослуховування, відображення портів хоста та URL-адресу клієнта замість того, щоб розглядати кожне відхилене з’єднання як загальну проблему мережі. Такий самий поділ матиме значення пізніше, коли Kubernetes додасть Services, probes та Pod networking до цього шляху.
Збирання образів контейнерів
Розділ «Збирання образів контейнерів»Збирання образу означає перетворення каталогу з файлами застосунку та Dockerfile на шаруватий артефакт. Docker не надсилає весь ваш комп’ютер збирачу; він надсилає контекст збирання, яким зазвичай є каталог наприкінці команди docker build. Якщо цей контекст включає .git, локальні віртуальні середовища, завантажені залежності, знімки екрана або секрети, Docker має просканувати та передати їх, а необережне COPY . . може помістити їх в образ. Хороший Dockerfile та хороший .dockerignore працюють разом.
Уявіть контекст збирання як запечатаний конверт, який ви передаєте збирачу. Dockerfile може копіювати файли лише з цього конверта, тому відсутній файл спричиняє очевидну помилку збирання, тоді як надто великий конверт призводить до повільного збирання та несподіваних включень. Саме тому docker build -t app . зазвичай має виконуватися з кореня проєкту або свідомо вибраного підкаталогу. Крапка в кінці не є прикрасою; вона вибирає межу файлової системи для збирання.
Dockerfile — це декларативний рецепт збирання, де кожна інструкція змінює файлову систему або метадані образу. FROM вибирає базовий образ, WORKDIR встановлює каталог за замовчуванням для подальших інструкцій, COPY переміщує файли з контексту збирання, RUN виконує команди під час збирання, а CMD описує команду за замовчуванням під час запуску контейнера. Ключовою відмінністю є час збирання порівняно з часом виконання: RUN відбувається під час створення образу, тоді як CMD виконується, коли з цього образу запускається контейнер.
# Base imageFROM python:3.12-slim
# Set working directoryWORKDIR /app
# Copy dependency fileCOPY requirements.txt .
# Install dependencies (use --no-cache-dir to save space by omitting downloaded wheels)RUN pip install --no-cache-dir -r requirements.txt
# Copy application codeCOPY . .
# Expose port (documentation)EXPOSE 8000
# Default commandCMD ["python", "app.py"]Команда build дає образу ім’я та тег за допомогою -t, а потім вказує Docker на контекст за допомогою .. Команда run створює контейнер з образу і відображає порт хоста на порт, який застосунок слухає внутрішньо. EXPOSE у Dockerfile — це документація та метадані; вона сама по собі не публікує порт, тому вам усе ще потрібен -p, якщо ви хочете отримувати трафік з хоста.
# Build imagedocker build -t myapp:v1 .
# Run container from imagedocker run -d -p 8000:8000 myapp:v1| Інструкція | Призначення |
|---|---|
FROM | Базовий образ для збирання |
WORKDIR | Встановлення робочого каталогу |
COPY | Копіювання файлів з хоста в образ |
ADD | Як COPY, але може розпаковувати архіви та отримувати файли за URL |
RUN | Виконання команди під час збирання |
ENV | Встановлення змінної середовища |
EXPOSE | Документування того, який порт використовує застосунок |
CMD | Команда за замовчуванням під час запуску контейнера |
ENTRYPOINT | Команда, яка завжди виконується (CMD стає аргументами) |
Тепер зберемо невеликий Flask-застосунок. Суть не в самому Flask; суть у тому, щоб побачити, як залежності, вихідні файли, змінні середовища та команда під час виконання взаємодіють між собою. Застосунок зчитує NAME із середовища, а це означає, що той самий образ може генерувати різні привітання під час виконання без повторного збирання. Це саме той патерн, який вам потрібен для контейнеризованого програмного забезпечення: незмінний образ, змінна конфігурація під час виконання.
Створіть файл застосунку як app.py; він навмисно невеликий, щоб механіка контейнера залишалася видимою, а не ховалася під кодом фреймворку.
from flask import Flaskimport os
app = Flask(__name__)
@app.route('/')def hello(): name = os.getenv('NAME', 'World') return f'Hello, {name}!'
if __name__ == '__main__': app.run(host='0.0.0.0', port=8000)Запишіть залежність Python у requirements.txt, щоб установлення залежностей можна було кешувати окремо від змін у вихідному коді застосунку.
flask==3.0.0Зупиніться та подумайте: Якщо ви запустите
docker build .у своєму домашньому каталозі без файлу.dockerignore, Docker спробує скопіювати все — Документи, Завантаження, Зображення — у контекст збирання, що може призвести до зависання збирання та вичерпання пам’яті. Саме тому контекст збирання має бути частиною проєктування вашого артефакту, а не залишатися напризволяще.
Тепер створіть Dockerfile для образу застосунку, зберігаючи копіювання залежностей перед копіюванням вихідного коду, щоб звичайні зміни коду не викликали перевстановлення Flask.
FROM python:3.12-slim
WORKDIR /app
# Install dependencies first (better caching)COPY requirements.txt .# Use --no-cache-dir to prevent pip from storing downloaded packages, keeping the image smallRUN pip install --no-cache-dir -r requirements.txt
# Copy application codeCOPY app.py .
EXPOSE 8000
CMD ["python", "app.py"]Зберіть образ, запустіть його зі змінною середовища та переконайтеся, що той самий образ може давати різну відповідь без повторного збирання.
# Builddocker build -t hello-flask:v1 .
# Rundocker run -d -p 8000:8000 -e NAME=Docker hello-flask:v1
# Testcurl http://localhost:8000# Hello, Docker!
# Cleanupdocker rm -f $(docker ps -q --filter ancestor=hello-flask:v1)Додайте файл .dockerignore, перш ніж довіряти COPY . . у реальному проєкті. Файл використовує патерни для видалення файлів, призначених лише для розробки, з контексту збирання, що прискорює збирання та зменшує ймовірність випадкового вбудовування облікових даних або артефактів, специфічних для хоста. Для контейнера це еквівалент того, щоб спакувати в подорож лише найнеобхідніше замість того, щоб скидати всю квартиру у валізу.
.git.venvnode_modules__pycache__*.log.envdistcoverageКешування шарів — це те, де Docker починає винагороджувати ретельне впорядкування. Кожна інструкція Dockerfile створює або повторно використовує шар, і Docker може пропустити роботу, коли інструкція та файли, від яких вона залежить, не змінилися. Якщо ви копіюєте всі вихідні файли перед установленням залежностей, кожна незначна зміна коду анулює шар установлення залежностей. Якщо ви спершу копіюєте маніфести залежностей, установлюєте залежності, а згодом копіюєте код застосунку, звичайні редагування коду повторно використовують “дорогий” шар залежностей.
Зупиніться та подумайте: Подумайте про це. Якщо ви змінюєте один рядок коду на Python, чи хочете ви, щоб Docker перевстановив усі залежності, що може зайняти 2 хвилини, чи просто скопіював змінений файл, що займає близько 1 секунди? Відповідь цілком залежить від порядку інструкцій у вашому Dockerfile, і приклад поганого й хорошого підходів нижче показує, чому порядок має значення.
# BAD: Code changes invalidate dependency cacheFROM python:3.12-slimWORKDIR /appCOPY . . # Any change busts cacheRUN pip install -r requirements.txt # Reinstalls every time!CMD ["python", "app.py"]
# GOOD: Dependencies cached separatelyFROM python:3.12-slimWORKDIR /appCOPY requirements.txt . # Only changes when deps changeRUN pip install -r requirements.txt # Cached unless deps changeCOPY . . # App changes don't bust pip cacheCMD ["python", "app.py"]Це саме правило шарів пояснює, чому видалення великого файлу в пізнішій інструкції не зменшує образ настільки, наскільки очікують люди. Попередній шар усе ще містить файл; пізніший шар лише приховує його від об’єднаного вигляду файлової системи. Коли вам потрібні тимчасові інструменти збирання, використовуйте одну інструкцію RUN, яка завантажує, використовує та видаляє тимчасові файли разом, або використовуйте багатоетапне збирання, щоб фінальний образ середовища виконання копіював лише готовий артефакт.
Історія шарів також пояснює, чому секрети не слід передавати як звичайні аргументи збирання, а потім видаляти. Якщо значення з’являється в шарі збирання, метаданих образу, історії оболонки або скопійованому файлі, подальше очищення може приховати його з фінальної файлової системи, не видаляючи з історії артефакту. Використовуйте BuildKit secret mounts для секретів під час збирання та впровадження секретів під час виконання для розгорнутих застосунків. Найбезпечніший образ — це той, який ніколи від самого початку не містив секрету.
Який підхід ви б обрали тут і чому: одноетапний образ, який включає компілятор для простішої діагностики, чи багатоетапний образ, який копіює лише скомпільований бінарний файл у мінімальну базу для виконання? Для локального навчального проєкту одноетапний образ може бути прийнятним, оскільки доступ до оболонки та інструментів збирання полегшує дослідження. Для продакшену зазвичай виграють багатоетапні збирання, оскільки вони зменшують час завантаження, кількість вразливостей і набір інструментів для зловмисника.
Docker Compose та локальні багатоконтейнерні робочі процеси
Розділ «Docker Compose та локальні багатоконтейнерні робочі процеси»Реальним застосункам зазвичай потрібен більше ніж один процес. Вебсервісу може знадобитися база даних, кеш, фоновий обробник (background worker) та зворотний проксі-сервер (reverse proxy). Docker може запускати кожен контейнер вручну, але командні рядки стають крихкими, оскільки вам доводиться пам’ятати мережі, імена, порти, змінні середовища та томи. Docker Compose надає невелику YAML-модель для локального стека, щоб команда могла запустити однакове середовище розробки однією командою.
Зупиніться та подумайте: Якщо у вас є вебзастосунок і база даних, чому розміщувати їх в одному Dockerfile та контейнері — це погана ідея? Ви б пов’язали їхні життєві цикли, журнали, використання ресурсів, сховище та рішення щодо масштабування. Compose зберігає їх відокремленими локально, тоді як Kubernetes пізніше використовує Pod, Service, Deployment та PersistentVolume для продакшн-версії такого ж розділення.
Для локальної розробки з кількома сервісами наведений нижче файл Compose визначає сервіс web, зібраний з поточного каталогу, та сервіс db, що використовує образ PostgreSQL. Сервіс web звертається до бази даних за ім’ям сервісу db, а не через localhost, оскільки кожен контейнер має власний інтерфейс loopback. Іменований том db_data зберігає файли бази даних поза контейнером бази даних, що дозволяє замінити контейнер без видалення даних.
Розмістіть визначення локального стека у файлі compose.yaml, де кожен сервіс володіє однією межею процесу, а Docker Compose забезпечує спільну мережу.
# version: '3.8' # Obsolete in modern Compose, left for backward compatibility
services: web: build: . ports: - "8000:8000" environment: - DATABASE_URL=postgres://db:5432/mydb depends_on: - db
db: image: postgres:16 environment: - POSTGRES_DB=mydb - POSTGRES_PASSWORD=secret volumes: - db_data:/var/lib/postgresql/data
volumes: db_data:# Start all servicesdocker compose up -d
# View logsdocker compose logs -f
# Stop all servicesdocker compose down
# Stop and remove volumesdocker compose down -vCompose — це інструмент для локальної розробки, а не оркестратор для продакшену в межах цього курсу. Ця відмінність є важливою, оскільки Compose не замінює планування Kubernetes, керування працездатністю, стратегію розгортання, мережу кластера або контроль політик. Однак він полегшує перехід, оскільки навчає іменам сервісів, постійним томам, роздільним контейнерам та декларативній конфігурації стека ще до того, як вам доведеться вивчати об’єкти кластера.
Compose також виявляє типове непорозуміння щодо мереж. З вашого хоста localhost вказує на ваш ноутбук. Зсередини контейнера localhost вказує на цей контейнер. Від одного сервісу Compose до іншого ім’я сервісу зазвичай є правильною адресою, оскільки Docker забезпечує DNS у мережі проєкту. Коли фронтенд-контейнер викликає localhost, очікуючи бекенд-контейнер, він звертається сам до себе, а не до сусіда.
flowchart TD A[Dockerfile] -->|1. WRITE| B[docker build] B -->|2. BUILD| C[(Image: myapp:v1)] C -->|3. PUSH optional| D[docker push] D --> E[(Registry: Docker Hub, ECR)] C -->|4. RUN| F[docker run] F --> G{{Container: running instance}}Діаграма робочого процесу навмисно лінійна, але реальні команди повторюють його багато разів на день. Розробник редагує код, перезбирає образ, запускає його локально, надсилає до реєстру і дозволяє CI або кластеру споживати його. Кожна погана звичка на ранніх етапах ускладнює ситуацію пізніше: надто великий образ уповільнює кожне завантаження (pull), плаваючий тег робить відкочування неоднозначними, а кореневий процес (root process) перетворює помилку в застосунку на більшу безпекову проблему.
Якість образів, безпека та форма виконання
Розділ «Якість образів, безпека та форма виконання»Вибір базового образу — одне з найперших архітектурних рішень у Dockerfile. Образ із повноцінною ОС здається зручним, бо має знайомі інструменти, але він також приносить пакунки, які вам не потрібні. Образ slim видаляє частину зайвого, зберігаючи основну сумісність. Alpine ще менший, але його musl libc може здивувати застосунки або залежності, що очікують поведінки glibc. Образи Distroless видаляють оболонки та менеджери пакунків, що чудово підходить для посилення безпеки в продакшені, але вимагає більш зрілої спостережуваності та дисципліни збирання.
| Тип базового образу | Приклад | Розмір | Поверхня атаки | Найкраще підходить для |
|---|---|---|---|---|
| Повноцінна ОС | ubuntu:24.04 | ~70MB | Велика (містить повний набір утиліт) | Складних застарілих застосунків, що потребують багатьох системних залежностей |
| Slim | python:3.12-slim | ~40MB | Середня (урізаний Debian) | Більшості стандартних застосунків, гарний баланс розміру та сумісності |
| Alpine | python:3.12-alpine | ~15MB | Мала (використовує musl замість glibc) | Ультрамалих образів, але може викликати проблеми з компіляцією C-розширень |
| Distroless | gcr.io/distroless/static | ~2MB | Мінімальна (немає оболонки чи менеджера пакунків) | Продакшн-розгортань компільованих мов (Go, Rust) |
Таблиця наводить приблизні розміри, оскільки теги з часом змінюються, але компроміс залишається стабільним. Менше — не означає автоматично краще, якщо це коштує годин зневадження нативних залежностей, а більше — не означає автоматично неправильно, якщо застосунок дійсно потребує системних бібліотек. Рішення має бути свідомим: обирайте найменшу базу, яка підтримує застосунок, не змушуючи вдаватися до крихких обхідних шляхів, і переглядайте свій вибір у міру розвитку сервісу.
Стратегія встановлення патчів належить до тієї ж розмови, що й вибір базового образу. Сервіс, який збирається з python:3.12-slim, успадковує пакети операційної системи з цього базового образу, тому перезбирання після того, як видавець оновить тег, може підтягнути виправлення безпеки. Сервіс, закріплений за дайджестом (digest), є більш відтворюваним, але він не зміниться, доки ви свідомо не оновите дайджест. Зрілі команди поєднують автоматизацію перезбирання, сканування на вразливості та перевірку релізів, а не покладаються виключно на плаваючі теги або постійні закріплення.
Зупиніться та подумайте: Якщо ви використовуєте образ повноцінної ОС, такий як
ubuntu:24.04, лише для запуску простого Python-застосунку, які два головних недоліки ви отримаєте? Ви зіткнетеся з повільнішим часом завантаження образу через великий розмір файлу, а також відкриєте більшу поверхню атаки, оскільки невикористані пакети все одно можуть містити вразливості.
# BAD: Full OS, huge imageFROM ubuntu:24.04RUN apt-get update && apt-get install -y python3 python3-pipCOPY . .# Note: In Ubuntu 24.04, system-wide pip installs require --break-system-packagesRUN pip3 install --break-system-packages -r requirements.txt
# GOOD: Slim base, smaller imageFROM python:3.12-slimCOPY requirements.txt .RUN pip install --no-cache-dir -r requirements.txtCOPY . .
# BETTER: Alpine (tiny base)FROM python:3.12-alpine# Note: Alpine uses apk, not apt, and musl instead of glibcЗапуск від імені користувача root усередині контейнера — ще одне стандартне налаштування, яке заслуговує на увагу. Контейнери ізолюють процеси, але root усередині контейнера все одно має більше влади, ніж не-root процес, особливо в поєднанні з файловими системами, доступними для запису, змонтованими шляхами хоста, додатковими можливостями Linux або вразливістю середовища виконання. Використання не-root користувача — не панацея, проте воно зменшує шкоду, якої може завдати зловмисник після експлуатації застосунку.
Власність файлів є практичною частиною образів без прав root. Якщо ви перемикаєтеся на USER appuser перед копіюванням файлів або створенням каталогів із правом запису, застосунок може завершитися з помилками доступу. У прикладі використовується COPY --chown=appuser:appuser, щоб користувач середовища виконання володів файлами застосунку. У більших образах вам слід створювати лише ті каталоги, в які процесу потрібно писати, свідомо призначати власника і залишати решту файлової системи доступною лише для читання там, де ваша платформа це дозволяє.
Зупиніться та подумайте: Що станеться, якщо зловмисник знайде вразливість віддаленого виконання коду (RCE) у вашому застосунку, а ваш контейнер працює від імені
root? Він негайно отримаєroot-привілеї в межах контейнера, що полегшує модифікацію файлів, перевірку змонтованих секретів, зловживання можливостями або спробу втечі з контейнера через слабкі місця хоста чи середовища виконання.
# BAD: Running as rootFROM python:3.12-slimCOPY . .CMD ["python", "app.py"] # Runs as root!
# GOOD: Non-root userFROM python:3.12-slimRUN useradd -m appuserWORKDIR /appCOPY --chown=appuser:appuser . .USER appuserCMD ["python", "app.py"]Один процес на контейнер — це скоріше не про чистоту, а про операційну ясність. Якщо nginx, Python-застосунок та Redis працюють в одному контейнері, їхні журнали перемішуються, перезапуски впливають на всі процеси, обмеження ресурсів стають розмитими, а масштабування вебрівня також випадково масштабує базу даних або кеш. Роздільні контейнери дозволяють кожному процесу володіти власним життєвим циклом, тоді як Compose або Kubernetes керують тим, як ці частини спілкуються між собою.
Існують винятки, але вони мають бути усвідомленими. Допоміжний процес, який готує конфігурацію та завершується, може міститися у скрипті entrypoint, тоді як тісно пов’язаний патерн “sidecar” більш природно вписується в Kubernetes, ніж в один Docker-контейнер. Стандартний підхід залишається простим: якщо два процеси мають різні потреби в перезапуску, різні журнали, різні профілі ресурсів або різну поведінку при масштабуванні, ймовірно, вони мають бути в окремих контейнерах.
flowchart TD subgraph BAD [BAD: Multiple services in one container] A[Container: nginx + python app + redis] end
subgraph GOOD [GOOD: Separate containers] B[Container 1: nginx] C[Container 2: python app] D[Container 3: redis] endВикористовуйте Docker Compose або Kubernetes для оркестрування цих роздільних контейнерів, оскільки оркестратор повинен керувати зв’язками, тоді як кожен контейнер зберігає одну чітку відповідальність під час виконання.
Найсильніші продакшн-образи є нудними. Вони використовують специфічні теги або дайджести, копіюють лише необхідні файли, детерміновано встановлюють залежності, працюють не від імені root, розкривають один головний процес і дозволяють конфігурації середовища виконання надходити через змінні середовища, змонтовані файли або примітиви оркестрації. Такі нудні рішення окупаються під час інцидентів, оскільки фахівці з реагування можуть швидко відповісти на базові запитання: який код виконується, від імені якого користувача він працює, який порт прослуховує і де зберігаються його постійні дані?
Патерни та антипатерни
Розділ «Патерни та антипатерни»Патерни Docker корисні лише тоді, коли вони пов’язані з причиною, що стоїть за правилом. “Використовуйте .dockerignore” — це просто гасло, доки ви не побачите, як збирання відправляє величезний контекст або випадково копіює локальний секрет. “Запускайте без прав root” — це гасло, доки ви не відстежите, як доступний для запису змонтований шлях змінює радіус ураження скомпрометованого процесу. Наведена нижче таблиця узагальнює рішення, які мають найбільше значення в повсякденній розробці.
| Патерн | Коли використовувати | Чому це працює | Врахування масштабування |
|---|---|---|---|
| Впорядкування Dockerfile із пріоритетом залежностей | Застосунки з маніфестами пакетів, такими як requirements.txt, package-lock.json або go.mod | Ресурсомісткі шари встановлення залишаються в кеші при зміні вихідних файлів | Перезбирання в CI залишаються швидкими в міру зростання сервісу |
| Невеликі сумісні базові образи | Більшість продакшн-сервісів та образів, зібраних у CI | Зменшує час завантаження та присутність невикористаних пакетів без шкоди для підтримки середовища виконання | Стандартизуйте базові родини для кожної мови, щоб спростити встановлення патчів |
| Конфігурація середовища виконання через змінні або монтування | Значення, що різняться залежно від середовища, наприклад імена хостів, feature flags (прапорці функцій) та облікові дані | Зберігає образ незмінним, дозволяючи специфічну для розгортання конфігурацію | Перенесіть секрети до менеджерів секретів або Kubernetes Secrets пізніше |
| Іменовані томи для постійних локальних даних | Бази даних, черги та сервіси розробки зі станом (stateful) | Дані переживають заміну контейнера, не залишаючись усередині образу | Спроєктуйте цю концепцію на PersistentVolume у Kubernetes |
Антипатерни часто виникають через спроби змусити контейнери поводитися як звичні сервери. Розробники додають SSH, запускають кілька демонів, запікають конфігурацію в образ або використовують тег latest, бо це здається зручним у даний момент. Кожен такий короткий шлях усуває властивість, яка робила контейнери цінними: відтворюваність, ізоляцію, швидку заміну або чітке володіння станом.
| Антипатерн | Що йде не так | Краща альтернатива |
|---|---|---|
| Ставлення до контейнера як до віртуальної машини | Команди встановлюють додаткові сервіси, мутують стан вручну і залежать від унікальних (snowflake) контейнерів | Перезберіть образ та замініть контейнер |
| Сліпе копіювання всього репозиторію | Контексти збирання зростають, локальні артефакти витікають, а інвалідація кешу стає постійною | Використовуйте .dockerignore та вибіркові інструкції COPY |
| Постачання інструментів збирання в образах для виконання | Образи стають великими й розкривають компілятори або менеджери пакунків для зловмисників | Використовуйте багатоетапне збирання (multi-stage builds) та копіюйте лише артефакти виконання |
Покладання на тег latest у спільних середовищах | Перезбирання стають невідтворюваними, а відкочування — незрозумілими | Використовуйте специфічні теги та записуйте дайджести для релізів |
Практичний тест полягає в тому, чи можна видалити та перестворити ваш контейнер без зайвих церемоній. Якщо під час видалення контейнера втрачаються важливі дані, вам потрібен том або зовнішній сервіс. Якщо перестворення образу вимагає виконання ручних кроків усередині запущеного контейнера, Dockerfile є неповним. Якщо член команди не може виконати ту саму команду й отримати таку ж поведінку, межа конфігурації не є чіткою.
Тому якісне рев’ю контейнера ставить операційні запитання, а не лише запитання щодо синтаксису. Які файли потрапляють до контексту збирання? Який шар змінюється найчастіше? Який користувач запускає процес? Як впроваджується конфігурація? Що станеться, якщо контейнер буде видалено? Куди потрапляють журнали? Ці запитання перетворюють перевірку Docker з особистих уподобань на інженерну дискусію про відтворюваність, відновлення та радіус ураження.
Структура прийняття рішень
Розділ «Структура прийняття рішень»Використовуйте Docker безпосередньо, коли ви тренуєтесь з одним контейнером, ізолюєте параметр середовища виконання або налагоджуєте дрібну помилку. Використовуйте Compose, коли проблема локальної розробки включає кілька сервісів, що взаємодіють. Використовуйте Kubernetes, коли планування, контроль розгортання, виявлення сервісів, політики та стійкість на рівні кластера важливіші за зручність однієї робочої станції. Ці інструменти частково дублюють одне одного, але операційні завдання відрізняються.
| Ситуація | Найкращий вибір | Компроміс |
|---|---|---|
| Зібрати та протестувати один образ локально | Docker CLI | Швидко та прямолінійно, але вручну для кількох сервісів |
| Запустити вебзастосунок з локальною базою даних | Docker Compose | Декларативний локальний стек, але не планувальник для продакшену |
| Розгорнути репліки за стабільним сервісом | Kubernetes 1.35+ | Потужна модель оркестрації, але більше об’єктів та концепцій |
| Захистити образ для продакшену | Багатоетапний Dockerfile з базою slim або distroless | Менший радіус ураження, але менше можливостей для інтерактивного налагодження |
+----------------------------+| What are you trying to do? |+-------------+--------------+ | v+----------------------------+ yes +-----------------------+| One container, local test? | --------------> | Use Docker CLI |+-------------+--------------+ +-----------------------+ | no v+----------------------------+ yes +-----------------------+| Several local services? | --------------> | Use Docker Compose |+-------------+--------------+ +-----------------------+ | no v+----------------------------+ yes +-----------------------+| Production orchestration? | --------------> | Use Kubernetes 1.35+ |+-------------+--------------+ +-----------------------+ | no v+----------------------------+| Reframe the problem first |+----------------------------+Коли ви сумніваєтеся, починайте з найменшого інструменту, який відповідає на ваше запитання. Якщо вам потрібно дізнатися, чи запускається образ і чи прослуховує він правильний порт, достатньо Docker CLI. Якщо вам потрібно перевірити, чи застосунок звертається до бази даних за іменем сервісу, достатньо Compose. Якщо вам потрібно дізнатися, чи безпечно розгортаються три репліки за Service, переходьте до Kubernetes, замість того щоб перетворювати Compose на симулятор кластера.
Така звичка приймати рішення запобігає двом типовим марнуванням часу. Перше марнування — це розгортання цілого кластера для налагодження неробочого Dockerfile, тоді як локальний docker run миттєво показав би відсутній файл. Інше марнування — спроба симулювати поведінку розгортання в продакшені за допомогою файлу Compose, коли реальне питання стосується readiness probes, Services, Deployments або планування на рівні кластера. Обирайте інструмент, який виявляє той тип збою, що ви насправді досліджуєте.
Чи знали ви?
Розділ «Чи знали ви?»- BuildKit став збирачем за замовчуванням у Docker Engine 23.0. Він покращив роботу з кешем, паралельне виконання збирання та підтримку секретів збирання порівняно зі старим збирачем.
- Docker Engine делегує низькорівневе виконання containerd та runc. Ці проєкти відповідають моделі середовища виконання Open Container Initiative, якої також дотримуються середовища, сумісні з Kubernetes.
- У версії 1.24 Kubernetes видалив dockershim. Сучасні вузли Kubernetes використовують CRI-сумісні середовища виконання безпосередньо, але вони все ще запускають образи контейнерів OCI, які може збирати Docker.
- Тег
latest— це лише тег. Він не є автоматично найновішим, найбезпечнішим чи закріпленим, і з часом може вказувати на інший вміст образу.
Типові помилки
Розділ «Типові помилки»| Помилка | Чому це трапляється | Як це виправити |
|---|---|---|
Використання тегу latest | Це здається зручним під час експериментів, але робить збирання та відкати непередбачуваними | Використовуйте конкретні теги, такі як nginx:1.26, і фіксуйте дайджести для релізів |
| Запуск від імені root | Багато базових образів за замовчуванням використовують root, а ранні приклади пропускають користувачів для простоти | Додайте користувача без прав root та встановіть USER перед CMD |
| Ігнорування порядку шарів | Розробники копіюють увесь репозиторій перед встановленням залежностей | Спочатку скопіюйте маніфести залежностей, встановіть їх, а потім копіюйте файли вихідного коду, що змінюються |
| Копіювання всього | Контекст збирання за замовчуванням включає локальне “сміття”, якщо його не вилучити | Додайте .dockerignore і використовуйте вибіркові шляхи для COPY |
| Відсутність очищення | Зупинені контейнери, “завислі” шари (dangling layers) та невикористані томи накопичуються непомітно | Використовуйте docker ps -a, docker image prune та обережне очищення томів |
| Жорстке кодування секретів | Локальні демонстрації розмивають межу між конфігурацією та вмістом образу | Ін’єктуйте значення під час виконання та використовуйте менеджери секретів у реальних середовищах |
| Використання ADD замість COPY | ADD виглядає як більш потужна інструкція копіювання | Використовуйте COPY за замовчуванням, якщо вам явно не потрібне розпакування локальних архівів |
| Ігнорування багатоетапних збірок | Команди оптимізують перший робочий образ і забувають про розмір для виконання | Збирайте на одному етапі, а потім копіюйте лише артефакти у кінцевий образ |
Вправа: Знайдіть помилки
Розділ «Вправа: Знайдіть помилки»Подивіться на наступний Dockerfile так, ніби ви перевіряєте pull request вашого колеги. Тут є щонайменше три серйозні антипатерни або помилки, і ґрунтовний огляд має пояснити як безпосередній симптом, так і подальший ризик для продакшену. Спробуйте визначити їх до того, як розгорнути відповідь, оскільки це та сама навичка рецензування, яку ви будете використовувати, коли Kubernetes Deployment вказуватиме на образ, який ви не створювали самостійно.
FROM node:22COPY . .RUN npm installCMD npm startПереглянути відповіді
- Відсутність .dockerignore або вибіркового COPY: Копіювання
.у.перед виконаннямnpm installозначає, що локальна папкаnode_modulesможе бути скопійована, що є занадто великим і залежить від платформи. Також будь-яка зміна коду робить недійсним кешnpm install. - Ігнорування кешування шарів: Слід спочатку зробити
COPY package*.json ./, потімRUN npm install, і лише після цьогоCOPY . .. Це гарантує, що залежності будуть перевстановлені лише тоді, коли зміниться package.json. - Використання повного базового образу ОС:
node:22є великим порівняно з варіантами slim або Alpine. Слід використовуватиnode:22-slimабоnode:22-alpine, якщо дозволяє сумісність, щоб зменшити поверхню атаки та час завантаження (pull time). - Запуск від імені root: Не вказано
USER, а це означає, що процес Node запускається від імені користувача root всередині контейнера, що є загрозою безпеці.
Контрольні запитання
Розділ «Контрольні запитання»Сценарій: Ви створюєте застосунок Node.js. Щоразу, коли ви змінюєте один рядок CSS, Docker витрачає кілька хвилин на перезбирання образу, оскільки він перевстановлює всі пакети NPM. Як можна переписати Dockerfile, щоб вирішити цю проблему з часом збирання?
Вам слід відокремити копіювання файлів залежностей від копіювання файлів вихідного коду. Спочатку скопіюйте package.json та lock-файл, запустіть встановлення пакетів, а потім скопіюйте решту застосунку. Після цього Docker зможе повторно використовувати шар залежностей, коли змінюватиметься лише CSS або код застосунку. Глибша причина полягає в тому, що ключ кешу для шару залежить від інструкції та її вхідних даних, тому широкі ранні інструкції COPY змушують непов’язані правки виглядати важливими для Docker.
Сценарій: У вас локально запущений контейнер із базою даних, але після його перезапуску всі збережені користувачі зникають. Якого параметра docker run вам бракує і як він вирішує проблему?
Вам бракує монтування тому (volume mount), наприклад -v db_data:/var/lib/postgresql/data для даних PostgreSQL. Записуваний шар контейнера прив’язаний до життєвого циклу контейнера, тому видалення контейнера призводить до видалення записаних там даних. Іменований том зберігає файли бази даних поза цим життєвим циклом і може бути приєднаний до нового контейнера. Це відповідає ідеї Kubernetes про те, що стан повинен жити в томі або зовнішньому сервісі, а не всередині змінного екземпляра контейнера.
Сценарій: Ваш контейнер із вебзастосунком успішно запускається, але `localhost:8080` повертає помилку 500 Internal Server Error замість вмісту застосунку. Які дві перші команди Docker вам слід виконати і що саме ви шукаєте?
Почніть з docker logs <container>, оскільки застосунок вже може повідомляти про трасування стека (stack trace), відсутню змінну середовища, невдале підключення до бази даних або попередження під час запуску. Якщо логів недостатньо, використовуйте docker exec -it <container> bash або sh, щоб оглянути середовище виконання зсередини контейнера. Усередині контейнера перевірте змінні середовища, змонтовані файли, стан процесу і те, чи відповідає застосунок на своєму внутрішньому порту. Така послідовність дозволяє уникнути ворожіння з боку хоста до того, як ви ознайомитеся з доказами, які вже створив процес.
Сценарій: Розробник намагається зменшити розмір образу, завантажуючи великий тимчасовий файл в одній інструкції `RUN` і видаляючи його в наступній. Підсумковий розмір образу не зменшується. Чому?
Кожна інструкція Dockerfile створює шар, а шари є незмінними (immutable) після створення. Шар завантаження все ще містить великий файл, тоді як пізніший шар видалення лише приховує його від остаточного вигляду об’єднаної файлової системи. Щоб уникнути зберігання файлу, завантажуйте, використовуйте та видаляйте його в тій самій інструкції RUN, або перенесіть тимчасову роботу на етап збирання (build stage), який не копіюється в кінцевий образ. Саме тому структура шарів впливає як на швидкість збирання, так і на розмір артефакту.
Сценарій: Ви розгортаєте скомпільований бінарний файл Go, і команда безпеки каже, що образ не повинен містити утиліт оболонки (shell) або менеджера пакетів. Який тип базового образу підходить найкраще і який компроміс він приносить?
Найкраще підходить базовий образ distroless, оскільки він містить застосунок і необхідні бібліотеки середовища виконання без загальних утиліт оболонки або менеджерів пакетів. Це зменшує набір інструментів для зловмисника, якщо застосунок буде скомпрометовано, і звужує площу поверхні образу. Компроміс полягає в тому, що інтерактивне налагодження стає складнішим, оскільки ви не можете просто виконати exec в контейнер і запустити звичні команди. Командам, які використовують образи distroless, потрібні кращі логи, метрики, перевірки працездатності (health checks), а іноді й debug-контейнери Kubernetes.
Сценарій: Ваш фронтенд у Compose намагається викликати `http://localhost:5000/api`, але бекенд міститься в окремому контейнері, і з'єднання відхиляється. Чому `localhost` є помилковим і що замість нього повинен використовувати фронтенд?
Усередині контейнера localhost означає loopback-інтерфейс того ж самого контейнера, а не хост і не інший сервіс. Тому фронтенд шукає процес бекенда всередині себе. У Compose сервіси спільно використовують внутрішню мережу, де імена сервісів розпізнаються через DNS Docker, тому фронтенд має викликати http://backend:5000/api, якщо сервіс має назву backend. Ця відмінність стає важливою і в Kubernetes, де Services забезпечують стабільні імена для зв’язку між подами (Pod-to-Pod).
Сценарій: Пакетний (batch) контейнер аварійно завершує роботу через помилку браку пам'яті (Out of Memory) і тепер має статус `Exited`. Ви хочете використати `docker exec`, щоб оглянути тимчасові файли. Що станеться і яким має бути правильний наступний крок?
docker exec завершиться помилкою, оскільки він може запустити новий процес лише всередині запущеного контейнера. Для зупиненого контейнера спочатку використайте docker logs та docker inspect, щоб зрозуміти причину збою, а потім використайте docker cp <container>:/path ./local-dir, якщо вам потрібні файли із записуваного шару. Інший варіант — закомітити зупинений контейнер у тимчасовий образ і запустити з нього новий інтерактивний контейнер, але копіювати файли зазвичай простіше. Головне — зберегти докази, а не одразу видаляти несправний контейнер.
Сценарій: Образ кандидата на реліз (release candidate) працює на ноутбуці одного розробника, але дає збій у CI, оскільки Dockerfile використовує `COPY . .`, а два середовища мають різні локальні файли. Який відгук (review feedback) ви б залишили?
Dockerfile залежить від неконтрольованого контексту збирання. Додайте файл .dockerignore, свідомо копіюйте маніфести залежностей і папки з вихідним кодом та переконайтеся, що згенеровані або локальні для машини файли виключені. Це покращує відтворюваність, оскільки вхідні дані для збирання образу стають явними, а не залежать від того, що випадково опинилося в робочій директорії. Це також зменшує ризик випадкового вбудовування облікових даних, кешів або специфічних для платформи залежностей.
Практична вправа
Розділ «Практична вправа»Ця лабораторна робота веде від єдиного статичного веб-контейнера до контейнера бази даних, що завершується з помилкою, і врешті — до оптимізованого образу для зневадження. Тримайте термінал відкритим для команд очищення та читайте логи перед виправленням кожного збою. У критеріях успіху використовуються прапорці, оскільки суть полягає не лише у виконанні команд; мета — довести, що кожен контейнер працює саме так, як ви задумали.
Рівень 1: Основи (Збирання та запуск)
Розділ «Рівень 1: Основи (Збирання та запуск)»Зібрати та запустити власний образ веб-сервера на базі мінімального nginx. Ця вправа перевіряє, чи можете ви поєднати Dockerfile, тег збирання, іменований контейнер та відображення порту хоста (host port mapping), не покладаючись на готовий образ застосунку.
- Створіть директорію та всередині неї створіть файл
index.htmlіз текстом “Hello KubeDojo!”. - Створіть
Dockerfile, який використовуєnginx:alpineяк базовий образ. - Скопіюйте ваш
index.htmlдо/usr/share/nginx/html/index.htmlвсередині образу. - Зберіть образ із назвою
dojo-web:v1. - Запустіть контейнер у фоновому режимі (detached mode), відобразивши порт 8080 на вашому хості на порт 80 у контейнері.
- Перевірте, виконавши
curl http://localhost:8080.
Переглянути рішення
mkdir dojo-web && cd dojo-webecho "Hello KubeDojo!" > index.html
cat <<EOF > DockerfileFROM nginx:alpineCOPY index.html /usr/share/nginx/html/index.htmlEOF
docker build -t dojo-web:v1 .docker run -d --name dojo-web-container -p 8080:80 dojo-web:v1
# Checkpoint verificationcurl http://localhost:8080Рівень 2: Середній (Оточення та логи)
Розділ «Рівень 2: Середній (Оточення та логи)»Діагностувати контейнер, що завершується з помилкою, використовуючи логи та змінні оточення. PostgreSQL навмисно завершує роботу, якщо відсутнє обов’язкове налаштування пароля, що робить цей випадок чудовою практикою для читання логів перед тим, як щось вгадувати.
- Виконайте
docker run -d --name db postgres:16. - Перевірте його статус за допомогою
docker ps -a. Зверніть увагу, що він одразу завершив роботу. - З’ясуйте причину помилки за допомогою
docker logs db. Лог пояснює вимогу щодо відсутнього пароля. - Видаліть контейнер, що завершився з помилкою.
- Запустіть його знову, цього разу передавши обов’язкову змінну оточення
POSTGRES_PASSWORD=secret. - Переконайтеся, що він продовжує працювати.
Переглянути рішення
docker run -d --name db postgres:16
# Checkpoint: verify it exiteddocker ps -a
# View logs to find the errordocker logs db
# Cleanupdocker rm db
# Run with correct environment variabledocker run -d --name db -e POSTGRES_PASSWORD=secret postgres:16
# Checkpoint: verify it stays runningdocker psРівень 3: Просунутий (Оптимізація та Exec)
Розділ «Рівень 3: Просунутий (Оптимізація та Exec)»Оптимізувати збирання та дослідити запущений контейнер. Цей рівень навмисно зроблено менш відшліфованим за production-образ, оскільки він навчає механіки встановлення пакетів, інтерактивних контейнерів та різниці між оболонкою на передньому плані (foreground) і довготривалим фоновим процесом (background).
- Напишіть
Dockerfile, який встановлюєcurlу базовий образubuntu. - Переконайтеся, що ви використовуєте
apt-get update && apt-get install -y curlв одній інструкціїRUN. Поясніть, чому це важливо для кешування шарів. - Зберіть та запустіть контейнер інтерактивно з
-itта командоюbash. - Всередині контейнера доведіть, що ви працюєте від імені root, ввівши
whoami. - Введіть
exit, щоб вийти. Зверніть увагу, що контейнер зупиняється. - Запустіть фоновий контейнер і скористайтеся
docker exec, щоб увійти в нього пізніше.
Переглянути рішення
cat <<EOF > DockerfileFROM ubuntu:24.04RUN apt-get update && apt-get install -y curlEOF
docker build -t my-ubuntu-curl .
# Run interactivelydocker run -it --name test-curl my-ubuntu-curl bash
# Inside container:# whoami# exit
# To run in background and exec later:docker run -d --name bg-curl my-ubuntu-curl sleep infinitydocker exec -it bg-curl bashПоєднання apt-get update та apt-get install гарантує, що індекс пакетів ніколи не буде кешуватися незалежно від пакетів, які встановлюються. Якби це були окремі шари, додавання нового пакета до списку встановлення пізніше могло б використати застарілий закешований шар оновлення і призвести до помилок розв’язання залежностей пакетів.
Критерії успіху
Розділ «Критерії успіху»- Ви зібрали
dojo-web:v1зDockerfileта перевірили його за допомогоюcurl http://localhost:8080. - Ви пояснили, чому порт хоста 8080 відображається на порт контейнера 80 у прикладі з nginx.
- Ви використали
docker logs dbдля діагностики контейнера PostgreSQL, що завершився помилкою, перш ніж виправити його. - Ви перезапустили PostgreSQL з обов’язковою змінною оточення та підтвердили, що він продовжує працювати.
- Ви зібрали
my-ubuntu-curlта увійшли в нього за допомогою інтерактивної оболонки. - Ви пояснили, чому
apt-get updateтаapt-get installмають бути в одній інструкціїRUN. - Ви очистили контейнери, які створили під час лабораторної роботи.
Джерела
Розділ «Джерела»- Огляд Docker
- Сторінка продукту Docker Desktop
- Довідник CLI для docker run
- Довідник Dockerfile
- Документація щодо кешу збирання Docker
- Документація щодо контексту збирання Docker
- Документація Docker Compose
- Документація щодо rootless режиму Docker
- Специфікація образів Open Container Initiative
- Специфікація середовища виконання Open Container Initiative
- Документація containerd
- Документація середовищ виконання контейнерів Kubernetes
Наступний модуль
Розділ «Наступний модуль»Модуль 1.3: Що таке Kubernetes? - Високорівневий огляд оркестрації контейнерів і того, чому існує Kubernetes, коли робочих процесів з одним контейнером стає недостатньо.