"Быстрый
<br />
Docker volumes — это три разных механизма с разными гарантиями сохранности:</p>
<ul>
<li><strong>Named volume</strong> — данные под управлением Docker (/var/lib/docker/volumes/), переживают пересоздание контейнера, теряются при <code>docker system prune —volumes</code></li>
<li><strong>Bind mount</strong> — данные в произвольной папке хоста, ты контролируешь путь и бэкап, идеален для баз данных в продакшне</li>
<li><strong>tmpfs</strong> — данные в оперативной памяти, исчезают при остановке контейнера, только для временных данных</li>
</ul>
<p>Правило продакшна: базы данных и критичные данные — только bind mount или named volume с настроенным бэкапом. Никогда не запускай <code>docker system prune —volumes</code> без бэкапа.<br />
<h2>Диагноз: где пропадают данные</h2>
<p>Типичная история. Поднял стек: PostgreSQL, Redis, приложение. Работало месяц. Потом решил почистить диск:</p>
<pre><code class="language-bash">
docker compose down
docker system prune --volumes
</code></pre>
<p>После этого <code>docker compose up -d</code> поднял контейнеры. Всё запустилось. Только базы данных пустые. Пользователи, заказы, настройки — gone.</p>
<p>Хранение данных <a class="wpil_keyword_link" href="https://it-apteka.com/tag/docker/" target="_blank" rel="noopener" title="Docker" data-wpil-keyword-link="linked" data-wpil-monitor-id="2884">Docker</a> volumes — это тема где цена ошибки равна потере всего. Эта статья закрывает вопрос полностью: разберём все три типа хранилищ, настроим правильно для продакшна, сделаем бэкап который реально восстанавливается, и перенесём данные на другой сервер.</p>
<p>Что нужно: Docker 24+, docker compose v2, доступ к серверу по SSH. Времени — час на первичную настройку.</p>
<p>Что будет в статье:</p>
<ul>
<li>Чем отличаются named volume, bind mount и tmpfs — с примерами</li>
<li>Как правильно настроить docker-compose.yml для продакшна</li>
<li>Бэкап volumes: tar, rsync, cron</li>
<li>Восстановление из бэкапа — полный сценарий</li>
<li>Миграция данных между серверами</li>
<li>Что делать если уже потерял данные после prune</li>
</ul>
<h2>Как Docker хранит данные: три механизма</h2>
<p>Контейнер — это процесс с изолированной файловой системой. По умолчанию все файлы внутри контейнера живут в его writable layer. Удалил контейнер — удалил данные. Вот три способа это исправить:</p>
<table>
<thead>
<tr>
<th>Тип</th>
<th>Где хранится</th>
<th>Кто управляет</th>
<th>Использование</th>
</tr>
</thead>
<tbody>
<tr>
<td>Named volume</td>
<td>/var/lib/docker/volumes/</td>
<td>Docker daemon</td>
<td>Продакшн, БД</td>
</tr>
<tr>
<td>Bind mount</td>
<td>Любой путь на хосте</td>
<td>Ты</td>
<td>Конфиги, данные БД где нужен прямой доступ</td>
</tr>
<tr>
<td>tmpfs mount</td>
<td>Оперативная память</td>
<td>Ядро</td>
<td>Секреты, кэш, временные данные</td>
</tr>
</tbody>
</table>
<p>Важное уточнение: bind mount, где ты указываешь путь вида <code>./data:/var/lib/postgresql/data</code> — это не то же самое что named volume. Bind mount привязан к конкретной папке на хосте. Named volume — абстракция которую Docker прячет в /var/lib/docker/volumes/. Оба переживают пересоздание контейнера, но ведут себя по-разному при prune.</p>
<pre class="mermaid">
%%{init: {
'theme': 'base',
'themeVariables': {
'primaryColor': '#ffffff',
'primaryTextColor': '#1e293b',
'primaryBorderColor': '#94a3b8',
'lineColor': '#64748b',
'fontSize': '15px',
'fontFamily': 'ui-sans-serif, system-ui, sans-serif'
},
'flowchart': {'curve': 'linear', 'nodeSpacing': 50, 'rankSpacing': 50}
}}%%
flowchart TD
C["Контейнер"] --> A["Named Volume"]
C --> B["Bind Mount"]
C --> D["tmpfs"]
A --> E["Docker Area /var/lib/docker/volumes/"]
B --> F["Любой путь хоста /home/user/data/"]
D --> G["RAM только пока контейнер работает"]
style C fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
style A fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
style B fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
style D fill:#f8fafc,stroke:#f97316,stroke-width:2px,color:#9a3412
style E fill:#f8fafc,stroke:#94a3b8,stroke-width:1px,color:#1e293b
style F fill:#f8fafc,stroke:#94a3b8,stroke-width:1px,color:#1e293b
style G fill:#f8fafc,stroke:#ef4444,stroke-width:1px,color:#7f1d1d
</pre>
<h2>Named volume: синтаксис и поведение</h2>
<p>Named volume создаётся явно и живёт отдельно от контейнера. Синтаксис в docker-compose.yml:</p>
<pre><code class="language-text">
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
volumes:
- pgdata:/var/lib/postgresql/data
restart: unless-stopped
volumes:
pgdata:
driver: local
</code></pre>
<p>Когда делаешь <code>docker compose down</code> — контейнер удаляется, volume pgdata остаётся. Когда делаешь <code>docker compose up</code> — контейнер пересоздаётся и подключается к тому же volume. Данные на месте.</p>
<p>Посмотри где физически лежит volume:</p>
<pre><code class="language-bash">
docker volume inspect pgdata
</code></pre>
<p>Вывод покажет Mountpoint — обычно что-то вроде <code>/var/lib/docker/volumes/myproject_pgdata/_data</code>. Это реальная папка на хосте. Можно зайти и посмотреть файлы напрямую от root.</p>
<p>Теперь про убийцу: <code>docker system prune --volumes</code> удаляет все volumes которые не подключены ни к одному запущенному контейнеру. Сделал <code>docker compose down</code> — контейнеры остановились и удалились, volume стал «сиротой». Следующий prune с флагом —volumes снесёт его без вопросов. Это не баг, это задокументированное поведение.</p>
"Критически
<br />
docker system prune без флага —volumes не трогает тома. Опасность появляется только с явным —volumes. Но многие копируют команду из интернета не читая что делает каждый флаг. Это классика жанра.<br />
<h2>Bind mount: ты контролируешь путь</h2>
<p>Bind mount — прямой проброс папки с хоста в контейнер. Указываешь абсолютный или относительный путь:</p>
<pre><code class="language-text">
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
volumes:
- /opt/appdata/postgres:/var/lib/postgresql/data
restart: unless-stopped
</code></pre>
<p>Данные лежат в /opt/appdata/postgres на хосте. Ты знаешь точно где. Бэкап делается обычным tar или rsync. <code>docker system prune --volumes</code> их не затронет никогда — это файловая система хоста, не Docker volume.</p>
<p>Минус: права доступа. PostgreSQL внутри контейнера работает от пользователя с UID 999. Если создал папку от root — Postgres не запустится и скажет что не может записать данные. Правим так:</p>
<pre><code class="language-bash">
mkdir -p /opt/appdata/postgres
chown -R 999:999 /opt/appdata/postgres
</code></pre>
<p>Для MySQL/MariaDB UID будет 999, для Redis — 999, для Nginx — 101. Смотри документацию образа или запусти контейнер без bind mount и проверь от кого работает процесс:</p>
<pre><code class="language-bash">
docker run --rm postgres:16-alpine id postgres
</code></pre>
<h2>Правильная структура каталогов для продакшна</h2>
<p>За годы работы выработалась структура которую можно использовать как шаблон. Всё организовано так что бэкап одной папки покрывает всё:</p>
<pre><code class="language-bash">
/opt/
└── apps/
└── myproject/
├── docker-compose.yml
├── .env
└── data/
├── postgres/
├── redis/
├── uploads/
└── backups/
</code></pre>
<p>docker-compose.yml с bind mount под эту структуру:</p>
<pre><code class="language-text">
services:
postgres:
image: postgres:16-alpine
env_file: .env
volumes:
- ./data/postgres:/var/lib/postgresql/data
restart: unless-stopped
redis:
image: redis:7-alpine
volumes:
- ./data/redis:/data
command: redis-server --appendonly yes
restart: unless-stopped
app:
image: myapp:latest
volumes:
- ./data/uploads:/app/uploads
depends_on:
- postgres
- redis
restart: unless-stopped
</code></pre>
<p>Плюс такого подхода: <code>rsync -avz /opt/apps/myproject/data/ backup-server:/backups/myproject/</code> — и у тебя полный бэкап всего проекта. Одна команда, один путь, ничего не забудешь.</p>
<h2>Когда брать named volume, а когда bind mount</h2>
<table>
<thead>
<tr>
<th>Сценарий</th>
<th>Рекомендация</th>
<th>Почему</th>
</tr>
</thead>
<tbody>
<tr>
<td>БД в продакшне</td>
<td>Bind mount</td>
<td>Прямой контроль над путём, простой бэкап</td>
</tr>
<tr>
<td>Shared volume между контейнерами</td>
<td>Named volume</td>
<td>Docker управляет правами и путём</td>
</tr>
<tr>
<td>Конфиги и сертификаты</td>
<td>Bind mount (readonly)</td>
<td>Версионирование в <a class="wpil_keyword_link" href="https://it-apteka.com/tag/git/" target="_blank" rel="noopener" title="Git" data-wpil-keyword-link="linked" data-wpil-monitor-id="2885">git</a>, чёткий путь</td>
</tr>
<tr>
<td>Данные которые не нужны после контейнера</td>
<td>tmpfs</td>
<td>Не засоряет диск, быстрее</td>
</tr>
<tr>
<td>Dev-окружение</td>
<td>Bind mount от home</td>
<td>Прямой доступ к файлам для правки</td>
</tr>
</tbody>
</table>
<h2>Бэкап named volume: три подхода</h2>
<p>Ситуация: у тебя уже есть named volume и менять конфиг нет возможности. Или ты принципиально используешь named volumes. Как делать бэкап.</p>
<h3>Подход 1: временный контейнер с tar</h3>
<p>Стандартный способ из документации Docker. Останови контейнер, запусти временный, смонтируй volume, упакуй tar:</p>
"Бэкап
<br />
Останови контейнер с данными. Запусти временный alpine с двумя маунтами: volume и папка для бэкапа. Упакуй tar и выйди. Данные сохранены на хосте.<br />
<pre><code class="language-bash">
# Останови контейнер (важно для консистентности данных)
docker compose stop postgres
# Создай бэкап
docker run --rm \
-v myproject_pgdata:/source:ro \
-v /opt/backups:/backup \
alpine \
tar czf /backup/pgdata-$(date +%Y%m%d-%H%M%S).tar.gz -C /source .
# Запусти контейнер обратно
docker compose start postgres
</code></pre>
<p>Флаг <code>:ro</code> у source монтирует volume в read-only. Это <a class="wpil_keyword_link" href="https://it-apteka.com/category/security/" target="_blank" rel="noopener" title="Безопасность" data-wpil-keyword-link="linked" data-wpil-monitor-id="2887">защита</a> от случайной записи во время бэкапа. Не пренебрегай.</p>
<h3>Подход 2: pg_dump для PostgreSQL</h3>
<p>Для баз данных горячий бэкап через pg_dump лучше холодного копирования файлов. Не нужно останавливать контейнер. Дамп консистентен даже при активных транзакциях:</p>
<pre><code class="language-bash">
# Логический дамп без остановки контейнера
docker exec postgres_container pg_dumpall -U postgres | \
gzip > /opt/backups/postgres-$(date +%Y%m%d-%H%M%S).sql.gz
</code></pre>
<p>Для конкретной базы:</p>
<pre><code class="language-bash">
docker exec postgres_container \
pg_dump -U postgres myapp | \
gzip > /opt/backups/myapp-$(date +%Y%m%d-%H%M%S).sql.gz
</code></pre>
<h3>Подход 3: bind mount + rsync</h3>
<p>Если перешёл на bind mount — бэкап максимально простой:</p>
<pre><code class="language-bash">
# Локальный бэкап с ротацией
rsync -avz --delete \
/opt/apps/myproject/data/ \
/opt/backups/myproject/
# Или сразу на удалённый сервер
rsync -avz -e "ssh -p 22" \
/opt/apps/myproject/data/ \
user@backup-server:/backups/myproject/
</code></pre>
<h2>Автоматический бэкап через cron</h2>
<p>Ручной бэкап который не запускается автоматически — это не бэкап, это намерение. Делаем <a href="https://it-apteka.com/zapusk-bash-skriptov-v-linux-cherez-terminal-cron-python-windows-i-raspberry-pi/" title="Запуск bash скрипта: chmod, cron, Python, Windows и Raspberry Pi" target="_blank" rel="noopener" data-wpil-monitor-id="2852">скрипт и вешаем на cron</a>.</p>
<p>Создай <a class="wpil_keyword_link" href="https://it-apteka.com/category/scripts/" target="_blank" rel="noopener" title="Скрипты" data-wpil-keyword-link="linked" data-wpil-monitor-id="2882">скрипт</a> /opt/scripts/backup-docker.sh:</p>
<pre><code class="language-bash">
#!/bin/bash
set -euo pipefail
BACKUP_DIR="/opt/backups/docker"
DATE=$(date +%Y%m%d-%H%M%S)
KEEP_DAYS=7
LOG_FILE="/var/log/docker-backup.log"
mkdir -p "$BACKUP_DIR"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
log "=== Начало бэкапа ==="
# PostgreSQL - логический дамп
log "Бэкап PostgreSQL..."
if docker exec myproject_postgres_1 pg_dumpall -U postgres | \
gzip > "${BACKUP_DIR}/postgres-${DATE}.sql.gz"; then
log "PostgreSQL OK: ${BACKUP_DIR}/postgres-${DATE}.sql.gz"
else
log "ОШИБКА: PostgreSQL бэкап не удался"
exit 1
fi
# Файлы загрузок через rsync
log "Бэкап uploads..."
rsync -avz --delete \
/opt/apps/myproject/data/uploads/ \
"${BACKUP_DIR}/uploads/"
log "Uploads OK"
# Удаляем старые бэкапы
find "$BACKUP_DIR" -name "postgres-*.sql.gz" -mtime +"$KEEP_DAYS" -delete
log "Старые бэкапы удалены (старше ${KEEP_DAYS} дней)"
log "=== Бэкап завершён ==="
</code></pre>
<pre><code class="language-bash">
chmod +x /opt/scripts/backup-docker.sh
</code></pre>
<p>Добавь в crontab:</p>
<pre><code class="language-bash">
crontab -e
</code></pre>
<pre><code class="language-text">
# Бэкап каждый день в 3:00
0 3 * * * /opt/scripts/backup-docker.sh >> /var/log/docker-backup.log 2>&1
</code></pre>
<p>Проверь что <a href="https://it-apteka.com/powershell-skripty-v-windows-kak-sozdat-zapustit-i-avtomatizirovat-vypolnenie/" title="PowerShell скрипты в Windows: как создать, запустить и автоматизировать выполнение" target="_blank" rel="noopener" data-wpil-monitor-id="2853">скрипт реально отрабатывает — запусти</a> вручную и посмотри файлы в BACKUP_DIR. Скрипт который написан но ни разу не запускался вручную — тоже не бэкап.</p>
<h2>Восстановление из бэкапа</h2>
<p>Самый важный раздел. Бэкап который не проверен на восстановление — это красивый файл с непонятным содержимым.</p>
<h3>Восстановление named volume из tar</h3>
<pre><code class="language-bash">
# Останови контейнер
docker compose stop postgres
# Очисти существующий volume (если нужно)
docker run --rm \
-v myproject_pgdata:/target \
alpine \
sh -c "rm -rf /target/*"
# Восстанови из архива
docker run --rm \
-v myproject_pgdata:/target \
-v /opt/backups:/backup:ro \
alpine \
tar xzf /backup/pgdata-20240315-030000.tar.gz -C /target
# Запусти контейнер
docker compose start postgres
</code></pre>
<h3>Восстановление PostgreSQL из sql.gz</h3>
<pre><code class="language-bash">
# Для полного дампа через pg_dumpall
gunzip -c /opt/backups/postgres-20240315-030000.sql.gz | \
docker exec -i myproject_postgres_1 psql -U postgres
# Для дампа конкретной базы через pg_dump
gunzip -c /opt/backups/myapp-20240315-030000.sql.gz | \
docker exec -i myproject_postgres_1 psql -U postgres myapp
</code></pre>
<h3>Восстановление bind mount</h3>
<p>Если использовал rsync — восстановление тривиальное:</p>
<pre><code class="language-bash">
# Останови контейнеры
docker compose down
# Восстанови данные
rsync -avz /opt/backups/myproject/ /opt/apps/myproject/data/
# Подними обратно
docker compose up -d
</code></pre>
<h2>Миграция volumes между серверами</h2>
<p>Переезжаешь на другой сервер или делаешь копию окружения. Схема одна для всех случаев: упаковал, передал, распаковал.</p>
<pre class="mermaid">
%%{init: {
'theme': 'base',
'themeVariables': {
'primaryColor': '#ffffff',
'primaryTextColor': '#1e293b',
'primaryBorderColor': '#94a3b8',
'lineColor': '#64748b',
'fontSize': '15px',
'fontFamily': 'ui-sans-serif, system-ui, sans-serif'
},
'flowchart': {'curve': 'linear', 'nodeSpacing': 50, 'rankSpacing': 50}
}}%%
flowchart LR
A["Старый сервер"] --> B["docker compose down"]
B --> C["Упаковать в tar.gz"]
C --> D["scp / rsync на новый сервер"]
D --> E["Распаковать"]
E --> F["docker compose up -d"]
style A fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
style F fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
style C fill:#f8fafc,stroke:#94a3b8,stroke-width:1px,color:#1e293b
style D fill:#f8fafc,stroke:#94a3b8,stroke-width:1px,color:#1e293b
style B fill:#f8fafc,stroke:#94a3b8,stroke-width:1px,color:#1e293b
style E fill:#f8fafc,stroke:#94a3b8,stroke-width:1px,color:#1e293b
</pre>
<h3>Миграция named volume</h3>
<p>На старом сервере:</p>
<pre><code class="language-bash">
# Останови стек
docker compose down
# Упакуй volume в tar
docker run --rm \
-v myproject_pgdata:/source:ro \
-v /tmp:/backup \
alpine \
tar czf /backup/pgdata-migration.tar.gz -C /source .
# Передай на новый сервер
scp /tmp/pgdata-migration.tar.gz user@new-server:/tmp/
</code></pre>
<p>На новом сервере:</p>
<pre><code class="language-bash">
# Создай volume если ещё нет
docker volume create myproject_pgdata
# Восстанови данные
docker run --rm \
-v myproject_pgdata:/target \
-v /tmp:/backup:ro \
alpine \
tar xzf /backup/pgdata-migration.tar.gz -C /target
# Скопируй docker-compose.yml и .env
scp user@old-server:/opt/apps/myproject/docker-compose.yml .
scp user@old-server:/opt/apps/myproject/.env .
# Запусти
docker compose up -d
</code></pre>
<h3>Миграция bind mount</h3>
<p>Проще. Копируешь папку целиком:</p>
<pre><code class="language-bash">
# На старом сервере - останови и скопируй
docker compose down
rsync -avz -e ssh \
/opt/apps/myproject/ \
user@new-server:/opt/apps/myproject/
# На новом сервере
cd /opt/apps/myproject
docker compose up -d
</code></pre>
<p>Обрати внимание: rsync копирует и docker-compose.yml, и .env, и data/ одной командой. Вот почему структура каталогов важна.</p>
<h2>Системные требования</h2>
<table>
<thead>
<tr>
<th>Компонент</th>
<th>Минимальная версия</th>
<th>Рекомендуемая</th>
</tr>
</thead>
<tbody>
<tr>
<td>Docker Engine</td>
<td>20.10</td>
<td>26.x</td>
</tr>
<tr>
<td>Docker Compose</td>
<td>v2.0</td>
<td>v2.24+</td>
</tr>
<tr>
<td>ОС</td>
<td><a href="https://it-apteka.com/nastrojka-staticheskogo-ip-v-ubuntu-22-04-24-04-i-debian-12-13-nov/" title="Настройка статического IP в Ubuntu 22.04-24.04 и Debian 12-13: Новый мир Netplan" target="_blank" rel="noopener" data-wpil-monitor-id="2854">Ubuntu 20.04 / Debian</a> 11</td>
<td><a href="https://it-apteka.com/linux-v-aprele-2026-jadro-7-0-ubuntu-26-04-lts-i-francija-brosaet-windows/" title="Linux в апреле 2026: ядро 7.0, Ubuntu 26.04 LTS и Франция бросает Windows" target="_blank" rel="noopener" data-wpil-monitor-id="2855">Ubuntu 24.04 LTS</a></td>
</tr>
<tr>
<td>Место на диске</td>
<td>10 GB для /var/lib/docker</td>
<td>50+ GB</td>
</tr>
</tbody>
</table>
<p>На момент публикации актуальна версия Docker Engine 26.x. Перед установкой проверь свежие релизы на <a href="https://docs.docker.com/engine/release-notes/" rel="nofollow" target="_blank">docs.docker.com</a>.</p>
<h2>Осложнения: типичные проблемы и решения</h2>
<p>Одна строка в docker-compose.yml без явного указания типа маунта — и через полгода не можешь вспомнить куда вообще пишутся данные. В crontab без комментариев то же самое. Пиши комментарии, будущий ты скажет спасибо.</p>
<h3>Ошибка: Permission denied при запуске контейнера</h3>
<p>Симптом: контейнер падает сразу после запуска, в логах <code>Permission denied</code> на директорию с данными.</p>
<p>Причина: папка для bind mount создана от root, а процесс внутри контейнера работает от другого пользователя.</p>
<p>Решение:</p>
<pre><code class="language-bash">
# Узнай UID процесса внутри контейнера
docker run --rm postgres:16-alpine id postgres
# Выведет: uid=999(postgres) gid=999(postgres)
# Исправь права на хосте
chown -R 999:999 /opt/apps/myproject/data/postgres
</code></pre>
<h3>Ошибка: volume не найден после docker compose up</h3>
<p>Симптом: <code>docker compose up</code> создаёт пустой новый volume вместо подключения к существующему.</p>
<p>Причина: имя volume в docker-compose.yml формируется как <code>project_name_volume_name</code>. Если запускаешь compose из папки с другим именем — получаешь новый volume.</p>
<pre><code class="language-bash">
# Посмотри существующие volumes
docker volume ls
# Проверь имя проекта
docker compose config | grep name
# Задай имя проекта явно в docker-compose.yml
name: myproject
</code></pre>
<p>Или запускай всегда с явным флагом:</p>
<pre><code class="language-bash">
docker compose -p myproject up -d
</code></pre>
<h3>Ошибка: нет места на диске, /var/lib/docker занял всё</h3>
<p>Симптом: <code>No space left on device</code>. Диск занят, но файлов не видно.</p>
<pre><code class="language-bash">
# Посмотри что занимает место в Docker
docker system df
# Безопасная очистка без --volumes
docker system prune -f
# Только builder cache
docker builder prune -f
# Только неиспользуемые образы
docker image prune -a -f
</code></pre>
"Правило
<br />
Никогда не запускай docker system prune —volumes в продакшне без бэкапа и без понимания какие volumes помечены как неиспользуемые. Сначала docker system df, потом принятие решений.<br />
<h3>Ошибка: данные потеряны после prune — что делать</h3>
<p>Если всё-таки случилось — данные из named volume после <code>docker system prune --volumes</code> восстановить стандартными средствами Docker нельзя. Файлы удаляются на уровне файловой системы.</p>
<p>Шанс есть если:</p>
<ul>
<li>Диск не перезаписывался после удаления (не было интенсивной записи)</li>
<li>Есть физический доступ к серверу</li>
</ul>
<pre><code class="language-bash">
# Останови все контейнеры и процессы записи немедленно
docker compose down
# Попробуй extundelete для ext4
apt install extundelete
extundelete /dev/sda1 --restore-directory /var/lib/docker/volumes/
# Или testdisk/photorec для более глубокого восстановления
apt install testdisk
testdisk /dev/sda1
</code></pre>
<p>Честно: шансы невысоки. Правильный ответ — настроить бэкап заранее, а не искать инструменты восстановления потом.</p>
<h2>Альтернативы: сторонние инструменты бэкапа</h2>
<table>
<thead>
<tr>
<th>Инструмент</th>
<th>Плюсы</th>
<th>Минусы</th>
<th>Подходит для</th>
</tr>
</thead>
<tbody>
<tr>
<td>tar + cron</td>
<td>Встроен, просто, надёжно</td>
<td>Нет дедупликации, нет шифрования</td>
<td>Небольшие данные</td>
</tr>
<tr>
<td>restic</td>
<td>Дедупликация, шифрование, S3</td>
<td>Надо устанавливать, сложнее</td>
<td>Продакшн с большими данными</td>
</tr>
<tr>
<td>rsync</td>
<td>Быстрый инкрементальный, SSH</td>
<td>Нет версионирования само по себе</td>
<td>Файлы, uploads</td>
</tr>
<tr>
<td>Volumes <a class="wpil_keyword_link" href="https://it-apteka.com/category/rezervnoe-kopirovanie/" target="_blank" rel="noopener" title="Резервное копирование" data-wpil-keyword-link="linked" data-wpil-monitor-id="2883">Backup</a> & Share (плагин)</td>
<td>GUI, простой</td>
<td>Только Docker Desktop</td>
<td>Dev-окружение</td>
</tr>
</tbody>
</table>
<p>Мой выбор для хоумлаба и небольшого продакшна: rsync для файлов + pg_dump для баз + cron. Ничего лишнего, всё понятно, восстановление предсказуемо.</p>
<p>Для серьёзного продакшна с объёмом данных от 50 GB — смотри на <a href="https://restic.net" rel="nofollow" target="_blank">restic</a> с бэкендом S3. Дедупликация съекономит место и время передачи.</p>
<h2>Профилактика: как не потерять данные</h2>
<h3>Мониторинг места на диске</h3>
<pre><code class="language-bash">
# Добавь в crontab проверку места
# Предупреждение если /var/lib/docker занял больше 80%
*/30 * * * * df /var/lib/docker | awk 'NR==2 {if ($5+0 > 80) print "ALERT: Docker disk " $5}' | mail -s "Docker disk alert" admin@example.com
</code></pre>
<h3>Документируй где лежат данные</h3>
<p>Добавь в docker-compose.yml комментарии:</p>
<pre><code class="language-text">
services:
postgres:
image: postgres:16-alpine
volumes:
# Данные БД: /opt/apps/myproject/data/postgres/
# Бэкап: /opt/backups/postgres-*.sql.gz (cron 3:00)
- ./data/postgres:/var/lib/postgresql/data
</code></pre>
<h3>Проверка бэкапа раз в месяц</h3>
<p>Восстанови бэкап в тестовое окружение. Проверь что данные читаются. Убедись что bэкап не битый:</p>
<pre><code class="language-bash">
# Проверка целостности архива без распаковки
gzip -t /opt/backups/postgres-20240315-030000.sql.gz && echo "OK" || echo "CORRUPT"
# Проверка sql дампа
gunzip -c /opt/backups/postgres-20240315-030000.sql.gz | head -20
</code></pre>
<h3>UFW: закрой прямой доступ к данным</h3>
<pre><code class="language-bash">
# Закрой порт PostgreSQL снаружи
ufw deny 5432
# Открой только для конкретного IP если нужен прямой доступ
ufw allow from 192.168.1.100 to any port 5432
</code></pre>
<h2>FAQ</h2>
<h3>Почему docker compose down удаляет данные?</h3>
<p><code>docker compose down</code> без флагов удаляет только контейнеры и <a class="wpil_keyword_link" href="https://it-apteka.com/category/networks/" target="_blank" rel="noopener" title="Сети" data-wpil-keyword-link="linked" data-wpil-monitor-id="2886">сети</a>. Данные в named volumes или bind mount не трогает. Данные теряются если добавить флаг <code>-v</code> или <code>--volumes</code>: тогда удаляются и анонимные volumes объявленные в сервисе. Или если после down запустить <code>docker system prune --volumes</code>. Сами по себе named volumes и bind mount данные переживают любой down.</p>
<h3>Как проверить что docker volume работает правильно и данные сохраняются?</h3>
<pre><code class="language-bash">
# Создай тестовые данные
docker exec postgres_container psql -U postgres -c "CREATE TABLE test (id serial, val text); INSERT INTO test(val) VALUES ('check');"
# Пересоздай контейнер
docker compose down && docker compose up -d
# Проверь что данные на месте
docker exec postgres_container psql -U postgres -c "SELECT * FROM test;"
</code></pre>
<p>Если SELECT вернул строку — volume работает. Если таблицы не существует — данные не персистентны, проверь конфиг volumes в docker-compose.yml.</p>
<h3>Что делать если docker system prune удалил нужный volume?</h3>
<p>Без бэкапа — шансы минимальны. Немедленно останови все процессы записи на диск. Попробуй extundelete или testdisk. Если это продакшн с ценными данными — вызывай специалистов по восстановлению данных, не экспериментируй сам — каждая запись на диск уменьшает шансы. На будущее: настрой автоматический бэкап по инструкции выше.</p>
<h3>Чем bind mount отличается от named volume в docker compose?</h3>
<p>В bind mount ты указываешь конкретный путь на хосте (<code>./data:/app/data</code> или <code>/opt/data:/app/data</code>) — Docker монтирует эту папку напрямую. В named volume ты указываешь имя (<code>mydata:/app/data</code>) — Docker сам создаёт папку в /var/lib/docker/volumes/ и управляет ею. Bind mount проще контролировать и бэкапить. Named volume лучше для shared volumes между контейнерами и для изоляции от файловой системы хоста.</p>
<h3>Можно ли сделать снапшот docker volume без остановки контейнера?</h3>
<p>Для файлов — технически да, через LVM snapshot или btrfs snapshot на уровне хоста, если том данных на отдельном LV/subvolume. Для баз данных горячий снапшот файлов небезопасен: данные могут быть в несогласованном состоянии. Используй логический дамп (pg_dump, mysqldump) — он консистентен без остановки. Для Redis с AOF включённым — можно копировать appendonly.aof на работающем инстансе.</p>
<h2>Итог</h2>
<p>Правило простое: любые данные которые тебе нужны после <code>docker compose down</code> — должны быть либо в bind mount с известным путём, либо в named volume с настроенным бэкапом. Нет третьего варианта.</p>
<p>Прямо сейчас: проверь свои docker-compose.yml файлы. Найди все строки с <code>volumes:</code>. Убедись что знаешь где физически лежат данные каждого сервиса. Настрой cron с бэкапом. Проверь восстановление. Потом спи спокойно.</p>
"Если
<br />
Настроил, запустил, не заработало — пиши в комментарии. Разберёмся. Описывай: версию Docker, что именно делал, что вывела консоль. Без этого — не угадаю.<br />
Быстрый ответ
Docker volumes — это три разных механизма с разными гарантиями сохранности:
- Named volume — данные под управлением Docker (/var/lib/docker/volumes/), переживают пересоздание контейнера, теряются при
docker system prune --volumes
- Bind mount — данные в произвольной папке хоста, ты контролируешь путь и бэкап, идеален для баз данных в продакшне
- tmpfs — данные в оперативной памяти, исчезают при остановке контейнера, только для временных данных
Правило продакшна: базы данных и критичные данные — только bind mount или named volume с настроенным бэкапом. Никогда не запускай docker system prune --volumes без бэкапа.
Диагноз: где пропадают данные
Типичная история. Поднял стек: PostgreSQL, Redis, приложение. Работало месяц. Потом решил почистить диск:
docker compose down
docker system prune --volumes
После этого docker compose up -d поднял контейнеры. Всё запустилось. Только базы данных пустые. Пользователи, заказы, настройки — gone.
Хранение данных Docker volumes — это тема где цена ошибки равна потере всего. Эта статья закрывает вопрос полностью: разберём все три типа хранилищ, настроим правильно для продакшна, сделаем бэкап который реально восстанавливается, и перенесём данные на другой сервер.
Что нужно: Docker 24+, docker compose v2, доступ к серверу по SSH. Времени — час на первичную настройку.
Что будет в статье:
- Чем отличаются named volume, bind mount и tmpfs — с примерами
- Как правильно настроить docker-compose.yml для продакшна
- Бэкап volumes: tar, rsync, cron
- Восстановление из бэкапа — полный сценарий
- Миграция данных между серверами
- Что делать если уже потерял данные после prune
Как Docker хранит данные: три механизма
Контейнер — это процесс с изолированной файловой системой. По умолчанию все файлы внутри контейнера живут в его writable layer. Удалил контейнер — удалил данные. Вот три способа это исправить:
| Тип |
Где хранится |
Кто управляет |
Использование |
| Named volume |
/var/lib/docker/volumes/ |
Docker daemon |
Продакшн, БД |
| Bind mount |
Любой путь на хосте |
Ты |
Конфиги, данные БД где нужен прямой доступ |
| tmpfs mount |
Оперативная память |
Ядро |
Секреты, кэш, временные данные |
Важное уточнение: bind mount, где ты указываешь путь вида ./data:/var/lib/postgresql/data — это не то же самое что named volume. Bind mount привязан к конкретной папке на хосте. Named volume — абстракция которую Docker прячет в /var/lib/docker/volumes/. Оба переживают пересоздание контейнера, но ведут себя по-разному при prune.
%%{init: {
'theme': 'base',
'themeVariables': {
'primaryColor': '#ffffff',
'primaryTextColor': '#1e293b',
'primaryBorderColor': '#94a3b8',
'lineColor': '#64748b',
'fontSize': '15px',
'fontFamily': 'ui-sans-serif, system-ui, sans-serif'
},
'flowchart': {'curve': 'linear', 'nodeSpacing': 50, 'rankSpacing': 50}
}}%%
flowchart TD
C["Контейнер"] --> A["Named Volume"]
C --> B["Bind Mount"]
C --> D["tmpfs"]
A --> E["Docker Area /var/lib/docker/volumes/"]
B --> F["Любой путь хоста /home/user/data/"]
D --> G["RAM только пока контейнер работает"]
style C fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
style A fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
style B fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
style D fill:#f8fafc,stroke:#f97316,stroke-width:2px,color:#9a3412
style E fill:#f8fafc,stroke:#94a3b8,stroke-width:1px,color:#1e293b
style F fill:#f8fafc,stroke:#94a3b8,stroke-width:1px,color:#1e293b
style G fill:#f8fafc,stroke:#ef4444,stroke-width:1px,color:#7f1d1d
Named volume: синтаксис и поведение
Named volume создаётся явно и живёт отдельно от контейнера. Синтаксис в docker-compose.yml:
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
volumes:
- pgdata:/var/lib/postgresql/data
restart: unless-stopped
volumes:
pgdata:
driver: local
Когда делаешь docker compose down — контейнер удаляется, volume pgdata остаётся. Когда делаешь docker compose up — контейнер пересоздаётся и подключается к тому же volume. Данные на месте.
Посмотри где физически лежит volume:
docker volume inspect pgdata
Вывод покажет Mountpoint — обычно что-то вроде /var/lib/docker/volumes/myproject_pgdata/_data. Это реальная папка на хосте. Можно зайти и посмотреть файлы напрямую от root.
Теперь про убийцу: docker system prune --volumes удаляет все volumes которые не подключены ни к одному запущенному контейнеру. Сделал docker compose down — контейнеры остановились и удалились, volume стал «сиротой». Следующий prune с флагом —volumes снесёт его без вопросов. Это не баг, это задокументированное поведение.
Критически важно
docker system prune без флага —volumes не трогает тома. Опасность появляется только с явным —volumes. Но многие копируют команду из интернета не читая что делает каждый флаг. Это классика жанра.
Bind mount: ты контролируешь путь
Bind mount — прямой проброс папки с хоста в контейнер. Указываешь абсолютный или относительный путь:
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
volumes:
- /opt/appdata/postgres:/var/lib/postgresql/data
restart: unless-stopped
Данные лежат в /opt/appdata/postgres на хосте. Ты знаешь точно где. Бэкап делается обычным tar или rsync. docker system prune --volumes их не затронет никогда — это файловая система хоста, не Docker volume.
Минус: права доступа. PostgreSQL внутри контейнера работает от пользователя с UID 999. Если создал папку от root — Postgres не запустится и скажет что не может записать данные. Правим так:
mkdir -p /opt/appdata/postgres
chown -R 999:999 /opt/appdata/postgres
Для MySQL/MariaDB UID будет 999, для Redis — 999, для Nginx — 101. Смотри документацию образа или запусти контейнер без bind mount и проверь от кого работает процесс:
docker run --rm postgres:16-alpine id postgres
Правильная структура каталогов для продакшна
За годы работы выработалась структура которую можно использовать как шаблон. Всё организовано так что бэкап одной папки покрывает всё:
/opt/
└── apps/
└── myproject/
├── docker-compose.yml
├── .env
└── data/
├── postgres/
├── redis/
├── uploads/
└── backups/
docker-compose.yml с bind mount под эту структуру:
services:
postgres:
image: postgres:16-alpine
env_file: .env
volumes:
- ./data/postgres:/var/lib/postgresql/data
restart: unless-stopped
redis:
image: redis:7-alpine
volumes:
- ./data/redis:/data
command: redis-server --appendonly yes
restart: unless-stopped
app:
image: myapp:latest
volumes:
- ./data/uploads:/app/uploads
depends_on:
- postgres
- redis
restart: unless-stopped
Плюс такого подхода: rsync -avz /opt/apps/myproject/data/ backup-server:/backups/myproject/ — и у тебя полный бэкап всего проекта. Одна команда, один путь, ничего не забудешь.
Когда брать named volume, а когда bind mount
| Сценарий |
Рекомендация |
Почему |
| БД в продакшне |
Bind mount |
Прямой контроль над путём, простой бэкап |
| Shared volume между контейнерами |
Named volume |
Docker управляет правами и путём |
| Конфиги и сертификаты |
Bind mount (readonly) |
Версионирование в git, чёткий путь |
| Данные которые не нужны после контейнера |
tmpfs |
Не засоряет диск, быстрее |
| Dev-окружение |
Bind mount от home |
Прямой доступ к файлам для правки |
Бэкап named volume: три подхода
Ситуация: у тебя уже есть named volume и менять конфиг нет возможности. Или ты принципиально используешь named volumes. Как делать бэкап.
Подход 1: временный контейнер с tar
Стандартный способ из документации Docker. Останови контейнер, запусти временный, смонтируй volume, упакуй tar:
Бэкап named volume через временный контейнер
Останови контейнер с данными. Запусти временный alpine с двумя маунтами: volume и папка для бэкапа. Упакуй tar и выйди. Данные сохранены на хосте.
# Останови контейнер (важно для консистентности данных)
docker compose stop postgres
# Создай бэкап
docker run --rm \
-v myproject_pgdata:/source:ro \
-v /opt/backups:/backup \
alpine \
tar czf /backup/pgdata-$(date +%Y%m%d-%H%M%S).tar.gz -C /source .
# Запусти контейнер обратно
docker compose start postgres
Флаг :ro у source монтирует volume в read-only. Это защита от случайной записи во время бэкапа. Не пренебрегай.
Подход 2: pg_dump для PostgreSQL
Для баз данных горячий бэкап через pg_dump лучше холодного копирования файлов. Не нужно останавливать контейнер. Дамп консистентен даже при активных транзакциях:
# Логический дамп без остановки контейнера
docker exec postgres_container pg_dumpall -U postgres | \
gzip > /opt/backups/postgres-$(date +%Y%m%d-%H%M%S).sql.gz
Для конкретной базы:
docker exec postgres_container \
pg_dump -U postgres myapp | \
gzip > /opt/backups/myapp-$(date +%Y%m%d-%H%M%S).sql.gz
Подход 3: bind mount + rsync
Если перешёл на bind mount — бэкап максимально простой:
# Локальный бэкап с ротацией
rsync -avz --delete \
/opt/apps/myproject/data/ \
/opt/backups/myproject/
# Или сразу на удалённый сервер
rsync -avz -e "ssh -p 22" \
/opt/apps/myproject/data/ \
user@backup-server:/backups/myproject/
Автоматический бэкап через cron
Ручной бэкап который не запускается автоматически — это не бэкап, это намерение. Делаем скрипт и вешаем на cron.
Создай скрипт /opt/scripts/backup-docker.sh:
#!/bin/bash
set -euo pipefail
BACKUP_DIR="/opt/backups/docker"
DATE=$(date +%Y%m%d-%H%M%S)
KEEP_DAYS=7
LOG_FILE="/var/log/docker-backup.log"
mkdir -p "$BACKUP_DIR"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
log "=== Начало бэкапа ==="
# PostgreSQL - логический дамп
log "Бэкап PostgreSQL..."
if docker exec myproject_postgres_1 pg_dumpall -U postgres | \
gzip > "${BACKUP_DIR}/postgres-${DATE}.sql.gz"; then
log "PostgreSQL OK: ${BACKUP_DIR}/postgres-${DATE}.sql.gz"
else
log "ОШИБКА: PostgreSQL бэкап не удался"
exit 1
fi
# Файлы загрузок через rsync
log "Бэкап uploads..."
rsync -avz --delete \
/opt/apps/myproject/data/uploads/ \
"${BACKUP_DIR}/uploads/"
log "Uploads OK"
# Удаляем старые бэкапы
find "$BACKUP_DIR" -name "postgres-*.sql.gz" -mtime +"$KEEP_DAYS" -delete
log "Старые бэкапы удалены (старше ${KEEP_DAYS} дней)"
log "=== Бэкап завершён ==="
chmod +x /opt/scripts/backup-docker.sh
Добавь в crontab:
crontab -e
# Бэкап каждый день в 3:00
0 3 * * * /opt/scripts/backup-docker.sh >> /var/log/docker-backup.log 2>&1
Проверь что скрипт реально отрабатывает — запусти вручную и посмотри файлы в BACKUP_DIR. Скрипт который написан но ни разу не запускался вручную — тоже не бэкап.
Восстановление из бэкапа
Самый важный раздел. Бэкап который не проверен на восстановление — это красивый файл с непонятным содержимым.
Восстановление named volume из tar
# Останови контейнер
docker compose stop postgres
# Очисти существующий volume (если нужно)
docker run --rm \
-v myproject_pgdata:/target \
alpine \
sh -c "rm -rf /target/*"
# Восстанови из архива
docker run --rm \
-v myproject_pgdata:/target \
-v /opt/backups:/backup:ro \
alpine \
tar xzf /backup/pgdata-20240315-030000.tar.gz -C /target
# Запусти контейнер
docker compose start postgres
Восстановление PostgreSQL из sql.gz
# Для полного дампа через pg_dumpall
gunzip -c /opt/backups/postgres-20240315-030000.sql.gz | \
docker exec -i myproject_postgres_1 psql -U postgres
# Для дампа конкретной базы через pg_dump
gunzip -c /opt/backups/myapp-20240315-030000.sql.gz | \
docker exec -i myproject_postgres_1 psql -U postgres myapp
Восстановление bind mount
Если использовал rsync — восстановление тривиальное:
# Останови контейнеры
docker compose down
# Восстанови данные
rsync -avz /opt/backups/myproject/ /opt/apps/myproject/data/
# Подними обратно
docker compose up -d
Миграция volumes между серверами
Переезжаешь на другой сервер или делаешь копию окружения. Схема одна для всех случаев: упаковал, передал, распаковал.
%%{init: {
'theme': 'base',
'themeVariables': {
'primaryColor': '#ffffff',
'primaryTextColor': '#1e293b',
'primaryBorderColor': '#94a3b8',
'lineColor': '#64748b',
'fontSize': '15px',
'fontFamily': 'ui-sans-serif, system-ui, sans-serif'
},
'flowchart': {'curve': 'linear', 'nodeSpacing': 50, 'rankSpacing': 50}
}}%%
flowchart LR
A["Старый сервер"] --> B["docker compose down"]
B --> C["Упаковать в tar.gz"]
C --> D["scp / rsync на новый сервер"]
D --> E["Распаковать"]
E --> F["docker compose up -d"]
style A fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
style F fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
style C fill:#f8fafc,stroke:#94a3b8,stroke-width:1px,color:#1e293b
style D fill:#f8fafc,stroke:#94a3b8,stroke-width:1px,color:#1e293b
style B fill:#f8fafc,stroke:#94a3b8,stroke-width:1px,color:#1e293b
style E fill:#f8fafc,stroke:#94a3b8,stroke-width:1px,color:#1e293b
Миграция named volume
На старом сервере:
# Останови стек
docker compose down
# Упакуй volume в tar
docker run --rm \
-v myproject_pgdata:/source:ro \
-v /tmp:/backup \
alpine \
tar czf /backup/pgdata-migration.tar.gz -C /source .
# Передай на новый сервер
scp /tmp/pgdata-migration.tar.gz user@new-server:/tmp/
На новом сервере:
# Создай volume если ещё нет
docker volume create myproject_pgdata
# Восстанови данные
docker run --rm \
-v myproject_pgdata:/target \
-v /tmp:/backup:ro \
alpine \
tar xzf /backup/pgdata-migration.tar.gz -C /target
# Скопируй docker-compose.yml и .env
scp user@old-server:/opt/apps/myproject/docker-compose.yml .
scp user@old-server:/opt/apps/myproject/.env .
# Запусти
docker compose up -d
Миграция bind mount
Проще. Копируешь папку целиком:
# На старом сервере - останови и скопируй
docker compose down
rsync -avz -e ssh \
/opt/apps/myproject/ \
user@new-server:/opt/apps/myproject/
# На новом сервере
cd /opt/apps/myproject
docker compose up -d
Обрати внимание: rsync копирует и docker-compose.yml, и .env, и data/ одной командой. Вот почему структура каталогов важна.
Системные требования
| Компонент |
Минимальная версия |
Рекомендуемая |
| Docker Engine |
20.10 |
26.x |
| Docker Compose |
v2.0 |
v2.24+ |
| ОС |
Ubuntu 20.04 / Debian 11 |
Ubuntu 24.04 LTS |
| Место на диске |
10 GB для /var/lib/docker |
50+ GB |
На момент публикации актуальна версия Docker Engine 26.x. Перед установкой проверь свежие релизы на docs.docker.com.
Осложнения: типичные проблемы и решения
Одна строка в docker-compose.yml без явного указания типа маунта — и через полгода не можешь вспомнить куда вообще пишутся данные. В crontab без комментариев то же самое. Пиши комментарии, будущий ты скажет спасибо.
Ошибка: Permission denied при запуске контейнера
Симптом: контейнер падает сразу после запуска, в логах Permission denied на директорию с данными.
Причина: папка для bind mount создана от root, а процесс внутри контейнера работает от другого пользователя.
Решение:
# Узнай UID процесса внутри контейнера
docker run --rm postgres:16-alpine id postgres
# Выведет: uid=999(postgres) gid=999(postgres)
# Исправь права на хосте
chown -R 999:999 /opt/apps/myproject/data/postgres
Ошибка: volume не найден после docker compose up
Симптом: docker compose up создаёт пустой новый volume вместо подключения к существующему.
Причина: имя volume в docker-compose.yml формируется как project_name_volume_name. Если запускаешь compose из папки с другим именем — получаешь новый volume.
# Посмотри существующие volumes
docker volume ls
# Проверь имя проекта
docker compose config | grep name
# Задай имя проекта явно в docker-compose.yml
name: myproject
Или запускай всегда с явным флагом:
docker compose -p myproject up -d
Ошибка: нет места на диске, /var/lib/docker занял всё
Симптом: No space left on device. Диск занят, но файлов не видно.
# Посмотри что занимает место в Docker
docker system df
# Безопасная очистка без --volumes
docker system prune -f
# Только builder cache
docker builder prune -f
# Только неиспользуемые образы
docker image prune -a -f
Правило безопасной очистки Docker
Никогда не запускай docker system prune —volumes в продакшне без бэкапа и без понимания какие volumes помечены как неиспользуемые. Сначала docker system df, потом принятие решений.
Ошибка: данные потеряны после prune — что делать
Если всё-таки случилось — данные из named volume после docker system prune --volumes восстановить стандартными средствами Docker нельзя. Файлы удаляются на уровне файловой системы.
Шанс есть если:
- Диск не перезаписывался после удаления (не было интенсивной записи)
- Есть физический доступ к серверу
# Останови все контейнеры и процессы записи немедленно
docker compose down
# Попробуй extundelete для ext4
apt install extundelete
extundelete /dev/sda1 --restore-directory /var/lib/docker/volumes/
# Или testdisk/photorec для более глубокого восстановления
apt install testdisk
testdisk /dev/sda1
Честно: шансы невысоки. Правильный ответ — настроить бэкап заранее, а не искать инструменты восстановления потом.
Альтернативы: сторонние инструменты бэкапа
| Инструмент |
Плюсы |
Минусы |
Подходит для |
| tar + cron |
Встроен, просто, надёжно |
Нет дедупликации, нет шифрования |
Небольшие данные |
| restic |
Дедупликация, шифрование, S3 |
Надо устанавливать, сложнее |
Продакшн с большими данными |
| rsync |
Быстрый инкрементальный, SSH |
Нет версионирования само по себе |
Файлы, uploads |
| Volumes Backup & Share (плагин) |
GUI, простой |
Только Docker Desktop |
Dev-окружение |
Мой выбор для хоумлаба и небольшого продакшна: rsync для файлов + pg_dump для баз + cron. Ничего лишнего, всё понятно, восстановление предсказуемо.
Для серьёзного продакшна с объёмом данных от 50 GB — смотри на restic с бэкендом S3. Дедупликация съекономит место и время передачи.
Профилактика: как не потерять данные
Мониторинг места на диске
# Добавь в crontab проверку места
# Предупреждение если /var/lib/docker занял больше 80%
*/30 * * * * df /var/lib/docker | awk 'NR==2 {if ($5+0 > 80) print "ALERT: Docker disk " $5}' | mail -s "Docker disk alert" admin@example.com
Документируй где лежат данные
Добавь в docker-compose.yml комментарии:
services:
postgres:
image: postgres:16-alpine
volumes:
# Данные БД: /opt/apps/myproject/data/postgres/
# Бэкап: /opt/backups/postgres-*.sql.gz (cron 3:00)
- ./data/postgres:/var/lib/postgresql/data
Проверка бэкапа раз в месяц
Восстанови бэкап в тестовое окружение. Проверь что данные читаются. Убедись что bэкап не битый:
# Проверка целостности архива без распаковки
gzip -t /opt/backups/postgres-20240315-030000.sql.gz && echo "OK" || echo "CORRUPT"
# Проверка sql дампа
gunzip -c /opt/backups/postgres-20240315-030000.sql.gz | head -20
UFW: закрой прямой доступ к данным
# Закрой порт PostgreSQL снаружи
ufw deny 5432
# Открой только для конкретного IP если нужен прямой доступ
ufw allow from 192.168.1.100 to any port 5432
FAQ
Почему docker compose down удаляет данные?
docker compose down без флагов удаляет только контейнеры и сети. Данные в named volumes или bind mount не трогает. Данные теряются если добавить флаг -v или --volumes: тогда удаляются и анонимные volumes объявленные в сервисе. Или если после down запустить docker system prune --volumes. Сами по себе named volumes и bind mount данные переживают любой down.
Как проверить что docker volume работает правильно и данные сохраняются?
# Создай тестовые данные
docker exec postgres_container psql -U postgres -c "CREATE TABLE test (id serial, val text); INSERT INTO test(val) VALUES ('check');"
# Пересоздай контейнер
docker compose down && docker compose up -d
# Проверь что данные на месте
docker exec postgres_container psql -U postgres -c "SELECT * FROM test;"
Если SELECT вернул строку — volume работает. Если таблицы не существует — данные не персистентны, проверь конфиг volumes в docker-compose.yml.
Что делать если docker system prune удалил нужный volume?
Без бэкапа — шансы минимальны. Немедленно останови все процессы записи на диск. Попробуй extundelete или testdisk. Если это продакшн с ценными данными — вызывай специалистов по восстановлению данных, не экспериментируй сам — каждая запись на диск уменьшает шансы. На будущее: настрой автоматический бэкап по инструкции выше.
Чем bind mount отличается от named volume в docker compose?
В bind mount ты указываешь конкретный путь на хосте (./data:/app/data или /opt/data:/app/data) — Docker монтирует эту папку напрямую. В named volume ты указываешь имя (mydata:/app/data) — Docker сам создаёт папку в /var/lib/docker/volumes/ и управляет ею. Bind mount проще контролировать и бэкапить. Named volume лучше для shared volumes между контейнерами и для изоляции от файловой системы хоста.
Можно ли сделать снапшот docker volume без остановки контейнера?
Для файлов — технически да, через LVM snapshot или btrfs snapshot на уровне хоста, если том данных на отдельном LV/subvolume. Для баз данных горячий снапшот файлов небезопасен: данные могут быть в несогласованном состоянии. Используй логический дамп (pg_dump, mysqldump) — он консистентен без остановки. Для Redis с AOF включённым — можно копировать appendonly.aof на работающем инстансе.
Итог
Правило простое: любые данные которые тебе нужны после docker compose down — должны быть либо в bind mount с известным путём, либо в named volume с настроенным бэкапом. Нет третьего варианта.
Прямо сейчас: проверь свои docker-compose.yml файлы. Найди все строки с volumes:. Убедись что знаешь где физически лежат данные каждого сервиса. Настрой cron с бэкапом. Проверь восстановление. Потом спи спокойно.
Если что-то пошло не так
Настроил, запустил, не заработало — пиши в комментарии. Разберёмся. Описывай: версию Docker, что именно делал, что вывела консоль. Без этого — не угадаю.