logo
홈블로그소개
3,119

Built with Next.js, Bun, Tailwind CSS and Shadcn/UI

Docker

Docker 학습 노트

Toma
2026년 1월 14일
목차
1. Docker Volume — Named Volume
질문
답
2. Bind Mount vs Named Volume
질문
Bind Mount 설정
비교
3. Named Volume의 진짜 목적
질문
답
4. 멀티 스테이지 빌드 vs 단일 스테이지 빌드
질문
핵심 개념
크기 비교
5. Docker 레이어 캐시 무효화
질문
답
나쁜 예 (비효율적)
좋은 예 (최적화)
효과
6. NEXT_PUBLIC_* 환경변수와 ARG/ENV
질문
답
7. ARG → ENV 복사 패턴
질문
답
NEXT_PUBLIC_* 의 특수성
8. 멀티스테이지 빌드 — 빌드 리소스 낭비 여부
질문
답
9. Dockerfile 보안 검토
발견된 개선사항
이전 포스트한입 TypeScript 챌린지 후기
다음 포스트FSD 아키텍처 적용 후기 — 실전에서 배운 것들

목차

1. Docker Volume — Named Volume
질문
답
2. Bind Mount vs Named Volume
질문
Bind Mount 설정
비교
3. Named Volume의 진짜 목적
질문
답
4. 멀티 스테이지 빌드 vs 단일 스테이지 빌드
질문
핵심 개념
크기 비교
5. Docker 레이어 캐시 무효화
질문
답
나쁜 예 (비효율적)
좋은 예 (최적화)
효과
6. NEXT_PUBLIC_* 환경변수와 ARG/ENV
질문
답
7. ARG → ENV 복사 패턴
질문
답
NEXT_PUBLIC_* 의 특수성
8. 멀티스테이지 빌드 — 빌드 리소스 낭비 여부
질문
답
9. Dockerfile 보안 검토
발견된 개선사항

1. Docker Volume — Named Volume

질문

volumes: - hera_pgdata:/var/lib/postgresql/data 설정 시 host pc에 해당 디렉토리가 자동 생성되는가?

답

Named Volume 구문의 구조는 <볼륨 이름>:<컨테이너 내부 경로> 이다.

Docker가 자동으로:

  1. hera_pgdata 볼륨이 없으면 생성
  2. 호스트(또는 VM)의 /var/lib/docker/volumes/hera_pgdata/_data 디렉토리 자동 생성
  3. 컨테이너의 /var/lib/postgresql/data와 연결

확인 명령어:

bash
docker volume inspect hera_pgdata
# Mountpoint 항목이 실제 호스트 경로

Mac 주의사항: Docker Desktop은 Linux VM 위에서 동작하므로 /var/lib/docker/volumes/가 Mac 파일시스템에 직접 보이지 않는다.


2. Bind Mount vs Named Volume

질문

Bind Mount 설정 방법과 "디버깅 상황에서만 유용하다"는 의미는?

Bind Mount 설정

yaml
services:
  db:
    image: postgres
    volumes:
      - ./data:/var/lib/postgresql/data
#       ↑ 호스트 경로  ↑ 컨테이너 내부 경로

비교

Named VolumeBind Mount
호스트 경로Docker가 자동 결정사용자가 직접 지정
선언 방식볼륨이름:/컨테이너경로./호스트경로:/컨테이너경로
용도DB, 영속 데이터소스코드, 설정 파일, 디버깅
이식성높음낮음 (경로 종속)
직접 관찰어려움쉬움

Bind Mount는 호스트 경로가 프로젝트 폴더 안에 있어 파일을 바로 열어볼 수 있다. 반면 Named Volume은 Docker가 관리하는 경로라 직접 접근이 어렵다.


3. Named Volume의 진짜 목적

질문

다른 개발자가 이미지를 pull해서 실행하면 볼륨이 생성되는데 어차피 데이터가 없는 거 아닌가?

답

