Перейти до вмісту

Модуль 1.2: Основи Docker

Складність: [СЕРЕДНЯ] — потрібна практична робота

Час на виконання: 45–50 хвилин

Передумови: Модуль 1 (Що таке контейнери?)


Що ви зможете зробити

Розділ «Що ви зможете зробити»

Після завершення цього модуля ви зможете:

  • Зібрати образ Docker з Dockerfile та пояснити призначення кожної інструкції
  • Запустити контейнери з прокиданням портів (port mapping), змінними середовища та монтуванням томів (volume mounts)
  • Діагностувати несправний контейнер, читаючи логи та виконуючи команди всередині нього через exec
  • Пояснити систему шарів образу та чому порядок шарів важливий для швидкості збірки

Docker є найпоширенішим інструментом для збірки образів контейнерів. Попри те, що Kubernetes більше не використовує Docker як середовище виконання (runtime), Docker залишається стандартом для:

  • Збірки образів контейнерів
  • Локальної розробки
  • Тестування та діагностики

Вам знадобиться “достатньо знань” про Docker, щоб розуміти Kubernetes, — не обов’язково бути його гуру.


Terminal window
# Встановіть Docker Desktop
# Завантажте з https://docker.com/products/docker-desktop
# Або скористайтеся Homebrew:
brew install --cask docker
Terminal window
# Офіційне встановлення
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker $USER
# Вийдіть і знову зайдіть у систему, щоб зміни груп набрали чинності

Перевірка встановлення

Розділ «Перевірка встановлення»
Terminal window
docker --version
# Docker version 24.x.x, build xxxxx
docker run hello-world
# Має з'явитися повідомлення "Hello from Docker!"

Ваш перший контейнер

Розділ «Ваш перший контейнер»

Зупиніться та подумайте: Коли ви запускаєте команду нижче, що станеться, якщо порт 8080 на вашій хост-машині вже зайнятий іншою програмою? Команда завершиться помилкою, оскільки Docker не зможе прив’язатися до вже зайнятого порту хоста. Вам доведеться обрати інший порт, наприклад -p 8081:80.

Terminal window
# Запустити nginx (вебсервер)
docker run -d -p 8080:80 nginx
# Що відбулося:
# - Завантажено образ nginx з Docker Hub
# - Створено контейнер із цього образу
# - Запущено контейнер у фоновому режимі (-d)
# - Прокинуто порт 8080 (хост) на порт 80 (контейнер)
Terminal window
# Тестування
curl http://localhost:8080
# Повертає HTML-код вітальної сторінки nginx
# Перегляд запущених контейнерів
docker ps
# CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES
# a1b2c3d4e5f6 nginx "/docker-entrypoint.…" Up 10 seconds 0.0.0.0:8080->80/tcp tender_tesla
# Зупинити контейнер
docker stop a1b2c3d4e5f6
# Видалити контейнер
docker rm a1b2c3d4e5f6

Основні команди Docker

Розділ «Основні команди Docker»

Життєвий цикл контейнера

Розділ «Життєвий цикл контейнера»
Terminal window
# Запустити контейнер
docker run [OPTIONS] IMAGE [COMMAND]
# Поширені опції:
docker run -d nginx # Фоновий режим (background)
docker run -it ubuntu bash # Інтерактивний термінал
docker run -p 8080:80 nginx # Прокидання портів (port mapping)
docker run -v /host/path:/container/path nginx # Монтування тому (volume mount)
docker run --name myapp nginx # Контейнер із власною назвою
docker run -e MY_VAR=value nginx # Змінна середовища
docker run --rm nginx # Видалити після зупинки
# Керування контейнерами
docker ps # Список запущених контейнерів
docker ps -a # Список усіх контейнерів
docker stop CONTAINER # Коректна зупинка
docker kill CONTAINER # Примусова зупинка
docker rm CONTAINER # Видалення зупиненого контейнера
docker rm -f CONTAINER # Примусове видалення (stop + rm)

Інспектування контейнерів

Розділ «Інспектування контейнерів»

