Главная/Работа с Images/Урок

3.5. Save, load, export, import

Цели

После этого материала вы сможете:

  • перенести образ между машинами без registry и подтвердить целостность переноса;
  • объяснить разницу между save/load и export/import на уровне того, что попадает в архив;
  • выбрать подходящую пару команд под задачу;
  • заглянуть внутрь архива образа и найти там конкретный слой;
  • извлечь файл из слоя — и понять, почему это важно для безопасности;
  • работать с multi-platform образами при экспорте в Docker Engine 29.

Предварительные знания

Ключевые термины

ТерминОбъяснение
docker saveЭкспорт образа со всеми слоями, историей и метаданными в tar-архив
docker loadИмпорт архива, созданного docker save
docker exportЭкспорт файловой системы container одним плоским слоем
docker importСоздание образа из плоского tar-архива файловой системы
flattenСхлопывание всех слоёв в один
air-gappedСреда без доступа к внешней сети

Теория

Две принципиально разные операции

Названия команд похожи, и это сбивает с толку. На деле они работают с разными объектами.

text
   docker save / load                    docker export / import
   ───────────────────                   ──────────────────────
   объект: ОБРАЗ                         объект: CONTAINER

   ┌──────────────────┐                  ┌──────────────────┐
   │ manifest         │                  │                  │
   │ config           │                  │  плоский срез    │
   │ ├─ Cmd           │                  │  файловой        │
   │ ├─ Env           │                  │  системы         │
   │ ├─ User          │                  │                  │
   │ └─ history       │                  │  (слои образа +  │
   │ слой 1           │                  │   writable layer,│
   │ слой 2           │                  │   объединённые)  │
   │ слой 3           │                  │                  │
   │ теги             │                  │                  │
   └──────────────────┘                  └──────────────────┘

   после load:                           после import:
   образ идентичен исходному             образ БЕЗ Cmd, Env, User,
                                         истории и слоёв — один слой

Сравнение

save / loadexport / import
Что экспортируетсяОбразФайловая система container
СлоиСохраняются всеСхлопываются в один
История сборкиСохраняетсяТеряется
CMD, ENTRYPOINTСохраняютсяТеряются
ENV, USER, WORKDIRСохраняютсяТеряются
ТегиСохраняютсяЗадаются при импорте
Несколько образов в одном архивеДаНет
Размер архиваБольше (все слои)Меньше (дедупликация внутри)
Данные volumesНе входятНе входят
Типичное применениеПеренос образаСнятие среза, схлопывание слоёв

Последняя строка первой колонки — главное. save/load — это то, что нужно почти всегда.

Почему import теряет конфигурацию

docker export выгружает только файлы. Конфигурация образа (Cmd, Env, User) хранится не в файловой системе, а в отдельном JSON-документе (урок 3.1). Плоский tar-архив файлов её не содержит и содержать не может.

Практическое последствие:

bash
docker export mycontainer | docker import - myimage:1
docker run myimage:1
text
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:

bash
docker save --platform linux/amd64 -o image.tar myapp:1.0

Без флага сохраняются все платформы, доступные локально. Это отличается от прежнего поведения, где сохранялась только текущая платформа.

Практическое следствие: при переносе на машину той же архитектуры указывайте --platform явно — архив будет меньше в разы.


Внутренний механизм

Что внутри архива docker save

Структура соответствует OCI Image Layout:

text
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 может занять три секунды, если базовые слои уже есть от других образов.


Команды и примеры

Базовый перенос образа

bash
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
text
-rw------- 1 evg evg 128M Jul 29 16:20 python-slim.tar

Разбор: -o задаёт выходной файл. Без него архив пойдёт в stdout — это удобно для конвейеров, но при выводе в терминал даст поток двоичного мусора. Docker в интерактивном режиме откажется это делать.

Теперь удалим образ и восстановим:

bash
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 совпал — образ идентичен"
text
Loaded image: python:3.13-slim
до:    sha256:c2f1e0d9b8a7c6d5e4f3a2b1908877665544332211aabbccddeeff0011223344
после: sha256:c2f1e0d9b8a7c6d5e4f3a2b1908877665544332211aabbccddeeff0011223344
Image ID совпал — образ идентичен

Совпадение Image ID — доказательство идентичности: это хэш конфигурации, а конфигурация ссылается на все слои.

Сжатие архива

bash
docker save python:3.13-slim | gzip > python-slim.tar.gz
ls -lh python-slim.tar python-slim.tar.gz
text
-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