Named Volume의 목적은 개발자 간 데이터 공유가 아니다.

컨테이너를 재시작/재생성해도 데이터가 유지되는 것이 목적이다.

bash
docker compose down      # 컨테이너 삭제
docker compose up        # 컨테이너 재생성
# → 볼륨은 살아있으므로 DB 데이터 그대로 유지

볼륨 없이 실행하면 docker compose down 후 재실행 시 DB 데이터가 전부 초기화된다.

개발자 간 데이터 공유는 별도 방법 사용:

  • DB 초기화 SQL 파일(seed 파일)을 git에 포함
  • DB dump/restore
  • 마이그레이션 스크립트

핵심 철학: 컨테이너는 언제든 삭제/재생성 가능한 일회용이어야 하고, 데이터만 영속적으로 남아야 한다.


4. 멀티 스테이지 빌드 vs 단일 스테이지 빌드

질문

멀티 스테이지 빌드는 지시어가 많아 레이어 수가 증가할 것 같은데, 그럼에도 장점이 있는가?

핵심 개념

이전 스테이지의 레이어는 최종 이미지에 포함되지 않는다.

docker
# 스테이지 1 (builder) — 최종 이미지에 포함 안됨
FROM node:20 AS builder
COPY . .
RUN npm install
RUN npm run build

# 스테이지 2 (최종) — 이 레이어만 최종 이미지에 포함됨
FROM node:20-alpine
COPY --from=builder /app/dist ./dist

크기 비교

단일 스테이지멀티 스테이지
Dockerfile 지시어 수적음많음
최종 이미지 레이어 수많음적음
최종 이미지 크기큼 (1GB+)작음 (200MB 내외)
보안빌드 도구 포함실행에 필요한 것만

5. Docker 레이어 캐시 무효화

질문

소스코드 변경 시 이후 레이어(npm ci, npm run build)도 무조건 새로 만드는가?

답

한 레이어의 캐시가 무효화되면, 그 이후 모든 레이어도 캐시가 무효화된다.

나쁜 예 (비효율적)

docker
FROM node:14
WORKDIR /app
COPY . .          # 소스코드 변경 → 캐시 무효화
RUN npm ci        # 무조건 새로 실행
RUN npm run build # 무조건 새로 실행

좋은 예 (최적화)

docker
FROM node:14
WORKDIR /app
COPY package.json package-lock.json ./  # 의존성 파일만 먼저 복사
RUN npm ci                               # package.json 안 바뀌면 캐시 사용
COPY . .                                 # 소스코드 복사
RUN npm run build                        # 소스코드 바뀌면 여기서만 무효화

효과

상황나쁜 순서좋은 순서
소스코드만 변경npm ci + build 전부 재실행build만 재실행
package.json 변경npm ci + build 전부 재실행npm ci + build 전부 재실행

황금 규칙: 변경 빈도가 낮은 것을 위에, 높은 것을 아래에 배치한다. Docker는 레이어 캐시 유효성을 파일 내용의 체크섬으로 판단한다.


6. NEXT_PUBLIC_* 환경변수와 ARG/ENV

질문

ARG NEXT_PUBLIC_API_BASE_URL 처럼 값 없이 선언만 하면 어떤 값이 들어가는가?

답

값은 .env.docker 파일에서 온다. 흐름은 다음과 같다:

javascript
.env.docker 파일 (git 비공개)
  NEXT_PUBLIC_API_BASE_URL=https://prod.api.com
        ↓
dotenv -e .env.docker -- docker build ...
  → .env.docker를 현재 shell 환경변수로 로드
        ↓
--build-arg NEXT_PUBLIC_API_BASE_URL  (값 없이 키만)
  → Docker가 현재 shell 환경변수에서 자동으로 값을 찾아 주입
        ↓
ARG NEXT_PUBLIC_API_BASE_URL  (Dockerfile에서 수신)

