폐쇄망 이미지 추출과 이전 (Save & Load)
납품 당일, 고객사 서버에서 docker pull이 실패하고 인터넷은 완전히 차단되어 있습니다.
이미지 파일을 미리 준비하지 않았다면 배포 일정 전체가 멈춥니다.
이때 필요한 것은 "명령어 암기"가 아니라 save/load와 export/import의 차이를 정확히 구분하는 판단입니다.
이 모듈은 오프라인 전달, 무결성 확인, 복원 검증까지 한 흐름으로 다룹니다.
금융권, 공공기관, 군 시스템 등 보안이 엄격한 환경은 인터넷과 완전히 차단된 **폐쇄망(Air-gapped Network)**에서 운영됩니다. 이런 환경에 Docker 이미지를 전달하려면 docker pull을 사용할 수 없고, 이미지를 파일로 추출해 물리적으로 전달해야 합니다. docker save와 docker load가 그 도구입니다.
인터넷이 차단된 폐쇄망 서버, CI 파이프라인 아티팩트 보관, 이미지 백업 등 실무에서 자주 마주치는 오프라인 이미지 이동 시나리오를 다룹니다.
- 1docker save / load — 이미지를 .tar 아카이브로 추출하고 다른 호스트에 복원
- 2docker export / import — 실행 중인 컨테이너 파일시스템을 스냅샷으로 추출
- 3save vs export의 차이 — 레이어 보존 여부와 사용 시나리오
- 4scp / rsync로 이미지 파일을 오프라인 환경으로 전송
- 5폐쇄망(air-gapped) 환경에 로컬 Docker Registry 구성하기
두 번째 호스트나 VM이 있으면 실제 전송 실습이 가능하지만, 없더라도 단일 머신에서 save → load 흐름은 모두 실습할 수 있습니다.
docker psdocker pull nginx:alpine && docker pull redis:7-alpinemkdir -p ~/image-transfer && cd ~/image-transferwhich scp단일 머신 실습 시에는 불필요
docker save와 docker load의 동작 원리
금융권 프로젝트 납품 당일, 현장 서버에서 docker pull myapp:2.0을 실행하면 "no such host: registry-1.docker.io"가 뜹니다. 납품 대상 서버는 인터넷이 완전히 차단된 폐쇄망이기 때문입니다. 사전에 이미지를 파일로 추출해두지 않았다면 납품 일정 전체가 위험해집니다. docker save는 이미지를 tar 아카이브로 추출하고, docker load는 그 파일을 다른 호스트에 그대로 복원합니다. 이 ConceptBlock에서는 save/load가 Docker 이미지 레이어 구조를 어떻게 보존하는지, 그리고 컨테이너 파일시스템만 추출하는 export/import와 무엇이 다른지를 다룹니다.

