Insightdocker

Dockerfile은 왜 package.json만 먼저 복사할까?

2026-06-11
CsDockerServer

1. Dockerfile은 Image를 만드는 명령서다

Dockerfile은 Docker Image를 만들기 위한 빌드 명령서다.

간단한 Dockerfile 예시
1
간단한 Dockerfile 예시
FROM node:20-alpine
 
WORKDIR /app
 
COPY . .
 
RUN npm install
 
CMD ["npm", "start"]

이 Dockerfile은 대략 다음과 같은 의미를 가진다.

  • node:20-alpine 이미지를 기반으로 시작한다.
  • 컨테이너 내부 작업 디렉터리를 /app으로 설정한다.
  • 현재 프로젝트 파일을 컨테이너 내부로 복사한다.
  • npm install을 실행해 의존성을 설치한다.
  • 컨테이너가 실행될 때 npm start를 실행한다.

겉으로 보면 단순한 순차 실행 스크립트처럼 보인다.
하지만 Dockerfile은 일반적인 shell script와는 다르게 이해해야 한다.

Dockerfile의 각 명령은 Image를 만드는 과정이고,
그 과정에서 생성된 결과는 Layer와 Cache에 영향을 준다.

즉, Dockerfile은 단순히 명령을 실행하는 파일이 아니라,
최종 Docker Image의 파일 시스템을 단계적으로 구성하는 파일이다.


2. Dockerfile은 위에서 아래로 실행된다

Dockerfile은 기본적으로 위에서 아래로 순서대로 실행된다.

Dockerfile 실행 순서
1
Dockerfile 실행 순서
FROM node:20-alpine
 
WORKDIR /app
 
COPY package.json package-lock.json ./
 
RUN npm ci
 
COPY . .
 
CMD ["npm", "start"]

빌드 과정은 다음처럼 진행된다.

Dockerfile 빌드 흐름
1
Dockerfile 빌드 흐름
1. FROM node:20-alpine
2. WORKDIR /app
3. COPY package.json package-lock.json ./
4. RUN npm ci
5. COPY . .
6. CMD ["npm", "start"]

각 단계는 이전 단계의 결과를 기반으로 실행된다.
따라서 앞 단계의 결과가 바뀌면 뒤 단계에도 영향을 줄 수 있다.

Dockerfile은 위에서 아래로 실행되고,
뒤의 명령은 앞의 명령 결과 위에서 실행된다.

이 특징 때문에 Dockerfile에서는 명령어의 순서가 중요하다.

같은 명령어를 사용하더라도 어떤 순서로 배치하느냐에 따라 빌드 속도와 캐시 효율이 크게 달라질 수 있다.


3. 각 명령은 보통 Image Layer를 만든다

Docker Image는 하나의 큰 파일이 아니라 여러 개의 Layer가 쌓인 구조다.

Docker Image Layer 구조
1
Docker Image Layer 구조
Docker Image
├── Layer 5: CMD 설정
├── Layer 4: source code copy
├── Layer 3: npm ci 실행 결과
├── Layer 2: package.json copy
└── Layer 1: node:20-alpine

Dockerfile의 명령은 이 Layer를 단계적으로 만든다.

  • 다만 모든 명령이 같은 방식으로 파일 시스템 Layer를 만드는 것은 아니다.
  • 일반적으로 RUN, COPY, ADD처럼 파일 시스템을 변경하는 명령은 Layer를 만든다.
  • 반면 CMD, ENTRYPOINT, ENV, EXPOSE 같은 명령은 실행 설정이나 메타데이터에 가까운 역할을 한다.

Dockerfile의 명령은 순서대로 실행되고,
그 결과가 Image Layer 또는 Image Metadata로 누적된다.

예를 들어 다음 명령을 보자.

파일 시스템을 변경하는 명령
1
파일 시스템을 변경하는 명령
COPY package.json package-lock.json ./
 
RUN npm ci
 
COPY . .

이 명령들은 컨테이너 파일 시스템에 변화를 만든다.

  • COPY package.json package-lock.json ./ → package 관련 파일을 Image 안으로 복사한다.

  • RUN npm cinode_modules 등 의존성 설치 결과를 만든다.

  • COPY . . → 전체 소스 코드를 Image 안으로 복사한다.

이렇게 만들어진 Layer는 이후 빌드에서 Cache로 재사용될 수 있다.