Сжатие даёт выигрыш почти втрое: слои внутри архива хранятся распакованными.

Загрузка сжатого архива:

bash
docker load -i python-slim.tar.gz

docker load определяет сжатие автоматически — распаковывать вручную не нужно. Поддерживаются gzip, bzip2, xz, zstd.

Прямая передача на другую машину без промежуточного файла:

bash
docker save myapp:1.0 | gzip | ssh user@remote 'gunzip | docker load'

Одна команда, ничего не остаётся на диске ни на одной стороне.

Несколько образов в одном архиве

bash
# 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
text
-rw------- 1 evg evg 232M Jul 29 16:25 bundle.tar

Проверим содержимое, не распаковывая:

bash
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 ['<без тега>']))
"
text
  python:3.13-slim
  alpine:3.21
  debian:trixie-slim

Загрузка восстановит все три:

bash
docker load -i bundle.tar

Заглянуть внутрь архива

bash
mkdir -p unpacked && tar -xf python-slim.tar -C unpacked
find unpacked -maxdepth 2 -type f | head -6
text
unpacked/oci-layout
unpacked/index.json
unpacked/manifest.json
unpacked/blobs/sha256/1a2b3c4d5e6f...
unpacked/blobs/sha256/7a8b9c0d1e2f...

Все объекты — в blobs/sha256/ под именами, равными их хэшам. Проверим это:

bash
BLOB="$(find unpacked/blobs/sha256 -type f | head -1)"
echo "имя файла:  $(basename "$BLOB")"
echo "sha256:     $(sha256sum "$BLOB" | cut -d' ' -f1)"
text
имя файла:  1a2b3c4d5e6f7890abcdef1234567890abcdef1234567890abcdef1234567890
sha256:     1a2b3c4d5e6f7890abcdef1234567890abcdef1234567890abcdef1234567890

Совпадает. Content-addressable storage в чистом виде — самопроверяемый формат.

Найдём конфигурацию и прочитаем её:

bash
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
text
Cmd: ['python3']
Env: 4 переменных
слоёв: 3

Вся конфигурация образа доступна из архива без Docker — это обычные JSON-файлы.

Извлечение файла из слоя

Практический навык, полезный и для диагностики, и для проверки безопасности.

bash
# найти слои и их размеры
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

Посмотреть содержимое конкретного слоя:

bash
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
text
bin/
bin/bash
bin/cat
bin/chgrp
bin/chmod
etc/
etc/apt/
...

Это содержимое базового слоя — обычный tar-архив.

Почему это важно для безопасности. Тот же приём извлекает секрет, «удалённый» последующей инструкцией Dockerfile (урок 3.2):

bash
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
text
=== в 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: что теряется

bash
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
text
-rw------- 1 evg evg 126M Jul 29 16:40 container.tar
sha256:9a8b7c6d5e4f3a2b190877665544332211aabbccddeeff00112233445566

Сравним:

bash
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)"
text
=== слоёв ===
исходный образ: 3
после import:   1

=== конфигурация ===
исходный Cmd: [python3]
flat Cmd:     []
исходный Env: 4 переменных
flat Env:     1 переменных

=== история ===
исходный: 12 записей
flat:     1 записей

Три слоя схлопнулись в один, Cmd пуст, история потеряна.

Попытка запустить:

bash
# head -1, а не tail: Docker 29 после ошибки печатает пустую строку
# и подсказку «Run 'docker run --help'» — tail забрал бы именно их
docker run --rm flat:1 2>&1 | head -1
text
docker: Error response from daemon: No command specified

Но данные container сохранились:

bash
docker run --rm flat:1 cat /container-data.txt
text
данные container

Конфигурацию можно восстановить вручную при импорте:

bash
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
text
Python 3.13.9

Заработало — но конфигурацию пришлось знать и вписать вручную. Для сложного образа это десятки строк, и любая забытая переменная проявится позже как непонятная ошибка.

Когда export/import уместны

Три реальных сценария:

1. Схлопывание слоёв. Образ с 60 слоями упирается в ограничения или просто неудобен. export/import даёт один слой. Цена — потеря истории и конфигурации; последнюю восстанавливают через --change.

2. Снятие среза состояния. Нужна файловая система container в конкретный момент — для анализа инцидента или отладки.

3. Создание образа из готовой файловой системы. Есть tar-архив корневой файловой системы (например, от debootstrap), нужно превратить его в образ.

Во всех остальных случаях — save/load.

Перенос в изолированную среду

