infra
Platform

모듈 맵

[Docker] pull, build, tag, push 이미지 관리 라이프사이클

0 / 27 완료

펼치기
0 / 27 완료0%

Docker · 03 / 27

[Docker] pull, build, tag, push 이미지 관리 라이프사이클

Docker 이미지의 레이어 구조를 이해하고 pull, build, tag, push 워크플로우를 익힙니다

🚨INCIDENT ALERT
HIGH

새 기능을 급하게 배포했는데 스테이징 서버는 myapp:latest, 운영 서버는 myapp:v1.2.3을 실행하고 있었습니다. 둘 다 같은 앱이라고 생각했지만 실제 이미지 ID가 달라서 한쪽에서만 버그가 재현됐습니다.

이미지는 단순한 압축 파일이 아니라 레이어, 태그, 레지스트리 참조가 얽힌 배포 단위입니다. 어떤 이미지를 pull했고, 어떤 태그로 build했으며, 어디에 push했는지 추적하지 못하면 롤백도 재현도 어려워집니다.

이미지 다루기 — pull, build, tag, push

이미지는 Docker의 핵심입니다. "한 번 빌드하고, 어디서든 실행한다(Build once, run anywhere)"는 철학은 이미지가 있기에 가능합니다. 이 모듈에서는 이미지의 레이어 구조를 이해하고, pull로 내려받고, build로 만들고, tag로 이름을 붙이고, push로 배포하는 전체 워크플로우를 익힙니다.

이번 챕터에서 배울 것
  • 1이미지 레이어 구조 — 왜 이미지를 레이어로 쌓는가
  • 2docker pull — 레지스트리에서 이미지 가져오기
  • 3docker build — Dockerfile로 이미지 만들기
  • 4docker tag — 이미지에 버전과 이름 붙이기
  • 5docker push — 레지스트리에 이미지 올리기
  • 6이미지 관리 — inspect, history, prune
실습 환경 준비
로컬 이미지 목록 확인
docker images
Docker Hub 연결 상태 확인
docker pull hello-world
실습 디렉토리 생성
mkdir -p /tmp/docker-image-lab && cd /tmp/docker-image-lab
💡개념

이미지 레이어 구조 — Union File System

docker pull로 이미지를 내려받을 때 'Already exists'가 표시되면서 일부 레이어만 다운로드되는 걸 본 적이 있을 겁니다. 이미지 크기가 수백 MB인데 다운로드가 몇 초 만에 끝나는 경우도 있습니다. 이게 가능한 이유가 레이어 구조입니다. 레이어를 이해하면 왜 빌드 최적화에서 Dockerfile 명령 순서가 중요한지, 왜 COPY requirements.txt를 소스코드보다 먼저 하는지 이해할 수 있습니다. 이 ConceptBlock에서는 Docker 이미지의 레이어 구조와 Union File System이 어떻게 작동하는지 다룹니다.

이미지 레이어 구조와 레이어 공유

왜 레이어인가?

Docker 이미지는 여러 개의 읽기 전용 레이어를 쌓아 만든 구조입니다. 각 레이어는 이전 레이어에서 변경된 파일만 포함합니다. 이 방식을 Union File System(UnionFS)이라고 합니다.

레이어 구조의 이점:

nginx:alpine 이미지            myapp:v1.0 이미지
┌─────────────────┐            ┌─────────────────┐
│ nginx 설정 레이어 │  ←다름→   │ 앱 코드 레이어   │
├─────────────────┤            ├─────────────────┤
│ nginx 바이너리  │  ←다름→    │ 의존성 레이어    │
├─────────────────┤            ├─────────────────┤
│ Alpine Linux    │ ←공유→    │ Alpine Linux     │
└─────────────────┘            └─────────────────┘
         ↑                              ↑
    같은 레이어 ID — 디스크에 한 번만 저장됨

두 이미지가 Alpine Linux 레이어를 공유합니다. 이미지 10개가 모두 Alpine Linux를 기반으로 해도, Alpine 레이어는 디스크에 한 번만 저장됩니다. docker pull 시 이미 있는 레이어는 다운로드하지 않아 속도도 빠릅니다.

레이어 확인

