유틸리티 컨테이너란 무엇인가?
기존 애플리케이션 컨테이너를 기억해보자.
- 애플리케이션 코드 + 실행 환경이 포함된다.
- Dockerfile을 빌드하고 docker run으로 실행하면 애플리케이션이 시작된다.
- Node API, React SPA, DB 등
유틸리티 컨테이너란?
정식 명칭은 아니다.
- 다만 특정 환경만 포함하는 컨테이너다. (etc: Node.js, PHP, Python 등)
- 애플리케이션을 시작하지 않고, 특정 작업/명령을 실행하기 위해 사용한다.
- 이미지 이름 뒤에 실행할 명령을 추가하는 방식이다.
왜 사용하는가?
아래와 같은 상황을 생각해보자. 새 프로젝트를 시작할 때를 기준으로 한다.
- Node.js 프로젝트 생성 시 npm init 명령이 필요하다.
- 하지만 npm은 Node.js가 설치되어 있어야만 사용 가능하다.
- PHP/Laravel(써본적 없음)을 사용하는 프로젝트의 경우 더 복잡한 로컬에서의 설정이 필요하다.
이는 곧 Docker의 본질과 충돌한다.
호스트 머신에 툴을 전역적으로 설치하지 않는다.
컨테이너에서 애플리케이션을 실행할 수 있지만
초기 프로젝트 생성에는 호스트에 도구 설치가 필요한 딜레마가 발생한다.
위와 같은 문제를 유틸리티 컨테이너가 해결할 수 있다.
그냥 호스트에 설치하면 안되나? 아래의 비교 표를 보자.
| 상황 | 호스트 설치 | 유틸리티 컨테이너 |
| 버전 충돌(Node 14 vs 18) | nvm 등 추가 도구가 필요하다 | 이미지 태그로 해결 가능하다 |
| 팀원 간 환경 통일 | X | O |
| CI/CD | 별도 설정 필요 | 동일한 컨테이너 사용으로 가능 |
| 프로젝트별 도구 버전 | 전역 설치시 충돌 | 프로젝트별로 독립적이다. |
컨테이너에서 명령을 실행하는 방법들
먼저 컨테이너에서 명령을 실행하는 다양한 방법들에 대해서 알아본다.
이는 곧 유틸리티 컨테이너를 사용하기 위한 기초가 되기 때문이다.
1. 기본적으로 컨테이너를 실행하는 방법은 아래와 같다. 그냥 terminal에서 바로 해볼 수 있다.
# 1. Node 이미지 실행 (즉시 종료됨 - 입력을 기다리는 이미지이기 때문)
docker run node
# 2. 인터렉티브 모드로 실행
docker run -it node
# → Node REPL이 열림. 1+1 같은 계산 가능
# → Ctrl+C 두 번으로 종료
# 3. 중지된 컨테이너 확인
docker ps -a
# 4. 중지된 컨테이너 정리
docker container prune
2. 실행 중인 컨테이너에서 명령을 실행하는 방법은 아래와 같다. exec을 사용하는 것이다.
# 1. 컨테이너를 백그라운드 + 인터렉티브 모드로 실행
docker run -it -d node
# 2. 실행 중인 컨테이너 확인
docker ps
# -> 자동 생성된 이름을 확인할 수 있다.
# 3. 실행 중인 컨테이너에서 npm init 실행
docker exec -it <컨테이너_이름> npm init
# -> 패키지 이름, 버전 등의 질문이 나온다.
# -> Enter를 계속 눌러 기본값을 사용할 수 있다.
# 4. 컨테이너 중지
docker stop <컨테이너_이름>
exec의 동작 원리는 아래와 같다.

