Перейти к содержанию

Bind mounts и named volumes

До этого контейнер получает исходники и каталог из image. Для изменения кода приходится пересобирать образ, а изменения данных в собственном слое исчезают при удалении экземпляра. В этом уроке решим две разные задачи через два разных mount: подключим исходники компьютера для разработки и отдельное хранилище для каталога.

Это два самостоятельных сценария, а не одна обязательная комбинация. В первом учимся обновлять код из рабочей папки. Во втором сохраняем данные между пересозданиями контейнера. Полные исходники развиваются последовательно, однако работающие контейнеры и volumes создаёт читатель; их состояние не находится внутри ZIP.

Bind mount показывает папку хоста

Bind mount связывает выбранный путь Docker-хоста с путём контейнера. Для разработки это удобно: редактор сохраняет server.mjs на компьютере, а контейнер видит подключённый файл. В случае удалённого Engine речь идёт о файловой системе удалённой машины, поэтому упражнение выполняем с локальным context. Это не пересылка любой папки ноутбука на любой daemon. Подробности приведены в документации bind mounts.

Подключим только api/src, а не весь проект. Так исходники API можно изменять, не перекрывая манифесты, данные и остальные файлы image. Mount пометили readonly для контейнера: редактор хоста сможет сохранять исходник, а процесс внутри контейнера не должен записывать туда. Это не делает весь компьютер неизменяемым, но выражает роль конкретного подключения.

Создайте полный api/Dockerfile.dev. В нём основная команда использует встроенный watch-режим Node.js вместо отдельной глобальной установки инструмента:

FROM node:24-bookworm-slim
WORKDIR /app
COPY --chown=node:node package.json package-lock.json ./
RUN npm ci
COPY --chown=node:node . .
USER node
EXPOSE 4100
CMD ["node", "--watch", "src/server.mjs"]

Перед использованием порта 4100 остановите собственный pw-catalog-api из предыдущего урока. Затем читатель собирает dev-image и запускает отдельный экземпляр. Команды рассчитаны на shell с синтаксисом POSIX; в Windows можно использовать подходящее окружение WSL или адаптировать передачу абсолютного пути, сохранив смысл mount.

docker stop pw-catalog-api
docker build --file ./api/Dockerfile.dev --tag professorweb-container-lab-api:dev-09 ./api
docker run --detach --name pw-catalog-dev --publish 127.0.0.1:4100:4100 --mount "type=bind,src=$(pwd)/api/src,dst=/app/src,readonly" professorweb-container-lab-api:dev-09

Ожидаемое упражнение — изменить диагностическое сообщение запуска в исходнике и увидеть повторный запуск процесса через watch. Доставка файловых событий зависит от платформы Desktop и файловой системы. Если события не доходят, сначала убедитесь, что внутри контейнера виден новый файл; ручной restart собственного dev-контейнера отделит отсутствие события от отсутствия mount. Не объявляем любой watcher универсально работающим без самостоятельной проверки.

Mount перекрывает содержимое image

При подключении папки к /app/src подготовленные файлы image по этому пути становятся скрыты подключением. Docker не объединяет две папки построчно и не выбирает более свежий server.mjs. Если выбранная папка хоста пустая, контейнер может потерять видимый сервер и завершиться с ошибкой отсутствия модуля.

Поэтому точный путь mount важен не меньше корректного image. Используем длинную форму --mount: она помогает явно назвать тип, источник и назначение. Для bind-пути ожидаем существующий каталог; ошибку пути нужно исправлять как ошибку входа, а не создавать случайную пустую папку и надеяться на содержимое образа.

Задача Тип подключения Где живёт содержимое
Редактировать сервер на ноутбуке Bind mount api/src В пользовательской папке хоста
Сохранить каталог после удаления контейнера Named volume В управляемом Docker хранилище
Получить начальный fixture Файлы image В выбранном образе до подключения
Временно записать внутри контейнера Собственный слой В конкретном экземпляре

Named volume сохраняет данные отдельно

Volume имеет Docker-имя и жизненный цикл, отдельный от контейнера. Пустой новый volume, подключённый к каталогу с подготовленными файлами image, может получить их начальное содержимое. Уже существующий непустой volume продолжит показывать свои данные: новая сборка image не обязана переписать каталог автоматически. Эти правила описаны в документации volumes.