Docker 이미지의 레이어 구조
Docker 이미지는 여러 **레이어(Layer)**의 스택으로 구성됩니다. 각 레이어는 이전 레이어의 변경사항만 포함하고, 이 레이어들이 합쳐져 최종 파일 시스템을 만듭니다.
myapp:1.0 이미지 구조
├── 레이어 1: Ubuntu 22.04 베이스 (72MB)
├── 레이어 2: Node.js 18 설치 (120MB)
├── 레이어 3: package.json 복사 + npm install (45MB)
└── 레이어 4: 애플리케이션 소스 복사 (2MB)
→ 총 이미지 크기: 239MB
docker save의 동작
docker save는 이미지의 모든 레이어, 메타데이터, 태그 정보를 tar 파일로 패키징합니다.
# 실습 디렉토리 준비
mkdir -p /tmp/docker/part4/exam_16 && cd /tmp/docker/part4/exam_16
docker save -o myapp.tar myapp:1.0
생성되는 tar 파일 내부 구조:
myapp.tar
├── manifest.json # 이미지 구성 정보 (레이어 순서, 태그)
├── repositories # 태그 정보
├── [레이어1-hash]/
│ ├── layer.tar # 레이어 파일 시스템
│ └── json # 레이어 메타데이터
├── [레이어2-hash]/
│ ├── layer.tar
│ └── json
└── [레이어3-hash]/
├── layer.tar
└── json
docker load의 동작
docker load -i myapp.tar
tar 파일에서 레이어를 하나씩 Docker 로컬 레이어 캐시로 복원합니다. 동일한 레이어가 이미 존재하면 건너뜁니다.
docker load 실행 결과:
Loaded image: myapp:1.0
# 또는 여러 이미지 포함 시:
Loaded image: myapp:1.0
Loaded image: mydb:latest
save/load vs export/import 비교
| 특징 | save/load | export/import |
|---|---|---|
| 대상 | 이미지 | 컨테이너 파일 시스템 |
| 레이어 보존 | 완전 보존 | 단일 레이어로 평탄화 |
| 메타데이터 | CMD, ENV 등 보존 | 대부분 소실 |
| 용도 | 이미지 배포/이전 | 파일 시스템 스냅샷/백업 |
| 결과물 크기 | 더 큼 (레이어 개별 저장) | 더 작음 (중복 제거) |
# export: 실행 중이거나 중지된 컨테이너의 파일 시스템 추출
docker export -o container_fs.tar my_container
# import: tar를 이미지로 가져오기 (레이어 없음)
docker import container_fs.tar my_restored:latest
압축 파이프라인과 여러 이미지 묶기
마이크로서비스 6개를 폐쇄망 서버로 전달해야 하는데 각 이미지가 300MB씩이면 압축 없이 USB에 담으면 2GB가 넘습니다. gzip으로 압축하면 같은 이미지가 100MB 이하로 줄고, 여러 이미지를 하나의 파일로 묶으면 공유 레이어 중복이 제거돼 더 작아집니다. "이미지 6개를 각각 USB에 담아서 갔더니 공간이 부족했다"는 상황은 압축 파이프라인을 모를 때 생기는 전형적인 실수입니다. 이 ConceptBlock에서는 docker save 출력을 gzip으로 압축하는 파이프라인과 여러 이미지를 하나의 tar.gz로 묶는 방법을 다룹니다.

