Docker Compose build와 image 설정 차이점 및 VPS 적용
Docker Compose에서 image 키는 레지스트리 이미지를 사용하고 build는 로컬에서 생성합니다. Dockerfile을 수정해도 docker compose up이 반영하지 않는 이유와 기존 이미지를 무시하고 강제로 다시 빌드하는 해결 방법을 설명합니다.
Docker Compose의 build와 image: 요약
Docker Compose 파일에서 image:은 레지스트리에서 가져올 이미지의 이름을 지정하며, build:은 Dockerfile을 사용하여 이 머신에서 이미지를 빌드하도록 Compose에 지시합니다. image:만 설정하면 Compose는 해당 태그를 가져와 실행합니다. build:만 설정하면 Compose는 로컬에서 이미지를 빌드하고, 프로젝트 이름과 서비스 이름을 조합하여 이미지 이름을 생성합니다. 두 항목을 모두 설정하면 Compose는 로컬에서 빌드한 뒤 image:에 지정된 이름으로 태그를 붙입니다. 이것이 바로 원하는 이름으로 이미지를 빌드하고 푸시하는 방법입니다.
이것이 두 설정의 차이점 전부입니다. 아래 내용은 서버 운영 시 어떤 의미를 갖는지 설명합니다. 이 설명은 Docker Engine과 Compose 플러그인이 이미 설치되어 있다는 가정하에 작성되었습니다. VPS에서 Docker 실행하기에서 해당 설치 과정을 다룹니다.
세 가지 전체 형식
게시된 태그를 가져와 실행합니다. 이 과정에서 Dockerfile은 사용되지 않습니다.
services:
web:
image: nginx:1.27
restart: unless-stopped
ports:
- "80:80"현재 디렉터리의 Dockerfile에서 빌드합니다. FROM에 명시된 베이스 이미지를 제외하고는 아무것도 가져오지 않습니다.
services:
web:
build: .
restart: unless-stopped
ports:
- "80:80"로컬에서 빌드하고 결과물에 태그를 지정합니다. 이후 docker compose push을 사용하여 해당 태그를 레지스트리로 전송할 수 있습니다.
services:
web:
build:
context: .
dockerfile: Dockerfile
image: registry.example.com/acme/web:1.4.2
restart: unless-stopped
ports:
- "80:80"context은 빌더로 전송되는 디렉터리입니다. dockerfile은 해당 컨텍스트를 기준으로 해석되므로, dockerfile: docker/prod.Dockerfile과 함께 context: .를 사용하는 것은 정상적이며 올바른 방식입니다. 각 서비스 컨테이너의 이미지 이름과 이미지 ID를 확인하려면 docker compose images을 실행하십시오. 이는 실제로 작성한 형식이 무엇인지 확인하는 가장 빠른 방법입니다.
Dockerfile을 수정했는데 왜 docker compose up이 다시 빌드하지 않습니까?
up는 이미지가 최신 상태인지가 아니라, 이미지가 존재하는지만 확인하기 때문입니다.
Compose가 build: 섹션이 있는 서비스를 시작할 때, 로컬 이미지 저장소에서 해당 이미지를 찾습니다. 만약 해당 이름의 이미지가 이미 존재한다면, Compose는 그 이미지를 사용합니다. Dockerfile을 읽거나, 소스 파일을 비교하거나, 타임스탬프를 확인하지 않습니다. Compose 명세는 이를 pull_policy 속성으로 규정하며, 기본 동작은 이미지가 없을 때만 빌드하는 것입니다. 이미지가 존재하면 충분하다고 간주합니다.
따라서 app.py를 수정하고 docker compose up -d을 실행하면, Compose는 컨테이너가 실행 중이라고 보고하고 이전 코드를 서비스합니다. 실패한 것이 없으므로 경고도 발생하지 않습니다. 이는 Compose에서 "변경 사항이 적용되지 않았다"는 가장 흔한 보고 사례입니다. Compose가 컨테이너 이름 옆에 출력하는 상태 단어를 보면 알 수 있습니다. Compose가 교체한 컨테이너는 recreated 또는 started로 표시되지만, 그대로 두기로 결정한 컨테이너는 running으로 표시됩니다.
두 가지 확인 방법으로 이를 해결할 수 있습니다. docker compose images은 각 컨테이너가 사용하는 이미지 ID를 출력하므로, 배포 전후를 비교해 보십시오. docker image ls에는 CREATED 열이 있으며, 마지막 커밋보다 이전에 생성된 이미지는 배포 스크립트의 출력 내용과 관계없이 오래된 이미지입니다.
리빌드를 강제하는 플래그
docker compose up -d --build는 먼저 빌드를 수행한 뒤, 이미지가 변경된 컨테이너를 재생성합니다. 대부분의 사용자가 찾는 플래그입니다.docker compose build web은 특정 서비스 하나만 빌드하고 아무것도 시작하지 않습니다. 이후docker compose up --no-deps -d web을 실행하면 해당 컨테이너만 교체되며 나머지 스택은 그대로 유지됩니다.docker compose build --no-cache web는 모든 캐시된 레이어를 폐기하고 첫 번째 명령어부터 다시 빌드합니다.docker compose build --pull은FROM에 정의된 베이스 이미지의 최신 버전을 가져오려 시도합니다. 따라서node:22와 같이 유동적인 태그를 사용할 경우, 지난 3월에 다운로드한 복사본 대신 현재 시점의 내용을 가져오게 됩니다.docker compose up -d --force-recreate은 이미 사용 중인 이미지로 컨테이너를 재생성합니다. 빌드는 수행하지 않습니다.--build을 의도했을 때 이 플래그를 사용하는 것은 흔히 겪는 막다른 길입니다.
빌드 여부를 파일 내에서 결정할 수도 있습니다. Compose 명세에 따르면 pull_policy: build은 Compose가 이미지를 빌드하며, 이미 존재하더라도 다시 빌드함을 의미합니다. 이 경우 모든 up마다 빌드 비용이 발생하므로, 노트북 환경에서는 유용할 수 있으나 서버 환경에서는 거의 권장되지 않습니다.
services:
web:
build: .
image: registry.example.com/acme/web:dev
pull_policy: build알아두어야 할 상호작용이 하나 더 있습니다. docker compose pull은 build 섹션이 있는 서비스에 대해서도 이미지 풀을 시도하며, 풀에 실패하면 이미지를 빌드해야 한다고 알립니다. --ignore-buildable를 전달하면 해당 서비스들을 조용히 건너뜁니다.
빌드 캐시가 배포 시간에 미치는 영향
Dockerfile의 각 명령어는 레이어를 생성하며, 빌더는 해당 명령어와 입력값이 변경되지 않았을 때 캐시된 레이어를 재사용합니다. COPY의 경우, 입력값은 복사되는 파일의 내용입니다. 일단 하나의 레이어에서 캐시 적중이 실패하면, 그 이후의 모든 레이어는 다시 빌드됩니다. 각 레이어는 이전 레이어가 생성한 파일 시스템을 기반으로 빌드되기 때문입니다.
이 규칙 하나가 배포에 수 초가 걸릴지 수 분이 걸릴지를 결정합니다. Dockerfile의 명령어를 거의 변경되지 않는 내용부터 커밋마다 변경되는 내용 순으로 배치하십시오.
FROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
CMD ["node", "server.js"]npm ci가 COPY . . 위에 위치하면, 소스 파일을 수정해도 설치 레이어는 캐시된 상태로 유지되며 빌드는 복사 단계부터 재개됩니다. 이 두 줄의 순서를 바꾸면 단 한 글자만 수정해도 모든 의존성을 다시 설치하게 됩니다. COPY . .이 npm ci이 빌드되는 기반 레이어를 무효화하기 때문입니다. 동일한 원리가 pip install -r requirements.txt과 go mod download에도 적용됩니다.
수정 사항이 오래된 레이어에 가려져 있다고 의심될 때 --no-cache은 적절한 도구입니다. 하지만 이를 기본값으로 사용하는 것은 좋지 않습니다. Dockerfile의 순서 최적화를 통해 얻을 수 있는 캐시 재사용의 이점을 모두 버리기 때문입니다.
이미지 설정과 Compose가 덮어쓸 수 있는 한 가지 요소가 있습니다. Dockerfile의 CMD은 이미지가 기본적으로 실행하는 명령어이며, 서비스의 command: 키는 이를 대체합니다. 명령어와 entrypoint의 상호작용 방식을 이해하는 것이 중요합니다. Compose의 덮어쓰기 설정으로 인해 새로 빌드된 이미지가 이전 이미지와 완전히 동일하게 동작할 수 있기 때문입니다.
빌드 컨텍스트와 .dockerignore
context: .은 해당 디렉터리를 패키징하여 첫 번째 명령이 실행되기 전에 빌더로 전송함을 의미합니다. .git를 포함하여 소스 코드 옆에 보관 중인 모든 데이터 디렉터리까지 함께 전송됩니다. 변경 사항이 없는 프로젝트임에도 빌드 단계 중 컨텍스트 전송 단계에서 멈춘다면, 이는 컨텍스트가 너무 크다는 신호입니다.
컨텍스트 루트에 위치한 .dockerignore 파일은 해당 경로들을 전송 대상에서 제외합니다. 문법은 .gitignore과 유사합니다.
.git
node_modules
*.log
data/
.env이를 통해 두 가지 이점을 얻을 수 있습니다. 첫째, 전송 데이터가 줄어들어 모든 빌드가 더 빠르게 시작됩니다. 둘째, COPY . .이 실수로 .env을 이미지 내부로 복사하는 일을 방지하여, 이미지를 내려받은 누군가가 이를 읽어내는 보안 사고를 막을 수 있습니다.
시간이 지날수록 빌드가 느려지는 주된 원인은 바인드 마운트입니다. 네임드 볼륨은 프로젝트 디렉터리 외부에 존재하지만, ./data:/var/lib/postgresql/data와 같은 바인드 마운트는 빌드 컨텍스트 내부에 위치합니다. 따라서 데이터베이스가 커질수록 매주 빌드 속도가 느려집니다. .dockerignore에 한 줄을 추가하면 이 문제를 해결할 수 있습니다. 네임드 볼륨과 바인드 마운트 비교에서 더 자세한 내용을 다룹니다.
빌드 인자(Build arguments) 역시 이와 유사하지만 규모가 작은 위험을 내포합니다. args:을 통해 전달된 값은 이미지 히스토리에 남으므로 이미지를 가진 누구나 확인할 수 있습니다. 따라서 버전 번호와 같은 정보만 사용하고, 토큰은 절대 포함하지 마십시오. Compose의 환경 변수 파일과 보안 정보에서 자격 증명을 어디에 보관해야 하는지 설명합니다.
VPS에서 직접 빌드해야 할까요, 아니면 외부에서 빌드한 뒤 가져와야 할까요?
트래픽을 처리하는 서버에서 직접 빌드하는 것은 가장 짧은 경로이기에 기본값으로 사용됩니다. git pull를 실행하고 docker compose up -d --build을 실행하는 방식입니다. 아직 아무도 의존하지 않는 작은 서버라면 이 방식도 괜찮습니다. 하지만 측정 가능한 두 가지 이유와 최악의 상황에서 드러나는 한 가지 이유 때문에 이 방식은 더 이상 권장되지 않습니다.
메모리. 빌드 과정에서는 컴파일러와 번들러가 실행 중인 애플리케이션과 함께 동작하며, 대부분의 스택에서 이 도구들은 메모리를 가장 많이 소모합니다. 1 GB 용량의 VPS에서 JavaScript 번들러나 Rust 컴파일러는 보통 시스템에서 가장 큰 프로세스가 됩니다. 커널의 메모리가 부족해지면 가장 큰 프로세스를 강제로 종료합니다. 빌드가 Killed와 함께 종료 코드 137을 반환하며 멈추거나, 아니면 데이터베이스가 종료되어 배포 도중 사이트가 다운될 수 있습니다. dmesg -T | grep -i oom를 확인하면 프로세스 이름과 함께 종료 로그가 출력되므로, 추측할 필요 없이 어떤 프로세스가 종료되었는지 알 수 있습니다.
디스크. 모든 빌드는 레이어를 남기며, 빌더는 이미지와 별도로 자체 캐시를 유지합니다. docker system df을 실행하면 두 가지 모두 확인할 수 있는데, 빌드 캐시 행은 계속 커지기만 합니다. 사용하지 않는 이미지는 docker image prune로, 캐시된 레이어는 docker builder prune로 정리하십시오. 디스크가 가득 차면 빌드만 멈추는 것이 아닙니다. 데이터베이스의 쓰기 작업도 중단되며, 이 장애는 느린 배포보다 훨씬 큰 비용을 초래합니다.
재현성. 서버에서 빌드한 이미지는 해당 서버에만 존재합니다. 롤백을 하려면 이전 커밋을 체크아웃하고 다시 빌드해야 하는데, 베이스 태그나 패키지 미러가 변경되었을 수 있으므로 이전과 동일한 결과물이 나온다는 보장이 없습니다. 외부에서 빌드하고 태그를 푸시하면 롤백은 단순히 설정을 수정하는 작업이 됩니다. image:를 이전 태그로 변경하고 docker compose up -d을 실행하기만 하면 됩니다.
가장 안정적인 구성은 명확합니다. 지속적 통합(CI) 환경에서 빌드를 수행하고 registry.example.com/acme/web:<git-sha>로 푸시합니다. VPS의 Compose 파일은 image:를 사용하며 build: 키는 아예 포함하지 않습니다. 이렇게 하면 배포는 메모리를 거의 사용하지 않는 두 개의 명령어로 완료됩니다.
docker compose pull
docker compose up -d서버에서 docker login registry.example.com를 한 번 실행하면, 이후부터 Compose가 비공개 태그를 가져올 수 있습니다.
빌드 섹션은 삭제하지 말고 개발용으로 남겨두되, 직접 이름을 정한 파일에 보관하십시오.
# compose.dev.yaml
services:
web:
build:
context: .
pull_policy: builddocker compose -f compose.yaml -f compose.dev.yaml up -d --build해당 파일의 이름은 compose.override.yaml이 아닌 compose.dev.yaml로 지정하십시오. Compose는 오버라이드 파일이 존재하면 자동으로 불러오기 때문에, 실수로 오버라이드 파일을 서버에 복사해두면 다시 서버에서 빌드가 시작될 수 있습니다. 여러 Compose 파일 계층화하기에서 각 키가 어떻게 병합되는지 설명합니다.
다른 환경에서 빌드할 때 발생하는 아키텍처 문제
이미지는 빌드된 CPU 아키텍처 정보를 포함합니다. Apple Silicon 노트북에서 빌드하여 푸시한 뒤 x86_64 VPS에서 해당 태그를 풀(pull)하면, Docker는 요청한 이미지 플랫폼이 감지된 호스트 플랫폼과 일치하지 않는다는 경고를 출력합니다. 이후 프로세스는 exec format error 오류와 함께 종료되는데, 이는 바이너리가 손상된 것처럼 보이지만 실제로는 그렇지 않습니다. 타겟 아키텍처를 명시하여 빌드하십시오.
docker buildx build --platform linux/amd64 \
-t registry.example.com/acme/web:1.4.2 --push .노트북이 x86인데 x86이 아닌 ARM VPS에서 실행하는 경우에도 동일한 불일치가 발생합니다. 배포할 아키텍처에서 CI가 직접 빌드하도록 설정하면 이러한 문제를 방지할 수 있습니다.
배포 후 확인 사항
docker compose images은 실행 중인 모든 컨테이너의 이미지와 태그를 출력합니다. 변경된 이미지 ID는 새 빌드가 서비스 중이라는 증거입니다.docker compose config는 변수 치환이 완료된 병합 파일을 출력하므로, 실행 전에 Compose가 사용할 최종 이미지 이름을 확인할 수 있습니다.docker compose logs -f web은 교체 후 첫 30초 동안 확인하십시오. 시작 후 즉시 종료되는 컨테이너는 계속 재시작 루프를 돌게 되며, 확인하지 않으면 이 루프는 조용히 진행됩니다.docker image ls은 CREATED 열을 보여줍니다. 마지막 커밋보다 오래된 이미지는 다시 빌드되지 않은 것입니다.
이 확인 작업에 사용되는 파일을 아직 작성 중이라면, VPS에서 Compose 파일의 기초에서 관련 키를 다루고 있으며, Compose 명령어 치트 시트에서 나머지 하위 명령어를 확인할 수 있습니다.
FAQ
하나의 서비스에서 build와 image를 동시에 사용할 수 있습니까?
네, 직접 빌드하는 프로젝트에서는 이것이 일반적인 설정입니다. Compose는 build: 섹션의 내용을 빌드하고 그 결과물에 image: 값을 태그로 지정합니다. docker compose push가 레지스트리로 전송하고 다른 머신이 풀(pull)하는 대상이 바로 이 태그입니다. image: 키가 없어도 Compose는 빌드를 수행하지만, 이미지 이름을 프로젝트와 서비스 이름으로 지정하며 해당 속성이 누락되어 이미지를 푸시할 수 없다는 경고를 표시합니다.
docker compose up 명령이 Dockerfile 변경 사항을 반영하지 않는 이유는 무엇입니까?
up은 해당 이름의 이미지가 존재하는지만 확인하기 때문입니다. 이미지가 존재하면 Compose는 이를 시작할 뿐, Dockerfile이나 소스 파일과 비교하지 않습니다. docker compose up -d --build을 실행하거나, docker compose build web을 실행한 뒤 docker compose up --no-deps -d web를 사용하여 특정 서비스만 교체하십시오. 서비스에 pull_policy: build을 설정하면 모든 up 실행 시 재빌드가 수행되므로 개발 환경에 적합합니다.
--build와 --force-recreate의 차이점은 무엇입니까?
--build는 이미지를 다시 빌드한 다음, 이미지가 변경된 컨테이너를 재생성합니다. --force-recreate은 기존 이미지를 사용하여 컨테이너를 재생성하므로 코드 변경 사항을 반영할 수 없습니다. 소스나 Dockerfile을 변경했다면 --build 플래그를 사용해야 합니다. --force-recreate는 컨테이너 자체를 초기화하기 위한 용도이며, 예를 들어 동일한 이미지를 유지하면서 쓰기 가능한 레이어를 비울 때 사용합니다.
Docker 이미지는 VPS에서 빌드해야 합니까, 아니면 다른 곳에서 빌드해야 합니까?
다른 곳에서 빌드하고 서버가 트래픽을 처리 중일 때는 태그를 풀(pull)하는 방식을 권장합니다. 빌드 과정은 애플리케이션과 메모리 자원을 두고 경쟁하며, 소규모 VPS에서는 커널이 가장 큰 프로세스를 강제 종료하는데 이때 빌드 프로세스나 데이터베이스가 종료될 수 있습니다. 또한 빌드는 디스크에 캐시를 남기며, 이를 자동으로 정리해 주는 기능은 없습니다. 사용자가 없는 소규모 프로젝트라면 서버에서 빌드해도 괜찮으며, 나중에 build: 섹션을 개발용 Compose 파일로 분리하면 쉽게 이전할 수 있습니다.
Docker 빌드 캐시가 디스크를 가득 채우지 않게 하려면 어떻게 해야 합니까?
docker system df을 실행하여 이미지와 빌드 캐시가 차지하는 용량을 확인하십시오. docker builder prune은 캐시된 레이어를 제거하고, docker image prune는 이전 빌드에서 남은 댕글링(dangling) 이미지를 제거합니다. 두 명령어에 -a을 추가하면 더 강력하게 정리하며 다음 빌드는 처음부터 다시 시작됩니다. 서버에서 docker system prune -af --volumes을 예약 작업으로 설정하지 마십시오. --volumes는 현재 사용 중이지 않은 모든 볼륨을 삭제하는데, 유지보수를 위해 중단한 스택의 데이터베이스가 바로 그런 볼륨에 저장되어 있기 때문입니다.