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
# 단계: 개발 (핫 리로드, 디버그 도구)
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 # 프론트엔드가 아닌 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. 기능 드롭 (컴포즈에서)
# 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 배포판에는 실제 컨테이너를 실행합니다.
- Docker는 Linux 커널을 공유하므로 macOS는 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의 실행 가능한 캐시를 실행 가능한
/tmp마운트의NPM_CONFIG_CACHE=/tmp/npm-cache에 유지합니다. 기본 크기는 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. - 고정된 Debian 기반 일반 Linux 이미지를 위한
real-cli. - 고정된 Ubuntu 이미지를 위한
real-cli-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 아티팩트를 생성합니다. 자체 생성된 tarball을 /tmp 아래에 추출하고, 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 # app 로그 팔로우
docker compose logs --tail=50 db # db의 마지막 50줄
# 실행 중인 컨테이너에서 명령 실행
docker compose exec app sh # app에 셸로 진입
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
안티 패턴
# 나쁨: 최신 태그 사용
FROM node:latest
# 나쁨: 루트로 실행
USER root
# 나쁨: 이미지에 시크릿 하드코딩
ENV DATABASE_URL=postgres://user:password@host:5432/db
# 나쁨: .dockerignore 없이 COPY . .
COPY . .
# 나쁨: 헬스 체크 생략
# HEALTHCHECK 지시어 없음
# 나쁨: 프로덕션에 바인드 마운트 사용
volumes:
- .:/app
# 나쁨: 컨테이너 간 네트워크 격리 없음
# 모든 서비스를 기본 네트워크에 배치