더 자세히 보기: Layer는 왜 중요한가?
  • Layer가 중요한 이유는 Docker가 빌드 결과를 재사용할 수 있기 때문이다.
  • 이전 빌드에서 만들어둔 Layer가 현재 빌드에서도 동일하다고 판단하면, 해당 단계를 다시 실행하지 않고 Cache를 사용한다.
캐시를 사용하는 빌드 흐름
1
캐시를 사용하는 빌드 흐름
Step 1: FROM node:20-alpine
        → Cache 사용
 
Step 2: WORKDIR /app
        → Cache 사용
 
Step 3: COPY package.json package-lock.json ./
        → Cache 사용
 
Step 4: RUN npm ci
        → Cache 사용
 
Step 5: COPY . .
        → 변경됨, 새로 실행

이 구조 덕분에 매번 모든 명령을 처음부터 다시 실행하지 않아도 된다.

Docker Build Cache는 이전에 만든 Layer를 재사용해서 빌드 시간을 줄여준다.


4. Docker Build Cache는 언제 깨질까?

Docker는 각 빌드 단계에서 이전 결과를 재사용할 수 있는지 판단한다.
재사용할 수 있으면 Cache를 사용하고, 그렇지 않으면 해당 단계부터 다시 실행한다.

Cache가 깨지는 대표적인 경우는 다음과 같다.

  • Dockerfile의 해당 명령어가 바뀐 경우
  • COPY 또는 ADD 대상 파일의 내용이 바뀐 경우
  • 이전 단계의 Layer가 바뀐 경우
  • 빌드 인자나 환경에 따라 결과가 달라진 경우

여기서 가장 중요한 것은 이것이다.

이전 Layer가 바뀌면, 그 이후 단계의 Cache도 사용할 수 없게 될 수 있다.

예를 들어 다음 구조를 보자.

캐시가 깨지기 쉬운 Dockerfile
1
캐시가 깨지기 쉬운 Dockerfile
FROM node:20-alpine
 
WORKDIR /app
 
COPY . .
 
RUN npm install
 
CMD ["npm", "start"]

이 Dockerfile에서는 COPY . .RUN npm install보다 먼저 실행된다.

즉, 프로젝트의 모든 파일을 먼저 복사한 뒤 의존성을 설치한다.

빌드 순서
1
빌드 순서
1. FROM node:20-alpine
2. WORKDIR /app
3. COPY . .
4. RUN npm install
5. CMD ["npm", "start"]

이 상태에서 소스 코드 한 줄만 수정했다고 해보자.

소스 코드 변경
1
소스 코드 변경
src/App.tsx 수정

그러면 COPY . . 단계의 입력이 바뀐다.
Docker는 이 단계를 이전과 같다고 볼 수 없기 때문에 Cache를 사용하지 못한다.

그리고 COPY . . 이후에 있는 RUN npm install도 다시 실행될 수 있다.

Cache가 깨지는 흐름
1
Cache가 깨지는 흐름
COPY . .
→ src/App.tsx 변경으로 Cache 무효화
 
RUN npm install
→ 이전 Layer가 바뀌었으므로 Cache 재사용 불가
→ 다시 실행

결과적으로 의존성 파일은 바뀌지 않았는데도,
소스 코드 변경 때문에 npm install이 다시 실행되는 상황이 생긴다.

이것이 “소스 한 줄 바꿨는데 왜 npm install이 다시 실행될까?”에 대한 핵심 이유다.


5. COPY . .를 너무 빨리 하면 캐시 효율이 나빠진다

COPY . .는 현재 빌드 컨텍스트의 파일을 컨테이너 내부로 복사한다.

전체 파일 복사
1
전체 파일 복사
COPY . .

이 명령은 편리하지만, 너무 일찍 사용하면 캐시 효율이 나빠질 수 있다.

왜냐하면 프로젝트 안에는 자주 바뀌는 파일과 잘 바뀌지 않는 파일이 섞여 있기 때문이다.

프로젝트 파일 예시
1
프로젝트 파일 예시
project
├── package.json
├── package-lock.json
├── src
│   └── App.tsx
├── README.md
├── .env.example
└── Dockerfile

여기서 package.json, package-lock.json은 의존성이 바뀔 때만 수정된다.
반면 src/App.tsx 같은 소스 코드는 개발 중 자주 수정된다.

그런데 COPY . .를 먼저 실행하면 이 파일들이 한 번에 복사된다.

좋지 않은 예시
1
좋지 않은 예시
FROM node:20-alpine
 
WORKDIR /app
 
COPY . .
 
RUN npm install
 
