infra
Platform

모듈 맵

[Docker] 인터넷이 안 되는 환경에서 Docker 이미지를 이전하는 Save & Load 기법

0 / 27 완료

펼치기
0 / 27 완료0%

도커 & 컨테이너 · 16 / 27

[Docker] 인터넷이 안 되는 환경에서 Docker 이미지를 이전하는 Save & Load 기법

docker save/load로 인터넷이 없는 폐쇄망 서버에 이미지를 오프라인으로 전달합니다

폐쇄망 이미지 추출과 이전 (Save & Load)

🚨INCIDENT ALERT
HIGH

납품 당일, 고객사 서버에서 docker pull이 실패하고 인터넷은 완전히 차단되어 있습니다. 이미지 파일을 미리 준비하지 않았다면 배포 일정 전체가 멈춥니다. 이때 필요한 것은 "명령어 암기"가 아니라 save/load와 export/import의 차이를 정확히 구분하는 판단입니다. 이 모듈은 오프라인 전달, 무결성 확인, 복원 검증까지 한 흐름으로 다룹니다.

금융권, 공공기관, 군 시스템 등 보안이 엄격한 환경은 인터넷과 완전히 차단된 **폐쇄망(Air-gapped Network)**에서 운영됩니다. 이런 환경에 Docker 이미지를 전달하려면 docker pull을 사용할 수 없고, 이미지를 파일로 추출해 물리적으로 전달해야 합니다. docker savedocker load가 그 도구입니다.


이번 챕터에서 배울 것

인터넷이 차단된 폐쇄망 서버, CI 파이프라인 아티팩트 보관, 이미지 백업 등 실무에서 자주 마주치는 오프라인 이미지 이동 시나리오를 다룹니다.

  • 1docker save / load로 이미지를 .tar 아카이브로 추출하고 다른 호스트에 복원할 수 있다
  • 2docker export / import로 실행 중인 컨테이너 파일시스템을 스냅샷으로 추출할 수 있다
  • 3save와 export의 레이어 보존 여부와 사용 시나리오 차이를 구분할 수 있다
  • 4scp / rsync로 이미지 파일을 오프라인 환경으로 전송할 수 있다
  • 5폐쇄망(air-gapped) 환경에 로컬 Docker Registry를 구성할 수 있다
실습 환경 준비

두 번째 호스트나 VM이 있으면 실제 전송 실습이 가능하지만, 없더라도 단일 머신에서 save → load 흐름은 모두 실습할 수 있습니다.

Docker 실행 중인지 확인
docker ps
실습용 이미지 준비
docker pull nginx:alpine && docker pull redis:7-alpine
실습용 디렉토리 생성
mkdir -p ~/image-transfer && cd ~/image-transfer
scp 사용 가능 여부 확인 (원격 전송 실습용)
which 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 save와 docker load의 동작 원리 — 인터넷이 차단된 폐쇄망에서는 docker pull이 불가하므로, 인터넷 되는 곳에서 docker save로 이미지(레이어·메타데이터)를 tar 아카이브로 추출하고, 폐쇄망 서버에서 docker load로 그 tar를 이미지로 복원. 레지스트리 없이 이미지를 파일로 운반확대

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 파일 내부 구조:

docker save로 만든 myapp.tar의 내부 구조입니다.

  • manifest.json — 이미지 구성 정보 (레이어 순서, 태그)
  • repositories — 태그 정보
  • [레이어N-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/loadexport/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로 묶는 방법을 다룹니다.

압축 파이프라인과 여러 이미지 묶기 — docker save를 gzip에 파이프(docker save img | gzip > img.tar.gz)하면 300MB 이미지가 100MB 이하로 축소. 여러 이미지를 한 docker save로 묶으면 공유 베이스 레이어 중복이 제거돼 더 작아짐 — USB로 폐쇄망에 옮길 때 용량 절감확대

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))"

💡개념