gzip 압축으로 크기 줄이기
Docker 이미지는 tar 형식으로 저장되는데, 이미지 레이어 내부의 파일들(텍스트, 코드, 설정 파일)은 gzip 압축률이 매우 좋습니다.
# 방법 1: -o 옵션으로 파일 저장 (압축 없음)
docker save -o myapp.tar myapp:1.0
ls -lh myapp.tar
# -rw-r--r-- 1 user user 239M myapp.tar
# 방법 2: 파이프라인으로 gzip 압축 (권장)
docker save myapp:1.0 | gzip > myapp.tar.gz
ls -lh myapp.tar.gz
# -rw-r--r-- 1 user user 87M myapp.tar.gz ← 63% 감소!
압축 수준 조정
# 기본 압축 (속도와 압축률 균형)
docker save myapp:1.0 | gzip > myapp.tar.gz
# 최고 압축률 (느리지만 더 작은 파일)
docker save myapp:1.0 | gzip -9 > myapp.tar.gz
# 빠른 압축 (크기보다 속도 우선)
docker save myapp:1.0 | gzip -1 > myapp.tar.gz
# 병렬 압축 (pigz 사용, 멀티코어 활용)
docker save myapp:1.0 | pigz > myapp.tar.gz
여러 이미지를 하나의 tar에 묶기
마이크로서비스 구성 전체를 한 번에 전달해야 할 때 여러 이미지를 하나의 파일로 묶습니다.
# 여러 이미지를 하나의 tar 파일로
docker save -o all_services.tar \
myapp-gateway:1.0 \
myapp-auth:1.0 \
myapp-catalog:1.0 \
myapp-cart:1.0 \
nginx:alpine \
postgres:14-alpine
# 압축 포함 버전
docker save \
myapp-gateway:1.0 \
myapp-auth:1.0 \
postgres:14-alpine \
| gzip > all_services.tar.gz
ls -lh all_services.tar.gz
# 여러 이미지가 레이어를 공유하므로 개별 압축의 합보다 작음
tar.gz에서 로드하기
# gzip 압축 해제 + load 파이프라인
gunzip -c myapp.tar.gz | docker load
# 또는
docker load -i <(gunzip -c myapp.tar.gz)
# 또는 (최신 docker는 gzip 자동 감지)
docker load -i myapp.tar.gz
이미지 목록을 파일로 관리하는 스크립트
#!/bin/bash
# save_images.sh — 배포할 이미지 목록 파일 기반 저장
IMAGES_FILE="images.txt"
OUTPUT="deploy_$(date +%Y%m%d).tar.gz"
# images.txt 내용 예시:
# myapp-gateway:1.2.0
# myapp-auth:1.2.0
# myapp-catalog:1.2.0
echo "이미지 저장 시작..."
docker save $(cat $IMAGES_FILE | tr '\n' ' ') | gzip > "$OUTPUT"
echo "저장 완료: $OUTPUT ($(du -sh $OUTPUT | cut -f1))"
이미지를 파일로 저장하고 복원하는 기본 흐름을 익힙니다.
1단계: 실습용 이미지 준비
실습 전 디렉토리와 예제 파일을 먼저 준비합니다.
# 실습 디렉토리 준비
mkdir -p /tmp/docker/part2/exam_5 && cd /tmp/docker/part2/exam_5
# 이미지 저장/로드 실습용 디렉토리 구조 생성
mkdir -p images backups
cat > README.txt << 'EOF'
docker save/load 실습 디렉토리
- images/: tar 형식으로 저장된 이미지 파일
- backups/: gzip 압축된 백업 이미지 파일
EOF
이제 실습을 진행합니다.
# 실습용 작은 이미지 pull
docker pull alpine:3.18
docker pull nginx:alpine
docker images | grep -E "alpine|nginx"
# alpine 3.18 abc123 ... 7.33MB
# nginx alpine def456 ... 23.5MB
2단계: 단일 이미지 저장 — gzip 압축으로 저장해 파일 크기를 줄입니다.
# tar 파일로 저장
docker save -o alpine_3.18.tar alpine:3.18
ls -lh alpine_3.18.tar
# 약 7-8MB
# 파이프라인으로 압축 저장
docker save alpine:3.18 | gzip > alpine_3.18.tar.gz
ls -lh alpine_3.18.tar.gz
# 약 3-4MB (압축 효과 확인)
3단계: 이미지 삭제 후 복원 테스트 — tar 파일로 이미지를 완전히 복원할 수 있는지 확인합니다.
# 이미지 삭제
docker rmi alpine:3.18
docker images | grep alpine
# (alpine:3.18 없음)
# tar 파일에서 복원
docker load -i alpine_3.18.tar
# Loaded image: alpine:3.18
# 복원 확인
docker images | grep alpine
# alpine 3.18 abc123 ... 7.33MB ← 완전히 복원됨!
4단계: gzip 파일 복원 — -i 옵션으로 압축된 tar 파일을 바로 로드합니다.
# alpine:3.18을 다시 삭제
docker rmi alpine:3.18
# gzip 파일에서 복원
gunzip -c alpine_3.18.tar.gz | docker load
# Loaded image: alpine:3.18
docker images | grep alpine
# 복원 확인
5단계: 정리 — 실습용 이미지와 tar 파일을 삭제합니다.
rm -f alpine_3.18.tar alpine_3.18.tar.gz
실제 폐쇄망 납품 시나리오처럼 여러 이미지를 하나의 파일로 관리합니다.
1단계: 여러 이미지 준비 — 묶어서 저장할 이미지들을 pull합니다.
docker pull alpine:3.18
docker pull redis:alpine
docker pull nginx:alpine
docker images | grep -E "alpine|redis|nginx"
2단계: 여러 이미지를 하나의 tar로 저장 — 여러 이미지를 한 파일에 묶어 전송 효율을 높입니다.
# 세 이미지를 하나의 파일로
docker save -o web_stack.tar \
alpine:3.18 \
redis:alpine \
nginx:alpine
ls -lh web_stack.tar
# 원본 세 이미지 크기의 합보다 작을 수 있음 (공유 레이어)
3단계: 압축 버전 생성 및 크기 비교 — gzip 압축으로 파일 크기를 줄이고 비교합니다.
docker save alpine:3.18 redis:alpine nginx:alpine \
| gzip > web_stack.tar.gz
ls -lh web_stack.tar web_stack.tar.gz
# web_stack.tar 47M
# web_stack.tar.gz 18M ← 약 62% 감소
4단계: 이미지 삭제 후 일괄 복원 — tar 파일 하나로 여러 이미지를 한 번에 복원합니다.
docker rmi alpine:3.18 redis:alpine nginx:alpine
# 복원 전 확인
docker images | grep -E "alpine|redis|nginx"
# (없음)
# 일괄 복원
docker load -i web_stack.tar
# Loaded image: alpine:3.18
# Loaded image: redis:alpine
# Loaded image: nginx:alpine
# 복원 확인
docker images | grep -E "alpine|redis|nginx"
# 세 이미지 모두 복원됨
5단계: 정리 — 실습용 tar 파일을 삭제합니다.
rm -f web_stack.tar web_stack.tar.gz
레이어 보존 여부를 통해 save/load와 export/import의 실질적 차이를 확인합니다.
1단계: 커스텀 이미지 생성 (레이어 포함) — 레이어가 있는 이미지를 만들어 비교 실습합니다.
# 레이어가 있는 간단한 커스텀 이미지 빌드
docker build -t custom-app:1.0 - <<'EOF'
FROM alpine:3.18
RUN echo "레이어 1: 패키지 설치 시뮬레이션" && mkdir /app
RUN echo "레이어 2: 설정 파일 추가" && echo "config=production" > /app/config.txt
ENV APP_VERSION=1.0
CMD ["sh", "-c", "echo APP_VERSION=$APP_VERSION; cat /app/config.txt"]
EOF
docker images custom-app
2단계: save로 저장 — 레이어 및 메타데이터 확인 — 레이어 구조와 메타데이터가 포함된 형태로 저장됩니다.
docker save -o custom_save.tar custom-app:1.0
# tar 내부 구조 확인 (레이어 디렉토리 존재)
tar -tf custom_save.tar | head -20
# manifest.json
# [레이어hash1]/
# [레이어hash1]/layer.tar
# [레이어hash2]/
# [레이어hash2]/layer.tar
# ...
# manifest.json 내용 확인 (태그, 레이어 정보)
tar -xOf custom_save.tar manifest.json
3단계: export로 저장 — 파일 시스템 스냅샷 — 레이어 없이 파일시스템만 추출합니다.
# export는 컨테이너 대상 (이미지 아님!)
docker run -d --name temp-container custom-app:1.0 sleep 30
docker export -o custom_export.tar temp-container
# tar 내부 구조 비교 (레이어 없이 파일 시스템 직접)
tar -tf custom_export.tar | head -20
# .dockerenv
# app/
# app/config.txt
# bin/
# etc/
# ... (파일 시스템 직접 나열)
4단계: 크기 비교 — save와 export 결과물의 크기 차이를 확인합니다.
ls -lh custom_save.tar custom_export.tar
# custom_save.tar 8.2M ← 레이어 메타데이터 포함
# custom_export.tar 5.9M ← 평탄화된 파일 시스템만
5단계: import로 복원 — 메타데이터 소실 확인 — import로 복원하면 CMD, ENTRYPOINT 등 메타데이터가 사라집니다.
docker import custom_export.tar custom-imported:1.0
# save/load로 복원한 것과 비교
docker rmi custom-app:1.0
docker load -i custom_save.tar
# CMD 확인 — save/load는 보존, import는 소실
docker inspect custom-app:1.0 --format '{{.Config.Cmd}}'
# [sh -c echo APP_VERSION=$APP_VERSION; cat /app/config.txt] ← 보존됨
docker inspect custom-imported:1.0 --format '{{.Config.Cmd}}'
# [] ← CMD 소실!
6단계: 정리 — 실습 환경을 정리합니다.
정리 명령어로 컨테이너/아티팩트를 강제 삭제하기 전에 확인
안전한 실행 조건: 실습 컨테이너이고, 디버깅에 필요한 로그/결과를 이미 수집한 경우에만 실행합니다.
실행 전 반드시 확인
- 삭제 대상 컨테이너 이름이 temp-container가 맞는가?
- 추가 확인이 필요한 로그/파일을 이미 저장했는가?
- 운영/공유 환경이 아닌 개인 실습 환경인가?
docker rm -f temp-container위 항목을 모두 확인한 후 복사할 수 있습니다
docker rm -f temp-container
docker rmi custom-app:1.0 custom-imported:1.0
rm -f custom_save.tar custom_export.tar
- save/load와 export/import의 차이를 레이어 보존 여부로 설명할 수 있는가?
- 오프라인 전달 후 load된 이미지 태그/메타데이터(CMD)가 기대대로 복원됐는지 확인했는가?
- 정리 명령 전에 필요한 산출물(tar, 로그, digest)을 보존했는가?
증상
docker load -i myapp.tar
# Loaded image ID: sha256:abc123def456...
docker images
# REPOSITORY TAG IMAGE ID CREATED
# <none> <none> abc123def456 2 days ago ← 이름 없음!
원인
이미지를 저장할 때 태그가 아닌 이미지 ID로 저장했거나, 이미지에 태그가 없었던 경우입니다.
# 잘못된 예 — ID로 저장
docker save -o myapp.tar abc123def456
# 태그 정보 없이 저장됨
# 올바른 예 — 태그로 저장
docker save -o myapp.tar myapp:1.0
해결 방법
# load 후 태그 추가
docker tag abc123def456 myapp:1.0
# 이제 정상 이름으로 사용 가능
docker images
# REPOSITORY TAG IMAGE ID
# myapp 1.0 abc123def456
예방 방법
# 저장 전 이미지 태그 확인
docker images myapp
# 반드시 태그(1.0, latest 등)가 있는지 확인
# 태그 명시적 지정 후 저장
docker tag myapp:latest myapp:1.2.3
docker save -o myapp_1.2.3.tar myapp:1.2.3
여러 이미지 포함 tar에서 내용 확인
# 로드 전 tar 파일에 어떤 이미지가 들어있는지 확인
tar -xOf myapp.tar manifest.json | python3 -m json.tool
# "RepoTags": ["myapp:1.0", "nginx:alpine"]
증상
USB나 SCP로 전달받은 tar 파일을 로드했을 때 오류가 발생합니다.
docker load -i myapp.tar
# open /var/lib/docker/tmp/xxx: unexpected EOF
# 또는
# Error response from daemon: invalid tar header
원인
파일 전송 과정에서 데이터가 손상됐습니다. USB 오류, SCP 전송 중 네트워크 단절, 디스크 불량 등이 원인일 수 있습니다.
진단 방법
# 1. 파일 크기 확인 (송신측과 비교)
ls -lh myapp.tar
# 송신측: 245MB, 수신측: 112MB → 전송 중 끊김!
# 2. tar 파일 무결성 검사
tar -tf myapp.tar > /dev/null
# tar: Unexpected EOF in archive → 손상 확인
# 3. MD5/SHA256 체크섬 비교
md5sum myapp.tar
# 송신측에서 기록한 값과 다르면 손상
송신측에서 체크섬 포함 전달하는 방법
# 이미지 저장 + 체크섬 파일 생성
docker save myapp:1.0 | gzip > myapp.tar.gz
sha256sum myapp.tar.gz > myapp.tar.gz.sha256
cat myapp.tar.gz.sha256
# abc123def456... myapp.tar.gz
# USB에 tar.gz와 sha256 파일 함께 전달
수신측에서 검증
# 체크섬 검증
sha256sum -c myapp.tar.gz.sha256
# myapp.tar.gz: OK ← 일치하면 안전
# myapp.tar.gz: FAILED ← 손상됨, 재전달 요청
# 검증 통과 후 로드
docker load -i myapp.tar.gz
SCP 전송 시 무결성 보장 방법
# rsync는 전송 후 자동으로 체크섬 검증
rsync -avz --checksum myapp.tar.gz user@target-server:/home/user/
폐쇄망 환경 납품은 국내 SI/솔루션 업계에서 매우 일반적인 상황입니다. 은행, 증권사, 정부기관, 군 시스템은 모두 외부 인터넷과 차단된 내부망에서 운영됩니다.
실제 납품 프로세스
[개발망/인터넷 연결 서버]
1. 이미지 빌드 및 태그 지정
docker build -t bankapp/gateway:2.1.0 .
docker build -t bankapp/auth:2.1.0 .
docker build -t bankapp/core:2.1.0 .
2. 이미지 검증 (취약점 스캔)
docker scout cves bankapp/gateway:2.1.0
trivy image bankapp/gateway:2.1.0
3. 이미지 묶음 파일 생성
docker save \
bankapp/gateway:2.1.0 \
bankapp/auth:2.1.0 \
bankapp/core:2.1.0 \
postgres:14-alpine \
redis:7-alpine \
| gzip > bankapp_v2.1.0_$(date +%Y%m%d).tar.gz
4. 체크섬 생성
sha256sum bankapp_v2.1.0_20240315.tar.gz > checksum.sha256
md5sum bankapp_v2.1.0_20240315.tar.gz >> checksum.sha256
5. 보안 매체에 복사
→ 암호화된 USB 또는 망분리 파일 전송 시스템 사용
폐쇄망 수신 서버에서의 설치 절차
#!/bin/bash
# deploy_airgapped.sh — 폐쇄망 배포 스크립트
set -e # 오류 시 즉시 중단
PACKAGE="bankapp_v2.1.0_20240315.tar.gz"
CHECKSUM_FILE="checksum.sha256"
echo "[1/4] 체크섬 검증..."
sha256sum -c "$CHECKSUM_FILE"
echo "✓ 파일 무결성 확인 완료"
echo "[2/4] 이미지 로드..."
docker load -i "$PACKAGE"
echo "[3/4] 로드된 이미지 확인..."
docker images | grep bankapp
echo "[4/4] 서비스 시작..."
docker-compose -f docker-compose.prod.yml up -d
echo "배포 완료!"
docker-compose ps
납품 시 자주 문제가 되는 사항들
# 문제 1: 기존 이미지와 버전 충돌
# → 태그 명명 규칙 엄수: [서비스명]/[컴포넌트]:[메이저.마이너.패치]
# 문제 2: 폐쇄망에 구버전 Docker가 설치되어 있음
# → docker version 확인 후 호환성 검증
docker version --format '{{.Server.Version}}'
# 20.10.x 이상 필요
# 문제 3: 아키텍처 불일치 (AMD64 이미지를 ARM 서버에 설치)
# → 빌드 시 --platform 명시
docker buildx build --platform linux/amd64 -t bankapp/gateway:2.1.0 .
# 이미지 아키텍처 확인
docker inspect bankapp/gateway:2.1.0 --format '{{.Architecture}}'
이미지 목록 관리 파일 (릴리즈 매니페스트)
# release_manifest.yaml — 납품 패키지 구성 정보
release:
version: "2.1.0"
date: "2024-03-15"
package: "bankapp_v2.1.0_20240315.tar.gz"
sha256: "abc123def456..."
images:
- name: bankapp/gateway
tag: "2.1.0"
digest: "sha256:..."
- name: bankapp/auth
tag: "2.1.0"
digest: "sha256:..."
- name: postgres
tag: "14-alpine"
source: "docker.io/library/postgres"
requirements:
docker: ">=20.10.0"
compose: ">=2.0.0"
disk_space: "5GB"
memory: "4GB"
폐쇄망 납품 경험은 국내 금융 SI, 공공 솔루션 업계에서 DevOps 엔지니어로 취업할 때 매우 높게 평가받는 역량입니다.
docker save vs docker export — 무엇이 다른가
docker save로 추출한 파일을 docker import로 복원하려 했는데 이미지가 뜨지 않습니다. CMD도 없고 ENV도 없고 빈 껍데기입니다. save와 export는 이름이 비슷하지만 전혀 다른 대상을 추출합니다. save는 이미지의 레이어 전체와 메타데이터(CMD, ENV, ENTRYPOINT)를 보존합니다. export는 실행 중인 컨테이너의 파일시스템 스냅샷만 가져오고 레이어 정보와 메타데이터는 사라집니다. 폐쇄망 이미지 배포에는 반드시 save/load 쌍을 써야 하고, export/import는 컨테이너 내부를 분석하거나 최소 이미지를 만드는 특수 용도에만 씁니다. 이 ConceptBlock에서는 두 명령어의 차이와 각각 어떤 상황에 써야 하는지를 다룹니다.