CMD ["npm", "start"]

이 구조에서는 src/App.tsx만 수정해도 COPY . . 단계가 바뀐다.
그 결과 뒤에 있는 RUN npm install도 다시 실행될 수 있다.

따라서 Dockerfile에서는 자주 바뀌는 파일과 잘 바뀌지 않는 파일을 분리해서 복사하는 것이 좋다.


6. package.json을 먼저 복사하는 이유

좋은 Dockerfile은 의존성 설치에 필요한 파일을 먼저 복사한다.

캐시를 잘 활용하는 Dockerfile
1
캐시를 잘 활용하는 Dockerfile
FROM node:20-alpine
 
WORKDIR /app
 
COPY package.json package-lock.json ./
 
RUN npm ci
 
COPY . .
 
CMD ["npm", "start"]

이 구조의 핵심은 다음 두 줄이다.

의존성 설치 파일만 먼저 복사
1
의존성 설치 파일만 먼저 복사
COPY package.json package-lock.json ./
 
RUN npm ci

의존성 설치에 필요한 파일만 먼저 복사하고, 그 다음에 npm ci를 실행한다.

이후에 전체 소스 코드를 복사한다.

소스 코드는 나중에 복사
1
소스 코드는 나중에 복사
COPY . .

이렇게 하면 소스 코드만 바뀌었을 때 npm ci 단계의 Cache를 재사용할 수 있다.

소스 코드만 변경된 경우
1
소스 코드만 변경된 경우
1. FROM node:20-alpine
   → Cache 사용
 
2. WORKDIR /app
   → Cache 사용
 
3. COPY package.json package-lock.json ./
   → package 파일 변경 없음, Cache 사용
 
4. RUN npm ci
   → Cache 사용
 
5. COPY . .
   → 소스 코드 변경으로 새로 실행
 
6. CMD ["npm", "start"]
   → 설정 반영

즉, src/App.tsx만 수정했다면 의존성 설치를 다시 할 필요가 없다.
Docker는 package.jsonpackage-lock.json이 이전과 같다고 판단하면 npm ci 결과 Layer를 재사용할 수 있다.

package.json과 lock file을 먼저 복사하는 이유는 의존성 설치 Layer를 소스 코드 변경으로부터 분리하기 위해서다.

이 구조를 사용하면 의존성이 바뀌지 않은 빌드에서는 npm ci를 다시 실행하지 않아도 되므로 빌드 시간이 줄어든다.

더 자세히 보기: npm install 대신 npm ci를 사용하는 이유

운영용 Dockerfile에서는 보통 npm install보다 npm ci를 사용하는 것이 더 적합하다.

운영 빌드에서 자주 사용하는 방식
1
운영 빌드에서 자주 사용하는 방식
RUN npm ci

npm installpackage.json을 기준으로 의존성을 설치하면서 lock file을 변경할 수 있다.
반면 npm cipackage-lock.json을 기준으로 정확한 버전의 의존성을 설치한다.

구분npm installnpm ci
기준 파일package.json 중심package-lock.json 중심
lock file 변경변경될 수 있음변경하지 않음
node_modules 존재 시기존 내용 활용 가능기존 node_modules 제거 후 설치
재현성상대적으로 낮음상대적으로 높음
CI/배포 환경덜 적합적합

npm ci는 lock file을 기준으로 의존성을 재현 가능하게 설치하므로, CI/CD나 Docker Image 빌드 환경에 더 적합하다.


7. .dockerignore는 빌드 컨텍스트를 줄인다

Dockerfile의 Cache만큼 중요한 것이 .dockerignore다.

Docker는 Image를 빌드할 때 현재 디렉터리의 파일들을 Docker Daemon에게 빌드 컨텍스트로 전달한다.

Docker Image 빌드
1
Docker Image 빌드
docker build -t my-app .

여기서 마지막의 .은 현재 디렉터리를 빌드 컨텍스트로 사용한다는 의미다.

빌드 컨텍스트
1
빌드 컨텍스트
docker build -t my-app .

                이 디렉터리의 파일들이
                Docker Build Context가 됨

문제는 빌드에 필요 없는 파일까지 모두 컨텍스트에 포함될 수 있다는 점이다.

예를 들어 다음과 같은 파일들이 있다.

프로젝트 디렉터리
1
프로젝트 디렉터리
project
├── node_modules
├── dist
├── .git
├── .env
├── coverage
├── package.json
├── package-lock.json
└── src