이미지의 레이어 구성을 직접 확인하면 각 Dockerfile 명령이 얼마나 큰 레이어를 만드는지, 어떤 레이어가 다른 이미지와 공유되는지 볼 수 있습니다. 빌드 최적화의 출발점이 됩니다.

로컬 터미널
# 실습 디렉토리 준비
mkdir -p /tmp/docker/part1/exam_3 && cd /tmp/docker/part1/exam_3

# 이미지 레이어 히스토리 보기
docker history nginx:alpine

# 출력 예시:
# IMAGE          CREATED        CREATED BY                    SIZE
# a3b4c5d6e7f8   2 days ago     CMD ["nginx" "-g" "daemon…"   0B
# <missing>      2 days ago     EXPOSE map[80/tcp:{}]          0B
# <missing>      2 days ago     COPY 10-listen-on-ipv6-by…    0B
# <missing>      2 days ago     ENV NGINX_VERSION=1.25.3      0B
# <missing>      2 days ago     RUN /bin/sh -c set -x...      12.6MB
# <missing>      3 weeks ago    /bin/sh -c #(nop) ADD fil…   7.34MB  ← Alpine 기반

# 이미지 상세 정보 (레이어 SHA256 등)
docker inspect nginx:alpine | grep -A5 "Layers"
🔍실행 후 확인할 것
  • docker history nginx:alpine 출력에서 여러 레이어가 순서대로 보이는가?
  • 가장 아래쪽 레이어가 Alpine 기반 레이어로 표시되는가?
  • docker inspect 결과의 RootFS.Layers 배열에 SHA256 레이어 목록이 보이는가?
  • 이미지 크기와 레이어 개수가 이후 빌드 최적화 판단 기준이 된다는 점을 설명할 수 있는가?
💡개념

docker pull — 레지스트리에서 이미지 내려받기

이미지 이름에 콜론이 들어간 형태(nginx:alpine, python:3.11-slim)를 보면서 어느 부분이 이름이고 어느 부분이 버전인지 헷갈릴 수 있습니다. 더 복잡하게는 123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:v1.0처럼 URL 형태의 이미지 이름이 등장하기도 합니다. 이 이름 체계를 이해해야 레지스트리에서 원하는 이미지를 정확하게 가져오고, 직접 만든 이미지를 레지스트리에 올릴 때 올바른 형식으로 태그할 수 있습니다. 이 ConceptBlock에서는 이미지 이름 규칙과 태그별 특성, pull 명령의 실전 사용법을 다룹니다.

이미지 이름 구조 — 레지스트리/계정명/이름:태그

이미지 이름 규칙

Docker 이미지 이름은 레지스트리 주소, 계정명, 이미지명, 태그 네 부분으로 구성됩니다. 생략된 부분은 기본값으로 채워집니다.

[레지스트리주소/][계정명/]이미지명[:태그]

예시:
  nginx                          → Docker Hub, nginx 공식 이미지, :latest
  nginx:1.25-alpine              → nginx 버전 1.25, Alpine 변형
  ubuntu:22.04                   → ubuntu 22.04 LTS
  python:3.11-slim               → Python 3.11, slim 변형 (불필요한 패키지 제거)
  myusername/myapp:v1.2.3        → Docker Hub, 개인 저장소
  ghcr.io/myorg/myapp:latest     → GitHub Container Registry
  123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:v1  → AWS ECR

태그 선택 가이드:

태그마다 크기, 보안, 호환성 특성이 다릅니다. 프로덕션에서는 가변 태그(:latest)보다 버전 고정 태그를 씁니다.

태그특성용도
:latest최신 버전 (가변)개발 환경에서만
:1.25.3정확한 버전 고정프로덕션 권장
:alpineAlpine Linux 기반, 경량보안 중요 + 크기 중요
:slim불필요 패키지 제거중간 크기
:bookwormDebian bookworm 기반호환성 우선
로컬 터미널
# 기본 pull (latest 태그)
docker pull nginx

# 특정 버전 지정 (프로덕션 권장)
docker pull nginx:1.25.3-alpine

# 여러 이미지 한 번에 pull
docker pull python:3.11-slim
docker pull postgres:15-alpine
docker pull redis:7-alpine

# pull 후 이미지 목록 확인
docker images
# REPOSITORY   TAG              IMAGE ID       CREATED       SIZE
# nginx        1.25.3-alpine    a3b4c5d6e7f8   2 days ago    42.6MB
# python       3.11-slim        b4c5d6e7f8a9   1 week ago    129MB

