Главная/Storage/Практика

Раздел 7. Практические задания

Задания выполняются в реальной системе. Разбор открывайте только после самостоятельной попытки.

Обозначения: [обяз.] — обязательное, [доп.] — дополнительное, [★] — повышенной сложности, [диаг.] — диагностическое.

Подготовка:

bash
mkdir -p ~/docker-course/07-storage && cd ~/docker-course/07-storage
# docker pull принимает РОВНО один образ:
# `docker pull a b` отвечает «docker pull requires 1 argument»
for img in alpine:3.21 python:3.13-slim postgres:17-alpine; do docker pull -q "$img"; done
docker system df -v | head -3      # зафиксируйте исходное состояние

Задание 1. Данные и удаление container [обяз.]

Постановка. Запишите файл в container. Покажите три вещи:

  1. Файл переживает docker stop и docker start.
  2. Файл исчезает после docker rm и создания нового container'а из того же образа.
  3. Тот же сценарий с named volume: данные доступны новому container'у.

Ожидаемый результат. Три пары команд с выводом, не допускающим двоякого толкования.

Проверка:

bash
docker run --rm -v <volume>:/d alpine:3.21 cat /d/file.txt

Разбор — в уроке 7.1.


Задание 2. Физическое расположение volume [обяз.]

Постановка. Создайте named volume, запишите в него файл из container'а, найдите этот файл на host и прочитайте его напрямую.

Дополнительно: объясните, почему путь оканчивается на _data и почему для чтения нужен sudo.

Ожидаемый результат. Содержимое файла, прочитанное с host, и путь, полученный командой, а не угаданный.

Проверка:

bash
sudo cat "$(docker volume inspect <volume> --format '{{.Mountpoint}}')/file.txt"

Разбор — в уроке 7.2.


Задание 3. Монтирование поверх файлов образа [обяз.]

Постановка. Соберите образ, в котором каталог /app содержит два файла. Затем:

  1. Смонтируйте туда каталог host с одним файлом — покажите, что видно.
  2. Смонтируйте туда пустой каталог host — покажите, что видно.
  3. Смонтируйте туда пустой named volume — покажите, что видно.
  4. Объясните разницу между пунктами 2 и 3.
  5. Докажите, что файлы образа не удалены.

Ожидаемый результат. Четыре разных вывода и объяснение механизма.

Проверка:

bash
docker run --rm <образ> ls /app        # без монтирования файлы на месте

Разбор — в уроке 7.3 и уроке 7.2.


Задание 4. Файл, который не удаляется [обяз.]

Постановка. Воспроизведите ситуацию: container создаёт файл в примонтированном каталоге проекта, и обычный пользователь host не может его удалить.

Покажите числовых владельцев (ls -ln), а не имена. Объясните, почему ls -l вводит в заблуждение.

Ожидаемый результат. Сообщение Permission denied при rm и объяснение через UID.

Проверка:

bash
ls -ln ./output
rm ./output/file.txt        # должно отказать

Разбор — в уроке 7.5.


Задание 5. Три способа исправления [обяз.]

Постановка. Исправьте задание 4 тремя разными способами:

  1. --user при запуске;
  2. UID образа, заданный при сборке;
  3. общая группа с битом setgid.

Для каждого способа покажите: файл создаётся, принадлежит нужному владельцу, удаляется без sudo. Составьте таблицу сравнения с указанием минусов каждого способа.

Ожидаемый результат. Три рабочих варианта и обоснованный выбор для разработки и для сервера.

Проверка:

bash
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD/out:/out" <образ> touch /out/t
rm ./out/t && echo "удалено без sudo"

Разбор — в уроке 7.5.


Задание 6. Минимальный набор tmpfs [доп.]

Постановка. Возьмите Python-приложение, которое пишет во временные файлы и создаёт кэш. Определите измерением, каким каталогам нужна запись, и запустите приложение с --read-only.

Требования: список каталогов получен из docker diff, а не подбором; каждому tmpfs задан размер; сумма размеров укладывается в лимит памяти.

Ожидаемый результат. Работающее приложение с --read-only и обоснование каждой точки монтирования.

Проверка:

bash
docker diff <container> | wc -l     # должно быть близко к нулю
docker exec <container> sh -c 'echo x > /probe' 2>&1 | tail -1