여기서 node_modules, .git, coverage, 로컬 .env 파일은 일반적으로 Image 빌드에 포함될 필요가 없다.

이때 사용하는 것이 .dockerignore다.

.dockerignore 예시
1
.dockerignore 예시
node_modules
dist
coverage
.git
.env
Dockerfile
docker-compose.yml
README.md

.dockerignore에 지정된 파일은 빌드 컨텍스트에서 제외된다.

  • .dockerignore는 Image 안에 복사하지 않을 파일을 정하는 것뿐만 아니라,
  • Docker Daemon에게 전달되는 빌드 컨텍스트 자체를 줄이는 역할을 한다.

빌드 컨텍스트가 줄어들면 다음과 같은 장점이 있다.

  • Docker Daemon에게 전달할 파일 수가 줄어든다.
  • 빌드 속도가 빨라질 수 있다.
  • 불필요한 파일이 Image에 포함될 가능성이 줄어든다.
  • 민감한 파일이 Image 안으로 들어가는 실수를 줄일 수 있다.
  • Cache 판단에 영향을 주는 파일 범위를 줄일 수 있다.
더 자세히 보기: .dockerignore와 .gitignore는 다르다

.dockerignore.gitignore는 비슷해 보이지만 목적이 다르다.

구분.gitignore.dockerignore
대상GitDocker Build Context
목적Git에 추적하지 않을 파일 제외Docker 빌드 컨텍스트에서 제외
영향커밋 대상 결정Image 빌드에 전달되는 파일 결정
예시node_modules, dist, .envnode_modules, dist, .git, .env

.gitignore에 있는 파일이라고 해서 Docker Build Context에서 자동으로 제외되는 것은 아니다.
Docker 빌드에서 제외하려면 .dockerignore에도 명시해야 한다.

Git에 올리지 않는 파일과 Docker 빌드에 포함하지 않을 파일은 목적이 다르다.
따라서 Docker 빌드 품질을 위해서는 .dockerignore를 별도로 관리해야 한다.


8. 개발용 Dockerfile과 운영용 Dockerfile은 목적이 다르다

Dockerfile을 작성할 때는 먼저 목적을 구분해야 한다.

개발용 Dockerfile과 운영용 Dockerfile은 같은 형태일 필요가 없다.
왜냐하면 두 환경에서 중요한 기준이 다르기 때문이다.

개발용 Dockerfile은 빠른 피드백과 편의성이 중요하고,
운영용 Dockerfile은 작은 이미지, 명확한 실행, 보안 표면 최소화가 중요하다.

개발용 Dockerfile

개발 환경에서는 다음이 중요하다.

  • 코드 수정 시 즉시 반영
  • 핫 리로드
  • 디버깅 편의성
  • 로컬 파일과 컨테이너 파일 동기화
  • volume 사용

예를 들어 개발용 Node.js Dockerfile은 다음처럼 작성할 수 있다.

개발용 Dockerfile 예시
1
개발용 Dockerfile 예시
FROM node:20-alpine
 
WORKDIR /app
 
COPY package.json package-lock.json ./
 
RUN npm ci
 
COPY . .
 
CMD ["npm", "run", "dev"]

그리고 docker compose에서는 소스 코드를 volume으로 연결할 수 있다.

개발용 docker-compose.yml 예시
1
개발용 docker-compose.yml 예시
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
    ports:
      - "5173:5173"
    volumes:
      - .:/app
      - /app/node_modules
    command: npm run dev

이 구조에서는 로컬 소스 코드 변경이 컨테이너 내부에 바로 반영될 수 있다.

다만 개발용 구성은 운영용으로 그대로 사용하기 적합하지 않을 수 있다.

  • 소스 코드 전체가 volume으로 연결된다.
  • 개발 서버가 실행된다.
  • 디버깅 도구나 개발 의존성이 포함될 수 있다.
  • 이미지 크기나 보안보다 개발 편의성을 우선한다.

운영용 Dockerfile

운영 환경에서는 다음이 중요하다.

  • 작은 이미지 크기
  • 명확한 실행 명령
  • 불필요한 파일 제거
  • devDependencies 제외
  • 보안 표면 최소화
  • 재현 가능한 빌드

운영용 Dockerfile은 보통 다음처럼 작성할 수 있다.

운영용 Dockerfile 예시
1
운영용 Dockerfile 예시
FROM node:20-alpine
 
WORKDIR /app
 
COPY package.json package-lock.json ./
 
RUN npm ci --omit=dev
 