Підхід до діагностики: Коли контейнер працює неправильно, ці три команди — ваш інструментарій для налагодження, саме в такому порядку: docker logs (що сталося?), docker exec -it ... bash (погляну всередину), docker inspect (покажи повну конфігурацію). Ця ж логіка згодом застосовуватиметься в Kubernetes: kubectl logs, kubectl exec, kubectl describe.

Terminal window
# Перегляд логів
docker logs CONTAINER
docker logs -f CONTAINER # Відстежувати (tail)
docker logs --tail 100 CONTAINER # Останні 100 рядків
# Виконання команди в запущеному контейнері
docker exec -it CONTAINER bash # Інтерактивна оболонка
docker exec CONTAINER ls /app # Запуск окремої команди
# Перегляд деталей контейнера
docker inspect CONTAINER # Повна інформація у форматі JSON
docker stats # Використання ресурсів
docker top CONTAINER # Запущені процеси

Керування образами

Розділ «Керування образами»
Terminal window
# Завантаження образів
docker pull nginx
docker pull nginx:1.25
docker pull gcr.io/project/image:tag
# Примітка: Образи Docker адресуються за вмістом. SHA256-хеш образу є його справжнім ідентифікатором. Теги — це лише зрозумілі для людей псевдоніми.
# Список образів
docker images
# Видалення образів
docker rmi nginx
docker image prune # Видалити невикористовувані образи
# Збірка образів (розглянемо далі)
docker build -t myapp:v1 .

Збірка образів контейнерів

Розділ «Збірка образів контейнерів»

Dockerfile — це текстовий файл з інструкціями для збірки образу:

# Базовий образ
FROM python:3.11-slim
# Робочий каталог
WORKDIR /app
# Копіювання файлу залежностей
COPY requirements.txt .
# Встановлення залежностей
RUN pip install --no-cache-dir -r requirements.txt
# Копіювання коду застосунку
COPY . .
# Відкриття порту (документація)
EXPOSE 8000
# Команда за замовчуванням
CMD ["python", "app.py"]
Terminal window
# Зібрати образ
docker build -t myapp:v1 .
# Запустити контейнер із образу
docker run -d -p 8000:8000 myapp:v1
ІнструкціяПризначення
FROMБазовий образ, на основі якого будується новий
WORKDIRВстановлює робочий каталог
COPYКопіює файли з хоста в образ
ADDСхожа на COPY, але може розпаковувати архіви та завантажувати URL
RUNВиконує команду під час збірки
ENVВстановлює змінну середовища
EXPOSEДокументує, який порт використовує застосунок
CMDКоманда за замовчуванням при старті контейнера
ENTRYPOINTКоманда, що виконується завжди (CMD стає аргументами)

Практичний приклад: вебзастосунок на Python

Розділ «Практичний приклад: вебзастосунок на Python»

Створимо простий застосунок на Python:

app.py

from flask import Flask
import 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)

requirements.txt

flask==3.0.0

Історія з життя: контекст Docker на 5 ГБ Одного разу розробник ввів docker build . у своєму домашньому каталозі замість каталогу проєкту. Оскільки в нього не було файлу .dockerignore, Docker сумлінно спробував скопіювати всі його папки Documents, Downloads та Pictures у контекст збірки Docker. Збірка “зависла” на 20 хвилин, перш ніж вичерпати всю пам’ять комп’ютера. Завжди використовуйте файл .dockerignore, щоб виключити .git, node_modules та локальні файли середовища — це зробить контекст збірки малим і швидким.

Dockerfile

FROM python:3.11-slim
WORKDIR /app
# Спочатку встановлюємо залежності (краще кешування)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Копіюємо код застосунку
COPY app.py .
EXPOSE 8000
CMD ["python", "app.py"]

Збірка та запуск

Terminal window
# Збірка
docker build -t hello-flask:v1 .
# Запуск
docker run -d -p 8000:8000 -e NAME=Docker hello-flask:v1
# Тестування
curl http://localhost:8000
# Hello, Docker!
# Очищення
docker rm -f $(docker ps -q --filter ancestor=hello-flask:v1)