# 이미지 삭제
docker rmi nginx:1.25.3-alpine

# 사용하지 않는 이미지 모두 삭제 (dangling images)
docker image prune

# 태그 없는 이미지 포함 모두 삭제 (주의: 신중히)
docker image prune -a
💡개념

docker build — Dockerfile로 이미지 만들기

서비스를 컨테이너로 배포하려면 결국 Dockerfile을 직접 작성해야 합니다. docker pull로 가져오는 공식 이미지는 서버나 런타임 환경이지, 내 애플리케이션이 담긴 이미지가 아닙니다. Dockerfile이 있어야 '내 코드 + 필요한 런타임 + 설정'을 하나의 이미지로 만들어 어느 서버에서든 동일하게 실행할 수 있습니다. 처음에는 Dockerfile 명령을 순서대로 나열하면 되지만, 빌드 속도를 결정하는 레이어 캐시 전략을 이해해야 실무에서 쓸 수 있는 Dockerfile을 작성할 수 있습니다. 이 ConceptBlock에서는 Dockerfile 작성 방법과 빌드 캐시 활용 전략을 다룹니다.

레이어 캐시 전략 — 의존성 먼저 복사해서 캐시 재사용

첫 번째 Dockerfile

Dockerfile은 이미지 빌드 레시피입니다. 각 명령이 하나의 레이어를 만듭니다.

로컬 터미널
# 실습 디렉토리 생성
mkdir -p /tmp/docker-image-lab && cd /tmp/docker-image-lab
Dockerfile
# Dockerfile (파일 이름 그대로 'Dockerfile')

# FROM: 베이스 이미지 지정 (항상 첫 번째 명령)
FROM python:3.11-slim

# WORKDIR: 컨테이너 내부 작업 디렉토리 설정
# 이후 명령들의 기본 경로
WORKDIR /app

# COPY: 호스트의 파일을 컨테이너 안으로 복사
# (빌드 컨텍스트 기준 경로)
COPY requirements.txt .

# RUN: 이미지 빌드 중 실행할 명령
# 새 레이어 생성 — 캐시 활용을 위해 변경 빈도 낮은 것을 앞에
RUN pip install --no-cache-dir -r requirements.txt

# 소스코드는 마지막에 복사 (코드 변경 시 pip 재설치 방지)
COPY . .

# EXPOSE: 문서화 목적 (실제 포트 바인딩은 docker run -p로)
EXPOSE 8000

# CMD: 컨테이너 시작 시 실행할 기본 명령
# exec 형식 (배열) 권장 — 신호를 직접 받을 수 있음
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

실습용 파일 만들기:

로컬 터미널
# requirements.txt 생성
cat > /tmp/docker-image-lab/requirements.txt << 'EOF'
fastapi==0.104.1
uvicorn==0.24.0
EOF

# 간단한 FastAPI 앱 생성
cat > /tmp/docker-image-lab/main.py << 'EOF'
from fastapi import FastAPI
app = FastAPI()

@app.get("/")
def read_root():
    return {"message": "Hello from Docker!", "version": "1.0"}

@app.get("/health")
def health_check():
    return {"status": "ok"}
EOF

# Dockerfile 생성
cat > /tmp/docker-image-lab/Dockerfile << 'EOF'
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
EOF

이미지 빌드:

로컬 터미널
cd /tmp/docker-image-lab

# 기본 빌드
docker build -t myapp:v1.0 .

# 빌드 진행 상황 확인 (각 단계가 레이어)
# Step 1/6 : FROM python:3.11-slim
# Step 2/6 : WORKDIR /app
# Step 3/6 : COPY requirements.txt .
# Step 4/6 : RUN pip install --no-cache-dir -r requirements.txt
# Step 5/6 : COPY . .
# Step 6/6 : CMD ["uvicorn", ...]

# 빌드 결과 확인
docker images myapp

# 실행 테스트
docker run -d --name test-app -p 8000:8000 myapp:v1.0
curl http://localhost:8000
# {"message":"Hello from Docker!","version":"1.0"}

curl http://localhost:8000/health
# {"status":"ok"}

# 정리
docker stop test-app && docker rm test-app

