Docker 기본기 + 실무 스타일 정리

목차


1. 핵심 개념

Image vs Container

Image   = 설계도 (읽기 전용, 불변)
Container = 실행 중인 인스턴스 (Image로 만든 프로세스)

하나의 Image → 여러 Container 실행 가능
docker images          # 이미지 목록
docker ps              # 실행 중인 컨테이너
docker ps -a           # 종료된 것 포함 전체

Registry

Registry = 이미지 저장소

DockerHub  : docker.io/library/nginx
ECR        : 317250221510.dkr.ecr.ap-northeast-2.amazonaws.com/timedeal-backend
GCR        : gcr.io/distroless/static

주요 명령어 흐름

# 빌드
docker build -t timedeal-backend:latest .
 
# 실행
docker run -d -p 8080:8080 --name backend timedeal-backend:latest
 
# 로그
docker logs -f backend
 
# 접속
docker exec -it backend sh
 
# 정리
docker stop backend
docker rm backend
docker rmi timedeal-backend:latest

2. 이미지 레이어와 캐시

레이어 구조

Dockerfile 명령어 한 줄 = 레이어 1개

FROM node:22-alpine        ← 레이어 1
WORKDIR /app               ← 레이어 2
COPY package*.json ./      ← 레이어 3
RUN npm ci                 ← 레이어 4  ← 캐시 핵심
COPY . .                   ← 레이어 5
RUN npm run build          ← 레이어 6

캐시가 작동하는 조건

해당 레이어와 그 이전 레이어가 변경 없으면 캐시 재사용

소스 코드 변경 시:
- COPY package*.json → 변경 없음 → 캐시 ✅
- RUN npm ci        → 변경 없음 → 캐시 ✅  (node_modules 재설치 안 함)
- COPY . .          → 변경됨   → 캐시 ❌
- RUN npm run build → 재실행   → 캐시 ❌

잘못된 순서 예시

# 나쁜 예시 - 소스 변경마다 npm ci 재실행
COPY . .
RUN npm ci
RUN npm run build
 
# 좋은 예시 - 의존성 먼저, 소스 나중
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

면접 포인트

“의존성 파일을 소스보다 먼저 COPY하는 이유는 레이어 캐시 활용 때문입니다. 소스가 바뀌어도 package.json이 안 바뀌면 npm ci 레이어는 캐시가 그대로 써집니다.”


3. 멀티스테이지 빌드

왜 필요한가

단일 스테이지:
golang:1.22 이미지 (300MB+) + 소스 + 빌드 툴 = 최종 이미지가 큼

멀티스테이지:
Stage 1 (빌드): golang으로 바이너리 생성
Stage 2 (실행): 바이너리만 복사 → 이미지 최소화

구조

# Stage 1: 빌드 환경 (AS로 이름 지정)
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o app .
 
# Stage 2: 실행 환경 (빌드 결과물만 가져옴)
FROM gcr.io/distroless/static:nonroot
COPY --from=builder /app/app .
ENTRYPOINT ["/app"]

최종 이미지에 builder 스테이지 내용은 포함되지 않음


4. 실제 Dockerfile 분석

Backend (Go) - timedeal-k8s/backend/Dockerfile

# Stage 1: 빌드
FROM golang:1.22-alpine AS builder
WORKDIR /app
 
# 의존성 캐시 레이어 (go.mod/sum만 먼저 복사)
COPY go.mod go.sum ./
RUN go mod download
 
# 소스 복사 및 빌드
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
    go build -ldflags="-w -s" -o timedeal-backend .
 
# Stage 2: 실행
FROM gcr.io/distroless/static:nonroot
WORKDIR /app
COPY --from=builder /app/timedeal-backend .
EXPOSE 8080
ENTRYPOINT ["/app/timedeal-backend"]

설계 포인트:

코드이유
go.mod go.sum 먼저 COPY캐시 최적화 (소스 바뀌어도 의존성 재다운로드 안 함)
CGO_ENABLED=0C 라이브러리 의존성 제거 → distroless에서 실행 가능
GOOS=linux GOARCH=amd64크로스 컴파일 (Mac에서 빌드해도 Linux용 바이너리 생성)
-ldflags="-w -s"디버그 심볼 제거 → 바이너리 크기 감소
distroless/static:nonrootOS 없는 최소 이미지 + root 아닌 유저 실행 (보안)

“distroless 이미지는 shell도 없고 패키지 매니저도 없습니다. 공격 표면이 거의 없어서 보안에 유리합니다. 대신 컨테이너 안에서 디버깅은 불가능하고, kubectl exec으로도 sh 접속이 안 됩니다.”

Frontend (React + Nginx) - timedeal-k8s/timedeal-front/Dockerfile

# Stage 1: 빌드
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci          # npm install이 아닌 ci 사용
COPY . .
RUN npm run build
 
# Stage 2: Nginx로 정적 파일 서빙
FROM nginx:alpine
COPY --from=builder /app/build /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

설계 포인트:

코드이유
npm ci vs npm installci는 package-lock.json 그대로 설치 (재현성 보장), install은 버전 범위 내 최신 설치
nginx:alpinenode 이미지(300MB+) 버리고 nginx(25MB)만 사용
nginx.conf 별도 복사SPA 라우팅 처리 (try_files $uri /index.html)
daemon off포그라운드 실행 (Docker는 PID 1이 종료되면 컨테이너 종료됨)

“빌드 후 최종 이미지에 Node.js가 없는 게 핵심입니다. React 빌드 결과물(정적 파일)만 nginx에 올리면 되므로, 실행 환경에 빌드 툴이 필요 없습니다.”


5. docker-compose

언제 쓰나

단일 서비스: docker run으로 충분
여러 서비스 조합 (앱 + DB + Redis): docker-compose