이미지를 파일로 옮길 때 단계별로 무슨 일이 일어나나 — save·전송·load와 어디서 깨지나

폐쇄망 납품 당일 docker loadinvalid tar header를 뱉거나, 분명 올렸는데 docker imagesnone:none으로만 뜨는 일이 있습니다. save/load가 직렬화 → 전송 → 역직렬화라는 단계로 동작한다는 걸 알면, 이런 실패가 어느 칸에서 생겼는지 바로 좁힐 수 있습니다. 레지스트리 없이 이미지를 파일로 나르는 것은 아래 흐름의 결과입니다.

TEXT
[인터넷 환경]  docker save -o myapp.tar myapp:1.0
   │
   ① 직렬화 → 이미지의 모든 레이어 + config + manifest를 tar 하나로 묶음
   │     레지스트리에 뭐가 있든 상관없이 전부 담음 = 자기완결적(self-contained)
   │
   ② (선택) 압축·체크섬 → docker save | gzip 으로 크기 축소, sha256sum으로 체크섬 파일 생성
   │
   ③ 물리 전송 → USB·승인된 내부 경로로 폐쇄망에 반입
   │
   ④ docker load -i myapp.tar → tar를 역직렬화
   │     → 레이어를 다이제스트로 식별해 복원(이미 있으면 skip)
   │     → manifest의 RepoTags로 이름:태그 복원
   ▼
[폐쇄망]  docker images에 myapp:1.0 등장 (레지스트리 없이 배포)

각 단계에서 무슨 일이 일어나고, 어긋나면 어떤 증상인가:

단계하는 일여기서 어긋나면
① 직렬화(save)이름:태그를 지정해 레이어·메타데이터를 tar로 묶음이미지 ID로 저장하면 태그가 안 담겨 load 후 none:nonedocker tag로 수동 복구. save(이미지) 대신 export(컨테이너)면 CMD·ENV·레이어 히스토리 소실
② 압축·체크섬비압축 tar는 docker images SIZE보다 큼 · gzip으로 30~70% 축소압축 없이 옮기면 용량 과다(공통 base 레이어가 tar마다 중복). 체크섬 없이 옮기면 손상 검출 불가
③ 전송tar를 손상 없이 반입USB 불량·전송 끊김 → unexpected EOF · invalid tar header. 받는 쪽에서 sha256sum -c로 사전 검증
④ 역직렬화(load)레이어를 복원하고 RepoTags로 태그 복원아키텍처 불일치(amd64 tar를 arm 서버에서 실행) → exec format error. --platform으로 맞춰 빌드해 반입

load가 실패하거나 이미지가 이상할 때 증상이 곧 단계 지도입니다 — none:none은 저장 단계의 태그(①), invalid tar header는 전송 손상(③), exec format error는 아키텍처 불일치(④)를 가리킵니다. 그리고 save/load는 이미지(레이어·메타데이터)를, export/import는 컨테이너 파일시스템만 옮긴다는 구분이 모든 판단의 출발점입니다.


docker save와 load 기본 실습

이미지를 파일로 저장하고 복원하는 기본 흐름을 익힙니다.

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와 export의 차이점 직접 확인

레이어 보존 여부를 통해 save/load와 export/import의 실질적 차이를 확인합니다.

1단계: 커스텀 이미지 생성 (레이어 포함) — 레이어가 있는 이미지를 만들어 비교 실습합니다.

Docker
# 레이어가 있는 간단한 커스텀 이미지 빌드
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로 저장 — 파일 시스템 스냅샷 — 레이어 없이 파일시스템만 추출합니다.

