← Docker
14 мин · средне · Урок 5 из 8

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/var/lib/docker/volumes bind mountпуть на хосте tmpfsоперативка (RAM) переживёт docker rm переживёт docker rm исчезнет при 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 валидный кейс, дефолт нужен. Правь конфиг.

Секреты

Чек-лист, чего не делать:

Что делать в проде:

Что важно запомнить

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.

Дрилл · вопросы как на собесе

Проверь себя без подсказок

Сформулируй ответ вслух или письменно, потом раскрой и честно отметь: знал или нет.

01

От кого по умолчанию запускается процесс в контейнере и почему это плохо?

DOC-021
02

В чём разница между `ARG` и `ENV` в Dockerfile?

DOC-008

Прошли материал до конца? Отметьте урок пройденным.