로컬 개발 환경에서 주로 사용

기본 구조

# docker-compose.yml
version: "3.9"
 
services:
  backend:
    build: ./backend
    ports:
      - "8080:8080"
    environment:
      - DB_HOST=postgres
      - REDIS_HOST=redis
    depends_on:
      - postgres
      - redis
 
  postgres:
    image: postgres:16
    environment:
      POSTGRES_DB: timedeal
      POSTGRES_PASSWORD: secret
    volumes:
      - pg_data:/var/lib/postgresql/data
 
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
 
volumes:
  pg_data:

주요 명령어

docker-compose up -d          # 전체 실행 (백그라운드)
docker-compose down           # 전체 종료
docker-compose down -v        # 볼륨까지 삭제
docker-compose logs -f backend # 특정 서비스 로그
docker-compose ps             # 상태 확인
docker-compose exec backend sh # 컨테이너 접속

depends_on 주의점

depends_on:
  - postgres

depends_on은 컨테이너 시작 순서만 보장하고, 서비스 준비 완료는 보장하지 않습니다. postgres 컨테이너가 시작됐어도 DB가 accept 상태가 되기 전에 앱이 연결 시도할 수 있습니다. 실무에서는 healthcheck + condition: service_healthy로 해결합니다.”


6. ECR + Terraform 연결

전체 흐름

1. Terraform으로 ECR 레포 생성
        ↓
2. docker build → ECR push
        ↓
3. EKS Pod가 ECR에서 이미지 pull

Terraform으로 ECR 생성 (timedeal-k8s)

resource "aws_ecr_repository" "backend" {
  name                 = "timedeal-backend"
  image_tag_mutability = "MUTABLE"
 
  image_scanning_configuration {
    scan_on_push = true  # push마다 취약점 스캔
  }
}

ECR push 명령어 (outputs.tf에서 자동 생성)

# 1. ECR 로그인
aws ecr get-login-password --region ap-northeast-2 \
  | docker login --username AWS --password-stdin \
    317250221510.dkr.ecr.ap-northeast-2.amazonaws.com
 
# 2. 빌드
docker build -t timedeal-backend ./backend
 
# 3. 태그
docker tag timedeal-backend:latest \
  317250221510.dkr.ecr.ap-northeast-2.amazonaws.com/timedeal-backend:latest
 
# 4. 푸시
docker push \
  317250221510.dkr.ecr.ap-northeast-2.amazonaws.com/timedeal-backend:latest

EKS 노드가 ECR pull 할 수 있는 이유

노드 IAM 역할 → AmazonEC2ContainerRegistryReadOnly 정책
→ ECR에서 이미지 pull 권한 자동 부여
→ imagePullSecrets 없이도 동작

7. 면접 빈출 Q&A

Q. Image와 Container 차이?

“Image는 읽기 전용 템플릿이고, Container는 Image를 실행한 인스턴스입니다. 하나의 Image로 여러 Container를 동시에 실행할 수 있습니다.”

Q. 멀티스테이지 빌드를 왜 쓰나요?

“빌드 환경과 실행 환경을 분리해서 최종 이미지 크기를 줄이기 위해서입니다. Go 백엔드의 경우 golang 이미지(300MB+)로 빌드하고, 결과 바이너리만 distroless 이미지(수 MB)에 올립니다. 이미지가 작으면 ECR 저장 비용, 네트워크 전송 시간, 보안 취약점 모두 줄어듭니다.”

Q. COPY . . 전에 COPY package.json ./을 먼저 하는 이유?

“레이어 캐시 활용입니다. 소스 코드가 바뀌어도 package.json이 바뀌지 않으면 npm ci 레이어는 캐시가 재사용됩니다. 순서를 바꾸면 소스 한 줄 바꿀 때마다 의존성 전체를 재설치해야 합니다.”

Q. CMD vs ENTRYPOINT 차이?

항목ENTRYPOINTCMD
역할고정 실행 명령기본 인자 (덮어쓰기 가능)
덮어쓰기--entrypoint로만docker run 뒤 인자로
조합ENTRYPOINT + CMD = 실행파일 + 기본 인자
ENTRYPOINT ["/app/timedeal-backend"]   # 항상 이걸 실행
CMD ["--port", "8080"]                 # 기본 인자 (덮어쓸 수 있음)

Q. docker run에서 -v 옵션은?

docker run -v /host/path:/container/path nginx
# 또는 named volume
docker run -v pg_data:/var/lib/postgresql/data postgres

“볼륨 마운트입니다. 컨테이너가 삭제되어도 데이터가 호스트에 남습니다. DB 컨테이너에서 필수입니다.”

Q. 컨테이너가 계속 재시작되면 어떻게 디버깅?

docker logs [container]           # 로그 확인
docker inspect [container]        # 상세 정보 (환경변수, 네트워크 등)
docker events                     # 실시간 이벤트

Q. distroless 이미지가 뭔가요?

“Google이 만든 최소화 이미지입니다. OS 패키지, shell, 패키지 매니저가 없고 앱 실행에 필요한 런타임만 있습니다. 보안 취약점의 대부분이 OS 레이어에서 나오는데, 그걸 없애버린 거예요. 단점은 컨테이너 안에서 디버깅이 불가능하다는 점입니다.”

Q. Terraform으로 Docker를 어떻게 활용하나요?

“직접 연결 포인트는 ECR입니다. Terraform으로 ECR 레포지토리를 만들고, CI/CD에서 docker build → ECR push를 하면, EKS가 그 이미지를 pull해서 Pod를 실행합니다. 인프라(ECR, EKS, IAM)는 Terraform이 관리하고, 이미지 빌드/배포는 Docker + CI/CD가 담당하는 구조입니다.”