Docker 패턴
컨테이너화된 개발을 위한 Docker 및 Docker Compose 모범 사례입니다.
로컬 개발용 Docker Compose
표준 웹 앱 스택
# docker-compose.yml
services:
app:
build:
context: .
target: dev # 멀티 스테이지 Dockerfile의 dev 스테이지 사용
ports:
- "3000:3000"
volumes:
- .:/app # 핫 리로드를 위한 바인드 마운트
- /app/node_modules # 익명 볼륨 -- 컨테이너 의존성 보존
environment:
- DATABASE_URL=postgres://postgres:postgres@db:5432/app_dev
- REDIS_URL=redis://redis:6379/0
- NODE_ENV=development
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
command: npm run dev
db:
image: postgres:16-alpine
ports:
- "5432:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: app_dev
volumes:
- pgdata:/var/lib/postgresql/data
- ./scripts/init-db.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redisdata:/data
mailpit: # 로컬 이메일 테스트
image: axllent/mailpit
ports:
- "8025:8025" # 웹 UI
- "1025:1025" # SMTP
volumes:
pgdata:
redisdata:
개발 vs 프로덕션 Dockerfile
# 스테이지: 의존성
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# 스테이지: dev (핫 리로드, 디버그 도구)
FROM node:22-alpine AS dev
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]
# 스테이지: 빌드
FROM node:22-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build && npm prune --production
# 스테이지: 프로덕션 (최소 이미지)
FROM node:22-alpine AS production
WORKDIR /app
RUN addgroup -g 1001 -S appgroup && adduser -S appuser -u 1001
USER appuser
COPY --from=build --chown=appuser:appgroup /app/dist ./dist
COPY --from=build --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --from=build --chown=appuser:appgroup /app/package.json ./
ENV NODE_ENV=production
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "dist/server.js"]
오버라이드 파일
# docker-compose.override.yml (자동 로드, 개발 전용 설정)
services:
app:
environment:
- DEBUG=app:*
- LOG_LEVEL=debug
ports:
- "9229:9229" # Node.js 디버거
# docker-compose.prod.yml (프로덕션용 명시적 설정)
services:
app:
build:
target: production
restart: always
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
# 개발 (자동으로 오버라이드 로드)
docker compose up
# 프로덕션
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
네트워킹
서비스 디스커버리
동일한 Compose 네트워크의 서비스는 서비스 이름으로 확인됩니다:
# "app" 컨테이너에서:
postgres://postgres:postgres@db:5432/app_dev # "db"가 db 컨테이너로 확인됨
redis://redis:6379/0 # "redis"가 redis 컨테이너로 확인됨
사용자 정의 네트워크
services:
frontend:
networks:
- frontend-net
api:
networks:
- frontend-net
- backend-net
db:
networks:
- backend-net # frontend가 아닌 api에서만 접근 가능
networks:
frontend-net:
backend-net:
필요한 것만 노출
services:
db:
ports:
- "127.0.0.1:5432:5432" # 호스트에서만 접근 가능, 네트워크에서는 불가
# 프로덕션에서는 포트를 완전히 생략 -- Docker 네트워크 내에서만 접근 가능
볼륨 전략
volumes:
# 명명된 볼륨: 컨테이너 재시작 후에도 유지, Docker가 관리
pgdata:
# 바인드 마운트: 호스트 디렉토리를 컨테이너에 매핑 (개발용)
# - ./src:/app/src
# 익명 볼륨: 바인드 마운트 오버라이드에서 컨테이너 생성 콘텐츠 보존
# - /app/node_modules
일반적인 패턴
services:
app:
volumes:
- .:/app # 소스 코드 (핫 리로드를 위한 바인드 마운트)
- /app/node_modules # 컨테이너의 node_modules를 호스트로부터 보호
- /app/.next # 빌드 캐시 보호
db:
volumes:
- pgdata:/var/lib/postgresql/data # 영구 데이터
- ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql # 초기화 스크립트
컨테이너 보안
Dockerfile 강화
# 1. 특정 태그 사용 (:latest 사용 금지)
FROM node:22.12-alpine3.20
# 2. 비루트로 실행
RUN addgroup -g 1001 -S app && adduser -S app -u 1001
USER app
# 3. 기능 제거 (compose에서)
# 4. 가능한 경우 읽기 전용 루트 파일시스템
# 5. 이미지 레이어에 비밀 정보 없음
Compose 보안
services:
app:
security_opt:
- no-new-privileges:true
read_only: true
tmpfs:
- /tmp
- /app/.cache
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE # 1024 미만 포트에 바인딩하는 경우에만
비밀 관리
# 좋음: 환경 변수 사용 (런타임에 주입)
services:
app:
env_file:
- .env # .env를 git에 커밋하지 마세요
environment:
- API_KEY # 호스트 환경에서 상속
# 좋음: Docker 시크릿 (Swarm 모드)
secrets:
db_password:
file: ./secrets/db_password.txt
services:
db:
secrets:
- db_password
# 나쁨: 이미지에 하드코딩
# ENV API_KEY=sk-proj-xxxxx # 절대 이렇게 하지 마세요
강화된 CLI 설치 도구 테스트 환경
컨테이너를 사용하여 일회용 프로젝트 복사본에 대해 설치 프로그램 동작을 테스트하고, 테스트가 원본 체크아웃을 변경하지 못하게 합니다.
플랫폼 경계 존중
- Debian 및 Ubuntu와 같은 Linux 배포판에는 실제 컨테이너를 실행합니다.
- macOS는 Docker가 Linux 커널을 공유하므로 Docker 컨테이너로 실행할 수 없습니다. 동일한 셸 프리 테스트 진입점을 macOS에서 네이티브로 실행합니다.
- Windows 컨테이너는 Windows Docker 엔진이 필요합니다. 플랫폼 독립적인 로직은 네이티브 Windows CI 러너에서 실행하고, Windows 컨테이너는 Windows 호스트용으로 예약합니다.
- 호스트별 경로, 명령 shim, 따옴표, 파일 시스템 동작을 위해 네이티브 Ubuntu/macOS/Windows CI 매트릭스를 유지합니다.
Linux 컨테이너가 macOS 또는 Windows 동작을 검증한다고 주장하지 마세요.
격리 계약 시행
- 불변 다이제스트로 기본 이미지를 고정하고 설치된 CLI 버전을 고정합니다.
- 배포판 계정 이름이 다른 경우 비루트 숫자 UID/GID로 실행합니다.
- 저장소와 소스 프로젝트를 읽기 전용으로 마운트합니다.
- 변형 전에 소스 프로젝트를 쓰기 가능한
tmpfs작업 공간으로 복사합니다. /workspace를noexec, UID/GID 1000,mode=0700으로 마운트하여 컨테이너 사용자만 프로젝트 데이터를 검사할 수 있도록 합니다.- npm 및 npx의 실행 파일 캐시를
NPM_CONFIG_CACHE=/tmp/npm-cache로 실행 가능한/tmp마운트에 유지합니다. 기본 크기는 2GiB이며ECC_TMPFS_SIZE로 조정할 수 있습니다.ECC_WORKSPACE_SIZE는 개인 작업 공간 마운트를 별도로 제어합니다. read_only: true,no-new-privileges:true,cap_drop: [ALL], 유한한pids_limit을 설정합니다.- 기본 실제 CLI 서비스를
network_mode: none으로 유지합니다. 인증된 공급자 세션을 위해 명시적으로 이름이 지정된 옵트인 서비스를 통해서만 네트워크 액세스를 추가하고, 우발적인 환경 기반 기본값이 되지 않도록 합니다. - 도구가 필요한 쓰기 가능한 임시 경로만 생성합니다.
- 기본적으로 호스트 자격 증명을 컨테이너에 전달하지 않습니다.
- 기본적으로 dry run을 수행하고
dry-run,install,plugin,shell모드만 명시적으로 허용합니다. - 크로스 플랫폼 러너에는 인수 배열 또는
spawnSync(..., { shell: false })를 사용합니다. 프로젝트 경로를 셸 명령에 절대 보간하지 마세요.
ECC 플러그인 설정 테스트 환경 실행
참조 구현으로 docker/plugin-setup/compose.yaml을 사용합니다. 다음을 제공합니다:
fixture-tests: 집중된 설치 매니페스트, 대상 및 실행기 제품군용.real-cli: 고정된 Debian 기반 일반 Linux 이미지용.real-cli-ubuntu: 고정된 Ubuntu 이미지용.
빌드 전에 Compose 모델을 검증합니다:
docker compose -f docker/plugin-setup/compose.yaml config --quiet
두 실제 Linux 이미지를 빌드합니다:
docker compose -f docker/plugin-setup/compose.yaml \
build real-cli real-cli-ubuntu
각 이미지에서 안전한 기본 흐름을 실행합니다:
docker compose -p ecc-plugin-debian-test \
-f docker/plugin-setup/compose.yaml \
run --rm -T real-cli dry-run
docker compose -p ecc-plugin-ubuntu-test \
-f docker/plugin-setup/compose.yaml \
run --rm -T real-cli-ubuntu dry-run
dry run은 현재 공개 명령 계약을 실행합니다:
ecc install --profile core --target claude-project --dry-run --json
해당 명령이 실행되기 전에 컨테이너는 npm pack --ignore-scripts를 사용하여 읽기 전용 체크아웃에서 로컬로 패키지된 npm 아티팩트를 생성합니다. /tmp 아래에서 자체 생성된 tarball을 추출하고, ecc-universal 패키지 이름, 필수 설치 매니페스트, 제한된 package.json bin.ecc 매핑을 검증한 다음, 추출된 ecc 실행 파일을 호출합니다. 런타임은 network_mode: none을 유지하고, 패키지 라이프사이클 스크립트를 실행하지 않으며, 호스트 node_modules에 의존하지 않습니다. 정확한 고정 프로덕션 의존성은 이미 이미지에 있습니다.
테스트 환경은 빈 계획, claude-project가 아닌 대상, /workspace/project/.claude 외부의 모든 작업, 또는 대상 디렉토리를 생성하는 dry run을 거부합니다. install은 격리된 적용을 두 번 수행하고, 관리되는 설치 상태를 확인하고, 설치된 대상을 나열하고, doctor를 실행합니다.
명명된 세션 시작, 열기, 재연결 및 정리
--rm 없이 분리된 컨테이너를 시작하여 터미널을 떠나도 세션이 제거되지 않도록 합니다:
docker compose -p ecc-plugin-session \
-f docker/plugin-setup/compose.yaml \
run --detach --name ecc-plugin-shell real-cli shell
컨테이너는 읽기 전용 fixture를 안정적인 개인 디렉토리 /workspace/project로 복사합니다. 실행 중인지 확인한 다음 터미널 오프너 v1 데이터 계약의 Docker 측을 출력합니다:
docker inspect --format '{{.State.Running}}' ecc-plugin-shell
node docker/plugin-setup/interactive-plan.js \
--container ecc-plugin-shell \
--workdir /workspace/project \
--json \
-- bash
JSON 결과에는 정확히 executable과 argv 경계( contractVersion: 1 포함)가 있습니다: 실행 파일은 docker이고, argv는 exec, -it, -w로 시작합니다. 별도의 터미널 오프너 스킬이 설치되면 해당 데이터를 전달합니다. 이 Docker 테스트 환경은 의도적으로 터미널 어댑터를 가져오거나, 셸 명령을 보간하거나, 호스트 GUI 프로세스를 관리하지 않습니다. 그때까지는 현재 호스트 터미널에서 동일한 PTY를 직접 엽니다:
docker exec -it -w /workspace/project ecc-plugin-shell bash
분리된 컨테이너를 중지하지 않고 셸을 종료합니다. 동일한 docker exec -it 명령으로 다시 연결합니다. 완료되면 정확히 명명된 컨테이너와 Compose 프로젝트 리소스를 제거합니다:
docker rm --force ecc-plugin-shell
docker compose -p ecc-plugin-session \
-f docker/plugin-setup/compose.yaml \
down --remove-orphans
호스트 자격 증명은 기본적으로 없으며 자격 증명 디렉토리는 절대 마운트되지 않습니다. 기본 서비스에는 네트워크 액세스도 없습니다. 인증된 공급자 세션에 실제로 네트워크가 필요한 경우 먼저 real-cli를 빌드한 다음 docker compose --profile networked run real-cli-networked shell로 명시적으로 옵트인합니다. 해당 일회성 세션 내에서 인증하는 것을 선호합니다. CI 실행이 호스트 환경 자격 증명을 상속해야 하는 경우 호출 시 명시적인 Compose --env NAME 플래그로 옵트인하고, 값이 검사 가능하며 컨테이너 수명 동안 유출될 수 있음을 이해하고, 정확히 명명된 컨테이너를 즉시 제거합니다.
호스트에서 동일한 집중 제품군을 네이티브로 실행합니다:
npm run test:plugin-setup-platform
이미지를 신뢰하기 전에 생성된 ID와 환경을 검사합니다:
docker image inspect ecc-plugin-setup:debian ecc-plugin-setup:ubuntu
관련 없는 볼륨이나 이미지를 삭제하지 않고 각 명명된 테스트 프로젝트를 정리합니다:
docker compose -p ecc-plugin-debian-test \
-f docker/plugin-setup/compose.yaml down --remove-orphans
docker compose -p ecc-plugin-ubuntu-test \
-f docker/plugin-setup/compose.yaml down --remove-orphans
.dockerignore
node_modules
.git
.env
.env.*
dist
coverage
*.log
.next
.cache
docker-compose*.yml
Dockerfile*
README.md
tests/
디버깅
일반적인 명령어
# 로그 보기
docker compose logs -f app # 앱 로그 팔로우
docker compose logs --tail=50 db # db에서 마지막 50줄
# 실행 중인 컨테이너에서 명령 실행
docker compose exec app sh # 앱 셸 진입
docker compose exec db psql -U postgres # postgres 연결
# 검사
docker compose ps # 실행 중인 서비스
docker compose top # 각 컨테이너의 프로세스
docker stats # 리소스 사용량
# 재빌드
docker compose up --build # 이미지 재빌드
docker compose build --no-cache app # 전체 재빌드 강제
# 정리
docker compose down # 컨테이너 중지 및 제거
docker compose down -v # 볼륨도 제거 (파괴적)
docker system prune # 사용하지 않는 이미지/컨테이너 제거
네트워크 문제 디버깅
# 컨테이너 내부의 DNS 확인 확인
docker compose exec app nslookup db
# 연결 확인
docker compose exec app wget -qO- http://api:3000/health
# 네트워크 검사
docker network ls
docker network inspect <project>_default
안티 패턴
# 나쁨: 최신 태그 사용 (`:latest`는 예측 불가능)
image: node:latest
# 나쁨: 루트로 실행
USER root
# 나쁨: 비밀번호 하드코딩
ENV DB_PASSWORD=supersecret
# 나쁨: healthcheck 없음
# 컨테이너가 준비되었는지 알 수 없음
# 나쁨: 모든 포트를 0.0.0.0에 노출
ports:
- "5432:5432" # 네트워크의 모든 사람이 접근 가능
# 나쁨: depends_on을 조건 없이 사용
depends_on:
- db
# db가 준비되기 전에 앱이 시작될 수 있음
# 나쁨: 재시작 정책 없음
# 컨테이너가 중단되면 다시 시작되지 않음
# 나쁨: 이미지 크기 최적화 없음
# 불필요한 패키지, 소스 맵, 캐시를 프로덕션 이미지에 포함
모범 사례 요약
- 특정 이미지 태그 사용: never
:latest, always pin to a specific version. - 멀티 스테이지 빌드: 개발 도구를 프로덕션 이미지에서 분리.
- 비루트 실행:
USER지시문으로 컨테이너 사용자 생성. - healthcheck 정의:
depends_on.condition: service_healthy와 함께 사용. - 최소한의 포트 노출:
127.0.0.1:을 사용하여 로컬 호스트로 제한. - 환경 변수 사용: 이미지 레이어에 시크릿을 하드코딩하지 마세요.
- .dockerignore 사용: 불필요한 파일을 빌드 컨텍스트에서 제외.
- 리소스 제한 설정: cpu, memory, pids_limit을 정의.
- 읽기 전용 파일시스템: 가능한 경우
read_only: true. - 재현 가능한 빌드:
package-lock.json또는 이에 상응하는 파일 커밋.