Docker
# 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
🔍실행 후 확인할 것
  • 먼저 ls -lh *.tar 로 파일 크기를 확인한다 — docker save는 압축 없이 저장하므로 docker images 에서 보이는 SIZE보다 30~50% 더 크게 나온다. gzip으로 압축하면 일반적으로 50~70%까지 줄어든다. 폐쇄망 USB 전달 시 8GB 이미지가 압축 후 3~4GB가 되면 전달 시간이 크게 단축된다
  • docker load 후 docker images 에서 REPOSITORY와 TAG가 표시되면 정상 복원 — none:none으로 표시되면 태그가 소실된 것이다. 이 경우 docker tag <IMAGE_ID> myapp:1.0 으로 수동 태그를 달아야 서비스에서 참조할 수 있다
  • docker load 후 이미지 ID가 원본과 동일하면 → 무결성 확인 완료. ID가 다르면 전송 중 파일이 손상된 것이므로 md5sum 또는 sha256sum 으로 파일 체크섬을 비교해야 한다

증상

로컬 터미널
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/

심화 — save가 만드는 tar는 자기완결적이다

💡개념

심화: save는 공유 레이어도 통째로 담고, 증분이 없다

docker save를 '레지스트리 대신 쓰는 가벼운 이동 수단'으로만 보면, 반복 배포에서 전송량이 왜 안 줄어드는지 이해하지 못합니다. save가 만드는 tar가 실제로 무엇을 담는지 보면 그 한계가 분명해집니다.

  • save의 tar는 자기완결적(self-contained)이다: tar 안에는 manifest.json(이미지 구성)과 이미지 config, 그리고 레이어별 tar가 모두 들어갑니다. save는 대상 서버에 무엇이 있든 신경 쓰지 않고, 그 이미지를 실행하는 데 필요한 모든 레이어를 통째로 담습니다. 그래서 base가 같은 이미지 3개를 각각 save하면 공통 base 레이어가 세 tar에 중복으로 들어가 합계가 커집니다.
  • load는 다이제스트 기준으로 중복을 제거한다: 반대로 docker load는 레이어를 콘텐츠 다이제스트로 식별해, 이미 로컬에 있는 레이어는 다시 저장하지 않습니다. 그래서 load 후 docker images의 총량은 옮긴 tar들의 합보다 작을 수 있습니다 — 저장 단계에서 레이어가 공유되기 때문입니다.
  • save에는 증분(delta)이 없다: 코드 한 줄만 바뀐 새 버전이라도 save는 base를 포함한 전체를 다시 담습니다. --incremental 같은 옵션은 없습니다. 그래서 매일 배포하는 폐쇄망이라면 매번 수백 MB~GB를 통째로 옮기게 됩니다.

이 특성을 알면, 일회성 반입에는 save/load가 최적이지만 반복 배포에는 폐쇄망 내부에 레지스트리를 두는 편이 낫다는 판단이 자연스럽게 나옵니다.

상황: 폐쇄망 서버에 매일 새 버전을 반입합니다. 실제 바뀐 건 애플리케이션 코드 몇 줄뿐인데도, 배포 때마다 docker save로 만든 1.2GB tar 파일 전체를 USB로 복사해 옮기고 docker load합니다. 반입·검증에만 30분이 걸려 배포가 병목이 됐습니다.

원인: docker save에는 증분 전송 개념이 없습니다. 이미지가 base 레이어 1.1GB + 앱 레이어 50MB로 이뤄져 있어도, save는 매번 base까지 포함한 자기완결적 tar를 만듭니다. 바뀐 것은 상위 50MB뿐인데 매번 1.2GB를 통째로 옮기고 있었던 것입니다.

진단: docker history <image>로 레이어별 크기를 보면 base가 대부분을 차지하고 앱 레이어는 작다는 것이 드러납니다. 그런데도 tar 크기는 버전이 바뀌어도 거의 그대로라면, 증분 없이 전체가 반복 전송되고 있다는 신호입니다.