Разбор — в уроке 7.4.


Задание 7. Backup и restore PostgreSQL [доп.]

Постановка. Поднимите PostgreSQL в Compose, наполните таблицу, сделайте резервную копию, удалите volume, восстановите и подтвердите целостность данных.

Требования: копия делается на работающей базе; проверка сравнивает контрольную сумму содержимого, а не только количество строк.

Ожидаемый результат. Совпадение контрольных сумм до и после.

Проверка:

bash
docker compose exec -T db psql -U postgres -d appdb -tAc \
  "SELECT md5(string_agg(payload, '' ORDER BY id)) FROM records"

Разбор — в уроке 7.6.


Задание 8. Безопасная уборка volumes [доп.]

Постановка. Создайте несколько anonymous volumes (запуск образа с VOLUME без --rm), затем найдите их, определите размер, загляните внутрь и удалите поимённо.

Отдельно объясните, почему docker volume prune --all в этой ситуации опасен.

Ожидаемый результат. Скрипт, который показывает содержимое перед удалением и ничего не удаляет сам.

Проверка:

bash
docker volume ls -qf dangling=true | wc -l
docker system df -v | sed -n '/VOLUME NAME/,$p'

Разбор — в уроке 7.6.


Задание 9. Диагностика: данные исчезли после compose down [диаг.]

Постановка. Дан Compose-файл:

yaml
services:
  db:
    image: postgres:17-alpine
    environment:
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: appdb
    volumes:
      - ./db-data:/var/lib/postgresql/data/pgdata

Разработчик наполняет базу, выполняет docker compose down, затем docker compose up -d — и база пуста. Каталог ./db-data на host существует и не пустой.

Найдите все причины (их больше одной), объясните каждую и предложите правильную конфигурацию.

Ожидаемый результат. Разбор каждой ошибки с доказательством и рабочий Compose-файл.

Разбор

В этом файле три независимые ошибки. Каждая по отдельности приводит к потере данных.

Ошибка 1. Путь монтирования не тот, который использует PostgreSQL.

Образ postgres хранит данные в каталоге из переменной PGDATA, по умолчанию /var/lib/postgresql/data. Здесь смонтирован вложенный /var/lib/postgresql/data/pgdata, который PostgreSQL не использует.

bash
docker compose up -d
sleep 12
echo "═══ где на самом деле данные ═══"
docker compose exec -T db sh -c 'echo "PGDATA=$PGDATA"; ls -d /var/lib/postgresql/data/*/ 2>/dev/null | head -3'
echo "═══ что в смонтированном каталоге ═══"
docker compose exec -T db ls -A /var/lib/postgresql/data/pgdata | head -3

Ожидаемый вывод:

text
═══ где на самом деле данные ═══
PGDATA=/var/lib/postgresql/data
/var/lib/postgresql/data/base/
/var/lib/postgresql/data/global/
/var/lib/postgresql/data/pg_wal/
═══ что в смонтированном каталоге ═══

Смонтированный каталог пуст: данные лежат уровнем выше, в writable layer, и погибают вместе с container'ом (урок 7.1).

Ошибка 2. Bind mount вместо volume для данных базы.

Даже с правильным путём bind mount здесь плохой выбор:

bash
ls -ldn ./db-data 2>/dev/null
docker compose exec -T db id

Ожидаемый вывод:

text
drwxr-xr-x 2 1000 1000 4096 Jul 30 13:30 ./db-data
uid=70(postgres) gid=70(postgres) groups=70(postgres)

Каталог принадлежит UID 1000, процесс работает от UID 70 — при правильном пути PostgreSQL просто не смог бы запуститься (урок 7.5). Named volume наследует владельца из образа и этой проблемы не создаёт (урок 7.2).

Ошибка 3. Не объявлен named volume — значит, нечему пережить пересоздание.

Проверим, что docker compose down действительно уносит данные:

bash
docker compose exec -T db psql -U postgres -d appdb -q \
    -c 'CREATE TABLE t (id int); INSERT INTO t VALUES (1),(2),(3);'
printf 'записей до down: %s\n' \
    "$(docker compose exec -T db psql -U postgres -d appdb -tAc 'SELECT count(*) FROM t' | tr -d ' \r')"

