3.5. Save, load, export, import
Цели
После этого материала вы сможете:
- перенести образ между машинами без registry и подтвердить целостность переноса;
- объяснить разницу между
save/loadиexport/importна уровне того, что попадает в архив; - выбрать подходящую пару команд под задачу;
- заглянуть внутрь архива образа и найти там конкретный слой;
- извлечь файл из слоя — и понять, почему это важно для безопасности;
- работать с multi-platform образами при экспорте в Docker Engine 29.
Предварительные знания
- 3.1. Архитектура image — manifest, config, слои;
- 3.2. Слои и copy-on-write — writable layer;
- базовое владение
tar.
Ключевые термины
| Термин | Объяснение |
|---|---|
docker save | Экспорт образа со всеми слоями, историей и метаданными в tar-архив |
docker load | Импорт архива, созданного docker save |
docker export | Экспорт файловой системы container одним плоским слоем |
docker import | Создание образа из плоского tar-архива файловой системы |
flatten | Схлопывание всех слоёв в один |
air-gapped | Среда без доступа к внешней сети |
Теория
Две принципиально разные операции
Названия команд похожи, и это сбивает с толку. На деле они работают с разными объектами.
docker save / load docker export / import
─────────────────── ──────────────────────
объект: ОБРАЗ объект: CONTAINER
┌──────────────────┐ ┌──────────────────┐
│ manifest │ │ │
│ config │ │ плоский срез │
│ ├─ Cmd │ │ файловой │
│ ├─ Env │ │ системы │
│ ├─ User │ │ │
│ └─ history │ │ (слои образа + │
│ слой 1 │ │ writable layer,│
│ слой 2 │ │ объединённые) │
│ слой 3 │ │ │
│ теги │ │ │
└──────────────────┘ └──────────────────┘
после load: после import:
образ идентичен исходному образ БЕЗ Cmd, Env, User,
истории и слоёв — один слой
Сравнение
save / load | export / import | |
|---|---|---|
| Что экспортируется | Образ | Файловая система container |
| Слои | Сохраняются все | Схлопываются в один |
| История сборки | Сохраняется | Теряется |
CMD, ENTRYPOINT | Сохраняются | Теряются |
ENV, USER, WORKDIR | Сохраняются | Теряются |
| Теги | Сохраняются | Задаются при импорте |
| Несколько образов в одном архиве | Да | Нет |
| Размер архива | Больше (все слои) | Меньше (дедупликация внутри) |
| Данные volumes | Не входят | Не входят |
| Типичное применение | Перенос образа | Снятие среза, схлопывание слоёв |
Последняя строка первой колонки — главное. save/load — это то, что нужно почти всегда.
Почему import теряет конфигурацию
docker export выгружает только файлы. Конфигурация образа (Cmd, Env, User) хранится не в файловой системе, а в отдельном JSON-документе (урок 3.1). Плоский tar-архив файлов её не содержит и содержать не может.
Практическое последствие:
docker export mycontainer | docker import - myimage:1
docker run myimage:1
docker: Error response from daemon: No command specified
Команду придётся задать вручную — либо при импорте флагом --change, либо при каждом запуске.
Данные volumes не входят никуда
Ни save, ни export не включают содержимое volumes. Это следует из модели: volume существует вне слоёв образа и вне writable layer.
Ошибка «сделал docker export, думал, что сохранил базу данных» встречается регулярно. Резервное копирование volumes — отдельная процедура, разбираемая в разделе 07.
Multi-platform в Docker Engine 29
С containerd image store docker save умеет сохранять несколько платформ. Появился флаг --platform:
docker save --platform linux/amd64 -o image.tar myapp:1.0
Без флага сохраняются все платформы, доступные локально. Это отличается от прежнего поведения, где сохранялась только текущая платформа.
Практическое следствие: при переносе на машину той же архитектуры указывайте --platform явно — архив будет меньше в разы.
Внутренний механизм
Что внутри архива docker save
Структура соответствует OCI Image Layout:
image.tar
├── oci-layout версия формата
├── index.json корневой index: какие образы и теги внутри
├── manifest.json совместимость с прежним форматом Docker
└── blobs/
└── sha256/
├── 1a2b3c... манифест
├── 4d5e6f... конфигурация
├── 7a8b9c... слой 1 (tar, возможно сжатый)
└── 0d1e2f... слой 2
Все объекты лежат по хэшу — тот же content-addressable принцип, что и в registry и в локальном хранилище. Это делает формат самопроверяемым: целостность подтверждается пересчётом хэшей.
Почему load быстрее, чем кажется
При импорте Docker проверяет наличие каждого слоя локально по его digest. Уже имеющиеся слои не распаковываются заново.
Отсюда практическое наблюдение: загрузка архива на 1.2 GB может занять три секунды, если базовые слои уже есть от других образов.
Команды и примеры
Базовый перенос образа
docker pull -q python:3.13-slim
mkdir -p /tmp/transfer && cd /tmp/transfer
docker save -o python-slim.tar python:3.13-slim
ls -lh python-slim.tar
-rw------- 1 evg evg 128M Jul 29 16:20 python-slim.tar
Разбор: -o задаёт выходной файл. Без него архив пойдёт в stdout — это удобно для конвейеров, но при выводе в терминал даст поток двоичного мусора. Docker в интерактивном режиме откажется это делать.
Теперь удалим образ и восстановим:
BEFORE="$(docker image inspect python:3.13-slim --format '{{.Id}}')"
docker rmi python:3.13-slim > /dev/null
docker images python 2>/dev/null | tail -1
docker load -i python-slim.tar
AFTER="$(docker image inspect python:3.13-slim --format '{{.Id}}')"
echo "до: $BEFORE"
echo "после: $AFTER"
[ "$BEFORE" = "$AFTER" ] && echo "Image ID совпал — образ идентичен"
Loaded image: python:3.13-slim
до: sha256:c2f1e0d9b8a7c6d5e4f3a2b1908877665544332211aabbccddeeff0011223344
после: sha256:c2f1e0d9b8a7c6d5e4f3a2b1908877665544332211aabbccddeeff0011223344
Image ID совпал — образ идентичен
Совпадение Image ID — доказательство идентичности: это хэш конфигурации, а конфигурация ссылается на все слои.
Сжатие архива
docker save python:3.13-slim | gzip > python-slim.tar.gz
ls -lh python-slim.tar python-slim.tar.gz
-rw------- 1 evg evg 128M Jul 29 16:20 python-slim.tar
-rw-rw-r-- 1 evg evg 46M Jul 29 16:22 python-slim.tar.gz
Сжатие даёт выигрыш почти втрое: слои внутри архива хранятся распакованными.
Загрузка сжатого архива:
docker load -i python-slim.tar.gz
docker load определяет сжатие автоматически — распаковывать вручную не нужно. Поддерживаются gzip, bzip2, xz, zstd.
Прямая передача на другую машину без промежуточного файла:
docker save myapp:1.0 | gzip | ssh user@remote 'gunzip | docker load'
Одна команда, ничего не остаётся на диске ни на одной стороне.
Несколько образов в одном архиве
# docker pull принимает РОВНО один образ:
# `docker pull a b` отвечает «docker pull requires 1 argument»
for img in alpine:3.21 debian:trixie-slim; do docker pull -q "$img"; done
docker save -o bundle.tar python:3.13-slim alpine:3.21 debian:trixie-slim
ls -lh bundle.tar
-rw------- 1 evg evg 232M Jul 29 16:25 bundle.tar
Проверим содержимое, не распаковывая:
tar -xOf bundle.tar index.json 2>/dev/null | python3 -c "
import json, sys
d = json.load(sys.stdin)
for m in d['manifests']:
name = m.get('annotations', {}).get('io.containerd.image.name') \
or m.get('annotations', {}).get('org.opencontainers.image.ref.name', '?')
print(f\" {name}\")
" 2>/dev/null || tar -xOf bundle.tar manifest.json | python3 -c "
import json, sys
for e in json.load(sys.stdin):
print(' ', ', '.join(e.get('RepoTags') or ['<без тега>']))
"
python:3.13-slim
alpine:3.21
debian:trixie-slim
Загрузка восстановит все три:
docker load -i bundle.tar
Заглянуть внутрь архива
mkdir -p unpacked && tar -xf python-slim.tar -C unpacked
find unpacked -maxdepth 2 -type f | head -6
unpacked/oci-layout
unpacked/index.json
unpacked/manifest.json
unpacked/blobs/sha256/1a2b3c4d5e6f...
unpacked/blobs/sha256/7a8b9c0d1e2f...
Все объекты — в blobs/sha256/ под именами, равными их хэшам. Проверим это:
BLOB="$(find unpacked/blobs/sha256 -type f | head -1)"
echo "имя файла: $(basename "$BLOB")"
echo "sha256: $(sha256sum "$BLOB" | cut -d' ' -f1)"
имя файла: 1a2b3c4d5e6f7890abcdef1234567890abcdef1234567890abcdef1234567890
sha256: 1a2b3c4d5e6f7890abcdef1234567890abcdef1234567890abcdef1234567890
Совпадает. Content-addressable storage в чистом виде — самопроверяемый формат.
Найдём конфигурацию и прочитаем её:
MANIFEST="$(tar -xOf python-slim.tar index.json 2>/dev/null \
| python3 -c "import json,sys; print(json.load(sys.stdin)['manifests'][0]['digest'].split(':')[1])" 2>/dev/null)"
if [ -n "${MANIFEST:-}" ] && [ -f "unpacked/blobs/sha256/$MANIFEST" ]; then
CONFIG="$(python3 -c "
import json
d = json.load(open('unpacked/blobs/sha256/$MANIFEST'))
print(d['config']['digest'].split(':')[1])
")"
python3 -c "
import json
c = json.load(open('unpacked/blobs/sha256/$CONFIG'))
print('Cmd:', c['config'].get('Cmd'))
print('Env:', len(c['config'].get('Env', [])), 'переменных')
print('слоёв:', len(c['rootfs']['diff_ids']))
"
fi
Cmd: ['python3']
Env: 4 переменных
слоёв: 3
Вся конфигурация образа доступна из архива без Docker — это обычные JSON-файлы.
Извлечение файла из слоя
Практический навык, полезный и для диагностики, и для проверки безопасности.
# найти слои и их размеры
for b in unpacked/blobs/sha256/*; do
if file "$b" | grep -q 'tar archive\|gzip'; then
printf '%-12s %s\n' "$(du -h "$b" | cut -f1)" "$(basename "$b" | cut -c1-16)..."
fi
done | sort -hr | head -3
Посмотреть содержимое конкретного слоя:
LAYER="$(for b in unpacked/blobs/sha256/*; do
file "$b" | grep -q 'tar archive' && echo "$(stat -c%s "$b") $b"
done | sort -rn | head -1 | cut -d' ' -f2)"
tar -tf "$LAYER" 2>/dev/null | head -10
bin/
bin/bash
bin/cat
bin/chgrp
bin/chmod
etc/
etc/apt/
...
Это содержимое базового слоя — обычный tar-архив.
Почему это важно для безопасности. Тот же приём извлекает секрет, «удалённый» последующей инструкцией Dockerfile (урок 3.2):
cd /tmp/transfer
cat > Dockerfile.leak <<'EOF'
FROM alpine:3.21
COPY secret.txt /tmp/secret.txt
RUN cat /tmp/secret.txt > /dev/null && rm /tmp/secret.txt
EOF
echo "API_TOKEN=super-secret-value-12345" > secret.txt
docker build -q -f Dockerfile.leak -t leaky:1 . > /dev/null
echo "=== в container файла нет ==="
docker run --rm leaky:1 ls /tmp/secret.txt 2>&1 | tail -1
echo "=== но в слоях он есть ==="
docker save leaky:1 -o leaky.tar
mkdir -p leakdir && tar -xf leaky.tar -C leakdir
for b in leakdir/blobs/sha256/*; do
tar -tf "$b" 2>/dev/null | grep -q 'tmp/secret.txt' && {
echo "найден в слое $(basename "$b" | cut -c1-16)..."
tar -xOf "$b" tmp/secret.txt 2>/dev/null
}
done
=== в container файла нет ===
ls: /tmp/secret.txt: No such file or directory
=== но в слоях он есть ===
найден в слое 3c4d5e6f7a8b9c0d...
API_TOKEN=super-secret-value-12345
Секрет извлечён из образа обычным tar. Никаких специальных инструментов не потребовалось.
Любой, кто получит доступ к образу, сможет это сделать. Правильное решение — secret mounts BuildKit (раздел 05) и проверка в CI (раздел 12).
export и import: что теряется
docker run -d --name exp-demo python:3.13-slim sleep 300 > /dev/null
docker exec exp-demo sh -c 'echo "данные container" > /container-data.txt'
docker export -o container.tar exp-demo
ls -lh container.tar
docker import container.tar flat:1
-rw------- 1 evg evg 126M Jul 29 16:40 container.tar
sha256:9a8b7c6d5e4f3a2b190877665544332211aabbccddeeff00112233445566
Сравним:
echo "=== слоёв ==="
printf 'исходный образ: %s\n' "$(docker image inspect python:3.13-slim --format '{{len .RootFS.Layers}}')"
printf 'после import: %s\n' "$(docker image inspect flat:1 --format '{{len .RootFS.Layers}}')"
echo
echo "=== конфигурация ==="
printf 'исходный Cmd: %s\n' "$(docker image inspect python:3.13-slim --format '{{.Config.Cmd}}')"
printf 'flat Cmd: %s\n' "$(docker image inspect flat:1 --format '{{.Config.Cmd}}')"
printf 'исходный Env: %s переменных\n' "$(docker image inspect python:3.13-slim --format '{{len .Config.Env}}')"
printf 'flat Env: %s переменных\n' "$(docker image inspect flat:1 --format '{{len .Config.Env}}')"
echo
echo "=== история ==="
printf 'исходный: %s записей\n' "$(docker history python:3.13-slim -q | wc -l)"
printf 'flat: %s записей\n' "$(docker history flat:1 -q | wc -l)"
=== слоёв ===
исходный образ: 3
после import: 1
=== конфигурация ===
исходный Cmd: [python3]
flat Cmd: []
исходный Env: 4 переменных
flat Env: 1 переменных
=== история ===
исходный: 12 записей
flat: 1 записей
Три слоя схлопнулись в один, Cmd пуст, история потеряна.
Попытка запустить:
# head -1, а не tail: Docker 29 после ошибки печатает пустую строку
# и подсказку «Run 'docker run --help'» — tail забрал бы именно их
docker run --rm flat:1 2>&1 | head -1
docker: Error response from daemon: No command specified
Но данные container сохранились:
docker run --rm flat:1 cat /container-data.txt
данные container
Конфигурацию можно восстановить вручную при импорте:
docker import \
--change 'CMD ["python3"]' \
--change 'ENV PATH=/usr/local/bin:/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin' \
--change 'ENV LANG=C.UTF-8' \
container.tar flat:2
docker run --rm flat:2 python3 --version
Python 3.13.9
Заработало — но конфигурацию пришлось знать и вписать вручную. Для сложного образа это десятки строк, и любая забытая переменная проявится позже как непонятная ошибка.
Когда export/import уместны
Три реальных сценария:
1. Схлопывание слоёв. Образ с 60 слоями упирается в ограничения или просто неудобен. export/import даёт один слой. Цена — потеря истории и конфигурации; последнюю восстанавливают через --change.
2. Снятие среза состояния. Нужна файловая система container в конкретный момент — для анализа инцидента или отладки.
3. Создание образа из готовой файловой системы. Есть tar-архив корневой файловой системы (например, от debootstrap), нужно превратить его в образ.
Во всех остальных случаях — save/load.
Перенос в изолированную среду
Полная процедура для машины без доступа к внешней сети.
На машине с доступом:
cd /tmp/transfer
IMAGES="python:3.13-slim alpine:3.21"
for img in $IMAGES; do docker pull -q "$img"; done
docker save $IMAGES | gzip > bundle.tar.gz
sha256sum bundle.tar.gz > bundle.tar.gz.sha256
# зафиксировать ожидаемые Image ID для последующей сверки
for img in $IMAGES; do
printf '%s %s\n' "$(docker image inspect "$img" --format '{{.Id}}')" "$img"
done > expected-ids.txt
ls -lh bundle.tar.gz bundle.tar.gz.sha256 expected-ids.txt
На изолированной машине:
# 1. Проверить целостность передачи
sha256sum -c bundle.tar.gz.sha256
# 2. Загрузить
docker load -i bundle.tar.gz
# 3. Сверить Image ID с ожидаемыми
while read -r expected img; do
actual="$(docker image inspect "$img" --format '{{.Id}}' 2>/dev/null)"
if [ "$actual" = "$expected" ]; then
echo " [+] $img"
else
echo " [!] $img — расхождение!"
echo " ожидалось: $expected"
echo " получено: ${actual:-образ отсутствует}"
fi
done < expected-ids.txt
bundle.tar.gz: OK
Loaded image: python:3.13-slim
Loaded image: alpine:3.21
[+] python:3.13-slim
[+] alpine:3.21
Две независимые проверки: контрольная сумма архива подтверждает целостность передачи, сверка Image ID — что развёрнуто именно то, что упаковывали.
Multi-platform экспорт
При containerd image store:
# docker pull принимает РОВНО один образ:
# `docker pull a b` отвечает «docker pull requires 1 argument»
for img in --platform linux/amd64 alpine:3.21; do docker pull -q "$img"; done
docker save --platform linux/amd64 -o alpine-amd64.tar alpine:3.21
ls -lh alpine-amd64.tar
Без --platform сохраняются все локально доступные платформы. Проверить, что внутри:
tar -xOf alpine-amd64.tar index.json | python3 -m json.tool | head -20
Уборка
cd /tmp
docker rm -f exp-demo 2>/dev/null || true
docker rmi flat:1 flat:2 leaky:1 2>/dev/null || true
rm -rf /tmp/transfer
Практическое упражнение
Задание. Напишите пару скриптов для переноса образов в изолированную среду: pack-images.sh и unpack-images.sh.
pack-images.sh <image>... должен:
- Убедиться, что все образы есть локально (загрузить недостающие).
- Сохранить их одним сжатым архивом.
- Создать манифест переноса: имя образа, Image ID, digest, размер.
- Посчитать контрольную сумму архива.
- Вывести размер результата и инструкцию для принимающей стороны.
unpack-images.sh <архив> должен:
- Проверить контрольную сумму.
- Загрузить образы.
- Сверить фактические Image ID с манифестом.
- Вернуть ненулевой exit code при любом расхождении.
Подсказки
Подсказка 1
docker save принимает несколько образов сразу — отдельные архивы не нужны.
Подсказка 2
Манифест удобно хранить рядом с архивом в JSON. Читать его в скрипте через python3 -c проще, чем разбирать текст в awk.
Подсказка 3
Локально собранный образ не имеет RepoDigests (урок 3.3). Обрабатывайте пустое значение, а не считайте это ошибкой.
Решение
Сначала выполните задание самостоятельно.
Показать решение
pack-images.sh
#!/usr/bin/env bash
# pack-images.sh — упаковка образов для переноса в изолированную среду.
set -euo pipefail
[ $# -ge 1 ] || { echo "Использование: $0 <image> [image...]" >&2; exit 2; }
STAMP="$(date +%Y%m%d-%H%M%S)"
ARCHIVE="images-${STAMP}.tar.gz"
MANIFEST="images-${STAMP}.manifest.json"
echo "=== 1. Проверка наличия образов ==="
for img in "$@"; do
if docker image inspect "$img" > /dev/null 2>&1; then
echo " [+] $img (локально)"
else
echo " [~] $img — загружаю..."
docker pull -q "$img" > /dev/null
echo " [+] $img (загружен)"
fi
done
echo
echo "=== 2. Манифест переноса ==="
{
echo '{'
echo " \"created\": \"$(date -Iseconds)\","
echo " \"archive\": \"$ARCHIVE\","
echo ' "images": ['
first=1
for img in "$@"; do
id="$(docker image inspect "$img" --format '{{.Id}}')"
digest="$(docker image inspect "$img" \
--format '{{if .RepoDigests}}{{index .RepoDigests 0}}{{end}}')"
size="$(docker image inspect "$img" --format '{{.Size}}')"
[ $first -eq 0 ] && echo ','
first=0
printf ' {"name": "%s", "id": "%s", "digest": "%s", "size": %s}' \
"$img" "$id" "${digest:-}" "$size"
done
echo
echo ' ]'
echo '}'
} > "$MANIFEST"
python3 -c "
import json
d = json.load(open('$MANIFEST'))
for i in d['images']:
print(f\" {i['name']:<34} {i['size']/10**6:8.1f} MB\")
print(f\" {'ИТОГО (до дедупликации)':<34} {sum(i['size'] for i in d['images'])/10**6:8.1f} MB\")
"
echo
echo "=== 3. Упаковка ==="
docker save "$@" | gzip > "$ARCHIVE"
sha256sum "$ARCHIVE" > "$ARCHIVE.sha256"
echo " архив: $ARCHIVE ($(du -h "$ARCHIVE" | cut -f1))"
echo " сумма: $ARCHIVE.sha256"
echo " манифест: $MANIFEST"
echo
echo "=== 4. Передайте на целевую машину три файла ==="
echo " $ARCHIVE"
echo " $ARCHIVE.sha256"
echo " $MANIFEST"
echo
echo " Затем выполните: ./unpack-images.sh $ARCHIVE"
unpack-images.sh
#!/usr/bin/env bash
# unpack-images.sh — загрузка и проверка перенесённых образов.
set -uo pipefail
ARCHIVE="${1:?Использование: $0 <архив.tar.gz>}"
MANIFEST="${ARCHIVE%.tar.gz}.manifest.json"
[ -r "$ARCHIVE" ] || { echo "Архив не найден: $ARCHIVE" >&2; exit 2; }
echo "=== 1. Проверка целостности архива ==="
if [ -r "$ARCHIVE.sha256" ]; then
if sha256sum -c "$ARCHIVE.sha256"; then
echo " [+] контрольная сумма совпала"
else
echo " [!] АРХИВ ПОВРЕЖДЁН — загрузка отменена" >&2
exit 1
fi
else
echo " [~] файл контрольной суммы отсутствует, проверка пропущена"
fi
echo
echo "=== 2. Загрузка образов ==="
docker load -i "$ARCHIVE"
echo
echo "=== 3. Сверка с манифестом ==="
if [ ! -r "$MANIFEST" ]; then
echo " [~] манифест отсутствует, сверка невозможна"
exit 0
fi
mismatch=0
while IFS=$'\t' read -r name expected_id; do
actual_id="$(docker image inspect "$name" --format '{{.Id}}' 2>/dev/null)"
if [ -z "$actual_id" ]; then
printf ' [!] %-34s образ отсутствует\n' "$name"
mismatch=$((mismatch + 1))
elif [ "$actual_id" = "$expected_id" ]; then
printf ' [+] %-34s ID совпал\n' "$name"
else
printf ' [!] %-34s РАСХОЖДЕНИЕ\n' "$name"
printf ' ожидалось: %s\n' "$expected_id"
printf ' получено: %s\n' "$actual_id"
mismatch=$((mismatch + 1))
fi
done < <(python3 -c "
import json
for i in json.load(open('$MANIFEST'))['images']:
print(f\"{i['name']}\t{i['id']}\")
")
echo
if [ "$mismatch" -eq 0 ]; then
echo "Все образы перенесены без искажений."
exit 0
else
echo "Расхождений: $mismatch" >&2
exit 1
fi
Проверка:
chmod +x pack-images.sh unpack-images.sh
./pack-images.sh alpine:3.21 python:3.13-slim
# имитация целевой машины
docker rmi alpine:3.21 python:3.13-slim > /dev/null
./unpack-images.sh images-*.tar.gz; echo "exit code: $?"
Ожидаемый вывод:
=== 1. Проверка наличия образов ===
[+] alpine:3.21 (локально)
[+] python:3.13-slim (локально)
=== 2. Манифест переноса ===
alpine:3.21 8.3 MB
python:3.13-slim 126.4 MB
ИТОГО (до дедупликации) 134.7 MB
=== 3. Упаковка ===
архив: images-20260729-164512.tar.gz (48M)
сумма: images-20260729-164512.tar.gz.sha256
манифест: images-20260729-164512.manifest.json
...
=== 1. Проверка целостности архива ===
images-20260729-164512.tar.gz: OK
[+] контрольная сумма совпала
=== 2. Загрузка образов ===
Loaded image: alpine:3.21
Loaded image: python:3.13-slim
=== 3. Сверка с манифестом ===
[+] alpine:3.21 ID совпал
[+] python:3.13-slim ID совпал
Все образы перенесены без искажений.
exit code: 0
Почему две проверки, а не одна. Контрольная сумма архива подтверждает, что файл дошёл без повреждений при копировании. Сверка Image ID подтверждает, что после распаковки в системе оказались именно те образы, которые упаковывали. Это разные утверждения: архив мог быть подменён целиком вместе с суммой, а сверка ID укажет на расхождение с независимо зафиксированным манифестом.
Ненулевой exit code делает unpack-images.sh пригодным для автоматического развёртывания: шаг упадёт, а не продолжится с неверными образами.
Проверка результата
docker pull -q alpine:3.21
ID_BEFORE=$(docker image inspect alpine:3.21 --format '{{.Id}}')
docker save alpine:3.21 | gzip > /tmp/a.tar.gz
docker rmi alpine:3.21 > /dev/null
docker load -i /tmp/a.tar.gz
ID_AFTER=$(docker image inspect alpine:3.21 --format '{{.Id}}')
[ "$ID_BEFORE" = "$ID_AFTER" ] && echo "перенос без искажений"
rm /tmp/a.tar.gz
Типичные ошибки
| Ошибка | Причина | Исправление |
|---|---|---|
export/import для переноса образа | Названия кажутся подходящими | Использовать save/load; import теряет CMD, ENV, историю |
No command specified после import | Конфигурация не хранится в файловой системе | Задать через --change или использовать save/load |
Ожидание, что export сохранит данные volumes | Кажется, что «выгружается всё» | Volumes экспортируются отдельно — раздел 07 |
docker save без -o в терминал | Архив идёт в stdout | Использовать -o или перенаправление в файл |
| Передача несжатого архива | Слои внутри хранятся распакованными | gzip уменьшает объём примерно втрое |
| Отсутствие проверки после переноса | Считают, что копирование надёжно | Сверять контрольную сумму и Image ID |
Мнение, что удалённый в Dockerfile секрет недоступен | Из merged он исчез | Извлекается из слоя обычным tar |
| Экспорт всех платформ при переносе на одну | В Engine 29 это поведение по умолчанию | Указывать --platform явно |
Контрольные вопросы
На понимание:
- Чем
docker saveотличается отdocker exportпо объекту работы? - Почему
importтеряетCMDиENV, аload— нет? - Почему архив
docker saveхорошо сжимается? - Почему совпадение Image ID подтверждает идентичность переноса?
- Почему данные volumes не попадают ни в
save, ни вexport?
На применение:
- Как перенести три образа на машину без доступа к сети одной командой?
- Как посмотреть, какие образы находятся внутри архива, не загружая его?
- Как задать
CMDдля образа, создаваемого черезdocker import?
На диагностику:
- После
docker loadобраз появился, но с тегом<none>. Что произошло? - Коллега прислал образ, «в котором точно нет паролей, потому что мы их удаляем в
Dockerfile». Как проверить это утверждение?
Краткое резюме
save/loadработают с образом,export/import— с файловой системой container.save/loadсохраняют слои, конфигурацию, историю и теги — образ восстанавливается идентичным.export/importсхлопывают всё в один слой и теряютCMD,ENTRYPOINT,ENV,USER, историю.- Для переноса образов почти всегда нужны
save/load. - Данные volumes не входят ни в один из вариантов.
- Архив
save— это OCI Image Layout:index.jsonплюс blob по хэшам. - Формат самопроверяем: имя каждого blob равно хэшу его содержимого.
- Сжатие даёт выигрыш примерно втрое;
docker loadраспознаёт сжатие автоматически. - Из слоя можно извлечь любой файл обычным
tar, включая «удалённые» секреты. - В Docker Engine 29
docker saveподдерживает--platform; без него сохраняются все локальные платформы.
Официальные источники
| Источник | Ссылка | Что подтверждает |
|---|---|---|
| docker save reference | https://docs.docker.com/reference/cli/docker/image/save/ | Экспорт образа со слоями и метаданными, флаги -o и --platform |
| docker load reference | https://docs.docker.com/reference/cli/docker/image/load/ | Импорт архива, автоматическое распознавание сжатия |
| docker export reference | https://docs.docker.com/reference/cli/docker/container/export/ | Экспорт файловой системы container, отсутствие volumes в архиве |
| docker import reference | https://docs.docker.com/reference/cli/docker/image/import/ | Создание образа из tar, флаг --change для восстановления конфигурации |
| OCI Image Layout Specification | https://github.com/opencontainers/image-spec/blob/main/image-layout.md | Структура архива: oci-layout, index.json, blobs/sha256/ |
| Docker Engine 29 release notes | https://docs.docker.com/engine/release-notes/29/ | Поддержка --platform в docker save и docker load |
| Build secrets | https://docs.docker.com/build/building/secrets/ | Почему удаление секрета следующей инструкцией не работает |
Навигация
← Предыдущий материал
Вернуться к разделу
Следующий материал → Практические задания
Главное оглавление