🕒 5 мин.
В этой статье мы рассмотрим проброс каталогов и томов в контейнеры Docker. Первое используется для удобства редактирования файлов, второе для надёжного хранения данных.
Суть проблемы
К контейнерам DevOps инженеры относятся как к чему-то одноразовому. То есть их запросто могут удалить и пересоздать, не заботясь о данных хранимых в них. Часто контейнеры вообще разворачиваются и пересоздаются автоматически с помощью CI/CD инструментов. А настраивать каждый контейнер с помощью docker exec — это медленно и увеличивает риск ошибиться.
Именно поэтому все полезные данные (например, базы данных) хранятся вне контейнера. Для хранения таких данных можно использовать: примонтированный каталог, либо проброшенный том (volume). Ни то, ни другое не удаляется при удалении контейнера.
То-есть у нас есть 2 способа хранить полезные данные:
- bind mount — способ привязать папку или файл с хоста внутрь контейнера.
- named volumes — это создание специального тома на хосте и проброс его в контейнер.
Bind mount
Начнём наше изучение с bind mount. Это используют для того чтобы: изменения в контейнере были сразу видны на хосте, а изменения на хосте были сразу видны в контейнере. Удобно во время разработки, потому-что позволяет редактировать код на хосте, а изменения будут сразу видны в контейнере.
Подготовим рабочий каталог docker-app, все файлы будем размещать в нём.
Создадим приложение на python app.py (я редактирую приложение из предыдущей статьи):
from flask import Flask
# Для работы с переменными
import os
app = Flask(__name__)
# Читаем переменные окружения
greeting = os.getenv("GREETING", "Hello")
name = os.getenv("NAME", "World")
@app.route('/')
def hello():
return f"""
<h1>{greeting}, {name}!</h1>
<a href="/file">Файл</a>
"""
@app.route('/file')
# Чтение из файла
def file():
try:
with open("data.txt", "r") as f:
content = f.read()
except:
content = "Файл data.txt не найден"
return f"<h1>{content}</h1>"
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
- я добавил маршрут
/fileс функцией чтения из файла; debug=True— необходим для автоматического перезапуска сервера при изменении кода.
А также, создадим requirements.txt — это зависимости для приложения:
Flask==2.3.3
Создадим файл, который будем читать — data.txt:
Text from a file located on the host!
Создадим Dockerfile:
# Базовый минимум (то-еть образ) FROM python:3.11-slim # Объявим порт EXPOSE 5000 # Рабочий каталог WORKDIR /app # Копируем и устанавливаем зависимости COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # Не копируем файлы в образ, чтобы показать что они на хосте # Запускаем приложение при старте контейнера CMD ["python", "app.py"]
Соберём образ:
$ docker build -t my_flask_app .
Запустим контейнер с bind mount (опция -v):
$ docker run -d -p 5000:5000 --name my_flask_app \ -v $(pwd):/app my_flask_app
$(pwd)— текущий каталог на хосте;/app— путь внутри контейнера (рабочий каталог из образа).
Проверим что контейнер работает:
$ docker ps IMAGE COMMAND STATUS PORTS NAMES my_flask_app "python app.py" Up 23 seconds 0.0.0.0:5000->5000/tcp my_flask_app
Проверим работу:
$ curl http://localhost:5000 <h1>Hello, World!</h1> <a href="/file">Файл</a> $ curl http://localhost:5000/file <h1>Text from a file located on the host!</h1>
- Я проверяю с помощью
curl, но если есть возможность, просто откройте страничку в браузере.
Изменим файл на хосте — data.txt:
Text changed!
Проверим, без пересоздания или перезагрузки контейнера:
$ curl http://localhost:5000/file <h1>Text changed!</h1>
Мы только что использовали Live Reload или Hot Reload без пере-сборки образа!
Изменим сам код приложения app.py (строку с return, где мы читаем файл):
# это
return f"<h1>{content}</h1>"
# заменим на это
return f"""
<h1>Live reload!</h1>
<p>{content}</p>
"""
Проверим:
$ curl http://localhost:5000/file <h1>Live reload!</h1> <p>Text changed!</p>
Удалим контейнер, и проверим что данные не исчезли:
$ docker stop my_flask_app $ docker rm my_flask_app $ cat data.txt Text changed!
Советы и предупреждения:
- Используйте
$(pwd)или абсолютные пути (/home/user/app); - В продакшене избегайте bind mounts — они привязаны к конкретной машине. Там лучше использовать named volumes, их мы рассмотрим далее.
- Bind mounts идеальны для разработки, так как позволяют быстро редактировать и отлаживать код.
Named volume
Именованные тома (named volumes) используется для:
- баз данных (MySQL, PostgreSQL);
- кеша (Redis);
- очередей (RabbitMQ, Kafka).
Такими томами управляет сам Docker.
Создадим рабочий каталог docker-volumes, все файлы будем создавать в нём.
Named volume необходимо предварительно создать:
$ docker volume create db_data
Посмотрим на список volumes:
$ docker volume ls DRIVER VOLUME NAME local db_data
С помощью команды docker volume inspect можем исследователь том:
$ docker volume inspect db_data
[
{
"CreatedAt": "2025-10-19T18:05:07+03:00",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/db_data/_data",
"Name": "db_data",
"Options": null,
"Scope": "local"
}
]
/var/lib/docker/volumes/db_data/_data— это путь на хосте, где физически находится volume.
Контейнер с Postgres
Для тестирования создадим контейнер с Postgres. Но для начала создадим файл .env — с помощью которого будем передавать необходимые переменные контейнеру:
POSTGRES_DB=myapp POSTGRES_USER=admin POSTGRES_PASSWORD=secret
Возьмём официальный образ из DockerHub. Он подготовлен специальным образом чтобы считывать эти переменные.
Для запуска контейнера воспользуемся такой командой:
$ docker run -d --name postgres-db --env-file .env \ -v db_data:/var/lib/postgresql/data postgres:15
Подключимся к контейнеру (сразу к psql):
$ docker exec -it postgres-db psql -U admin -d myapp
Создадим таблицу, вставим в неё одну строчку и выйдем:
myapp=# CREATE TABLE users (id SERIAL, name TEXT);
myapp=# INSERT INTO users (name) VALUES ('Alice');
myapp=# \q
Пересоздадим контейнер с тем же volume:
$ docker stop postgres-db $ docker rm postgres-db $ docker run -d --name postgres-db --env-file .env \ -v db_data:/var/lib/postgresql/data postgres:15
Подключимся и проверим что данные на месте:
$ docker exec -it postgres-db psql -U admin -d myapp myapp=# SELECT * FROM users; id | name ----+------- 1 | Alice (1 row) myapp=# \q
Чтобы не засорять систему удалим контейнер:
$ docker stop postgres-db $ docker rm postgres-db
В отличии от bind mount, named volume:
- Лучше переносимы: работают одинокого на любом хосте, не зависят от путей в ОС.
- Управление Docker: резервное копирование, инспектирование, очистка.
Контейнер с Redis
Для закрепления создадим ещё один контейнер с Redis.
Создадим volume для Redis:
$ docker volume create app_cache
Создадим контейнер redis:
$ docker run -d --name my_redis -v app_cache:/data redis
Подключимся к контейнеру:
$ docker exec -it my_redis redis-cli
Добавим пару ключей:
127.0.0.1:6379> SET test "hello" 127.0.0.1:6379> SET test2 "hello2" 127.0.0.1:6379> exit
Пересоздадим контейнер:
$ docker stop my_redis $ docker rm my_redis $ docker run -d --name my_redis -v app_cache:/data redis
Проверим что данные остались:
$ docker exec -it my_redis redis-cli 127.0.0.1:6379> get test "hello" 127.0.0.1:6379> get test2 "hello2" 127.0.0.1:6379> exit
Посмотрим где физически лежат данные:
$ docker volume inspect app_cache
[
{
"CreatedAt": "2025-09-19T15:55:49+03:00",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/app_cache/_data",
"Name": "app_cache",
"Options": null,
"Scope": "local"
}
]
$ sudo ls /var/lib/docker/volumes/app_cache/_data
dump.rdb
Итог
Мы научились пробрасывать каталог с хоста в контейнер — это называется Bind mount.
-v $(pwd):/app
А также научились создавать именованные тома (named volume) для контейнеров:
docker volume create app_cache
И использовать named volume при создании контейнеров:
-v app_cache:/data redis
Запомните что опция для проброса одна -v. Мы просто можем указать каталог (или даже файл), либо можем указать заранее подготовленный том.