docker compose down > /dev/null
docker compose up -d > /dev/null
sleep 12
docker compose exec -T db psql -U postgres -d appdb -tAc 'SELECT count(*) FROM t' 2>&1 | tail -1

Ожидаемый вывод:

text
записей до down: 3
ERROR:  relation "t" does not exist

Данные ушли вместе с writable layer.

Правильная конфигурация:

yaml
services:
  db:
    image: postgres:17-alpine
    environment:
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: appdb
    volumes:
      # Named volume на тот путь, который PostgreSQL реально использует
      - db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -d appdb"]
      interval: 5s
      retries: 10

volumes:
  db-data:

Проверка исправления:

bash
docker compose up -d > /dev/null
sleep 12
docker compose exec -T db psql -U postgres -d appdb -q \
    -c 'CREATE TABLE t (id int); INSERT INTO t VALUES (1),(2),(3);'
docker compose down > /dev/null
docker compose up -d > /dev/null
sleep 12
printf 'записей после down/up: %s\n' \
    "$(docker compose exec -T db psql -U postgres -d appdb -tAc 'SELECT count(*) FROM t' | tr -d ' \r')"
docker compose down -v > /dev/null

Ожидаемый вывод:

text
записей после down/up: 3

Отдельно: когда bind mount для базы всё же используют. Если каталог данных должен лежать на конкретном диске, применяют volume с драйвером local и опцией bind (урок 7.2):

yaml
volumes:
  db-data:
    driver: local
    driver_opts:
      type: none
      device: /srv/postgres-data
      o: bind

Права при этом всё равно придётся согласовать вручную: sudo chown 70:70 /srv/postgres-data.

Чего разбор не покрывает. Если разработчик запускал docker compose down -v, данные исчезли бы и при правильной конфигурации — флаг -v удаляет объявленные volumes. Это четвёртая возможная причина, и проверяется она историей команд, а не файлом. Ключевое различие: docker compose down named volume не трогает, down -v — удаляет.


Задание 10. Разработка без чужих файлов [★]

Постановка. Организуйте окружение разработки Python-приложения, удовлетворяющее восьми требованиям.

  1. Код монтируется с host; правка видна без пересборки.
  2. Установленные в образе пакеты не скрываются монтированием.
  3. Приложение работает не от root.
  4. Все файлы, созданные приложением в смонтированном каталоге, принадлежат вашему UID.
  5. Эти файлы удаляются с host без sudo.
  6. Конфигурация монтируется read-only; правка редактором видна в container'е.
  7. Кэш приложения не попадает в репозиторий на host.
  8. Тот же репозиторий работает у коллеги с другим UID без правки файлов.

Каждое требование подтвердите проверкой; скрипт проверки должен возвращать ненулевой код при провале.

Подсказки

Подсказка 1

Требования 2 и 7 решаются расположением и дополнительными монтированиями, а не флагами доступа.

Подсказка 2

Требование 6 не выполнится при монтировании отдельного файла: редактор меняет inode.

Подсказка 3

Требование 8 означает, что UID не должен быть записан в файлы репозитория.

Подсказка 4

Требование 4 проверяйте на файле, созданном приложением, а не командой touch из вашей оболочки.

Решение

Показать решение
bash
mkdir -p ~/docker-course/07-storage/devenv/{src/app,config,output}
cd ~/docker-course/07-storage/devenv

cat > src/app/__init__.py <<'PY'
"""Приложение окружения разработки."""
PY

cat > src/app/main.py <<'PY'
"""Пишет отчёт, кэш и читает конфигурацию — как обычное приложение."""
from __future__ import annotations

import configparser
import os
import sys
from pathlib import Path

VERSION = "1"          # правится на host для проверки требования 1

OUTPUT = Path(os.environ.get("OUTPUT_DIR", "/output"))
CONFIG = Path(os.environ.get("CONFIG_FILE", "/app/config/app.ini"))
CACHE = Path(os.environ.get("APP_CACHE", "/app/.cache"))