해결: 폐쇄망 내부에 프라이빗 레지스트리(registry 컨테이너 등)를 하나 둡니다. 레지스트리는 콘텐츠 주소로 레이어를 저장하므로, 최초에 base를 한 번 올려 두면 이후 docker push 때 이미 있는 공통 레이어는 Layer already exists로 건너뛰고 변경된 상위 레이어만 전송됩니다. 레지스트리 자체를 폐쇄망에 세우기 어려우면, skopeo copy로 레이어 단위 복사를 하거나 최소한 자주 바뀌는 앱과 거의 안 바뀌는 base를 별도 이미지로 나눠 base 반입 빈도를 줄입니다. docker export는 레이어를 평탄화해 증분을 오히려 없애므로 이 상황에선 반대 방향입니다.


💼
실무 맥락
현업 패턴

폐쇄망 환경 납품은 국내 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}}'

이미지 목록 관리 파일 (릴리즈 매니페스트)

YAML
# 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도 없고 빈 껍데기입니다. saveexport는 이름이 비슷하지만 전혀 다른 대상을 추출합니다. save는 이미지의 레이어 전체와 메타데이터(CMD, ENV, ENTRYPOINT)를 보존합니다. export는 실행 중인 컨테이너의 파일시스템 스냅샷만 가져오고 레이어 정보와 메타데이터는 사라집니다. 폐쇄망 이미지 배포에는 반드시 save/load 쌍을 써야 하고, export/import는 컨테이너 내부를 분석하거나 최소 이미지를 만드는 특수 용도에만 씁니다. 이 ConceptBlock에서는 두 명령어의 차이와 각각 어떤 상황에 써야 하는지를 다룹니다.

docker save vs docker export — 무엇이 다른가확대

두 명령어의 근본적인 차이

docker save:  이미지 → tar
              모든 레이어 + 메타데이터(CMD, ENV, ENTRYPOINT 등) + 태그 포함
              복원 시 레이어 캐시 재사용 가능

docker export: 컨테이너의 현재 파일시스템 → tar
               레이어가 병합된 단일 스냅샷
               메타데이터 사라짐 (CMD, ENV, ENTRYPOINT 없음)
               레이어 히스토리 없음
구분docker savedocker export
대상이미지컨테이너 (실행 중 또는 정지)
레이어전체 보존병합된 단일 레이어
메타데이터포함 (CMD, ENV, 태그 등)없음
복원 명령docker loaddocker 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에서는 에어갭 환경을 위한 완전한 이미지 번들 생성, 체크섬 검증, 내부 레지스트리 연동까지 실전 배포 절차를 다룹니다.

에어갭(Air-gapped) 환경 이미지 배포 패턴 — 폐쇄망 납품은 재전달이 어려워 파일 손상·아키텍처 불일치가 곧 납품 실패. tar 추출 후 sha256 체크섬으로 무결성을 검증하고, --platform으로 대상 아키텍처(amd64/arm64)를 맞추며, 이미지·체크섬·로드 스크립트를 한 번들로 묶어 전달확대

에어갭 환경이란?

인터넷이 완전히 차단된 망분리 환경입니다. 금융, 공공기관, 국방, 산업 제어 시스템 등에서 보안 요구사항으로 인터넷 연결을 차단합니다. 이런 환경에서 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
SSH 접속 후
# === 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.tartar 파일에서 이미지 복원
gunzip -c file.tar.gz | docker load압축 파일에서 이미지 복원
docker export -o file.tar container컨테이너 파일 시스템 추출
docker import file.tar image:tagtar를 이미지로 임포트 (레이어 없음)

폐쇄망 납품 시 항상 체크섬(sha256sum) 파일을 함께 전달해 파일 무결성을 검증받으세요. 그리고 save에는 반드시 이미지 ID가 아닌 이름:태그 형식을 사용해야 load 후 이름이 정상적으로 복원됩니다.


명령어·단축키 빠른 참조

폐쇄망 이미지 운반에서 실제로 쓴 저장·압축·복원·무결성 검증 명령을 모았습니다.