Зупиніться та подумайте: Поміркуйте над цим — якщо ви зміните один рядок у коді Python, чи хочете ви, щоб Docker знову встановлював усі залежності (що може тривати 2 хвилини)? Чи достатньо просто скопіювати змінений файл (що займає 1 секунду)? Відповідь повністю залежить від ПОРЯДКУ інструкцій у вашому Dockerfile. Прочитайте ПОГАНИЙ та ХОРОШИЙ приклади нижче та спробуйте зрозуміти, чому порядок має значення.

Docker кешує шари для швидшої збірки. Порядок важливий:

# ПОГАНО: зміна коду анулює кеш залежностей
FROM python:3.11-slim
WORKDIR /app
COPY . . # Будь-яка зміна скидає кеш
RUN pip install -r requirements.txt # Перевстановлюється щоразу!
CMD ["python", "app.py"]
# ДОБРЕ: залежності кешуються окремо
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt . # Змінюється лише при зміні deps
RUN pip install -r requirements.txt # Кешується, якщо deps не змінені
COPY . . # Зміни в коді не скидають кеш pip
CMD ["python", "app.py"]

Docker Compose (локальна робота з кількома контейнерами)

Розділ «Docker Compose (локальна робота з кількома контейнерами)»

Зупиніться та подумайте: Якщо у вас є вебзастосунок та база даних, чому поганою ідеєю буде помістити їх обох в один Dockerfile та контейнер? Як би ви масштабували вебзастосунок незалежно від бази даних, якби вони були жорстко пов’язані?

Для локальної розробки з кількома сервісами використовується:

docker-compose.yml

version: '3.8'
services:
web:
build: .
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgres://db:5432/mydb
depends_on:
- db
db:
image: postgres:15
environment:
- POSTGRES_DB=mydb
- POSTGRES_PASSWORD=secret
volumes:
- db_data:/var/lib/postgresql/data
volumes:
db_data:
Terminal window
# Запустити всі сервіси
docker compose up -d
# Перегляд логів
docker compose logs -f
# Зупинити всі сервіси
docker compose down
# Зупинити та видалити томи (volumes)
docker compose down -v

Примітка: Docker Compose призначений для локальної розробки. Для продакшну його замінює Kubernetes.


Візуалізація: робочий процес Docker

Розділ «Візуалізація: робочий процес Docker»
┌─────────────────────────────────────────────────────────────┐
│ РОБОЧИЙ ПРОЦЕС DOCKER │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. НАПИСАННЯ │
│ ┌─────────────┐ │
│ │ Dockerfile │ │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ 2. ЗБІРКА │
│ ┌─────────────┐ │
│ │docker build │──────► Образ (myapp:v1) │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ 3. ПУБЛІКАЦІЯ (опційно) │
│ ┌─────────────┐ │
│ │docker push │──────► Реєстр (Docker Hub, ECR...) │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ 4. ЗАПУСК │
│ ┌─────────────┐ │
│ │docker run │──────► Контейнер (екземпляр, що працює)│
│ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

Порівняння базових образів

Розділ «Порівняння базових образів»

Вибір правильного базового образу критично важливий для безпеки та розміру:

Тип базового образуПрикладРозмірПоверхня атакиНайкраще для
Повна ОСubuntu:22.04~70MBВелика (містить усі утиліти)Складні застарілі застосунки з багатьма системними залежностями
Slimpython:3.11-slim~40MBСередня (спрощений Debian)Більшість стандартних застосунків, баланс розміру та сумісності
Alpinepython:3.11-alpine~15MBМала (використовує musl замість glibc)Надмалі образи, але можуть виникати проблеми з компіляцією C-розширень
Distrolessgcr.io/distroless/static~2MBМінімальна (без оболонки чи менеджера пакетів)Продакшн-розгортання компільованих мов (Go, Rust)
# ПОГАНО: повна ОС, величезний образ
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y python3 python3-pip
COPY . .
RUN pip3 install -r requirements.txt
# ДОБРЕ: база slim, менший образ
FROM python:3.11-slim
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# ЩЕ КРАЩЕ: Alpine (крихітна база)
FROM python:3.11-alpine
# Примітка: Alpine використовує apk замість apt, та musl замість glibc
# ПОГАНО: запуск від імені root
FROM python:3.11-slim
COPY . .
CMD ["python", "app.py"] # Запускається як root!
# ДОБРЕ: користувач без прав root
FROM python:3.11-slim
RUN useradd -m appuser
WORKDIR /app
COPY --chown=appuser:appuser . .
USER appuser
CMD ["python", "app.py"]