Подготовим утилиту записи

Создайте api/tools/set-title.mjs перед сборкой lab-09. Это полный файл. Он не является HTTP-методом записи и не предоставляет пользователям сайта административные полномочия.

import { readFile, writeFile, rename, unlink } from 'node:fs/promises';
const [id, title] = process.argv.slice(2);
if (!/^[a-z][a-z0-9-]{0,39}$/.test(id ?? '')
  || typeof title !== 'string' || title.trim().length < 1 || title.length > 100) {
  throw new Error('Usage: node tools/set-title.mjs COURSE_ID TITLE');
}
const file = process.env.CATALOG_FILE
  ?? new URL('../data/catalog.json', import.meta.url);
const temporary = typeof file === 'string'
  ? `${file}.tmp` : new URL(`${file.href}.tmp`);
const courses = JSON.parse(await readFile(file, 'utf8'));
if (!Array.isArray(courses)) throw new Error('Catalog must be an array');
const course = courses.find(item => item.id === id);
if (!course) throw new Error('Unknown course id');
course.title = title.trim();
try {
  await writeFile(temporary, JSON.stringify(courses, null, 2) + '\n', 'utf8');
  await rename(temporary, file);
} finally {
  await unlink(temporary).catch(error => {
    if (error.code !== 'ENOENT') throw error;
  });
}
console.log(`catalog_title_updated:${id}`);

Утилита принимает известный идентификатор курса и новый заголовок, пишет временный файл в той же папке и заменяет основной файл. Договор упражнения предполагает одного писателя. Это не механизм конкурентных транзакций: две одновременно запущенные утилиты могут помешать друг другу и потерять изменения. Для настоящей совместной записи понадобится другое хранилище и правила согласования.

Запустим отдельный сценарий данных

Для этого сценария остановите dev-контейнер, чтобы освободить host-порт. Соберите обычный image из текущего API: в него добавлена утилита записи, приведённая ниже. Создайте собственное named-хранилище и подключите его к /app/data.

docker stop pw-catalog-dev
docker build --tag professorweb-container-lab-api:lab-09 ./api
docker volume create pw-catalog-data
docker run --detach --name pw-catalog-data --publish 127.0.0.1:4100:4100 --mount type=volume,src=pw-catalog-data,dst=/app/data professorweb-container-lab-api:lab-09

Имя volume и имя контейнера здесь совпадают для запоминания, но относятся к разным категориям объектов. Это самостоятельное учебное хранилище; будущий Compose создаст своё имя, и его данные не окажутся автоматически теми же. Если такое volume уже существует от предыдущего повторения, сначала выясните его содержимое, а не рассчитывайте на повторную инициализацию.

docker exec pw-catalog-data node tools/set-title.mjs docker "Docker: контейнеры для веб-проектов"
curl --include http://127.0.0.1:4100/api/courses

Ожидается изменение только title записи docker. API читает каталог при запросе, поэтому новый заголовок должен появиться без пересборки. Если получен отказ записи, проверьте права volume и пользователя: подготовленные файлы image принадлежат node, но чужое ранее созданное хранилище может иметь другую историю. Не исправляйте права всей системы случайным chmod; изучайте конкретную папку и нужного владельца.

Проверим пересоздание как отдельное действие

После собственной успешной записи читатель может остановить и удалить только контейнер, затем выполнить тот же run с тем же named volume. Ожидается сохранение изменённого заголовка. Если подключить другой новый volume, он должен получить начальный fixture; это другой ресурс, а не исчезновение данных старого.

Удаление volume уничтожает его собственные данные. Поэтому при обычном переходе к следующему уроку такую операцию не выполняем. Остановка контейнера не требует удалить хранилище, а удаление container rm само по себе не равно намеренному удалению этого named volume. Резервная копия и восстановление остаются отдельной обязанностью, даже когда данные находятся в управляемой Docker папке.

Полный after и исходные fixtures находятся в архиве серии; STATE сохраняет not_run. Следующий урок перенесёт повторяемые параметры запуска в Compose. Мы сохраним различие исходников и данных, а самостоятельный named volume этого занятия не будем выдавать за автоматически мигрировавшее хранилище нового Compose-проекта.

Оглавление серии