def main() -> int:
    # Требование 2: пакет из образа должен быть доступен
    try:
        import click
        pkg = f"click {click.__version__}"
    except ModuleNotFoundError as exc:
        print(f"  пакет:   ОШИБКА — {exc}")
        return 1

    # Требование 6: конфигурация читается с host
    parser = configparser.ConfigParser()
    if not CONFIG.exists():
        print(f"  конфиг:  ОТСУТСТВУЕТ {CONFIG}")
        return 1
    parser.read(CONFIG)
    setting = parser.get("app", "setting", fallback="?")

    # Требование 7: кэш пишется, но не в репозиторий host
    CACHE.mkdir(parents=True, exist_ok=True)
    (CACHE / "state.txt").write_text("кэш\n", encoding="utf-8")

    # Требование 4: файлы приложения в смонтированном каталоге
    OUTPUT.mkdir(parents=True, exist_ok=True)
    (OUTPUT / "report.txt").write_text(f"версия {VERSION}\n", encoding="utf-8")
    nested = OUTPUT / "nested"
    nested.mkdir(exist_ok=True)
    (nested / "deep.txt").write_text("вложенный файл\n", encoding="utf-8")

    print(f"  версия:  {VERSION}")
    print(f"  UID:     {os.getuid()}:{os.getgid()}")
    print(f"  HOME:    {os.environ.get('HOME', '(не задан)')}")
    print(f"  пакет:   {pkg}")
    print(f"  конфиг:  setting={setting}")
    print(f"  вывод:   {OUTPUT}/report.txt, {nested}/deep.txt")
    return 0


if __name__ == "__main__":
    sys.exit(main())
PY

cat > config/app.ini <<'EOF'
[app]
setting = исходное
EOF

cat > requirements.txt <<'EOF'
click==8.3.0
EOF

cat > .dockerignore <<'EOF'
.git
.venv
__pycache__
*.py[cod]
output
.cache
.env
EOF

cat > .gitignore <<'EOF'
.env
output/
.cache/
EOF

cat > Dockerfile <<'EOF'
# syntax=docker/dockerfile:1

FROM python:3.13-slim AS base
ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PYTHONPATH=/app/src \
    PATH="/opt/venv/bin:$PATH"

# Требование 2: окружение ВНЕ /app, монтирование его не скроет
RUN python -m venv /opt/venv
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt
COPY src/ ./src/

# ── production: фиксированный UID ──
FROM base AS runtime
RUN groupadd -g 10001 app && useradd -u 10001 -g 10001 -m -d /home/app app && \
    mkdir -p /output /app/.cache && chown -R 10001:10001 /output /app/.cache
ENV HOME=/home/app
USER 10001:10001
CMD ["python", "-m", "app.main"]

# ── разработка: UID приходит снаружи (требование 8) ──
FROM base AS dev
ARG UID=1000
ARG GID=1000
# Пользователь или группа с таким номером могут уже быть в базовом образе
RUN groupadd -g ${GID} dev 2>/dev/null || true; \
    useradd -u ${UID} -g ${GID} -m -d /home/dev dev 2>/dev/null || true; \
    mkdir -p /home/dev /app/.cache && \
    chown -R ${UID}:${GID} /home/dev /app/.cache
ENV HOME=/home/dev
USER ${UID}:${GID}
CMD ["python", "-m", "app.main"]
EOF

# Требование 8: UID вычисляется локально, в репозиторий не попадает
printf 'UID=%s\nGID=%s\n' "$(id -u)" "$(id -g)" > .env

cat > compose.yaml <<'EOF'
services:
  dev:
    build:
      context: .
      target: dev
      args:
        UID: ${UID:-1000}
        GID: ${GID:-1000}
    environment:
      OUTPUT_DIR: /output
      CONFIG_FILE: /app/config/app.ini
      APP_CACHE: /app/.cache
    volumes:
      # Требование 1: код с host
      - ./src:/app/src
      # Требование 4: вывод приложения виден на host
      - ./output:/output
      # Требование 6: КАТАЛОГ, не файл — правка редактором видна
      - ./config:/app/config:ro
      # Требование 7: кэш в volume, на host не попадает
      - dev-cache:/app/.cache

  prod:
    build:
      context: .
      target: runtime
    environment:
      OUTPUT_DIR: /output
    volumes:
      - prod-output:/output

volumes:
  dev-cache:
  prod-output:
EOF

cat > check.sh <<'SH'
#!/usr/bin/env bash
# Проверка всех восьми требований. Ненулевой код при любом провале.
set -uo pipefail
fail=0
ok()  { printf '  ✓ %s\n' "$1"; }
bad() { printf '  ✗ %s\n' "$1"; fail=1; }