--build-arg KEY (값 없이 키만) 구문은 현재 shell 환경변수에서 해당 키를 찾아 자동으로 주입한다.


7. ARG → ENV 복사 패턴

질문

ENV NEXT_PUBLIC_API_BASE_URL=${NEXT_PUBLIC_API_BASE_URL} 은 무슨 의미인가? ARG 없이 바로 쓸 수는 없나?

답

Dockerfile의 ${} 는 이미 선언된 ARG 또는 ENV를 참조하는 변수 치환이다.

ARGENV
유효 범위선언된 스테이지 내부이미지 레이어에 저장, 이후 모든 RUN + 컨테이너 실행 시까지 유지
멀티스테이지스테이지 경계에서 소멸스테이지 경계에서 소멸 (다음 FROM 이후엔 없음)
이미지 검사docker inspect에 안 보임docker inspect에 보임

ARG만으로는 충분하지 않은 이유:

  • 같은 스테이지의 RUN에서는 ARG도 참조 가능하나, 명시성이 떨어짐
  • docker inspect로 어떤 값으로 빌드됐는지 추적 불가

NEXT_PUBLIC_* 의 특수성

Next.js는 빌드 시점에 NEXT_PUBLIC_* 변수를 JS 번들에 문자열로 직접 치환(인라이닝)한다.

javascript
// 빌드 전
fetch(process.env.NEXT_PUBLIC_API_BASE_URL + "/api")

// 빌드 후 번들
fetch("https://prod.api.com/api")

따라서 runner 스테이지에 NEXT_PUBLIC_* ENV가 없는 것은 의도적 설계다. 이미 번들에 구워져 있으므로 런타임 ENV는 무의미하다.


8. 멀티스테이지 빌드 — 빌드 리소스 낭비 여부

질문

3단계(deps → builder → runner)가 모두 실행된다면 단일 스테이지보다 리소스 낭비가 심하지 않은가?

답

낭비처럼 보이지만 실제로는 낭비가 아니다. Docker 레이어 캐시 덕분이다.

소스코드만 수정했을 때:

  • deps 스테이지: package.json 안 바뀜 → 캐시 hit (0초)
  • builder 스테이지: node_modules 캐시 hit, pnpm build만 재실행
  • runner 스테이지: 빌드 결과물 복사만

최종 이미지는 마지막 스테이지(runner)만 남는다. deps와 builder는 중간 과정에서 사용 후 버려진다.

deps 스테이지 분리의 목적은 이미지 크기가 아닌 캐시 최적화다:

docker
# deps 없이 builder에서 작성했을 경우
COPY . .          # 소스코드가 바뀌면 아래 pnpm install도 재실행
RUN pnpm install  # 불필요한 재실행 발생

deps를 분리하면 package.json이 바뀌지 않는 한 pnpm install 캐시가 항상 유효하다.


9. Dockerfile 보안 검토

발견된 개선사항

  1. .dockerignore에 .env.docker 미제외 (보안 — 수정 필요)

.gitignore는 .env* 패턴으로 전부 제외하지만, .dockerignore는 .env*.local만 제외한다. COPY . . 시 .env.docker(실제 API 키 포함)가 이미지에 포함될 수 있다.

javascript
# .dockerignore에 추가 필요
.env*
  1. Dockerfile.dev에 API 키 하드코딩 (보안 — 수정 필요)
docker
ARG NEXT_PUBLIC_KAKAOMAP_KEY="67e2193950f3313798d669596a26b9f6"

카카오맵 키가 git에 올라가는 파일에 평문으로 노출되어 있다. Dockerfile처럼 기본값 없이 --build-arg로 주입받는 방식으로 통일해야 한다.

  1. 전체적으로 잘 된 부분
  • next.config.ts의 output: "standalone" 설정과 runner 스테이지의 COPY 경로가 일치
  • deps 스테이지로 pnpm install 캐시 분리
  • runner 스테이지에 불필요한 NEXT_PUBLIC_* ENV 없음 (의도적 설계)