Полная процедура для машины без доступа к внешней сети.

На машине с доступом:

bash
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

На изолированной машине:

bash
# 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
text
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:

bash
# 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 сохраняются все локально доступные платформы. Проверить, что внутри:

bash
tar -xOf alpine-amd64.tar index.json | python3 -m json.tool | head -20

Уборка

bash
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>... должен:

  1. Убедиться, что все образы есть локально (загрузить недостающие).
  2. Сохранить их одним сжатым архивом.
  3. Создать манифест переноса: имя образа, Image ID, digest, размер.
  4. Посчитать контрольную сумму архива.
  5. Вывести размер результата и инструкцию для принимающей стороны.

unpack-images.sh <архив> должен:

  1. Проверить контрольную сумму.
  2. Загрузить образы.
  3. Сверить фактические Image ID с манифестом.
  4. Вернуть ненулевой exit code при любом расхождении.

Подсказки

Подсказка 1

docker save принимает несколько образов сразу — отдельные архивы не нужны.

Подсказка 2

Манифест удобно хранить рядом с архивом в JSON. Читать его в скрипте через python3 -c проще, чем разбирать текст в awk.

Подсказка 3

Локально собранный образ не имеет RepoDigests (урок 3.3). Обрабатывайте пустое значение, а не считайте это ошибкой.

Решение

Сначала выполните задание самостоятельно.

Показать решение

pack-images.sh

bash
#!/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

bash
#!/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

Проверка:

bash
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: $?"

Ожидаемый вывод:

text
=== 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 пригодным для автоматического развёртывания: шаг упадёт, а не продолжится с неверными образами.

Проверка результата

bash
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 явно

Контрольные вопросы

На понимание:

  1. Чем docker save отличается от docker export по объекту работы?
  2. Почему import теряет CMD и ENV, а load — нет?
  3. Почему архив docker save хорошо сжимается?
  4. Почему совпадение Image ID подтверждает идентичность переноса?
  5. Почему данные volumes не попадают ни в save, ни в export?

На применение:

  1. Как перенести три образа на машину без доступа к сети одной командой?
  2. Как посмотреть, какие образы находятся внутри архива, не загружая его?
  3. Как задать CMD для образа, создаваемого через docker import?

На диагностику:

  1. После docker load образ появился, но с тегом <none>. Что произошло?
  2. Коллега прислал образ, «в котором точно нет паролей, потому что мы их удаляем в Dockerfile». Как проверить это утверждение?

Краткое резюме

  1. save/load работают с образом, export/import — с файловой системой container.
  2. save/load сохраняют слои, конфигурацию, историю и теги — образ восстанавливается идентичным.
  3. export/import схлопывают всё в один слой и теряют CMD, ENTRYPOINT, ENV, USER, историю.
  4. Для переноса образов почти всегда нужны save/load.
  5. Данные volumes не входят ни в один из вариантов.
  6. Архив save — это OCI Image Layout: index.json плюс blob по хэшам.
  7. Формат самопроверяем: имя каждого blob равно хэшу его содержимого.
  8. Сжатие даёт выигрыш примерно втрое; docker load распознаёт сжатие автоматически.
  9. Из слоя можно извлечь любой файл обычным tar, включая «удалённые» секреты.
  10. В Docker Engine 29 docker save поддерживает --platform; без него сохраняются все локальные платформы.

Официальные источники

ИсточникСсылкаЧто подтверждает
docker save referencehttps://docs.docker.com/reference/cli/docker/image/save/Экспорт образа со слоями и метаданными, флаги -o и --platform
docker load referencehttps://docs.docker.com/reference/cli/docker/image/load/Импорт архива, автоматическое распознавание сжатия
docker export referencehttps://docs.docker.com/reference/cli/docker/container/export/Экспорт файловой системы container, отсутствие volumes в архиве
docker import referencehttps://docs.docker.com/reference/cli/docker/image/import/Создание образа из tar, флаг --change для восстановления конфигурации
OCI Image Layout Specificationhttps://github.com/opencontainers/image-spec/blob/main/image-layout.mdСтруктура архива: oci-layout, index.json, blobs/sha256/
Docker Engine 29 release noteshttps://docs.docker.com/engine/release-notes/29/Поддержка --platform в docker save и docker load
Build secretshttps://docs.docker.com/build/building/secrets/Почему удаление секрета следующей инструкцией не работает

Навигация

← Предыдущий материал
Вернуться к разделу
Следующий материал → Практические задания
Главное оглавление

Markdown на GitHub ↗