두 명령어의 근본적인 차이
docker save: 이미지 → tar
모든 레이어 + 메타데이터(CMD, ENV, ENTRYPOINT 등) + 태그 포함
복원 시 레이어 캐시 재사용 가능
docker export: 컨테이너의 현재 파일시스템 → tar
레이어가 병합된 단일 스냅샷
메타데이터 사라짐 (CMD, ENV, ENTRYPOINT 없음)
레이어 히스토리 없음
| 구분 | docker save | docker export |
|---|---|---|
| 대상 | 이미지 | 컨테이너 (실행 중 또는 정지) |
| 레이어 | 전체 보존 | 병합된 단일 레이어 |
| 메타데이터 | 포함 (CMD, ENV, 태그 등) | 없음 |
| 복원 명령 | docker load | docker import |
| 레이어 캐시 재사용 | 가능 | 불가 |
| 사용 사례 | 이미지 이전/배포 | 컨테이너 상태 스냅샷 |
docker save / load (이미지 이전에 사용)
# 이미지 → tar 파일로 저장
docker save nginx:alpine > nginx-alpine.tar
docker save nginx:alpine | gzip > nginx-alpine.tar.gz # 압축 저장
# 여러 이미지를 하나의 tar로 묶기
docker save nginx:alpine redis:7-alpine myapp:v1.0 | gzip > bundle.tar.gz
# tar 파일 → 이미지로 복원
docker load < nginx-alpine.tar
docker load < nginx-alpine.tar.gz # gzip 자동 감지
# 복원 확인
docker images
# REPOSITORY TAG IMAGE ID
# nginx alpine abc123 ← 태그까지 복원됨
# redis 7-alpine def456
# myapp v1.0 ghi789
docker export / import (특수한 경우에만 사용)
# 실행 중인 컨테이너의 파일시스템 스냅샷
docker export my-container > container-snapshot.tar
docker export my-container | gzip > snapshot.tar.gz
# 이미지로 import (단, 메타데이터는 직접 지정해야 함)
docker import snapshot.tar.gz myapp:snapshot
# CMD 지정 (import 후 실행 명령어가 없으므로 직접 설정)
docker import --change "CMD [\"node\", \"server.js\"]" \
snapshot.tar.gz myapp:snapshot
주의: docker import 후에는 docker history에 레이어 정보가 없고,
docker run 시 CMD가 없어 실행이 안 될 수 있습니다.
이미지 이전은 항상 docker save / load를 사용하세요.
에어갭(Air-gapped) 환경 이미지 배포 패턴
tar 파일을 USB에 담아 폐쇄망 서버로 가져갔는데 docker load가 "invalid tar header" 에러를 냅니다. USB 전송 중 파일이 손상된 것입니다. 무결성 검증 없이 전달했기 때문입니다. 폐쇄망 납품은 "한 번 들어가면 재전달이 어렵다"는 특성 때문에 파일 손상이나 아키텍처 불일치가 납품 실패로 이어집니다. 이 ConceptBlock에서는 에어갭 환경을 위한 완전한 이미지 번들 생성, 체크섬 검증, 내부 레지스트리 연동까지 실전 배포 절차를 다룹니다.

