Docker를 이용한 컨테이너화
NestJS 실행 환경을 다단계 Docker 이미지로 만들고 환경 변수와 데이터베이스를 Compose로 함께 구동합니다.
11장에서는 NestJS 애플리케이션 배포와 운영을 다루며, 먼저 Docker를 이용한 컨테이너화를 설명합니다.
개발 환경과 운영 환경의 차이는 흔히 내 컴퓨터에서는 잘 돌아가는데...라는 문제로 드러납니다. Docker는 애플리케이션과 사용자 공간 의존성을 이미지로 묶어 이 차이를 줄입니다.
컨테이너(Container)란 무엇인가?
컨테이너는 애플리케이션 코드, 런타임, 시스템 도구와 라이브러리를 묶어 실행하는 단위입니다. Linux 컨테이너는 네임스페이스와 cgroup 같은 커널 기능으로 프로세스와 자원을 상대적으로 격리하지만, VM처럼 별도 게스트 커널을 포함하지 않고 호스트 또는 Docker Desktop VM의 커널을 공유합니다.
컨테이너의 주요 특징- 격리성: 프로세스·파일 시스템·네트워크 경계를 분리할 수 있습니다. 다만 권한, 마운트, 커널 취약점에 따라 경계 강도가 달라지므로 절대적인 보안 경계로 간주하지 않습니다.
- 이식성: 동일한 대상 OS, CPU 아키텍처와 런타임 조건에서는 같은 이미지를 여러 환경에 일관되게 배포할 수 있습니다. 다른 아키텍처까지 지원하려면 멀티 플랫폼 이미지를 빌드해야 합니다.
- 경량성: 게스트 OS 전체를 포함하는 VM보다 일반적으로 시작이 빠르고 이미지가 작습니다.
- 일관성: 이미지로 고정한 사용자 공간 의존성 덕분에 개발·테스트·운영 환경 차이를 줄입니다.
| 특징 | 컨테이너 | 가상 머신(VM) |
|---|---|---|
| 격리 방식 | OS 수준 격리, 호스트/VM 커널 공유 | 하드웨어 가상화, 게스트 OS 포함 |
| 오버헤드 | 일반적으로 낮음 | 일반적으로 높음 |
| 배포 단위 | 애플리케이션과 사용자 공간 의존성 이미지 | 게스트 OS를 포함한 VM 이미지 |
| 호환 조건 | 대상 OS·아키텍처와 이미지 플랫폼이 맞아야 함 | 하이퍼바이저와 게스트 OS 지원이 필요함 |
| 예시 | Docker, containerd | VMware, VirtualBox, KVM, Hyper-V |
Docker란?
Docker는 컨테이너 이미지를 빌드·배포·실행하고 관리하는 도구와 서비스를 제공합니다.
Docker의 주요 구성 요소- Dockerfile: 이미지를 빌드하는 명령을 정의한 텍스트 파일입니다.
- Docker Image: 컨테이너를 생성하는 읽기 전용 이미지입니다.
- Docker Container: 이미지를 기반으로 실행되는 프로세스 집합입니다.
- Docker Daemon: 이미지와 컨테이너를 관리하는 백그라운드 서비스입니다.
- Docker CLI: Docker Daemon과 상호작용하는 명령줄 인터페이스입니다.
- Registry: 이미지를 저장하고 배포하는 저장소입니다. Docker Hub는 대표적인 공개 Registry입니다.
아래 구조는 소스 입력부터 운영 경계까지 이어지는 안전한 빌드·배포 경로(Secure paved road)입니다. .dockerignore로 빌드 입력을 줄이고, 빌드 전용 의존성을 최종 이미지에서 제외한 뒤, 승인한 digest를 Registry와 배포 기록에 남깁니다. 실행 시점의 설정·비밀, 헬스, 재시도, 로그는 이미지 바깥의 운영 경계로 분리합니다.
NestJS 컨테이너의 Secure paved road
차단된 입력을 제외한 build 결과가 runtime image·registry digest·동일 digest 배포·실행 관측으로 이어진다.
Nest · Container trust boundaries
빌드 입력에서 운영 컨테이너까지
빌드 입력·이미지 내용·배포 digest·런타임 책임을 분리합니다. .dockerignore와 다단계 빌드 예제에, 검증한 digest 승인과 외부 설정·관측을 결합하는 운영 설계를 보여줍니다.
01 · build context gate
.dockerignore가 Builder 입력을 줄인다
이 예시에서는 소스, lockfile과 Dockerfile은 컨텍스트에 남기고, .env, 호스트 node_modules, dist, Git 메타데이터와 로그처럼 .dockerignore에 나열한 패턴만 전송 전에 제외합니다.
입력 필터는 우발적 복사를 줄일 뿐 비밀 저장소가 아닙니다. 빌드 비밀은 BuildKit secret mount로 별도 전달합니다.
02 · build / deps
두 빌드 경로가 작은 runtime image로 합쳐진다
builder는 개발 의존성으로 dist를 만들고, deps는 운영 의존성만 설치합니다. 최종 이미지는 Node 런타임에 dist·운영 의존성·package 메타데이터를 복사하고 비루트 사용자로 실행합니다.
03 · release gate
Registry 게시 결과의 digest가 운영 승인과 배포 기록으로 이어진다
메이저 태그는 읽기 쉬운 이름이고 변경될 수 있습니다. Registry 게시 결과의 manifest digest를 기록하고 검증·운영 승인의 단위로 고정합니다. 승인 뒤에는 동일한 digest를 배포하며 업데이트는 새 검증을 거칩니다.
04 · runtime boundary
이미지 바깥에서 설정하고 상태를 관측한다
플랫폼은 일반 설정과 secret을 실행 시 주입하고, 컨테이너는 헬스 상태와 stdout/stderr 로그를 내보냅니다.
실행 중 연결 복구는 앱의 retry·timeout 책임이고, 정상 종료는 NestJS shutdown hook을 명시적으로 활성화해야 합니다.
- 핵심 통제
- 런타임 주입
- 빌드 컨텍스트에서 차단
보안 경계의 핵심은 한 도구가 모든 것을 해결하는 것이 아니라, 입력 필터·최소 이미지·불변 digest·외부 설정·상태와 관측 책임을 이어 붙이는 데 있습니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- Workspace, Build and Release, Runtime 세 영역을 위에서 아래로 읽는다. dockerignore가 환경 파일과 호스트 산출물처럼 나열한 패턴을 차단하고, 제외되지 않은 입력을 builder와 production dependencies 단계로 보낸다. 두 단계의 결과는 최소 runtime image로 합쳐져 Registry에 게시된다. 게시 결과의 manifest digest를 기록하고 검증·운영 승인한 동일 digest를 배포 입력으로 사용한다. 운영 플랫폼은 설정과 비밀을 컨테이너 실행 시 주입하고, 컨테이너는 헬스 상태와 표준 출력 로그를 내보내며 애플리케이션은 재시도와 종료 훅을 책임진다.
- 입력은 builder와 deps로 별도 전달되고 두 결과가 runtime image에 합쳐집니다. 검증한 digest와 플랫폼 설정은 각각 컨테이너로 들어갑니다. 설정 노드가 digest 배포의 다음 실행 단계라는 뜻은 아닙니다. 컨테이너의 상태와 로그는 운영 경계로 나갑니다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 1 | NestJS 컨테이너의 Secure paved road |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 빌드 입력 경계 dockerignore가 환경 파일·호스트 산출물 등 나열한 패턴을 제외합니다. | 입력: 제외되지 않은 workspace · 비밀 파일: 패턴으로 제외 | 빌드에 넘길 입력 범위가 결정됩니다. |
| 두 빌드 산출물 합치기 builder와 production dependencies의 결과를 최소 runtime image로 합칩니다. | builder: 빌드 산출물 · dependencies: 운영 의존성 · image: 최소 runtime | 독립 빌드 결과가 실행 이미지로 합쳐집니다. |
| digest 고정·배포 registry의 manifest digest를 기록하고 검증·승인된 같은 digest를 배포 입력으로 씁니다. | registry: manifest digest · 배포: 동일 승인 digest | 승인 대상과 실행 대상의 identity를 고정합니다. |
| runtime 설정과 관찰 운영 플랫폼이 설정·비밀을 실행 시 주입하고 컨테이너가 health·표준 출력 로그를 제공합니다. | 비밀: runtime 주입 · 관찰: health·stdout · 앱 책임: retry·종료 훅 | 이미지가 실제 운영 프로세스로 실행됩니다. |
NestJS 컨테이너의 Secure paved road
차단된 입력을 제외한 build 결과가 runtime image·registry digest·동일 digest 배포·실행 관측으로 이어진다.
Nest · Container trust boundaries
빌드 입력에서 운영 컨테이너까지
빌드 입력·이미지 내용·배포 digest·런타임 책임을 분리합니다. .dockerignore와 다단계 빌드 예제에, 검증한 digest 승인과 외부 설정·관측을 결합하는 운영 설계를 보여줍니다.
01 · build context gate
.dockerignore가 Builder 입력을 줄인다
이 예시에서는 소스, lockfile과 Dockerfile은 컨텍스트에 남기고, .env, 호스트 node_modules, dist, Git 메타데이터와 로그처럼 .dockerignore에 나열한 패턴만 전송 전에 제외합니다.
입력 필터는 우발적 복사를 줄일 뿐 비밀 저장소가 아닙니다. 빌드 비밀은 BuildKit secret mount로 별도 전달합니다.
02 · build / deps
두 빌드 경로가 작은 runtime image로 합쳐진다
builder는 개발 의존성으로 dist를 만들고, deps는 운영 의존성만 설치합니다. 최종 이미지는 Node 런타임에 dist·운영 의존성·package 메타데이터를 복사하고 비루트 사용자로 실행합니다.
03 · release gate
Registry 게시 결과의 digest가 운영 승인과 배포 기록으로 이어진다
메이저 태그는 읽기 쉬운 이름이고 변경될 수 있습니다. Registry 게시 결과의 manifest digest를 기록하고 검증·운영 승인의 단위로 고정합니다. 승인 뒤에는 동일한 digest를 배포하며 업데이트는 새 검증을 거칩니다.
04 · runtime boundary
이미지 바깥에서 설정하고 상태를 관측한다
플랫폼은 일반 설정과 secret을 실행 시 주입하고, 컨테이너는 헬스 상태와 stdout/stderr 로그를 내보냅니다.
실행 중 연결 복구는 앱의 retry·timeout 책임이고, 정상 종료는 NestJS shutdown hook을 명시적으로 활성화해야 합니다.
- 핵심 통제
- 런타임 주입
- 빌드 컨텍스트에서 차단
보안 경계의 핵심은 한 도구가 모든 것을 해결하는 것이 아니라, 입력 필터·최소 이미지·불변 digest·외부 설정·상태와 관측 책임을 이어 붙이는 데 있습니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- Workspace, Build and Release, Runtime 세 영역을 위에서 아래로 읽는다. dockerignore가 환경 파일과 호스트 산출물처럼 나열한 패턴을 차단하고, 제외되지 않은 입력을 builder와 production dependencies 단계로 보낸다. 두 단계의 결과는 최소 runtime image로 합쳐져 Registry에 게시된다. 게시 결과의 manifest digest를 기록하고 검증·운영 승인한 동일 digest를 배포 입력으로 사용한다. 운영 플랫폼은 설정과 비밀을 컨테이너 실행 시 주입하고, 컨테이너는 헬스 상태와 표준 출력 로그를 내보내며 애플리케이션은 재시도와 종료 훅을 책임진다.
- 입력은 builder와 deps로 별도 전달되고 두 결과가 runtime image에 합쳐집니다. 검증한 digest와 플랫폼 설정은 각각 컨테이너로 들어갑니다. 설정 노드가 digest 배포의 다음 실행 단계라는 뜻은 아닙니다. 컨테이너의 상태와 로그는 운영 경계로 나갑니다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 1 | NestJS 컨테이너의 Secure paved road |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 빌드 입력 경계 dockerignore가 환경 파일·호스트 산출물 등 나열한 패턴을 제외합니다. | 입력: 제외되지 않은 workspace · 비밀 파일: 패턴으로 제외 | 빌드에 넘길 입력 범위가 결정됩니다. |
| 두 빌드 산출물 합치기 builder와 production dependencies의 결과를 최소 runtime image로 합칩니다. | builder: 빌드 산출물 · dependencies: 운영 의존성 · image: 최소 runtime | 독립 빌드 결과가 실행 이미지로 합쳐집니다. |
| digest 고정·배포 registry의 manifest digest를 기록하고 검증·승인된 같은 digest를 배포 입력으로 씁니다. | registry: manifest digest · 배포: 동일 승인 digest | 승인 대상과 실행 대상의 identity를 고정합니다. |
| runtime 설정과 관찰 운영 플랫폼이 설정·비밀을 실행 시 주입하고 컨테이너가 health·표준 출력 로그를 제공합니다. | 비밀: runtime 주입 · 관찰: health·stdout · 앱 책임: retry·종료 훅 | 이미지가 실제 운영 프로세스로 실행됩니다. |
NestJS 애플리케이션 컨테이너화
Dockerfile 작성
프로젝트 루트에 Dockerfile을 생성합니다. 다음 예시는 현재 LTS 라인인 Node.js 24의 공식 Alpine 이미지를 사용하며, 빌드·프로덕션 의존성·실행 단계를 분리합니다.
먼저 같은 프로젝트에서 npm ci와 npm run build가 성공하고 실행 파일이 dist/main.js에 생기는지 확인합니다. 모노레포나 별도 출력 디렉터리를 쓰면 아래 COPY와 CMD가 그 산출물을 가리켜야 합니다. Docker는 연결되지 않은 Nest 모듈이나 빠진 환경 설정을 자동으로 만들어 주지 않습니다. 데이터베이스가 필수인 누적 프로젝트는 뒤의 Compose 예제로 실행하고, 단일 컨테이너 예제는 필요한 외부 연결과 설정이 준비된 조건에서 사용합니다.
# syntax=docker/dockerfile:1
FROM node:24-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:24-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
FROM node:24-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY --chown=node:node --from=deps /app/node_modules ./node_modules
COPY --chown=node:node --from=builder /app/dist ./dist
COPY --chown=node:node package*.json ./
USER node
EXPOSE 3000
CMD ["node", "dist/main.js"]npm ci는package-lock.json과 같은 lockfile을 전제로 의존성을 재현 가능하게 설치합니다.builder에는 TypeScript와 빌드 도구가 있지만,runtime에는 프로덕션 의존성과dist만 복사합니다.USER node는 공식 Node 이미지의 비루트 사용자를 이용합니다. 애플리케이션이 쓰기 디렉터리를 필요로 한다면 해당 경로의 소유권도 명시적으로 부여해야 합니다.EXPOSE 3000은 이미지 메타데이터이며, 호스트 포트를 게시하려면docker run -p또는 Compose의ports가 필요합니다.CMD의 exec 형식은 Node 프로세스가 종료 신호를 직접 받도록 합니다.
node:24-alpine 같은 메이저 태그는 재빌드할 때 가리키는 이미지가 바뀔 수 있습니다. 실제 릴리스 파이프라인에서는 검증한 @sha256:... digest로 고정하고, 보안 업데이트 시 새 digest를 검증해 의도적으로 갱신합니다.
.dockerignore 파일 작성
.dockerignore는 빌드 컨텍스트가 Builder로 전송되기 전에 불필요한 입력을 제외합니다.
node_modules
dist
.env
.git
*.lognode_modules와 dist는 컨테이너 안에서 다시 설치하거나 생성합니다. .env를 제외하면 우발적인 복사를 줄일 수 있지만, .dockerignore는 비밀 저장소나 완전한 보안 경계가 아닙니다. 빌드 중 비밀이 필요하면 ARG나 ENV에 넣지 말고 BuildKit secret mount를 사용합니다.
Docker 이미지 빌드
docker build --pull -t nestjs-app:1.0.0 .--pull은 캐시에 같은 태그가 있어도 최신 기반 이미지를 확인합니다. 태그는 사람이 읽는 릴리스 이름이고, 실제 배포 후보는 테스트한 이미지 digest까지 함께 기록해야 재현성과 감사 가능성이 높아집니다.
Docker 컨테이너 실행
docker run -d --name my-nestjs-container \
-p 3000:3000 \
nestjs-app:1.0.0-p 3000:3000은 호스트의 3000번 포트를 컨테이너의 3000번 포트에 게시하고, -d는 백그라운드에서 실행합니다.
런타임 설정과 비밀 주입
이미지에는 환경별 설정을 넣지 않고 실행 시점에 주입합니다. 다음처럼 일반 설정은 환경 변수로 전달할 수 있습니다.
다음 명령은 앞의 docker run에 설정을 더한 대안입니다. 두 명령을 차례로 실행하면 같은 my-nestjs-container 이름을 두 번 만들 수 없습니다. 아직 컨테이너를 만들지 않았다면 필요한 명령 하나를 선택하고, 이미 실행했다면 기존 컨테이너를 정리할지 별도 이름으로 시험할지 먼저 결정합니다. db.internal도 예시 주소이므로 실제로 연결 가능한 호스트가 있어야 합니다.
docker run -d --name my-nestjs-container \
-p 3000:3000 \
-e DATABASE_HOST=db.internal \
-e DATABASE_PORT=5432 \
-e NODE_ENV=production \
nestjs-app:1.0.0환경 변수와 평문 .env 파일 자체는 비밀 저장소가 아닙니다. 비밀번호와 토큰은 운영 플랫폼의 secret 기능이나 전용 비밀 관리자를 통해 최소 권한으로 주입하고, 애플리케이션이 그 값을 로그에 남기지 않도록 합니다.
헬스, 재시도, 종료와 로그
헬스 엔드포인트는 프로세스 생존 여부와 트래픽을 받을 준비 상태를 구분해 드러내야 합니다. 데이터베이스나 Redis 연결은 실행 중에도 끊길 수 있으므로 애플리케이션 계층에서 제한된 재시도, 지수 백오프, 타임아웃을 설계합니다.
NestJS의 종료 훅은 기본으로 활성화되지 않습니다. SIGTERM을 받아 연결과 진행 중인 작업을 정리하려면 bootstrap에서 명시적으로 활성화합니다.
const app = await NestFactory.create(AppModule);
app.enableShutdownHooks();
await app.listen(3000);애플리케이션 로그는 컨테이너 내부 파일보다 stdout과 stderr로 내보내 수집기가 가져가게 하고, 요청 ID 같은 상관관계 필드와 구조화된 로그 형식을 사용합니다.
Docker Compose를 이용한 다중 컨테이너 관리
Docker Compose는 앱, 데이터베이스, 캐시처럼 여러 컨테이너로 구성된 애플리케이션을 하나의 모델로 정의하고 실행합니다. 아래 배치도에서 app, db, redis는 Compose 기본 네트워크의 서비스 이름이며 내부 DNS 이름으로도 사용됩니다. 클라이언트에는 앱 포트만 게시하고, 데이터 서비스는 내부 네트워크에 둡니다.
Nest · Compose deployment
Compose 서비스와 초기 준비 게이트
외부에는 app의 3000번 포트만 게시하고 app은 db:5432·redis:6379로 연결합니다. health 조건은 초기 생성 게이트이며 실행 중 재접속·재시도·종료는 앱의 책임입니다.
01 · ingress
호스트는 app의 3000번 포트에만 들어온다
3000:3000을 게시하고 PostgreSQL과 Redis의 포트는 Compose 네트워크 안에 둡니다. 개발 도구가 꼭 필요할 때만 데이터 포트를 별도로 엽니다.
02 · service dns
app은 IP 대신 서비스 이름과 내부 포트를 사용한다
데이터베이스는 db:5432, 캐시는 redis:6379로 연결합니다. 서비스가 재생성되면 IP가 바뀔 수 있으므로 연결 종료를 감지하고 이름을 다시 확인해 재접속합니다.
03 · initial gate
health 조건은 app 생성 전 준비 상태만 확인한다
pg_isready와 redis-cli ping이 healthy가 되면 app을 만듭니다.
이 게이트는 실행 중 가용성이나 자동 연결 복구를 보장하지 않습니다. app의 timeout, backoff와 retry가 별도로 필요합니다.
04 · persistence / lifecycle
이름 있는 볼륨과 앱 수명 주기는 별도 책임이다
db_data는 PostgreSQL 18의 /var/lib/postgresql에, redis_data는 /data에 연결합니다. 볼륨은 컨테이너 삭제 뒤에도 유지되지만 백업을 대신하지 않습니다.
NestJS는 enableShutdownHooks()를 켜야 종료 신호에 맞춰 연결과 진행 중인 작업을 정리할 수 있습니다.
- 외부 게시 경로
- 서비스 DNS 트래픽·볼륨 연결
- health 기반 초기 생성 게이트
Compose는 로컬 다중 컨테이너 모델과 초기 시작 순서를 제공하지만, 지속 가용성·애플리케이션 재시도·비밀 관리·백업·정상 종료를 대신하지 않습니다.
compose.yaml 파일 작성
프로젝트 루트에 compose.yaml을 생성합니다. 이 예시는 현재 공식 이미지에 존재하는 Node 24 LTS, PostgreSQL 18, Redis 8 메이저 태그를 사용합니다. PostgreSQL 18부터 공식 이미지의 볼륨 대상이 /var/lib/postgresql로 바뀌었으므로 이전 /var/lib/postgresql/data 경로를 그대로 사용하지 않습니다.
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
DATABASE_HOST: db
DATABASE_PORT: 5432
DATABASE_USER: ${DB_USER:?set DB_USER}
DATABASE_PASSWORD: ${DB_PASSWORD:?set DB_PASSWORD}
DATABASE_NAME: ${DB_NAME:?set DB_NAME}
REDIS_HOST: redis
REDIS_PORT: 6379
JWT_SECRET: ${JWT_SECRET:?set JWT_SECRET}
NODE_ENV: production
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
db:
image: postgres:18-alpine
environment:
POSTGRES_USER: ${DB_USER:?set DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD:?set DB_PASSWORD}
POSTGRES_DB: ${DB_NAME:?set DB_NAME}
volumes:
- db_data:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
redis:
image: redis:8-alpine
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
volumes:
db_data:
redis_data:- Compose의 기본 네트워크에서
app은db:5432,redis:6379처럼 서비스 이름과 컨테이너 포트로 연결합니다. IP 주소는 재생성될 수 있으므로 고정 IP에 의존하지 않습니다. db와redis에는 호스트ports를 열지 않았습니다. 호스트 도구가 직접 접근해야 하는 개발 상황에서만 명시적으로 게시합니다.condition: service_healthy는 앱 컨테이너를 처음 생성하기 전의 준비 게이트입니다. 실행 중 의존 서비스의 지속 가용성을 보장하거나 연결을 자동 복구하지 않으므로 앱의 재연결·재시도 정책이 별도로 필요합니다.- 이름 있는 볼륨은 컨테이너 수명과 독립적으로 데이터를 유지하지만 백업과 복구를 대신하지 않습니다.
docker compose down -v처럼 볼륨을 명시적으로 제거하면 데이터도 삭제됩니다. ${VAR}보간은 값을 전달하는 방법일 뿐 비밀 저장소가 아닙니다. 위 예시는 로컬 학습용이며, 운영에서는 Compose secrets 또는 배포 플랫폼의 secret 기능을 사용합니다.
Docker Compose 실행
필수 변수를 안전한 실행 환경에서 주입한 뒤 현재 Compose CLI를 사용합니다.
docker compose config
docker compose up --build -d
docker compose ps
docker compose logs -f app
docker compose downdocker compose config로 보간과 최종 모델을 먼저 확인하고, ps와 로그로 상태를 관찰합니다. 비밀 값이 포함된 렌더링 결과나 로그를 공유하지 않도록 주의합니다.
예시 호스트 접속과 앱의 데이터베이스 접속을 따라가기
브라우저에서 http://localhost:3000으로 보낸 요청은 게시된 호스트 포트 3000을 거쳐 app의 포트 3000으로 들어갑니다. 그 앱에서 PostgreSQL에 연결할 때는 db:5432를 사용합니다. 앱 컨테이너의 localhost는 앱 자신의 네트워크 공간이므로 이 Compose의 데이터베이스가 아닙니다. db와 redis가 healthy가 된 뒤 앱이 시작하더라도, 필수 변수 해석이나 Nest 초기화가 실패하면 앱 로그를 따로 확인해야 합니다.
컨테이너 실행 중에는 docker compose logs -f app이 계속 로그를 따라가는 명령입니다. 관찰을 마쳤으면 로그 따라가기를 끝내고 다음 명령을 실행합니다. 정상적인 docker compose down은 이름 있는 볼륨을 명시적으로 삭제하는 명령과 구분합니다.
연결 경로 점검
별도 시험 환경에서 호스트 게시 포트만 8080:3000으로 바꾸고 데이터 서비스 설정은 그대로 두었다고 가정합니다. 브라우저의 접속 주소, 앱의 데이터베이스 주소는 각각 무엇인가요? 앱 시작 뒤 데이터베이스 연결이 끊기면 depends_on이 연결을 복구해 줄까요?
게시 포트·내부 DNS·실행 중 복구를 나누기
브라우저는 http://localhost:8080, 앱은 여전히 db:5432를 사용합니다. 바뀐 것은 호스트에서 앱으로 들어오는 포트이며 컨테이너 내부의 서비스 이름과 포트는 같습니다. 초기 health 게이트는 지속적인 연결 복구 기능이 아니므로 앱의 타임아웃·재연결·제한된 재시도를 확인합니다. 볼륨에 데이터가 남는 것과 데이터베이스가 현재 요청을 처리하는 것도 별개의 조건입니다.
Docker 기반 컨테이너화의 핵심은 단순히 이미지를 만드는 데 있지 않습니다. 빌드 입력을 제한하고, 빌드 도구를 런타임에서 제거하며, 승인된 이미지 digest를 배포하고, 설정·비밀·헬스·재시도·종료·로그 경계를 명확히 해야 반복 가능한 운영 단위가 됩니다.