다양한 상황에서 사용이 가능하다.
# 로그 파일 확인
docker exec <container> cat /var/log/app.log
# 환경 변수 확인
docker exec <container> env
# 프로세스 목록 확인
docker exec <container> ps aux
# 네트워크 상태 확인
docker exec <container> netstat -tlnp
3. docker run에서 기본 명령을 오버라이드하는 방법도 존재한다.
docker run [옵션] 이미지명 [오버라이드할_명령]
# Node 이미지의 기본 명령(node REPL) 대신 npm init 실행
docker run -it node npm init
# → npm init 프롬프트가 나타남
# → 질문에 답하면 package.json이 컨테이너 내부에 생성됨
# → 완료 후 컨테이너 자동 종료
간단하게 표로 정리하면 아래와 같다.
| 명령어 | 용도 | 특징 |
| docker run -it node | 기본 명령으로 컨테이너 시작 | Node REPL 실행 |
| docker run -it node npm init | 기본 명령 오버라이드 | npm init 실행 후 종료 |
| docker exec -it <container_name> npm init | 실행 중인 컨테이너에 추가 명령 | 메인 프로세스 유지 |
이걸 왜 나열을 했냐면 유틸리티 컨테이너의 핵심 사용법이 오버라이드 방식이기 때문이다.
- 유틸리티 컨테이너 = 환경 이미지 + 명령 오버라이드 기법
docker run --rm -it -v $(pwd):/app -w /app node npm init
─────────────────────────────── ───────────
환경(node 이미지) 오버라이드할 명령
유틸리티 컨테이너를 직접 구축해보자.
- 첫번째 유틸리티 컨테이너 구축
1.1 프로젝트를 정의할 폴더를 만들어주자.
mkdir utility-container-demo
cd utility-container-demo
1.2 Dockerfile을 작성한다.
FROM node:14-alpine
WORKDIR /app
- cmd를 지정하면 해당 명령만 실행되도록 제한되므로 지정하지 않았다.
- 유틸리티 컨테이너는 다양한 명령을 실행할 수 있어야 한다.
- npm init, install, test 등 모든 npm 명령을 실행할 수 있어야 한다.
1.3 이미지를 빌드한다.
# 이미지 빌드
docker build -t node-util .
# 빌드 결과 확인
docker images | grep node-util