에어갭 환경이란?
인터넷이 완전히 차단된 망분리 환경입니다. 금융, 공공기관, 국방, 산업 제어 시스템 등에서 보안 요구사항으로 인터넷 연결을 차단합니다. 이런 환경에서 Docker 이미지를 배포하려면 docker save/load가 유일한 방법입니다.
전체 배포 절차
# === 1단계: 인터넷 연결 환경 (빌드/준비 서버) ===
# 필요한 모든 이미지 pull
docker pull nginx:1.25-alpine
docker pull redis:7.2-alpine
docker pull myapp:2.1.0
# 모든 이미지를 하나의 번들 파일로 묶기
docker save \
nginx:1.25-alpine \
redis:7.2-alpine \
myapp:2.1.0 \
| gzip > prod-images-$(date +%Y%m%d).tar.gz
# 파일 크기 확인
ls -lh prod-images-20260405.tar.gz
# -rw-r--r-- 1 root root 312M Apr 5 10:30 prod-images-20260405.tar.gz
# 무결성 체크섬 생성
sha256sum prod-images-20260405.tar.gz > prod-images-20260405.tar.gz.sha256
cat prod-images-20260405.tar.gz.sha256
# abc123...def456 prod-images-20260405.tar.gz
# === 2단계: 보안 매체로 전달 ===
# USB 드라이브, SCP, SFTP, 물리적 전달 등
# 체크섬 파일도 함께 전달
scp prod-images-20260405.tar.gz prod-images-20260405.tar.gz.sha256 \
admin@airgap-server:/tmp/
# === 3단계: 에어갭 환경 (타겟 서버) ===
# 무결성 검증
sha256sum -c prod-images-20260405.tar.gz.sha256
# prod-images-20260405.tar.gz: OK ← 변조 없음 확인
# 이미지 로드
docker load < /tmp/prod-images-20260405.tar.gz
# Loaded image: nginx:1.25-alpine
# Loaded image: redis:7.2-alpine
# Loaded image: myapp:2.1.0
# 로드 확인
docker images
# REPOSITORY TAG SIZE
# nginx 1.25-alpine 43MB
# redis 7.2-alpine 30MB
# myapp 2.1.0 185MB
# === 4단계: 내부 레지스트리에 push (선택) ===
# 에어갭 환경 내부에 사설 레지스트리가 있다면:
docker tag myapp:2.1.0 internal-registry.local:5000/myapp:2.1.0
docker push internal-registry.local:5000/myapp:2.1.0
이미지 정기 업데이트 운영
# 정기 업데이트 스크립트 (인터넷 연결 서버에서 실행)
#!/bin/bash
DATE=$(date +%Y%m%d)
IMAGES=(
"nginx:1.25-alpine"
"redis:7.2-alpine"
"myapp:$(cat VERSION)"
)
# 최신 이미지 pull
for img in "${IMAGES[@]}"; do
docker pull $img
done
# 번들 생성
docker save "${IMAGES[@]}" | gzip > prod-images-$DATE.tar.gz
sha256sum prod-images-$DATE.tar.gz > prod-images-$DATE.tar.gz.sha256
echo "Bundle ready: prod-images-$DATE.tar.gz ($(du -sh prod-images-$DATE.tar.gz | cut -f1))"
정리
docker save/load의 핵심 명령어를 요약합니다.
| 명령 | 설명 |
|---|---|
docker save -o file.tar image:tag | 이미지를 tar 파일로 저장 |
docker save img1 img2 | gzip > file.tar.gz | 여러 이미지를 압축하여 저장 |
docker load -i file.tar | tar 파일에서 이미지 복원 |
gunzip -c file.tar.gz | docker load | 압축 파일에서 이미지 복원 |
docker export -o file.tar container | 컨테이너 파일 시스템 추출 |
docker import file.tar image:tag | tar를 이미지로 임포트 (레이어 없음) |
폐쇄망 납품 시 항상 체크섬(sha256sum) 파일을 함께 전달해 파일 무결성을 검증받으세요. 그리고 save에는 반드시 이미지 ID가 아닌 이름:태그 형식을 사용해야 load 후 이름이 정상적으로 복원됩니다.