새 기능을 급하게 배포했는데 스테이징 서버는 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 imagesdocker pull hello-worldmkdir -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 | 정확한 버전 고정 | 프로덕션 권장 |
:alpine | Alpine Linux 기반, 경량 | 보안 중요 + 크기 중요 |
:slim | 불필요 패키지 제거 | 중간 크기 |
:bookworm | Debian 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')
# 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 방법을 다룹니다.

docker tag — 이미지에 이름 붙이기
docker tag는 이미지를 복사하지 않고 같은 레이어를 가리키는 새 이름(참조)을 추가합니다. 하나의 이미지를 레지스트리별로 다른 이름으로 push할 때 씁니다.
# 빌드 시 바로 태그 지정
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 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 엔진의 내부 구조를 설치 과정과 함께 확인합니다.