명령어/단축키용도자주 쓰는 예
docker save -o이미지를 tar로 저장(비압축)docker save -o myapp.tar myapp:1.0
docker save + gzip압축 저장으로 전송 용량 절감docker save myapp:1.0 | gzip -9 > myapp.tar.gz / | pigz(병렬)
docker save(다중)여러 이미지를 한 파일로 묶기docker save img1:1.0 img2:1.0 | gzip > bundle.tar.gz
docker loadtar/tar.gz에서 이미지 복원docker load -i bundle.tar.gz / gunzip -c f.tar.gz | docker load
docker export / import컨테이너 파일시스템 스냅샷(레이어 X)docker export -o fs.tar ctrdocker import fs.tar app:snap
sha256sum전송 무결성 체크섬 생성·검증sha256sum f.tar.gz > f.sha256 → 수신측 sha256sum -c f.sha256
tar -tf / -xOfload 전 tar 내용·매니페스트 확인tar -xOf myapp.tar manifest.json | python3 -m json.tool
docker tagload 후 none:none 태그 복구docker tag <IMAGE_ID> myapp:1.0
docker images복원된 이미지 이름·태그 확인docker images | grep -E "alpine|nginx"
docker inspect이미지 ID·아키텍처 검증docker inspect myapp:1.0 --format '{{.Architecture}}'
rsync --checksum전송 후 자동 무결성 검증 이전rsync -avz --checksum f.tar.gz user@host:/path/

관련 모듈로 더 깊이:

다음 모듈에서는 빌드 산출물과 최종 실행 환경을 분리해 이미지 크기를 획기적으로 줄이는 멀티 스테이지 빌드 기법을 다룹니다.

지식 확인

퀴즈 — 8문제

Q1

폐쇄망 서버에 myapp:v2.0 이미지를 전달해야 합니다. 전달 후 동일한 이미지 이름과 태그로 실행되어야 하고, CMD와 ENTRYPOINT 같은 메타데이터도 보존되어야 합니다. `docker save`와 `docker export` 중 어떤 명령을 사용해야 하나요?

Q2

1.2GB 이미지를 USB로 전달해야 하는데 USB 여유 공간이 800MB밖에 없습니다. `docker save myapp:1.0 > myapp.tar`로 저장하면 파일이 너무 큽니다. 어떻게 해결할 수 있나요?

Q3

폐쇄망 서버에 myapp:1.0과 mydb:latest 두 이미지를 함께 전달해야 합니다. 파일 수를 최소화해 USB 한 장으로 전달하려고 합니다. 어떤 명령을 사용해야 하나요?

Q4

폐쇄망 서버에서 `docker load -i myapp.tar`를 실행했습니다. 복원된 이미지의 이름과 태그를 확인해 `docker run`에 사용하려 합니다. 어떤 명령으로 확인하나요?

Q5

폐쇄망 환경에서 이미지를 전달할 때 save로 저장한 tar 파일과 함께 확인해야 할 중요한 정보는 무엇인가요?

Q6

인터넷이 차단된 폐쇄망 서버에 이미지를 올릴 때 docker pull 대신 docker load(save한 tar 사용)를 쓰는 이유는?

Q7

[심화] base 이미지가 같은 서비스 이미지 3개를 각각 docker save로 tar로 만들었더니, 세 tar 파일 크기의 합이 docker load 후 docker images가 보여주는 총 용량보다 훨씬 컸다. 왜인가?

Q8

[심화] 폐쇄망에 매일 새 버전을 배포하는데, 코드 몇 줄만 바뀌어도 매번 1.2GB tar 전체를 USB로 옮겨야 해 배포가 오래 걸린다. 전송량을 줄이는 가장 근본적인 방법은?

0 / 8 답변

🧪 실습으로 확인하기

Docker Compose 멀티 서비스 구성

초급

docker-compose.yml로 nginx + 앱 컨테이너를 함께 정의하고, 서비스 간 통신과 볼륨 마운트를 구성한다.

35📋 4단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요