Один процес на контейнер

Розділ «Один процес на контейнер»
# ПОГАНО: кілька сервісів в одному контейнері
Контейнер: nginx + python app + redis
# ДОБРЕ: окремі контейнери
Контейнер 1: nginx
Контейнер 2: python app
Контейнер 3: redis
(Використовуйте Docker Compose або Kubernetes для оркестрації)

  • Багатоетапна збірка (multi-stage builds) суттєво зменшує розмір образу. Збірка відбувається на одному етапі, а на фінальний копіюються лише готові артефакти.
  • Тег latest не є особливим. Docker не оновлює його автоматично. Це просто домовленість, яка означає “те, що було завантажено останнім”.
  • Docker Desktop — це не те саме, що Docker. Docker (інструмент) безплатний. Docker Desktop (GUI/VM для Mac/Windows) має ліцензійні вимоги для бізнесу.
  • BuildKit — це стандартний інструмент збірки. Починаючи з Docker 23.0, двигун BuildKit є стандартним, він пропонує паралельне виконання, краще кешування та вищу продуктивність порівняно зі старим builder.

ПомилкаЧому це шкодитьРішення
Використання тегу latestНепередбачувані версіїВикористовуйте конкретні теги (:1.25.3)
Запуск від імені rootРизик для безпекиДодайте інструкцію USER
Ігнорування порядку шарівПовільна перезбіркаСтавте елементи, що часто змінюються, в кінець
Копіювання всього підрядВеликі образи, витік секретівВикористовуйте .dockerignore
Відсутність очищенняДиск переповнюєтьсяРегулярно запускайте docker system prune
Жорстке кодування секретівПаролі залишаються в образіВикористовуйте build secrets або ін’єкцію під час виконання
Використання ADD замість COPYНеочікуване розпакування архівівВикористовуйте COPY за замовчуванням
Ігнорування multi-stage buildsКомпілятори залишаються в продакшн-образіВідокремлюйте інструменти збірки від середовища виконання

Вправа: знайдіть баги

Розділ «Вправа: знайдіть баги»

Зупиніться та подумайте: Подивіться на наступний Dockerfile. Тут є щонайменше три серйозні антипатерни або баги. Чи зможете ви їх знайти до того, як прочитаєте відповіді?

FROM node:18
COPY . .
RUN npm install
CMD npm start
Переглянути відповіді
  1. Відсутній .dockerignore або вибірковий COPY: Копіювання . у . перед запуском npm install означає, що локальна папка node_modules може бути скопійована, а вона зазвичай завелика та специфічна для певної платформи. Також будь-яка зміна коду скидає кеш npm install.
  2. Ігнорування кешування шарів: Спочатку слід зробити COPY package*.json ./, потім RUN npm install, а вже після цього COPY . .. Це гарантує, що залежності перевстановлюватимуться лише при зміні package.json.
  3. Використання повної ОС як бази: Образ node:18 займає майже 1 ГБ. Краще використати node:18-slim або node:18-alpine, щоб зменшити поверхню атаки та час завантаження образу.
  4. Запуск від імені root: Користувач USER не вказаний, що означає запуск процесу Node з правами root всередині контейнера — це ризик для безпеки.

Контрольні запитання

