Запускаешь Postgres в контейнере, заливаешь туда схему, тестовые данные, пару дней работаешь. Потом обновляешь образ на свежий тег, делаешь
Код: Выделить всё
docker rm -f db && docker run ...Это не баг, это дизайн. Контейнер эфемерен по своей природе. Под капотом его файловая система - это OverlayFS: стопка read-only слоёв образа плюс тонкий writable-слой сверху (upperdir). Всё, что процесс пишет внутрь, садится в этот верхний слой. Удалил контейнер - удалил upperdir вместе со всем, что там накопилось. Контейнер задуман как одноразовый: его должно быть не страшно убить и пересоздать. А значит, состояние (state) в нём держать нельзя. Состояние надо вынести наружу - и ровно для этого существуют docker volume и bind mount. Разберём хранение данных в Docker до самого дна: где физически лежат байты, кто их владелец по UID, как не потерять и как забэкапить.

Три способа дать контейнеру память: volume, bind mount, tmpfs
У Docker три типа монтирования, и путать их - классика граблей.
Named volume (именованный том) - область хранения, которой управляет сам Docker. Ты говоришь "хочу том pgdata", а где он физически лежит и кто там владелец - забота движка. По умолчанию на Linux это
Код: Выделить всё
/var/lib/docker/volumes/<имя>/_dataBind mount (привязка к каталогу хоста) - ты руками говоришь "вот эта папка на хосте = вот этот путь в контейнере". Никакого посредника, прямой проброс. Идеально для разработки: монтируешь исходники с ноутбука в контейнер, правишь код в IDE, внутри сразу подхватывается. Docker bind mount не управляется движком: за каталог, права и существование отвечаешь ты. Команды
Код: Выделить всё
docker volumetmpfs mount - монтирование в оперативную память. Данные живут только пока жив контейнер, на диск не попадают вообще. Нужно для секретов, которые нельзя ронять на диск, для горячих временных файлов, для ускорения тяжёлых временных записей. Только Linux. После остановки контейнера - всё исчезает, это его смысл.
Аналогия. Named volume - камера хранения на вокзале: сдал чемодан, тебе дали номер ячейки, где она физически - не твоя забота, придёшь за вещами в любой момент. Bind mount - ящик у тебя дома, в который ты дал контейнеру ключ: полный контроль, но и порядок наводишь сам. tmpfs - карман: удобно, быстро, но вывернул куртку (остановил контейнер) - и пусто.
Жизненный цикл тома: create, ls, inspect, rm, pruneЭмпирика: данные приложения, которые должны жить - named volume. Код и конфиги, которые ты активно правишь в редакторе - bind mount. Секреты и временное, что не должно касаться диска - tmpfs.
Named volume - самостоятельный объект. Его можно создать заранее, до всякого контейнера:
Код: Выделить всё
docker volume create pgdata
docker volume lsКод: Выделить всё
DRIVER VOLUME NAME
local pgdataКод: Выделить всё
/var/lib/docker/volumesКод: Выделить всё
docker volume inspect pgdataКод: Выделить всё
[
{
"CreatedAt": "2026-06-15T10:22:14Z",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/pgdata/_data",
"Name": "pgdata",
"Options": null,
"Scope": "local"
}
]Важная тонкость про Docker Desktop. На Mac и Windows никакого
Код: Выделить всё
/var/lib/dockerКод: Выделить всё
/var/lib/docker/volumesЕсли том не создавать заранее, Docker создаст его сам при первом упоминании в
Код: Выделить всё
docker runКод: Выделить всё
docker volume rm pgdata
docker volume pruneКод: Выделить всё
pruneКод: Выделить всё
--allКод: Выделить всё
docker volume lsДва синтаксиса монтирования: -v против --mount
Подключить том к контейнеру можно двумя способами, и это исторически больное место.
Короткий -v (он же
Код: Выделить всё
--volumeКод: Выделить всё
источник:цель:опцииКод: Выделить всё
docker run -d --name db \
-v pgdata:/var/lib/postgresql/data \
postgres:17Код: Выделить всё
-vДлинный --mount - явные пары ключ=значение:
Код: Выделить всё
docker run -d --name db \
--mount type=volume,source=pgdata,target=/var/lib/postgresql/data \
postgres:17Код: Выделить всё
docker run -d --name app \
--mount type=bind,source=/srv/app/src,target=/app/src,readonly \
myapp:devКод: Выделить всё
--mountКод: Выделить всё
type=bindКод: Выделить всё
--mountКод: Выделить всё
type=tmpfsКод: Выделить всё
--mountКод: Выделить всё
--tmpfsКод: Выделить всё
readonlyКод: Выделить всё
-vКод: Выделить всё
:roПрава и владелец: где новички теряют полдня
Самая болезненная тема хранения - не "куда", а "кто владеет". Внутри контейнера процесс работает под каким-то UID. Файлы на томе и на bind mount принадлежат конкретному числовому UID/GID, и ядру глубоко всё равно, как этого пользователя зовут.
Named volume. Когда том пустой и его первый раз монтируют в контейнер, Docker копирует туда содержимое целевого пути из образа вместе с владельцем и правами. Поэтому official-образы вроде postgres сами раскладывают данные под нужным UID, и обычно всё работает из коробки. Это ещё один довод за named volume: меньше ручной возни с правами.
Bind mount. Тут магии нет. Docker монтирует твою хостовую папку как есть, с её UID/GID. Классика:
Код: Выделить всё
docker run --rm -v $(pwd)/data:/data alpine \
sh -c 'echo test > /data/file.txt'Код: Выделить всё
sh: can't create /data/file.txt: Permission deniedЛечится осознанно, а не
Код: Выделить всё
chmod 777Код: Выделить всё
docker run --rm postgres:17 id -uКод: Выделить всё
sudo chown -R 999:999 ./pgdataКод: Выделить всё
docker run --rm -u $(id -u):$(id -g) \
-v $(pwd)/data:/data alpine touch /data/file.txtКод: Выделить всё
userns-remapБэкап тома и перенос между машинами
Named volume нельзя просто заархивировать из домашней папки - до
Код: Выделить всё
/var/lib/docker/volumesКод: Выделить всё
docker run --rm \
-v pgdata:/data:ro \
-v $(pwd):/backup \
alpine \
tar czf /backup/pgdata-2026-06-15.tar.gz -C /data .Код: Выделить всё
:roКод: Выделить всё
docker volume create pgdata-restore
docker run --rm \
-v pgdata-restore:/data \
-v $(pwd):/backup \
alpine \
tar xzf /backup/pgdata-2026-06-15.tar.gz -C /dataКод: Выделить всё
pg_dumpКод: Выделить всё
mysqldumpТома в Compose v2
В реальной жизни тома почти всегда описаны в compose.yaml, а не в длинных
Код: Выделить всё
docker runКод: Выделить всё
version:Код: Выделить всё
services:
db:
image: postgres:17
environment:
POSTGRES_PASSWORD: secret
volumes:
- pgdata:/var/lib/postgresql/data
- ./initdb:/docker-entrypoint-initdb.d:ro
tmpfs:
- /tmp
volumes:
pgdata:Код: Выделить всё
./initdbКод: Выделить всё
/tmpКод: Выделить всё
volumes:Код: Выделить всё
docker volume lsКод: Выделить всё
myproject_pgdataГрабли и антипаттерны
- Опечатка в пути bind mount при не даёт ошибки - Docker создаёт пустую папку, приложение тихо стартует без данных. Используй
Код: Выделить всё
-vдля bind.Код: Выделить всё
--mount - на томе - не починка, а дыра. Разбирайся с UID, а не глуши права.
Код: Выделить всё
chmod 777 - без проверки
Код: Выделить всё
docker volume pruneсносит данные. Особенно опасно на dev-машине, где много стеков.Код: Выделить всё
docker volume ls - контейнера БД без вынесенного тома - мгновенная потеря данных, потому что состояние было в writable-слое.
Код: Выделить всё
docker rm -f - Bind mount на каталог с node_modules или vendor с хоста в контейнер затирает то, что собралось внутри образа: монтирование скрывает содержимое целевого пути. Для node_modules часто делают anonymous volume поверх bind, чтобы зависимости из образа не перетёрлись хостом.
- Anonymous volumes (без имени) плодятся незаметно при каждом и копят мусор и место. Периодически чисти осознанно.
Код: Выделить всё
run - Хранить состояние в writable-слое и надеяться на - антипаттерн. Состояние - только в том/bind, образ остаётся неизменным.
Код: Выделить всё
docker commit
- Создай том: и посмотри его Mountpoint через
Код: Выделить всё
docker volume create labvol.Код: Выделить всё
docker volume inspect labvol - Запиши в него файл одноразовым контейнером: .
Код: Выделить всё
docker run --rm -v labvol:/data alpine sh -c 'echo hello > /data/test.txt' - Убедись, что данные пережили контейнер: подними новый и прочитай: . Должно вывести hello.
Код: Выделить всё
docker run --rm -v labvol:/data alpine cat /data/test.txt - Сделай бэкап тома в tar.gz приёмом со служебным alpine-контейнером, удали том через , создай заново и восстанови из архива. Проверь, что test.txt на месте.
Код: Выделить всё
docker volume rm labvol - Спровоцируй Permission denied: примонтируй bind mount в каталог, которым владеет чужой UID, и запусти запись под . Затем почини через
Код: Выделить всё
-u 1000:1000, а не через 777.Код: Выделить всё
chown
- Почему данные исчезают после , если не использовать том, и при чём тут writable-слой OverlayFS?
Код: Выделить всё
docker rm - Чем отличается поведение и
Код: Выделить всё
-v, когда исходного каталога bind mount не существует?Код: Выделить всё
--mount - Где физически лежат named volume на Linux и почему этот путь не виден напрямую на Docker Desktop для Mac?
- Откуда берётся Permission denied на bind mount и почему - неправильный ответ?
Код: Выделить всё
chmod 777
Контейнер эфемерен, состояние выносим наружу. Named volume - для продовых данных, ими управляет Docker и сам разруливает владельцев. Bind mount - для кода и конфигов при разработке, прямой проброс с хоста, но права и существование каталога на тебе. tmpfs - для временного и секретного в памяти. Для bind предпочитай явный
Код: Выделить всё
--mountКод: Выделить всё
prune