COPY . .
 
CMD ["node", "server.js"]

프론트엔드 정적 빌드처럼 빌드 단계와 실행 단계를 분리해야 한다면 multi-stage build를 사용할 수 있다.

Multi-stage build 예시
1
Multi-stage build 예시
FROM node:20-alpine AS builder
 
WORKDIR /app
 
COPY package.json package-lock.json ./
 
RUN npm ci
 
COPY . .
 
RUN npm run build
 
FROM nginx:alpine
 
COPY --from=builder /app/dist /usr/share/nginx/html
 
CMD ["nginx", "-g", "daemon off;"]

이 구조에서는 Node.js로 빌드만 수행하고,
최종 Image에는 빌드 결과물과 Nginx만 포함된다.

개발용 Dockerfile은 개발 편의성을 위해 존재하고,
운영용 Dockerfile은 안정적이고 가벼운 실행 환경을 만들기 위해 존재한다.

더 자세히 보기: Multi-stage build를 사용하는 이유

Multi-stage build는 하나의 Dockerfile 안에서 여러 빌드 단계를 사용하는 방식이다.

Multi-stage build 개념
1
Multi-stage build 개념
builder stage
└── 소스 코드, devDependencies, 빌드 도구 포함
    └── npm run build 실행
 
production stage
└── 실행에 필요한 결과물만 복사
    └── 작은 최종 Image 생성

이 방식의 장점은 다음과 같다.

  • 빌드 도구를 최종 Image에 포함하지 않아도 된다.
  • devDependencies를 최종 Image에서 제거할 수 있다.
  • 최종 Image 크기를 줄일 수 있다.
  • 운영 환경에 필요한 파일만 명확히 포함할 수 있다.
  • 보안 표면을 줄일 수 있다.

Multi-stage build는 빌드에 필요한 환경과 실행에 필요한 환경을 분리하는 방법이다.


9. 좋은 Dockerfile은 자주 바뀌는 것과 잘 바뀌지 않는 것을 분리한다

Dockerfile 최적화의 핵심은 단순하다.

잘 바뀌지 않는 것은 위에 두고, 자주 바뀌는 것은 아래에 둔다.

왜냐하면 Dockerfile은 위에서 아래로 실행되고,
앞 단계의 Cache가 깨지면 뒤 단계도 영향을 받을 수 있기 때문이다.

Node.js 프로젝트에서는 보통 다음 기준으로 나눌 수 있다.

변경 빈도파일Dockerfile에서의 위치
낮음base image, 시스템 패키지위쪽
낮음package.json, package-lock.json소스보다 먼저 복사
중간의존성 설치 결과package 파일 복사 후 실행
높음src, public, config 등 소스 코드의존성 설치 후 복사
높음README, 테스트 결과, 로컬 파일보통 .dockerignore로 제외

그래서 좋은 Dockerfile은 다음 흐름을 가진다.

Cache를 고려한 Dockerfile 구조
1
Cache를 고려한 Dockerfile 구조
FROM node:20-alpine
 
WORKDIR /app
 
COPY package.json package-lock.json ./
 
RUN npm ci
 
COPY . .
 
CMD ["npm", "start"]

이 구조의 의도는 명확하다.

Dockerfile 순서의 의도
1
Dockerfile 순서의 의도
1. Base Image 선택
   → 자주 바뀌지 않음
 
2. 의존성 정의 파일 복사
   → package.json, lock file이 바뀔 때만 변경
 
3. 의존성 설치
   → 의존성 파일이 같으면 Cache 재사용
 
4. 소스 코드 복사
   → 자주 바뀌는 파일은 나중에 반영
 
5. 실행 명령 지정
   → 컨테이너 실행 방식 정의

결국 좋은 Dockerfile은 단순히 동작하는 Dockerfile이 아니다.

좋은 Dockerfile은 자주 바뀌는 것과 잘 바뀌지 않는 것을 분리해서,
Docker Build Cache를 잘 활용하는 Dockerfile이다.

이 관점으로 Dockerfile을 보면 package.json만 먼저 복사하는 이유도 자연스럽게 이해된다.

의존성 파일이 바뀌지 않았다면 의존성 설치 Layer를 재사용하고,
자주 바뀌는 소스 코드는 그 이후에 복사해서 필요한 부분만 다시 빌드하기 위함이다.

즉, Dockerfile 최적화의 핵심은 명령어 암기가 아니라 Layer와 Cache의 흐름을 설계하는 것이다.