Volumes и переменные окружения
bind mount vs named volume vs tmpfs. Анонимные тома и почему их надо чистить. Классическая ловушка с DB_HOST=localhost.
Writable-слой эфемерный
Из урока про образы мы знаем: контейнер — это writable-слой поверх
read-only стопки. Всё, что писали туда без volume, уходит вместе с
контейнером, как только сделаешь docker rm.
Это нормально для веб-серверов и stateless-сервисов. Но для базы данных, которая копит данные неделями, это катастрофа.
Решение — маунты: внешнее хранилище, привязанное к пути внутри контейнера. Их три типа.
Три типа маунтов
| Тип | Куда пишутся данные | Переживёт docker rm | Дефолт для |
|---|---|---|---|
| named volume | в каталог Docker (/var/lib/docker/volumes/...) | да | базы данных, любое состояние |
| bind mount | в путь хоста, который ты сам указал | да | dev: исходники, конфиги |
| tmpfs | в оперативку, не на диск | нет, размонтируется при stop | секреты, временный кеш |
Named volume
docker volume create pgdata
docker run -d --name db -v pgdata:/var/lib/postgresql/data postgres:16
Docker сам управляет каталогом на хосте, ты обращаешься по имени.
Переживёт docker rm db. Чтобы примонтировать тот же volume к новому
контейнеру, укажи тот же -v pgdata:/var/lib/postgresql/data.
Это дефолт для stateful сервисов: postgres, redis-с-сохранением, любой сервис, который пишет данные.
Bind mount
docker run -d --name dev -v $PWD/src:/app/src node:22-alpine sleep infinity
Слева стоит полный путь на хосте, справа — путь в контейнере. Удобно для разработки: ты редактируешь файлы в IDE на хосте, изменения видны сразу внутри контейнера.
Тонкое место — права. UID процесса внутри контейнера должен иметь права на
запись в файлы на хосте, иначе будут permission denied. У
postgres-образа внутренний uid обычно 999: если ты подсунешь
bind mount с файлами от своего uid 1000, postgres не сможет туда писать.
С named volume этой проблемы нет: docker сам выставляет владельца.
tmpfs
docker run --tmpfs /tmp:size=100m,mode=1777 alpine sh
Файлы лежат в оперативке, на диск не пишутся. При остановке контейнера исчезают. Используется для:
- временного кеша, который не жалко потерять;
- секретов, которые приложение читает один раз при старте и которые не должны попадать на диск.
VOLUME в Dockerfile и анонимные тома
Если в Dockerfile стоит VOLUME /var/lib/postgresql/data, docker при
docker run сам создаст анонимный volume под этот путь, даже если
ты ничего не указал.
Это та же история, что и с named, только имя — случайный 64-символьный хеш. Подключить такой volume обратно к новому контейнеру можно только по этому хешу. На практике почти никто этого не делает, и анонимные тома копятся.
Проверка:
docker volume ls
# DRIVER VOLUME NAME
# local 8f4a3c1...e7
# local b2c9e44...91
# local pgdata
Чистить:
docker container prune # сначала удаляем остановленные контейнеры
docker volume prune # потом тома; том, привязанный к контейнеру, не тронет
Тут есть нюанс, на котором путаются даже те, кто с Docker давно.
Начиная с Engine 23.0 docker volume prune по умолчанию сносит
только анонимные тома. Именованные он оставляет, даже если ими
никто не пользуется. Чтобы забрать и их:
docker volume prune -a # анонимные + неиспользуемые именованные
Осторожно: под -a попадёт и забытый pgdata от проекта, который ты
не поднимал месяц. Сначала посмотри docker volume ls.
Или одной командой со всем сразу:
docker system prune --volumes # тоже только анонимные тома
Build cache — отдельная категория, она не входит ни в images, ни в volumes. Может занимать десятки гигабайт:
docker system df # покажет 4 строки: Images, Containers, Volumes, Build Cache
docker builder prune # чистит build cache
Бэкап анонимного тома
Имени нет, в compose не подключишь, но данные забрать можно через одноразовый контейнер:
docker run --rm \
-v 8f4a3c1...e7:/data:ro \
-v $PWD:/backup \
alpine tar czf /backup/dump.tgz /data
Анонимный том маунтится read-only (:ro), текущая папка — на запись.
tar упаковывает, контейнер исчезает, на хосте остаётся dump.tgz.
Восстановление — то же, но tar xzf.
Переменные окружения
Три способа задать.
-e KEY=VAL точечно:
docker run -e DB_HOST=db -e DB_PORT=5432 myapp
--env-file ./.env из файла:
docker run --env-file .env myapp
В compose это блок environment: или env_file:. В Dockerfile — ENV,
но это будет значение по умолчанию, видное через docker history -
для секретов не использовать.
Классическая ловушка: localhost внутри контейнера
Самая частая ошибка. Сценарий:
В коде:
const dbHost = process.env.DB_HOST || 'localhost';
const dbPort = process.env.DB_PORT || 5432;
В compose:
services:
api:
image: myapp
# environment не задан!
db:
image: postgres:16
Запускаешь. Получаешь ECONNREFUSED 127.0.0.1:5432. Почему?
Потому что DB_HOST не задан, клиент скатился на дефолт localhost. А
localhost внутри контейнера — это сам контейнер, не хост, не
другой контейнер. На localhost внутри api нет postgres, там вообще
ничего нет.
Лечение в compose:
services:
api:
image: myapp
environment:
DB_HOST: db # ← имя сервиса
DB_PORT: 5432
depends_on: [db]
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: example
db — это имя сервиса в compose, оно автоматически становится
DNS-именем во внутренней сети (подробно в уроке про сети). Поэтому
api стучится в db:5432 и попадает в нужный контейнер.
Не правь код, чтобы убрать fallback на localhost. Локальная разработка без compose валидный кейс, дефолт нужен. Правь конфиг.
Секреты
Чек-лист, чего не делать:
- Не клади
.envс реальными ключами в Git..gitignoreдля.env*- must-have. - Не клади секреты в Dockerfile через
ENV/ARG: они видны в манифесте. - Не клади секреты в
imageтэги или имена контейнеров.
Что делать в проде:
- Docker secrets (для Swarm) или Kubernetes secrets.
- Внешнее хранилище секретов: HashiCorp Vault, AWS Secrets Manager, Yandex Lockbox. Секреты живут в сервисе, приложение тянет их по токену.
- Шифрование секретов прямо в репозитории: sops (шифрует значения в файле, ключ хранится отдельно).
- В минимальном варианте —
--env-fileиз файла, который не уходит в Git и лежит на сервере с правами600.
Что важно запомнить
Writable-слой контейнера эфемерный. Состояние — в named volumes (для БД), bind mounts (для dev), tmpfs (для секретов).
VOLUME в Dockerfile = автоматический анонимный том = мусор, который
надо периодически чистить через docker system prune.
localhost внутри контейнера — это сам контейнер. Для связи с другим
сервисом используй имя сервиса compose как DNS-имя.
Секреты не суём в ENV/ARG Dockerfile. В рантайме — через -e /
--env-file / secret manager.
В следующем уроке — сети Docker, и почему имя сервиса работает как DNS.
Дрилл · вопросы как на собесе
Проверь себя без подсказок
Сформулируй ответ вслух или письменно, потом раскрой и честно отметь: знал или нет.
От кого по умолчанию запускается процесс в контейнере и почему это плохо?
DOC-021В чём разница между `ARG` и `ENV` в Dockerfile?
DOC-008Прошли материал до конца? Отметьте урок пройденным.
Прогресс сохранён в этом браузере
Почистишь историю или откроешь с телефона, и отметки пропадут. Оставь почту, они переедут в аккаунт. Пароль придумывать не нужно.
Готово. теперь в аккаунте: прогресс виден с любого устройства. Мой прогресс