레이어 캐시 활용

Dockerfile 명령 순서 하나로 빌드 시간이 5초와 5분으로 갈립니다. 레이어가 변경되지 않으면 Docker는 이전 빌드의 캐시를 재사용합니다. COPY requirements.txt를 소스코드보다 먼저 두는 이유가 바로 이 캐시 전략 때문입니다.

로컬 터미널
# 소스코드만 변경 후 재빌드
echo 'print("updated")' >> /tmp/docker-image-lab/main.py
docker build -t myapp:v1.1 .

# 출력에서 캐시 활용 확인:
# Step 1/6 : FROM python:3.11-slim
#  ---> Using cache          ← 캐시 사용
# Step 2/6 : WORKDIR /app
#  ---> Using cache          ← 캐시 사용
# Step 3/6 : COPY requirements.txt .
#  ---> Using cache          ← requirements.txt 미변경
# Step 4/6 : RUN pip install ...
#  ---> Using cache          ← 의존성 재설치 안 함!
# Step 5/6 : COPY . .
#  ---> 새로 실행             ← 코드 변경됨

의존성(requirements.txt)을 소스코드보다 먼저 COPY하는 이유가 바로 이 캐시 전략입니다. 코드만 바꿀 때 pip install을 다시 실행하지 않아 빌드가 빠릅니다.

💡개념

docker tag와 docker push — 이미지 버전 관리와 배포

이미지를 빌드했는데 레지스트리에 어떻게 올리고 팀원들이 어떻게 내려받는지 모르면, 결국 docker save로 파일을 만들어 scp로 전송하는 방법을 쓰게 됩니다. 이미지를 팀과 공유하고 CI/CD 파이프라인에서 자동 배포하려면 tag와 push가 필수입니다. 특히 태그를 어떻게 붙이느냐가 운영 환경에서 어떤 버전이 돌고 있는지 추적하는 데 직결됩니다. :latest 태그만 쓰다가 '이 서버에 지금 뭐가 올라가 있지?'를 모르는 상황은 실제로 자주 발생합니다. 이 ConceptBlock에서는 이미지 버전 관리 전략과 레지스트리 push 방법을 다룹니다.

태그 전략 — 버전, Git SHA, 환경별 태그 관리

docker tag — 이미지에 이름 붙이기

docker tag는 이미지를 복사하지 않고 같은 레이어를 가리키는 새 이름(참조)을 추가합니다. 하나의 이미지를 레지스트리별로 다른 이름으로 push할 때 씁니다.

Docker
# 빌드 시 바로 태그 지정
docker build -t myapp:v1.0 .

# 기존 이미지에 추가 태그 부여
docker tag myapp:v1.0 myapp:latest
docker tag myapp:v1.0 myapp:stable

# Docker Hub에 push하기 위한 태그 (사용자명/이미지명 형식 필수)
docker tag myapp:v1.0 myusername/myapp:v1.0
docker tag myapp:v1.0 myusername/myapp:latest

# AWS ECR 형식
docker tag myapp:v1.0 123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:v1.0

# GitHub Container Registry 형식
docker tag myapp:v1.0 ghcr.io/myorg/myapp:v1.0

# 모든 태그 확인
docker images myapp
# REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
# myapp        v1.0      a3b4c5d6e7f8   5 minutes ago    145MB
# myapp        latest    a3b4c5d6e7f8   5 minutes ago    145MB  ← 같은 IMAGE ID
# myapp        stable    a3b4c5d6e7f8   5 minutes ago    145MB  ← 같은 IMAGE ID

docker push — 레지스트리에 업로드

push 전에 docker login으로 레지스트리에 인증하고, 이미지 이름이 계정명/이미지명 형식으로 태그되어 있어야 합니다.

로컬 터미널
# Docker Hub 로그인
docker login
# Username: myusername
# Password: ****

# push (태그가 '사용자명/이미지명' 형식이어야 함)
docker push myusername/myapp:v1.0
docker push myusername/myapp:latest

# 특정 레지스트리에 로그인
docker login ghcr.io -u myusername --password-stdin <<< $GITHUB_TOKEN

# AWS ECR 로그인 (AWS CLI 필요)
aws ecr get-login-password --region us-east-1 | \
  docker login --username AWS --password-stdin \
  123456789.dkr.ecr.us-east-1.amazonaws.com

