Уявіть ситуацію: ви запустили контейнер з PostgreSQL, створили базу даних, додали таблиці, заповнили їх даними. Все працює ідеально. Але потім ви зупиняєте контейнер для оновлення або перезапуску сервера, і... всі дані зникли. База даних порожня, таблиці відсутні, роботу потрібно починати спочатку.
Це не баг — це фундаментальна особливість Docker. Контейнери за своєю природою ефемерні (ephemeral) — тимчасові та одноразові. Коли контейнер видаляється, всі зміни, зроблені всередині нього, теж видаляються. Це чудово для stateless додатків (Web API без локального стану), але катастрофічно для stateful додатків (бази даних, файлові сховища, черги повідомлень).
Саме для вирішення цієї проблеми Docker надає механізми persistent storage — збереження даних поза життєвим циклом контейнера. У цій статті ми детально розглянемо три типи storage в Docker: Volumes (керовані Docker томи), Bind Mounts (монтування директорій хоста) та tmpfs (тимчасове сховище в пам'яті). Ви навчитеся зберігати дані PostgreSQL, організовувати hot-reload для .NET додатків під час розробки, та застосовувати найкращі практики роботи з даними в контейнерах.
Пригадаємо архітектуру Docker-образу з попередніх статей. Образ складається з незмінних (immutable) read-only шарів. Коли ви запускаєте контейнер, Docker додає зверху тонкий writable шар (container layer), куди записуються всі зміни.
Що відбувається з даними:
Проблема: Container layer існує лише поки існує контейнер. При видаленні контейнера (docker rm) цей шар теж видаляється.
Приклад: PostgreSQL без persistent storage
# Запустити PostgreSQL
docker run -d \
--name postgres-temp \
-e POSTGRES_PASSWORD=mysecret \
postgres:16
# Дочекатися запуску (5-10 секунд)
docker logs postgres-temp
# Підключитися та створити базу даних
docker exec -it postgres-temp psql -U postgres -c "CREATE DATABASE myapp;"
docker exec -it postgres-temp psql -U postgres -c "\l"
# Вивід:
# myapp | postgres | UTF8 | ...
Тепер видалимо контейнер:
# Зупинити та видалити контейнер
docker stop postgres-temp
docker rm postgres-temp
# Запустити знову
docker run -d \
--name postgres-temp \
-e POSTGRES_PASSWORD=mysecret \
postgres:16
# Перевірити бази даних
docker exec -it postgres-temp psql -U postgres -c "\l"
# Вивід: база даних myapp відсутня!
База даних зникла, бо вона зберігалася у container layer, який був видалений разом з контейнером.
Для stateless додатків (Web API):
Для stateful додатків (БД, файлові сховища):
Docker надає три механізми для збереження даних поза контейнером:
Volumes — це керовані Docker сховища даних, які існують незалежно від життєвого циклу контейнера.
Характеристики:
/var/lib/docker/volumes/)Коли використовувати:
Приклад:
# Створити volume
docker volume create postgres-data
# Запустити контейнер з volume
docker run -d \
--name postgres \
-v postgres-data:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=mysecret \
postgres:16
Bind Mounts — це пряме монтування директорії або файлу з хост-системи в контейнер.
Характеристики:
Коли використовувати:
Приклад:
# Монтувати поточну директорію в контейнер
docker run -d \
--name myapp-dev \
-v $(pwd):/app \
myapp:latest
tmpfs — це тимчасове сховище в оперативній пам'яті, яке не зберігається на диску.
Характеристики:
Коли використовувати:
Приклад:
# Монтувати tmpfs для тимчасових файлів
docker run -d \
--name myapp \
--tmpfs /tmp:rw,size=100m \
myapp:latest
| Характеристика | Volumes | Bind Mounts | tmpfs |
|---|---|---|---|
| Керування Docker | ✅ | ❌ | ✅ |
| Persistent storage | ✅ | ✅ | ❌ |
| Швидкість | Середня | Середня | Дуже висока |
| Ізоляція | Висока | Низька | Висока |
| Кросплатформність | ✅ | ⚠️ | ❌ (Linux) |
| Hot-reload | ❌ | ✅ | ❌ |
| Production | ✅ | ⚠️ | ✅ (для кешу) |
| Development | ✅ | ✅ | ⚠️ |
Синтаксис:
docker volume create [OPTIONS] [VOLUME_NAME]
Приклади:
# Створити volume з автоматичною назвою
docker volume create
# Створити volume з конкретною назвою
docker volume create postgres-data
# Створити volume з мітками (labels)
docker volume create \
--label environment=production \
--label app=myapp \
myapp-data
# Створити volume з конкретним driver
docker volume create \
--driver local \
--opt type=nfs \
--opt o=addr=192.168.1.100,rw \
--opt device=:/path/to/dir \
nfs-volume
# Список всіх volumes
docker volume ls
# Вивід:
# DRIVER VOLUME NAME
# local postgres-data
# local myapp-data
# local abc123def456 (анонімний volume)
# Детальна інформація про volume
docker volume inspect postgres-data
Вивід docker volume inspect:
[
{
"CreatedAt": "2026-04-14T11:20:30Z",
"Driver": "local",
"Labels": {},
"Mountpoint": "/var/lib/docker/volumes/postgres-data/_data",
"Name": "postgres-data",
"Options": {},
"Scope": "local"
}
]
Ключові поля:
Mountpoint — де фізично зберігаються дані на хостіDriver — драйвер storage (local, nfs, cloud)Labels — мітки для організації volumesДва синтаксиси:
1. Короткий синтаксис (-v):
docker run -v VOLUME_NAME:CONTAINER_PATH [OPTIONS] IMAGE
Приклади:
# Монтувати існуючий volume
docker run -d \
--name postgres \
-v postgres-data:/var/lib/postgresql/data \
postgres:16
# Монтувати volume read-only
docker run -d \
--name app \
-v config-data:/app/config:ro \
myapp:latest
# Створити анонімний volume (Docker згенерує назву)
docker run -d \
--name app \
-v /app/data \
myapp:latest
2. Довгий синтаксис (--mount):
docker run --mount type=volume,source=VOLUME_NAME,target=CONTAINER_PATH [OPTIONS] IMAGE
Приклади:
# Базове монтування
docker run -d \
--name postgres \
--mount type=volume,source=postgres-data,target=/var/lib/postgresql/data \
postgres:16
# Read-only монтування
docker run -d \
--name app \
--mount type=volume,source=config-data,target=/app/config,readonly \
myapp:latest
# З додатковими опціями
docker run -d \
--name app \
--mount type=volume,source=myapp-data,target=/app/data,volume-driver=local \
myapp:latest
-v та --mount:-v — коротший, зручніший для простих випадків--mount — більш явний, рекомендований для production (легше читати, менше помилок)Крок 1: Створити volume
docker volume create postgres-data
Крок 2: Запустити PostgreSQL з volume
docker run -d \
--name postgres \
-e POSTGRES_PASSWORD=mysecret \
-e POSTGRES_DB=myapp \
-v postgres-data:/var/lib/postgresql/data \
-p 5432:5432 \
postgres:16
Крок 3: Створити дані
# Підключитися до БД
docker exec -it postgres psql -U postgres -d myapp
# Створити таблицю та додати дані
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100)
);
INSERT INTO users (name, email) VALUES
('Alice', 'alice@example.com'),
('Bob', 'bob@example.com');
SELECT * FROM users;
Крок 4: Видалити контейнер
docker stop postgres
docker rm postgres
Крок 5: Запустити новий контейнер з тим самим volume
docker run -d \
--name postgres-new \
-e POSTGRES_PASSWORD=mysecret \
-v postgres-data:/var/lib/postgresql/data \
-p 5432:5432 \
postgres:16
Крок 6: Перевірити дані
docker exec -it postgres-new psql -U postgres -d myapp -c "SELECT * FROM users;"
# Вивід:
# id | name | email
# ----+-------+-------------------
# 1 | Alice | alice@example.com
# 2 | Bob | bob@example.com
Дані збереглися! Volume існує незалежно від контейнера.
Кілька контейнерів можуть монтувати один volume одночасно.
Приклад: Web API + Worker з спільним volume для файлів
# Створити volume
docker volume create shared-uploads
# Запустити Web API (приймає файли)
docker run -d \
--name api \
-v shared-uploads:/app/uploads \
-p 8080:8080 \
myapi:latest
# Запустити Worker (обробляє файли)
docker run -d \
--name worker \
-v shared-uploads:/app/uploads \
myworker:latest
Потік даних:
/app/uploads (volume)/app/uploads (той самий volume) → обробляє їх# Видалити конкретний volume
docker volume rm postgres-data
# Видалити всі невикористовувані volumes
docker volume prune
# Видалити всі volumes (небезпечно!)
docker volume prune -a
Важливо: Volume не можна видалити, якщо він використовується контейнером (навіть зупиненим).
# Спочатку видалити контейнер
docker rm postgres
# Потім видалити volume
docker volume rm postgres-data
Backup volume:
# Створити backup volume у tar-архів
docker run --rm \
-v postgres-data:/data \
-v $(pwd):/backup \
alpine \
tar czf /backup/postgres-backup-$(date +%Y%m%d).tar.gz -C /data .
Пояснення:
postgres-data у /data/backup/data--rm)Restore volume:
# Створити новий volume
docker volume create postgres-data-restored
# Відновити дані з backup
docker run --rm \
-v postgres-data-restored:/data \
-v $(pwd):/backup \
alpine \
tar xzf /backup/postgres-backup-20260414.tar.gz -C /data
Docker підтримує різні драйвери для volumes:
1. local (за замовчуванням)
Зберігає дані на локальному диску хоста.
docker volume create --driver local myvolume
2. NFS (Network File System)
Зберігає дані на віддаленому NFS-сервері.
docker volume create \
--driver local \
--opt type=nfs \
--opt o=addr=192.168.1.100,rw \
--opt device=:/path/to/share \
nfs-volume
3. Cloud drivers (AWS EBS, Azure Disk, GCP Persistent Disk)
Інтеграція з хмарними сховищами через плагіни.
# Приклад для AWS EBS (потрібен плагін)
docker volume create \
--driver rexray/ebs \
--opt size=10 \
aws-volume
4. Сторонні драйвери
local драйвера. Cloud та мережеві драйвери потрібні для кластерних середовищ (Kubernetes, Docker Swarm).Іменований volume:
docker run -v postgres-data:/var/lib/postgresql/data postgres:16
docker volume ls)Анонімний volume:
docker run -v /var/lib/postgresql/data postgres:16
abc123def456)Bind Mount — це пряме монтування файлу або директорії з хост-системи в контейнер. На відміну від volumes, bind mounts не керуються Docker — ви повністю контролюєте розташування файлів на хості.
Синтаксис:
# Короткий синтаксис
docker run -v HOST_PATH:CONTAINER_PATH [OPTIONS] IMAGE
# Довгий синтаксис
docker run --mount type=bind,source=HOST_PATH,target=CONTAINER_PATH [OPTIONS] IMAGE
1. Монтування поточної директорії:
# Монтувати поточну директорію в /app
docker run -d \
--name myapp-dev \
-v $(pwd):/app \
myapp:latest
# Або з --mount
docker run -d \
--name myapp-dev \
--mount type=bind,source=$(pwd),target=/app \
myapp:latest
2. Read-only монтування:
# Монтувати конфігурацію read-only
docker run -d \
--name nginx \
-v $(pwd)/nginx.conf:/etc/nginx/nginx.conf:ro \
nginx:alpine
# Або з --mount
docker run -d \
--name nginx \
--mount type=bind,source=$(pwd)/nginx.conf,target=/etc/nginx/nginx.conf,readonly \
nginx:alpine
3. Монтування окремого файлу:
# Монтувати лише один файл
docker run -d \
--name app \
-v $(pwd)/appsettings.json:/app/appsettings.json:ro \
myapp:latest
Bind mounts ідеально підходять для розробки — зміни коду на хості одразу видимі в контейнері.
Dockerfile для розробки:
FROM mcr.microsoft.com/dotnet/sdk:8.0
WORKDIR /app
# Копіюємо лише .csproj для restore (кешування)
COPY *.csproj .
RUN dotnet restore
# Код буде монтуватися через bind mount
# Тому не копіюємо його в образ
# Запускаємо з hot-reload
ENTRYPOINT ["dotnet", "watch", "run", "--no-launch-profile"]
Запуск з bind mount:
# Монтувати код для hot-reload
docker run -d \
--name myapp-dev \
-v $(pwd):/app \
-p 8080:8080 \
-e ASPNETCORE_ENVIRONMENT=Development \
myapp-dev:latest
Тепер:
Program.cs на хостіdotnet watch автоматично перекомпілює кодЛоги:
watch : Started
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://[::]:8080
watch : File changed: /app/Program.cs
watch : Building...
watch : Started
Проблема: Файли, створені контейнером, можуть мати неправильні права доступу на хості.
# Запустити контейнер, який створює файл
docker run --rm \
-v $(pwd):/data \
alpine \
sh -c "echo 'test' > /data/file.txt"
# Перевірити власника на хості
ls -l file.txt
# -rw-r--r-- 1 root root 5 Apr 14 11:30 file.txt
Файл належить root! Це може створити проблеми, якщо ваш користувач не має прав root.
Рішення 1: Запуск від non-root користувача
# Запустити від поточного користувача
docker run --rm \
-v $(pwd):/data \
-u $(id -u):$(id -g) \
alpine \
sh -c "echo 'test' > /data/file.txt"
# Тепер файл належить вашому користувачу
ls -l file.txt
# -rw-r--r-- 1 youruser yourgroup 5 Apr 14 11:30 file.txt
Рішення 2: Налаштування USER в Dockerfile
FROM mcr.microsoft.com/dotnet/sdk:8.0
# Створити користувача з тим самим UID, що на хості
ARG USER_ID=1000
ARG GROUP_ID=1000
RUN addgroup -g ${GROUP_ID} appuser && \
adduser -u ${USER_ID} -G appuser -s /bin/sh -D appuser
USER appuser
WORKDIR /app
Збірка з передачею UID:
docker build \
--build-arg USER_ID=$(id -u) \
--build-arg GROUP_ID=$(id -g) \
-t myapp-dev .
| Характеристика | Volumes | Bind Mounts |
|---|---|---|
| Керування | Docker | Користувач |
| Розташування | /var/lib/docker/volumes/ | Будь-де на хості |
| Створення | docker volume create | Автоматично при монтуванні |
| Backup | docker run з tar | Звичайні інструменти хоста |
| Права доступу | Керуються Docker | Залежать від хоста |
| Продуктивність | Оптимізована | Залежить від FS хоста |
| Кросплатформність | ✅ | ⚠️ (шляхи відрізняються) |
| Production | ✅ Рекомендовано | ⚠️ Обережно |
| Development | ✅ | ✅ Рекомендовано |
tmpfs — це файлова система в оперативній пам'яті (RAM), яка не зберігається на диску. Дані існують лише поки працює контейнер.
Характеристики:
Синтаксис:
# Короткий синтаксис
docker run --tmpfs CONTAINER_PATH:OPTIONS IMAGE
# Довгий синтаксис
docker run --mount type=tmpfs,target=CONTAINER_PATH,tmpfs-size=SIZE IMAGE
1. Тимчасові файли:
# Монтувати /tmp у RAM
docker run -d \
--name myapp \
--tmpfs /tmp:rw,size=100m \
myapp:latest
2. Кеш:
# Монтувати директорію кешу у RAM
docker run -d \
--name myapp \
--tmpfs /app/cache:rw,size=200m,mode=1777 \
myapp:latest
3. Секрети (паролі, токени):
# Монтувати директорію для секретів у RAM
docker run -d \
--name myapp \
--tmpfs /run/secrets:rw,size=10m,mode=0700 \
myapp:latest
Опції tmpfs:
size — максимальний розмір (напр. 100m, 1g)mode — права доступу (напр. 1777, 0700)uid — власник (User ID)gid — група (Group ID)Dockerfile:
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine
WORKDIR /app
COPY --from=build /app/publish .
# tmpfs буде монтуватися при запуску
ENV ASPNETCORE_TEMP=/tmp
ENTRYPOINT ["dotnet", "MyWebApi.dll"]
Запуск:
docker run -d \
--name myapi \
-p 8080:8080 \
--tmpfs /tmp:rw,size=100m \
--tmpfs /app/cache:rw,size=200m \
myapi:latest
Переваги:
Тест: запис 1000 файлів по 1 КБ
| Storage | Час | Швидкість |
|---|---|---|
| HDD | 5.2 с | 192 файлів/с |
| SSD | 0.8 с | 1250 файлів/с |
| tmpfs (RAM) | 0.05 с | 20000 файлів/с |
tmpfs у 16 разів швидше за SSD!
docker-compose.yml:
version: '3.8'
services:
postgres:
image: postgres:16-alpine
container_name: postgres
environment:
POSTGRES_USER: myuser
POSTGRES_PASSWORD: mysecret
POSTGRES_DB: myapp
volumes:
# Named volume для даних БД
- postgres-data:/var/lib/postgresql/data
# Bind mount для init-скриптів
- ./init-scripts:/docker-entrypoint-initdb.d:ro
ports:
- "5432:5432"
restart: unless-stopped
volumes:
postgres-data:
driver: local
init-scripts/01-create-tables.sql:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_users_email ON users(email);
Запуск:
docker compose up -d
Переваги:
Структура проєкту:
MyWebApi/
├── Controllers/
│ └── WeatherController.cs
├── Program.cs
├── MyWebApi.csproj
├── Dockerfile.dev
└── docker-compose.dev.yml
Dockerfile.dev:
FROM mcr.microsoft.com/dotnet/sdk:8.0
WORKDIR /app
# Копіюємо .csproj для restore
COPY *.csproj .
RUN dotnet restore
# Код монтується через bind mount
# Тому не копіюємо його
# Встановлюємо dotnet-ef для міграцій (опціонально)
RUN dotnet tool install --global dotnet-ef
ENV PATH="${PATH}:/root/.dotnet/tools"
# Запускаємо з hot-reload
CMD ["dotnet", "watch", "run", "--no-launch-profile", "--urls", "http://0.0.0.0:8080"]
docker-compose.dev.yml:
version: '3.8'
services:
api:
build:
context: .
dockerfile: Dockerfile.dev
container_name: myapi-dev
volumes:
# Bind mount для hot-reload
- .:/app
# Exclude bin та obj (не монтувати)
- /app/bin
- /app/obj
ports:
- "8080:8080"
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=http://+:8080
depends_on:
- postgres
postgres:
image: postgres:16-alpine
container_name: postgres-dev
environment:
POSTGRES_PASSWORD: dev_password
POSTGRES_DB: myapp_dev
volumes:
- postgres-dev-data:/var/lib/postgresql/data
ports:
- "5432:5432"
volumes:
postgres-dev-data:
Запуск:
docker compose -f docker-compose.dev.yml up
Тепер:
Controllers/WeatherController.csdotnet watch автоматично перекомпілюєhttp://localhost:8080Логи:
api-dev | watch : File changed: /app/Controllers/WeatherController.cs
api-dev | watch : Building...
api-dev | watch : Build succeeded
api-dev | watch : Started
api-dev | info: Now listening on: http://[::]:8080
docker-compose.yml:
version: '3.8'
services:
# Frontend (React/Vue) з hot-reload
frontend:
image: node:20-alpine
working_dir: /app
command: npm run dev
volumes:
- ./frontend:/app
- /app/node_modules # Exclude node_modules
ports:
- "3000:3000"
environment:
- NODE_ENV=development
# Backend (.NET API) з hot-reload
backend:
build:
context: ./backend
dockerfile: Dockerfile.dev
volumes:
- ./backend:/app
- /app/bin
- /app/obj
ports:
- "8080:8080"
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ConnectionStrings__DefaultConnection=Host=postgres;Database=myapp;Username=postgres;Password=secret
depends_on:
- postgres
- redis
# PostgreSQL з persistent storage
postgres:
image: postgres:16-alpine
volumes:
- postgres-data:/var/lib/postgresql/data
- ./database/init:/docker-entrypoint-initdb.d:ro
environment:
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
ports:
- "5432:5432"
# Redis з tmpfs для швидкого кешу
redis:
image: redis:7-alpine
command: redis-server --save ""
tmpfs:
- /data:rw,size=100m
ports:
- "6379:6379"
# Nginx reverse proxy
nginx:
image: nginx:alpine
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- nginx-logs:/var/log/nginx
ports:
- "80:80"
depends_on:
- frontend
- backend
volumes:
postgres-data:
nginx-logs:
Типи storage у цьому прикладі:
node_modules, bin, obj (не монтувати з хоста)Мета: Навчитися зберігати дані БД у volume.
Кроки:
docker volume create postgres-dataОчікуваний результат: Дані не втрачаються при видаленні контейнера.
Мета: Налаштувати розробницьке середовище з автоматичним перезапуском.
Кроки:
Dockerfile.dev з dotnet watchОчікуваний результат: Зміни коду видимі одразу після збереження файлу.
Мета: Навчитися робити backup даних з volume.
Кроки:
Очікуваний результат: Дані успішно відновлені з backup.
Мета: Виміряти різницю швидкості між volume, bind mount та tmpfs.
Кроки:
Очікуваний результат: tmpfs найшвидший, volume та bind mount приблизно однакові.
У цій статті ми детально розглянули збереження даних у Docker:
Проблема ефемерності:
Docker Volumes:
/var/lib/docker/volumes/docker volume create, ls, inspect, rm, pruneBind Mounts:
-u або USER в Dockerfile)tmpfs Mounts:
Практичні сценарії:
/var/lib/postgresql/datadotnet watch/data (швидко, не persistent)nginx.conf (легко редагувати)Найкращі практики:
node_modules, bin, obj при bind mountПорівняльна таблиця:
| Сценарій | Рекомендація |
|---|---|
| Production БД | Named volume |
| Development hot-reload | Bind mount |
| Кеш, тимчасові файли | tmpfs |
| Конфігураційні файли | Bind mount (read-only) |
| Логи для аналізу | Named volume або bind mount |
| Секрети (паролі, токени) | tmpfs (не залишає слідів) |
| Спільні дані між контейнерами | Named volume |