1.4 유틸리티 컨테이너를 실행해보자.
docker run -it -v $(pwd):/app node-util npm init
아래와 같이 기본 실행을 하게 되면 컨테이너 내부에서만 파일이 생성되므로 우리가 하고자 한 목적에 맞지 않는다.
docker run -it node-util npm init
이 말은 즉슨, 컨테이너 내부에서 npm init이 실행되고, 질문에 답을 하면 컨테이너 내부 /app 폴더에 package.json이 만들어지는데
- 문제는 컨테이너가 종료되는 순간, 그 컨테이너 내부에 있는 파일들은 격리된 상태로 갇혀있게 된다.
- 따라서 내 컴퓨터(Host) 폴더에는 아무 파일도 없게 된다..
- 우리가 이걸 하는 이유가 내 컴퓨터에서 개발을 하기 위한 package.json을 만들고 싶었던 건데, 정작 호스트 폴더에는 아무것도 없으니 말짱 도루묵인 거다.
1.4 바로 아래 있는 명령어를 다시 봐보자.
-v $(pwd):/app
이 옵션이 Bind Mount 역할을 해주는 건데
- 앞에서 봤던 내용이지만 다시 정리하면, 내 컴퓨터의 현재 폴더 $(pwd)와 컨테이너 내부 폴더(/app)를 실시간으로 공유하는 거다.
- 이후 컨테이너가 /app에 package.json을 만들면, 내 컴퓨터의 현재 폴더에도 동시에 package.json이 생성된다.
- 따라서 컨테이너가 종료되어 사라져도, 파일은 내 컴퓨터에 남아있게 된다.
docker run -it -v $(pwd):/app node-util npm init ─╯
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.
See `npm help init` for definitive documentation on these fields
and exactly what they do.
Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.
Press ^C at any time to quit.
package name: (app)
version: (1.0.0)
description:
entry point: (index.js)
test command:
git repository:
keywords:
author:
license: (ISC)
About to write to /app/package.json:
{
"name": "app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
Is this OK? (yes)
간단하게 실행 흐름을 정리해보면 아래 그림과 같다.

1.5 위에서 만든 유틸리티 컨테이너를 이용해 새로운 프로젝트를 만들어보자.
╰─ docker run -it -v $(pwd):/app node-util npm init ─╯
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.
See `npm help init` for definitive documentation on these fields
and exactly what they do.
Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.
Press ^C at any time to quit.
package name: (app)
version: (1.0.0)
description:
entry point: (index.js)
test command:
git repository:
keywords:
author:
license: (ISC)
About to write to /app/package.json:
{
"name": "app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
Is this OK? (yes)
╭─░▒▓ ~/Downloads/my-project ▓▒░··········································································································································░▒▓ ✔ 30s system 22:33:18 ▓▒░─╮
╰─ cat package.json ─╯
{
"name": "app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
cat package.json을 통해 컨테이너가 종료된 후, 내 컴퓨터 터미널에서 파일을 열었는데 내용이 그대로 보이는 것을 확인할 수 있다.
- 내 컴퓨터에서 node를 깔지 않아도 된다는 것에서 매우 유용한 것 같다.
- 컨테이너가 Node.js 환경을 제공하고, 바인드 마운트가 결과물을 호스트(내 컴퓨터)로 전달한다.
ENTRYPOINT 활용
그런데 여기도 문제점이 있다.
# 매번 npm을 타이핑해야 함
docker run -it -v $(pwd):/app node-util npm init
docker run -it -v $(pwd):/app node-util npm install
docker run -it -v $(pwd):/app node-util npm install express --save
docker run -it -v $(pwd):/app node-util npm test
매번 npm 타이핑을 해야한다.
추가로 보안 문제도 있다.
# 이런 위험한 명령도 실행 가능하다..
docker run -it -v $(pwd):/app node-util rm -rf /app/*
# → 바인드 마운트 때문에 호스트의 파일도 삭제됨!
어 그럼 CMD로 해결할 수 있나?
- 아쉽지만 CMD는 docker run의 인자가 CMD를 '대체'하게 된다.
- 따라서 rm -rf를 해버려도 기존 CMD를 대체해서 실행이 되고 만다.
따라서 ENTRYPOINT를 정의한다.
FROM node:14-alpine
WORKDIR /app
ENTRYPOINT [ "npm" ]
- 이는 docker run의 인자를 ENTRYPOINT(npm) 뒤에 추가시켜준다.
따라서 rm -rf를 run에서 인자로 넣어도 npm rm -rf로 들어가게 되어 에러를 뱉게 된다.
CMD, ENTRYPOINT를 적절히 활용해서 편리하면서도 보안을 지키며 사용할 수 있다.
FROM node:14-alpine
WORKDIR /app
ENTRYPOINT [ "npm" ]
CMD [ "start" ]
docker run myimage → npm start (ENTRYPOINT + CMD)
docker run myimage test → npm test (CMD가 test로 대체)
docker run myimage install express → npm install express
인자 없이 실행하면 기본 동작 정의된 대로 동작하고, 유연하게 다른 명령들도 실행할 수 있다.
다시 이를 적용하기 위해 빌드를 하자.
# 새 이름으로 빌드 (mynpm)
docker build -t mynpm .
# 이미지 확인
docker images | grep mynpm
아래와 같이 ENTRYPOINT가 적용된 실행을 해볼 수 있다.
# 기존 방식 (node-util)
docker run -it -v $(pwd):/app node-util npm init
docker run -it -v $(pwd):/app node-util npm install express --save
# 새 방식 (mynpm) - npm을 생략
docker run -it -v $(pwd):/app mynpm init
docker run -it -v $(pwd):/app mynpm install express --save
# ENTRYPOINT가 npm이므로 다른 명령 실행 불가
docker run -it -v $(pwd):/app mynpm rm -rf /app/*
# → "npm rm -rf /app/*" 로 해석됨 → npm 에러 발생 → 안전하다.
Docker Compose 사용
물론 지금도 불편하다.
매번 이런 긴 명령어들을 타이핑해야 되기 때문이다.
docker run -it -v $(pwd):/app mynpm init
docker run -it -v $(pwd):/app mynpm install
즉, 위의 코드는
- 바인드 마운트 경로를 매번 입력해야하고
- 오타를 낼 수도 있으며
- 팀원들에게 공유하기가 까다로운 코드다.
docker-compose.yaml을 작성해보자.
version: "3.8"
services:
npm:
build: ./
stdin_open: true
tty: true
volumes:
- ./:/app
| 설정 | 의미 | docker run는 어떻게 해석을 하는가 |
| build: ./ | 현재 폴더의 Dockerfile로 빌드 | docker build -t mynpm . |
| stdin_open: true | 표준 입력 열어둠 | -i 플래그 |
| tty: true | 가상 터미널 할당 | -t 플래그 |
| volumes: - ./:/app | 바인드 마운트 | -v $(pwd):/app |
이후에 아래와 같이 실행을 시켜야 한다.
docker-compose run npm init
왜 docker-compose up이 아니라 run일까?
| docker-compose up | docker-compose run |
| 모든 서비스를 시작 | 단일 서비스만 실행 |
| 애플리케이션 컨테이너용 (장기 실행) | 유틸리티 컨테이너용 (일회성 명령) |
| 서비스들이 계속 실행됨 | 명령 완료 후 종료 |
| docker-compose down으로 종료 | 추가 명령어를 인자로 전달 가능 |
| etc: 웹서버 + DB + Redis 동시에 시작 | etc: npm init, npm install 등 일회성 작업 |
즉, 유틸리티 컨테이너의 성질에 따라 run을 하는 것이 더 알맞은 것이다.
docker-compose run은 아래와 같이 사용 가능하다.
# 기본 문법
docker-compose run <서비스명> <추가_명령>
# 실제 사용
docker-compose run npm init
docker-compose run npm install
docker-compose run npm install express --save
docker-compose run npm test
그런데 이와 같이 사용 시 종료된 컨테이너들이 계속 쌓이는 문제가 발생한다.
- 이는 docker ps -a로 확인 가능하다.
따라서 권장되는 사용법으로는 아래와 같다. (혹은 그냥 prune으로 정리할 수도)
docker-compose run --rm npm init
최종장
아까 전 Dockerfile까지 정의했다고 했을 때
docker-compose.yaml을 만들어보자.
version: "3.8"
services:
npm:
build: ./
stdin_open: true
tty: true
volumes:
- ./:/app
그 후 build를 해보자.
docker-compose build
이제 우리는 Node.js가 없어도 아래 명령어로 프로젝트 세팅이 가능해졌다.
# 5. package.json 생성
docker-compose run --rm npm init -y
# 6. Express 설치 (라이브러리 추가)
docker-compose run --rm npm install express --save
# 7. Nodemon 개발 의존성 설치
docker-compose run --rm npm install nodemon --save-dev
# 8. 결과 확인
ls -la
cat package.json
터미널에서 실행해보면 주루룩 깔리고
╭─░▒▓ ~/Downloads/utility-container-demo ▓▒░························································································░▒▓ ✔ 8s system 00:01:07 ▓▒░─╮
╰─ ls -la ─╯
total 88
drwxr-xr-x 7 parkseonggeun staff 224 Dec 7 00:01 .
drwx------@ 628 parkseonggeun staff 20096 Dec 6 22:32 ..
-rw-r--r-- 1 parkseonggeun staff 110 Dec 6 23:59 docker-compose.yaml
-rw-r--r-- 1 parkseonggeun staff 72 Dec 6 23:59 Dockerfile
drwxr-xr-x 95 parkseonggeun staff 3040 Dec 7 00:01 node_modules
-rw-r--r--@ 1 parkseonggeun staff 30280 Dec 7 00:01 package-lock.json
-rw-r--r--@ 1 parkseonggeun staff 319 Dec 7 00:01 package.json
╭─░▒▓ ~/Downloads/utility-container-demo ▓▒░·····························································································░▒▓ ✔ system 00:01:22 ▓▒░─╮
╰─ cat package.json ─╯
{
"name": "app",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"keywords": [],
"description": "",
"dependencies": {
"express": "^5.2.1"
},
"devDependencies": {
"nodemon": "^3.1.11"
}
}
이렇게 결과가 터미널에 뜨게 된다.
작업이 끝나면 로컬 폴더는 아래와 같이 구성된다.

심화
더 복잡한 요구사항을 해결하기 위해 유틸리티 컨테이너를 확장해서 사용할 수도 있다.
역할별 유틸리티 서비스 분리
npm 명령뿐만 아니라 node 스크립트 실행, npx 명령 실행 등을 분리하여 정의할 수 있다
# docker-compose.yaml
version: "3.8"
services:
# 1. 패키지 관리용 (npm install ...)
npm:
build: ./
entrypoint: ["npm"]
volumes:
- ./:/app
# 2. 스크립트 실행용 (node server.js ...)
node:
build: ./
entrypoint: ["node"]
volumes:
- ./:/app
# 3. 보일러플레이트 생성용 (npx create-react-app ...)
npx:
build: ./
entrypoint: ["npx"]
volumes:
- ./:/app
사용 예시
- docker-compose run --rm npx create-react-app my-app
- docker-compose run --rm npm install
하이브리드 패턴 (앱 실행 + 유틸리티)
웹 서버는 계속 실행(up) 해두고, 패키지 설치는 별도로(run) 수행한다.
# docker-compose.yaml
version: "3.8"
services:
# [App Container] 웹 서버용 (장기 실행)
app:
build: ./
ports:
- "3000:3000"
volumes:
- ./:/app
command: ["npm", "start"] # 서버 시작 명령
# [Utility Container] 도구용 (일회성)
npm:
build: ./
entrypoint: ["npm"]
volumes:
- ./:/app
실행 흐름
- 서버 실행: docker-compose up app (서버가 3000번 포트에서 돔)
- 개발 중 패키지 필요: (서버를 끄지 않고 새 터미널을 열어서)
- 설치: docker-compose run --rm npm install lodash
- 반영: 소스 코드에서 import _ from 'lodash' 즉시 사용 가능
Docker를 공부하다보면 드는 생각이...
참으로 다양한 것들이 많았었구나...다.
예전 학교에서 진행했던 웹 프로그래밍 수업같은 경우, node를 매번 내 컴퓨터에 설치하고 했떤 기억이 있다.
또한 여러 프로젝트를 해보며 서로 환경 설정을 맞춰가며 느낀 경험...
등을 해결할 수 있는 좋은 도구가 Docker 인 것 같다.. 이다.
이전까진 서버를 띄우는 Hosting 용도로서 도커를 계속 바라봤다면, 그걸 넘어 작업반장을 맡길 수도 있는 툴로서도 바라 볼 수 있었다.
캡슐화가 잘 녹여진 느낌인데...
이렇듯, 매번 설치하고, 명령어 치고.. 할 필요 없이 인프라를 코드로 관리할 수 있다는 거 자체가 너무 매력적으로 다가오는 것 같다.
'Infra > Docker' 카테고리의 다른 글
| Container의 근반이 되는 기술에 대해 알아보자(완) UnionFS (0) | 2025.12.11 |
|---|---|
| Container의 근반이 되는 기술에 대해 알아보자(2) cgroup (0) | 2025.12.11 |
| Container의 근반이 되는 기술에 대해 알아보자(1) namespace (0) | 2025.12.11 |
| 도커(Docker)로 PHP / Laravel 개발 환경 구축하기 (0) | 2025.12.08 |
| Docker Compose (0) | 2025.12.06 |