Розділ «Контрольні запитання»
  1. Сценарій: Ви розробляєте Node.js застосунок. Щоразу, коли ви змінюєте один рядок CSS, Docker витрачає 3 хвилини на перезбірку образу, бо перевстановлює всі NPM-пакети. Як переписати Dockerfile, щоб виправити це?

    Відповідь Слід відокремити копіювання файлів залежностей від коду застосунку. Спочатку виконайте `COPY package.json` та `RUN npm install`. Потім використовуйте окрему команду `COPY . .` для решти коду. Це активує кеш шарів Docker: шар `npm install` повторно виконуватиметься лише при зміні `package.json`, а зміна CSS відбуватиметься миттєво.
  2. Сценарій: У вас локально запущений контейнер із базою даних, але після його перезапуску всі збережені користувачі зникають. Якого прапорця команди docker run вам бракує?

    Відповідь Вам бракує монтування тому (volume mount), зокрема прапорця `-v` або `--mount` (наприклад, `-v db_data:/var/lib/postgresql/data`). За замовчуванням контейнери ефемерні — будь-які дані, записані в шар контейнера, видаляються разом із ним. Том зберігає дані поза життєвим циклом контейнера безпосередньо на хост-машині.
  3. Сценарій: Ваш вебзастосунок успішно запускається в контейнері, але при переході на localhost:8080 ви отримуєте помилку 500 Internal Server Error. Які дві команди Docker ви використаєте першими для діагностики?

    Відповідь Першою має бути `docker logs `, щоб переглянути стандартний вивід застосунку на предмет помилок або звітів про збої. Якщо це не допомогло, наступний крок — `docker exec -it sh`, щоб отримати доступ до командної оболонки всередині контейнера. Там можна перевірити конфігураційні файли, змінні середовища або виконати локальний запит curl, щоб перевірити, чи слухає процес потрібний порт.
  4. Сценарій: Розробник намагається зменшити розмір образу, додавши RUN rm -rf /tmp/large-data одразу після кроку RUN curl -o /tmp/large-data https://example.com/file. Однак розмір фінального образу не змінився. Чому?

    Відповідь Образи Docker будуються за допомогою об’єднаної файлової системи (union file system), де кожна інструкція (як `RUN`, `COPY`) створює новий незмінний шар. Коли файл було завантажено, він назавжди зафіксувався в цьому конкретному шарі. Наступна команда `RUN rm` створює *новий* шар, який лише позначає файл як видалений, приховуючи його, але дані залишаються в попередньому шарі. Щоб заощадити місце, завантаження та видалення мають бути в одній інструкції `RUN`, поєднані через `&&`.
  5. Сценарій: Ви розгортаєте компільований Go-бінарний файл. Команда безпеки вимагає, щоб образ не містив жодних оболонок (як bash чи curl), щоб мінімізувати поверхню атаки. Який тип образу (Full OS, Slim, Alpine чи Distroless) ви оберете?

    Відповідь Правильним вибором буде образ Distroless (наприклад, `gcr.io/distroless/static`). Такі образи містять лише застосунок та його залежності, без менеджерів пакетів, оболонок та стандартних утиліт Unix. Alpine хоч і малий, але містить менеджер пакетів (`apk`) та оболонку (`sh`), якими може скористатися зловмисник.
  6. Сценарій: У вас є файл Docker Compose з сервісами frontend та backend. Фронтенд намагається отримати дані за адресою http://localhost:5000/api, але з’єднання відхиляється. Чому localhost тут не працює і що треба використовувати замість нього?

    Відповідь У контексті контейнера `localhost` вказує лише на внутрішній мережевий інтерфейс самого контейнера. Контейнер фронтенду шукає бекенд всередині себе, де його немає. Замість цього фронтенд має використовувати `http://backend:5000/api`, оскільки Docker Compose автоматично налаштовує внутрішню мережу DNS, де назви сервісів перетворюються на відповідні IP-адреси контейнерів.
  7. Сценарій: Контейнер зі скриптом обробки даних завершився з помилкою Out of Memory і тепер має статус Exited. Ви хочете запустити docker exec -it <container_id> bash, щоб переглянути тимчасові файли. Що станеться?

    Відповідь Команда `docker exec` не спрацює, бо вона може запускати нові процеси лише в контейнерах зі статусом `Running`. Ви не можете запустити оболонку в зупиненому контейнері. Щоб отримати доступ до файлів, скористайтеся `docker cp :/path/to/files ./local-dir` або створіть новий образ із зупиненого контейнера за допомогою `docker commit`.
  8. Сценарій: У вас є локальний файл archive.tar.gz з ассетами, які треба розпакувати в /var/www/html образу. Ви використаєте ADD archive.tar.gz /var/www/html/ чи COPY archive.tar.gz /var/www/html/?

    Відповідь У цьому випадку краще використати `ADD`, оскільки він має функцію автоматичного розпакування локальних tar-архівів. Якщо ви використаєте `COPY`, архів буде просто скопійовано як стиснутий файл, і вам знадобиться додаткова команда `RUN tar -xzf` (та наявність утиліти `tar` в образі) для його розпакування. Проте для звичайного копіювання файлів `COPY` є кращим вибором через свою прозорість.