run() { docker compose run --rm -T dev "$@" 2>/dev/null; }

printf '\n═══ Сборка ═══\n'
docker compose build dev > /dev/null 2>&1 && ok "образ собран" || { bad "сборка"; exit 1; }

printf '\n═══ Требования 2, 3, 6: базовый запуск ═══\n'
out="$(docker compose run --rm -T dev 2>/dev/null)"
echo "$out" | sed 's/^/  /'
echo "$out" | grep -q "click" && ok "пакет доступен (требование 2)" || bad "пакет скрыт монтированием"
uid="$(run id -u | tr -d '\r')"
[ "$uid" != "0" ] && ok "работает не от root: UID=$uid (требование 3)" || bad "работает от root"

printf '\n═══ Требование 1: правка кода без пересборки ═══\n'
sed -i 's/^VERSION = "1"/VERSION = "2-ПРАВКА"/' src/app/main.py
docker compose run --rm -T dev 2>/dev/null | grep версия | sed 's/^/  /'
docker compose run --rm -T dev 2>/dev/null | grep -q "2-ПРАВКА" \
    && ok "правка видна без пересборки" || bad "правка не видна"

printf '\n═══ Требование 4: владельцы файлов приложения ═══\n'
mine="$(id -u):$(id -g)"
bad_owner=0
while read -r p; do
    owner="$(stat -c '%u:%g' "$p")"
    printf '    %-26s %s\n' "$p" "$owner"
    [ "$owner" = "$mine" ] || bad_owner=1
done < <(find output -mindepth 1 | sort)
[ "$bad_owner" -eq 0 ] && ok "все файлы принадлежат $mine" || bad "есть файлы с чужим владельцем"

printf '\n═══ Требование 5: удаление без sudo ═══\n'
rm -rf output/report.txt output/nested 2>/dev/null \
    && ok "удалено без sudo" || bad "потребовался sudo"

printf '\n═══ Требование 6: правка конфигурации редактором ═══\n'
before="$(stat -c '%i' config/app.ini)"
sed -i 's/setting = .*/setting = ИЗМЕНЕНО/' config/app.ini
after="$(stat -c '%i' config/app.ini)"
printf '    inode: %s → %s\n' "$before" "$after"
docker compose run --rm -T dev 2>/dev/null | grep -q "setting=ИЗМЕНЕНО" \
    && ok "новое значение видно (inode менялся: $([ "$before" != "$after" ] && echo да || echo нет))" \
    || bad "конфигурация не обновилась"
docker compose run --rm -T dev sh -c 'echo x > /app/config/app.ini' 2>&1 | grep -qi "read-only" \
    && ok "конфигурация недоступна на запись" || bad "конфигурацию можно перезаписать"

printf '\n═══ Требование 7: кэш не в репозитории ═══\n'
run sh -c 'ls /app/.cache' | grep -q state.txt && ok "кэш пишется в container" || bad "кэш не пишется"
[ ! -d .cache ] && ok "каталога .cache на host нет" || bad "кэш просочился на host"

printf '\n═══ Требование 8: другой UID ═══\n'
UID=4242 GID=4242 docker compose build dev > /dev/null 2>&1
other="$(UID=4242 GID=4242 docker compose run --rm -T dev id -u 2>/dev/null | tr -d '\r')"
[ "$other" = "4242" ] && ok "собралось под UID 4242 без правки репозитория" || bad "UID не подставился (получено '$other')"
git_tracked_uid="$(grep -rl "UID=$(id -u)" --include='*.yaml' --include='*.yml' --include='Dockerfile' . 2>/dev/null | wc -l)"
[ "$git_tracked_uid" -eq 0 ] && ok "UID не записан в файлы репозитория" || bad "UID зашит в конфигурацию"

# Возвращаем исходное состояние
sed -i 's/^VERSION = "2-ПРАВКА"/VERSION = "1"/' src/app/main.py
sed -i 's/setting = ИЗМЕНЕНО/setting = исходное/' config/app.ini
UID="$(id -u)" GID="$(id -g)" docker compose build dev > /dev/null 2>&1

printf '\n═══ ИТОГ ═══\n'
[ "$fail" -eq 0 ] && echo "  все восемь требований выполнены" || echo "  ЕСТЬ ПРОВАЛЕННЫЕ ПРОВЕРКИ"
exit "$fail"
SH
chmod +x check.sh