# push
docker push 123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:v1.0

CI/CD에서의 이미지 버전 관리 패턴

태그 전략이 없으면 '지금 운영에 어떤 버전이 올라가 있지?'라는 질문에 답하기 어렵습니다. 프로덕션에서는 :latest 대신 Git 커밋 해시나 릴리즈 버전을 태그로 사용해 어떤 코드로 빌드된 이미지인지 추적할 수 있게 합니다.

로컬 터미널
# Git 커밋 해시를 태그로 사용 (추적 가능성)
GIT_SHA=$(git rev-parse --short HEAD)
docker build -t myapp:${GIT_SHA} .
docker tag myapp:${GIT_SHA} myapp:latest

# 릴리즈 버전 태그
VERSION="v2.1.3"
docker build -t myapp:${VERSION} .
docker tag myapp:${VERSION} myapp:latest

# 브랜치별 태그
BRANCH=$(git branch --show-current | sed 's/\//-/g')
docker build -t myapp:${BRANCH}-${GIT_SHA} .

기본 실습

이미지 탐색과 빌드, 태그, 관리를 실습합니다.

로컬 터미널
# 1. 다양한 이미지 크기 비교
docker pull python:3.11
docker pull python:3.11-slim
docker pull python:3.11-alpine
docker images python
# REPOSITORY   TAG          IMAGE ID       CREATED       SIZE
# python       3.11         a3b4c5...      3 days ago    1.01GB  ← 전체 버전
# python       3.11-slim    b4c5d6...      3 days ago    129MB   ← 불필요 패키지 제거
# python       3.11-alpine  c5d6e7...      2 days ago    51.7MB  ← Alpine 기반

# 2. 이미지 레이어 탐색
docker history python:3.11-slim
docker inspect python:3.11-slim | python3 -c "
import json, sys
data = json.load(sys.stdin)
print('OS:', data[0]['Os'])
print('Architecture:', data[0]['Architecture'])
print('Layers:', len(data[0]['RootFS']['Layers']), 'layers')
"

# 3. 빌드한 이미지 탐색
cd /tmp/docker-image-lab
docker build -t myapp:v1.0 . 2>/dev/null || echo "Already built"