Рівень 1: Основи (Збірка та запуск)

Розділ «Рівень 1: Основи (Збірка та запуск)»

Завдання: Зібрати та запустити власний образ вебсервера.

  1. Створіть каталог і всередині нього файл index.html із текстом “Hello KubeDojo!”.
  2. Створіть Dockerfile, що використовує nginx:alpine як базовий образ.
  3. Скопіюйте ваш index.html у /usr/share/nginx/html/index.html всередині образу.
  4. Зберіть образ із назвою dojo-web:v1.
  5. Запустіть контейнер у фоновому режимі, прокинувши порт 8080 вашого хоста на порт 80 контейнера.
  6. Перевірте результат за допомогою curl http://localhost:8080.

Рівень 2: Середній (Середовище та логи)

Розділ «Рівень 2: Середній (Середовище та логи)»

Завдання: Діагностувати несправний контейнер за допомогою логів та змінних середовища.

  1. Запустіть docker run -d --name db postgres:15.
  2. Перевірте його статус за допомогою docker ps -a. Ви побачите, що він одразу зупинився.
  3. Дізнайтеся причину зупинки через docker logs db. (Підказка: база скаржиться на відсутність пароля).
  4. Видаліть несправний контейнер.
  5. Запустіть його знову, передавши необхідну змінну середовища POSTGRES_PASSWORD=secret.
  6. Переконайтеся, що він продовжує працювати.

Рівень 3: Просунутий (Оптимізація та Exec)

Розділ «Рівень 3: Просунутий (Оптимізація та Exec)»

Завдання: Оптимізувати збірку та дослідити запущений контейнер.

  1. Напишіть Dockerfile, який встановлює curl у базовий образ ubuntu.
  2. Переконайтеся, що ви використовуєте apt-get update && apt-get install -y curl в одній інструкції RUN. Поясніть, чому це важливо для кешування шарів.
  3. Зберіть та запустіть контейнер інтерактивно (-it) з командою bash.
  4. Всередині контейнера підтвердьте, що ви працюєте від імені root, ввівши whoami.
  5. Введіть exit, щоб вийти. Помітьте, що контейнер зупинився. Як би ви залишили його працювати у фоновому режимі, щоб пізніше підключитися через exec?

Основи Docker для Kubernetes:

Команди:

  • docker run — запуск контейнерів
  • docker ps — список контейнерів
  • docker logs — перегляд виводу
  • docker exec — виконання команд у контейнерах
  • docker build — створення образів
  • docker push/pull — обмін образами

Основи Dockerfile:

  • FROM — базовий образ
  • COPY — додавання файлів
  • RUN — виконання під час збірки
  • CMD — команда запуску за замовчуванням

Найкращі практики:

  • Використовуйте конкретні теги образів
  • Оптимізуйте кешування шарів
  • Запускайте не від імені root
  • Один процес на один контейнер

Модуль 1.3: Що таке Kubernetes? — Огляд оркестрації контейнерів високого рівня.