./check.sh
echo "КОД: $?"
docker compose down -v > /dev/null 2>&1

Ожидаемый вывод:

text
═══ Сборка ═══
  ✓ образ собран

═══ Требования 2, 3, 6: базовый запуск ═══
    версия:  1
    UID:     1000:1000
    HOME:    /home/dev
    пакет:   click 8.3.0
    конфиг:  setting=исходное
    вывод:   /output/report.txt, /output/nested/deep.txt
  ✓ пакет доступен (требование 2)
  ✓ работает не от root: UID=1000 (требование 3)

═══ Требование 1: правка кода без пересборки ═══
    версия:  2-ПРАВКА
  ✓ правка видна без пересборки

═══ Требование 4: владельцы файлов приложения ═══
    output/nested              1000:1000
    output/nested/deep.txt     1000:1000
    output/report.txt          1000:1000
  ✓ все файлы принадлежат 1000:1000

═══ Требование 5: удаление без sudo ═══
  ✓ удалено без sudo

═══ Требование 6: правка конфигурации редактором ═══
    inode: 1443521 → 1443907
  ✓ новое значение видно (inode менялся: да)
  ✓ конфигурация недоступна на запись

═══ Требование 7: кэш не в репозитории ═══
  ✓ кэш пишется в container
  ✓ каталога .cache на host нет

═══ Требование 8: другой UID ═══
  ✓ собралось под UID 4242 без правки репозитория
  ✓ UID не записан в файлы репозитория

═══ ИТОГ ═══
  все восемь требований выполнены
КОД: 0

Три решения, определяющие качество.

Проверка требования 4 обходит все созданные файлы, включая вложенные. Проверить только report.txt было бы недостаточно: каталог nested/ создаётся приложением отдельно, и при неверной настройке владелец у него может отличаться от владельца файла в корне вывода. Цикл по find ловит и такие случаи.

Требование 6 проверяется по факту изменения inode. Скрипт печатает inode до и после sed -i и подтверждает, что он действительно менялся. Без этого проверка была бы бессмысленной: если бы sed записал файл на месте, тест прошёл бы и при монтировании отдельного файла — то есть не проверил бы ровно то, ради чего написан.

Требование 8 проверяется дважды: запуском под чужим UID и поиском своего UID в файлах репозитория. Первая проверка показывает, что механизм работает; вторая — что он работает по правильной причине, а не потому, что у проверяющего тот же UID. Вторая проверка — единственная, которая поймала бы UID: 1000, случайно закоммиченный в compose.yaml.

Чего решение не делает. Если каталог ./output уже существует и принадлежит другому пользователю — например, остался от запусков от root до внедрения этой схемы, — bind mount владельца не изменит, и приложение упрётся в отказ записи. Промышленная обёртка проверяет владельца перед запуском и подсказывает sudo chown. Не покрыт и случай, когда UID разработчика совпадает с системным UID внутри базового образа: useradd тогда не создаст пользователя, а || true это скроет — сработает chown домашнего каталога, но whoami покажет чужое имя.


Очистка после раздела

bash
# ВНИМАНИЕ: НЕ `docker ps -aq | xargs -r docker rm -f`.
# Такая строка удаляет ВСЕ container'ы на машине, включая чужие:
# базу коллеги, кластер kind, работающий стенд. Удаляем только
# созданные из образов этого раздела.
for img in alpine:3.21 python:3.13-slim postgres:17-alpine; do
    docker ps -aq --filter "ancestor=$img" | xargs -r docker rm -f
done
docker volume ls -qf dangling=true | xargs -r docker volume rm
docker image prune -f
docker system df -v | head -3

Сравните с состоянием, зафиксированным в начале раздела.

Обратите внимание: вторая строка удаляет все неиспользуемые volumes, включая named. Перед выполнением на рабочей машине посмотрите их содержимое (урок 7.6).


Критерии завершения

Раздел закрыт, когда выполнены обязательные задания 1–5 и вы можете без подсказок ответить на вопросы из MAIN.md раздела.

Дальше: Quiz 07.


Навигация

← Предыдущий материал
Вернуться к разделу
Следующий раздел → Docker Networking
Главное оглавление

Markdown на GitHub ↗