docker inspect myapp:v1.0 | python3 -c "
import json, sys
data = json.load(sys.stdin)
print('Created:', data[0]['Created'])
print('Size:', data[0]['Size'] // 1024 // 1024, 'MB')
print('Cmd:', data[0]['Config']['Cmd'])
print('WorkDir:', data[0]['Config']['WorkingDir'])
print('ExposedPorts:', list(data[0]['Config'].get('ExposedPorts', {}).keys()))
"

# 4. .dockerignore로 빌드 컨텍스트 최적화
cat > /tmp/docker-image-lab/.dockerignore << 'EOF'
.git
__pycache__
*.pyc
*.pyo
.env
.venv
venv/
*.log
EOF

# 빌드 컨텍스트 크기 비교 (dockerignore 효과)
docker build -t myapp:v1.0-clean . 2>&1 | head -5
# Sending build context to Docker daemon  X.XXkB  ← .git 등 제외됨

운영 서버에서 항상 myapp:latest로 배포했습니다. 어느 날 새벽 배포 후 서버가 다운됩니다. 원인은 latest가 가리키는 이미지가 의도치 않게 교체된 것이었습니다.

로컬 터미널
# 문제 상황: latest가 무엇인지 추적 불가
docker pull myapp:latest  # 누가 최근에 push한 이미지
docker run -d myapp:latest  # 뭐가 실행될지 불확실

# 현재 실행 중인 이미지의 실제 digest 확인
docker inspect myapp:latest | grep -i digest
# "Id": "sha256:a3f4b1c2d0e9..."
# 이 ID가 기대하는 버전인지 확인 방법이 없음

# 올바른 접근: 불변(Immutable) 태그 사용

# 빌드 시 버전 태그 + latest 함께 부여
VERSION="v1.2.3"
GIT_SHA=$(git rev-parse --short HEAD 2>/dev/null || echo "local")
docker build -t myapp:${VERSION} -t myapp:${GIT_SHA} .

# latest는 편의 참조용으로만 (배포 스크립트에서는 사용 금지)
docker tag myapp:${VERSION} myapp:latest

# 배포 스크립트는 항상 정확한 버전 사용
docker run -d --name api myapp:v1.2.3
# 이렇게 하면 "롤백"도 명확
docker run -d --name api myapp:v1.2.2  # 이전 버전으로 즉시 롤백

# 운영 중인 컨테이너의 정확한 이미지 확인
docker inspect api --format '{{.Config.Image}}'
# myapp:v1.2.3  ← 명확

# digest로 완벽한 불변성 보장 (CI/CD 고급 패턴)
docker pull myapp@sha256:a3f4b1c2d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e1f0

교훈: :latest 태그는 "최신"을 의미하지만 실제로는 "마지막으로 push된 것"입니다. 프로덕션 배포에는 반드시 버전 또는 Git SHA 태그를 사용하고, 롤백 계획을 명확히 세워두세요.

증상

docker images를 실행했더니 이름과 태그가 모두 <none>인 이미지가 수십 개 쌓여 있습니다.

로컬 터미널
docker images
# REPOSITORY   TAG       IMAGE ID       CREATED        SIZE
# myapp        latest    a1b2c3d4e5f6   2 hours ago    512MB
# <none>       <none>    f1e2d3c4b5a6   1 day ago      508MB
# <none>       <none>    9a8b7c6d5e4f   3 days ago     505MB
# <none>       <none>    3c4d5e6f7a8b   5 days ago     501MB

docker system df
# TYPE        TOTAL   ACTIVE   SIZE      RECLAIMABLE
# Images      15      3        8.2GB     6.1GB (74%)  ← 대부분이 dangling

원인

같은 태그(myapp:latest)로 반복 빌드할 때마다 기존 태그가 새 이미지로 이동하고, 이전 이미지는 태그를 잃어 <none>:<none>이 됩니다.

Docker
# 매번 같은 태그로 빌드하면
docker build -t myapp:latest .  # 1차 빌드 → ID: abc123 → myapp:latest
docker build -t myapp:latest .  # 2차 빌드 → ID: def456 → myapp:latest
# abc123은 태그를 잃고 <none>:<none>이 됨

해결

로컬 터미널
# dangling 이미지만 정리 (안전)
docker image prune
# WARNING! This will remove all dangling images.
# Are you sure you want to continue? [y/N] y

# 정리 전 크기 확인 후 정리
docker images -f "dangling=true" -q | wc -l  # 개수 확인
docker image prune -f  # 확인 프롬프트 없이 정리

# 예방 — 빌드 시 버전 태그 추가 (latest와 함께)
docker build -t myapp:$(git rev-parse --short HEAD) -t myapp:latest .

증상

이미지를 pull하려는데 인증 오류가 납니다.

로컬 터미널
docker pull ghcr.io/myorg/myapp:v1.2.3
# Error response from daemon: Head "https://ghcr.io/v2/myorg/myapp/manifests/v1.2.3":
# unauthorized: authentication required

원인 진단

로컬 터미널
# 현재 로그인 상태 확인
cat ~/.docker/config.json | grep -A5 "ghcr.io"
# → 없거나 토큰이 만료된 경우

# 토큰 만료 여부 직접 확인
docker login ghcr.io
# Login Succeeded → 토큰 갱신됨

해결

로컬 터미널
# GitHub Container Registry 로그인
echo $GITHUB_TOKEN | docker login ghcr.io -u USERNAME --password-stdin

# Docker Hub 로그인
docker login -u myusername
# Password: (입력)

# CI 환경에서는 환경변수로 자동 로그인
docker login ghcr.io \
  -u ${{ github.actor }} \
  -p ${{ secrets.GITHUB_TOKEN }}

# 로그인 후 재시도
docker pull ghcr.io/myorg/myapp:v1.2.3
💼
실무 맥락
현업 패턴

시나리오: 멀티 환경 배포 파이프라인 — 하나의 이미지로 dev/stg/prod 배포

"개발 빌드와 운영 빌드가 달라서 생기는 버그"를 없애는 올바른 Docker 이미지 전략입니다. 이미지는 한 번만 빌드하고, 환경별 설정은 환경변수로 주입합니다.

로컬 터미널
# CI 파이프라인 (GitHub Actions / Jenkins)에서 한 번만 빌드

# 1. 빌드 (환경 독립적, 설정 없음)
VERSION=$(git describe --tags --always)
docker build -t myapp:${VERSION} .

# 2. 레지스트리에 push
docker push myapp:${VERSION}

# --- 배포 단계 (환경마다 다른 설정 주입) ---

# dev 배포
docker run -d \
  --name api-dev \
  -e DATABASE_URL=postgresql://dev-db/myapp_dev \
  -e LOG_LEVEL=debug \
  -e FEATURE_NEW_UI=true \
  myapp:${VERSION}

# staging 배포 (같은 이미지)
docker run -d \
  --name api-stg \
  -e DATABASE_URL=postgresql://stg-db/myapp_stg \
  -e LOG_LEVEL=info \
  -e FEATURE_NEW_UI=true \
  myapp:${VERSION}

# production 배포 (같은 이미지)
docker run -d \
  --name api-prod \
  -e DATABASE_URL=postgresql://prod-db/myapp_prod \
  -e LOG_LEVEL=warn \
  -e FEATURE_NEW_UI=false \
  myapp:${VERSION}

# 이미지 내부에 환경별 설정이 없어야 하는 이유:
# 1. 보안: DB 비밀번호, API 키가 이미지에 포함되면 레지스트리 접근자 모두 노출
# 2. 불변성: "dev에서 테스트한 이미지 = stg에서 테스트한 이미지 = prod에 올라간 이미지"
# 3. 롤백: 이전 버전 태그로 즉시 전환 가능

# 현재 prod에 어떤 버전이 떠 있는지 확인
docker inspect api-prod --format '{{.Config.Image}}'
# myapp:v1.4.2-abc1234

# 버그 발견 시 롤백
docker stop api-prod
docker run -d --name api-prod \
  -e DATABASE_URL=postgresql://prod-db/myapp_prod \
  -e LOG_LEVEL=warn \
  myapp:v1.4.1-def5678  # 이전 버전으로 즉시 복구

실무 포인트: 이 패턴을 "Build once, deploy many"라고 합니다. 이미지를 환경별로 따로 빌드하는 팀은 "스테이징에서 됐는데 운영에서 왜 안 되지?"를 경험합니다. 하나의 이미지에 환경변수로 설정을 주입하는 방식이 이를 방지합니다.

핵심 요약

이 모듈에서 다룬 이미지 관련 명령어를 정리합니다.

명령용도
docker pull 이미지:태그레지스트리에서 이미지 다운로드
docker images로컬 이미지 목록
docker build -t 이름:태그 .Dockerfile로 이미지 빌드
docker tag 원본:태그 새이름:태그이미지에 새 태그 추가
docker push 이름:태그레지스트리에 이미지 업로드
docker history 이미지레이어 히스토리 확인
docker inspect 이미지이미지 상세 정보
docker rmi 이미지이미지 삭제
docker image prune사용하지 않는 이미지 정리

이미지 이름 규칙: [레지스트리/][계정/]이름:태그 프로덕션 원칙: :latest 대신 버전 태그 사용, 하나의 이미지로 모든 환경 배포

다음 모듈에서는 이미지와 컨테이너가 실제로 리눅스 커널 위에서 어떻게 격리되는지 살펴봅니다. Docker 엔진의 내부 구조를 설치 과정과 함께 확인합니다.

지식 확인

퀴즈 — 5문제

Q1

docker pull nginx:alpine 에서 'alpine'의 역할은?

Q2

Docker 이미지의 레이어(Layer) 구조에 대한 설명으로 옳은 것은?

Q3

docker build -t myapp:v1.0 . 명령에서 마지막 '.'의 의미는?

Q4

docker tag myapp:latest myapp:v2.0 명령의 효과는?

Q5

docker push를 실행하기 전에 반드시 필요한 준비는?

0 / 5 답변

🧪 실습으로 확인하기

Docker Compose 멀티 서비스 구성

초급

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

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

이것도 배워보세요

docker입문 · 40
[Docker] 포트 바인딩 오류 방지와 컨테이너의 핵심 상태 변화
Docker 트랙 계속
networking입문 · 45
[Network] OSI 7계층과 TCP/IP 4계층 모델 실무적 관점 분석
Networking 트랙 시작점