"제 컴퓨터에서는 잘 되는데요?" 개발자 간 흔하게 일어나는 환경의 차이로 인한 버그는 역사적으로 수많은 시간적 비용을 요구했습니다. 운영체제 버전, Node.js 버전, 시스템 라이브러리 버전이 조금씩 달라 발생하는 이 문제를 완전히 해결한 도구가 바로 도커(Docker)와 컨테이너(Container) 기술입니다. 이번 포스트에서는 도커의 기본 개념부터 실무에서 자주 쓰이는 패턴까지 상세히 알아보겠습니다.
1. 가상 머신(VM)과 컨테이너의 차이
과거에는 다른 환경을 구축하기 위해 가상 머신(Virtual Machine)을 활용했습니다. VM은 하드웨어 자체를 가상화하고 각각 독립된 게스트(OS)를 요구했기 때문에 리소스 사용량이 크고 실행 속도 또한 상당히 느렸습니다. VM 하나를 부팅하는 데 수십 초에서 수 분이 걸리고, 메모리를 수 GB씩 차지할 수 있습니다.
반면 컨테이너는 Host OS의 커널을 공유하며 프로세스를 격리하는 기술입니다. 운영 체제의 오버헤드 없이, 애플리케이션과 그 종속성 부분만을 포장합니다. 이는 리소스 소모를 기하급수적으로 낮추고 부팅 시간을 몇 초 이내로 단축시켰습니다. 하나의 서버에서 수십, 수백 개의 컨테이너를 동시에 실행하는 것이 가능합니다.
기술적으로는 리눅스 커널의 네임스페이스(Namespace)와 cgroups(Control Groups)를 활용합니다. 네임스페이스는 각 컨테이너에게 격리된 파일시스템, 네트워크, 프로세스 공간을 제공하고, cgroups는 CPU, 메모리 등 자원 사용에 제한을 두어 컨테이너 간 간섭을 방지합니다.
2. Docker Image와 Dockerfile
도커는 Dockerfile이라는 명세서를 바탕으로 이미지(Image)를 빌드합니다. 이미지는 파일 시스템, 라이브러리, 환경 변수 등 애플리케이션 구동에 필요한 모든 것들의 '읽기 전용' 스냅샷입니다.
FROM node:18-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
COPY . .
CMD ["node", "app.js"]
Dockerfile의 각 명령어(FROM, WORKDIR, COPY, RUN)는 레이어(Layer)를 생성합니다. 도커는 이 레이어들을 캐시로 관리합니다. 예를 들어 소스 코드만 변경했을 때는 npm install 레이어를 다시 실행할 필요 없이 캐시된 레이어를 재활용하여 빌드 속도를 크게 높일 수 있습니다.
이렇게 생성된 이미지를 실행한 것이 바로 컨테이너(Container)입니다. 읽기 전용 이미지 위에 쓰기 가능한(Writable) 레이어를 더하여 프로세스가 구동됩니다. 컨테이너를 삭제하면 쓰기 레이어의 데이터도 함께 사라지므로, 영속적인 데이터는 별도의 볼륨에 저장해야 합니다.
3. 멀티스테이지 빌드로 이미지 최적화
프로덕션 환경에서는 이미지 크기를 최소화하는 것이 중요합니다. 빌드 도구, 테스트 라이브러리 등 실행에 필요 없는 것들이 이미지에 포함되면 보안 취약점과 불필요한 용량 문제가 발생합니다. 멀티스테이지 빌드(Multi-stage Build)는 이 문제를 해결하는 강력한 패턴입니다.
# 빌드 단계 (빌드 도구 포함)
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# 실행 단계 (최소한의 런타임만 포함)
FROM node:18-alpine AS runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/app.js"]
위 예시처럼 빌드 단계와 실행 단계를 분리하면, 최종 이미지에는 컴파일된 결과물과 프로덕션 의존성만 포함되어 이미지 크기를 대폭 줄일 수 있습니다. Node.js 풀 이미지(~900MB)와 alpine 기반 최적화 이미지(~100MB)는 9배 가까이 차이납니다.
4. Docker Compose의 편의성
현대의 웹 서비스는 백엔드 서버 1대로만 구성되지 않습니다. 데이터베이스(MySQL, Redis), 프론트엔드 등 다중 컨테이너를 함께 구동해야 합니다.
docker-compose.yml 파일을 이용하면 다수의 컨테이너 관계, 네트워크 환경, 볼륨 관리를 한 파일 내에 정의하여 간결하게 실행할 수 있습니다.
version: '3.8'
services:
web:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgres://user:pass@db:5432/mydb
- REDIS_URL=redis://cache:6379
depends_on:
- db
- cache
db:
image: "postgres:15-alpine"
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=pass
- POSTGRES_USER=user
- POSTGRES_DB=mydb
cache:
image: "redis:alpine"
volumes:
postgres_data:
docker compose up -d 한 줄로 위의 전체 스택을 실행할 수 있습니다. depends_on으로 서비스 시작 순서를 제어하고, 명명된 볼륨(named volume)으로 데이터베이스 데이터를 영속적으로 유지합니다. 개발 환경에서의 생산성이 크게 향상됩니다.
5. 볼륨과 네트워크 심화
도커에서 볼륨(Volume)은 컨테이너 외부에 데이터를 영속적으로 저장하는 메커니즘입니다. 컨테이너가 재시작되거나 삭제되어도 볼륨에 저장된 데이터는 유지됩니다. 볼륨에는 세 가지 유형이 있습니다.
- Named Volume: 도커가 관리하는 영역에 생성됩니다. 컨테이너 간 데이터 공유와 데이터 영속성에 가장 적합합니다. (
docker volume create mydata) - Bind Mount: 호스트의 특정 디렉토리를 컨테이너에 마운트합니다. 개발 중 소스 코드를 실시간으로 컨테이너에 반영할 때 유용합니다.
- tmpfs Mount: 메모리에만 데이터를 저장합니다. 민감한 데이터를 임시로 처리할 때 사용합니다.
네트워크(Network) 측면에서, Docker Compose는 기본적으로 각 프로젝트에 격리된 가상 네트워크를 생성합니다. 같은 네트워크 내의 컨테이너들은 서비스 이름(예: db, cache)을 호스트명처럼 사용하여 서로 통신할 수 있습니다. 이는 DNS 기반 서비스 디스커버리를 제공하므로, IP 주소를 하드코딩할 필요가 없습니다.
6. 도커 보안 모범 사례
컨테이너를 프로덕션에 배포할 때는 보안을 반드시 고려해야 합니다.
- 루트 사용자 실행 금지: 기본적으로 컨테이너는 root로 실행됩니다. Dockerfile에
USER명령어로 일반 사용자를 지정하는 것이 필수입니다. - 최소 기반 이미지 사용:
alpine또는distroless이미지를 사용하여 공격 표면을 최소화합니다. - 이미지 취약점 스캔:
docker scout또는trivy를 CI/CD 파이프라인에 통합하여 알려진 CVE를 자동으로 탐지합니다. - 시크릿 관리: 비밀번호, API 키를 이미지에 포함하지 않습니다. Docker Secrets 또는 환경 변수(`.env` 파일은 `.gitignore`)를 통해 관리합니다.
- 읽기 전용 파일시스템: 가능한 경우
--read-only플래그로 컨테이너의 파일시스템을 읽기 전용으로 설정합니다.
7. 요약 및 쿠버네티스로의 연결
컨테이너화(Containerization)를 통해 우리는 언제, 어디서든, 동일한 런타임을 가지게 되었습니다. 배포에 필요한 설정과 수고를 극적으로 줄여주며, 개발자와 운영팀 간의 협업(DevOps)을 크게 향상시킵니다. Docker가 단일 서버에서의 컨테이너 관리라면, 수백 개의 컨테이너를 여러 서버에 걸쳐 자동으로 배치하고 관리하는 것이 쿠버네티스(Kubernetes)의 역할입니다.
실무에서는 개발 환경에 Docker Compose를, 프로덕션에는 Kubernetes(또는 AWS ECS, Google Cloud Run 같은 관리형 컨테이너 서비스)를 사용하는 패턴이 일반적입니다. Docker의 핵심을 이해하면 이 모든 상위 플랫폼들을 훨씬 수월하게 이해할 수 있습니다.