WebPiki
tutorial

Docker 입문 가이드 — 컨테이너 개념부터 실전까지

Docker가 왜 필요한지, 컨테이너가 뭔지, 이미지는 어떻게 만드는지. 명령어 위주로 정리한 Docker 입문 가이드.

"내 컴에서는 되는데?" — 개발자라면 한 번쯤 이 말을 했거나 들어봤을 거다. 로컬에서는 잘 돌아가던 앱이 서버에 올리니까 안 된다. Node.js 버전이 다르거나, Python 패키지 의존성이 꼬이거나, 환경 변수가 빠져있거나. 환경 차이 때문에 생기는 문제들.

Docker는 이 문제를 해결하는 도구다. 앱이 돌아가는 환경 자체를 패키징해서, 어디서든 동일하게 실행할 수 있게 만든다.

컨테이너가 뭔데

가상 머신(VM)과 비교하면 이해가 빠르다.

VM은 운영체제 전체를 가상화한다. 호스트 OS 위에 하이퍼바이저를 올리고, 그 위에 게스트 OS를 설치하고, 거기에 앱을 돌린다. 무겁다. Ubuntu VM 하나가 수 GB씩 잡아먹고, 부팅에 몇 분씩 걸린다.

컨테이너는 OS 커널을 호스트와 공유한다. 게스트 OS가 필요 없으니까 가볍다. 이미지 크기는 수십수백 MB 수준이고, 시작하는 데 12초면 충분하다. 대신 호스트와 같은 커널을 쓰니까 Linux 컨테이너는 Linux 위에서 돌아간다는 제약이 있다. (맥이나 윈도우에서는 경량 Linux VM 위에서 컨테이너가 돌아간다.)

핵심은 격리다. 각 컨테이너는 자기만의 파일 시스템, 네트워크, 프로세스 공간을 가진다. 컨테이너 A에서 Node 20을 쓰든, 컨테이너 B에서 Node 18을 쓰든 서로 영향이 없다.

Docker를 쓰는 이유

첫째, 환경 일관성. 개발, 테스트, 프로덕션 환경이 동일하니까 "내 컴에서는 되는데" 문제가 사라진다.

둘째, 빠른 셋업. 새 팀원이 들어왔을 때 "이거 설치하고, 저거 설정하고, 그다음에..." 대신 docker compose up 한 줄이면 개발 환경이 세팅된다.

셋째, 격리와 정리. 프로젝트마다 다른 DB 버전, 다른 런타임 버전을 쓸 수 있다. 더 이상 쓰지 않는 프로젝트의 환경도 컨테이너를 지우면 깔끔하게 정리된다.

핵심 개념 세 가지

이미지

컨테이너의 설계도다. "이 앱을 실행하려면 이런 OS에, 이런 패키지가 필요하고, 이 코드를 이 위치에 넣어라"라는 정보가 레이어 형태로 담겨있다.

이미지는 읽기 전용이다. 한번 만들어지면 변하지 않는다. 같은 이미지에서 컨테이너를 10개 만들어도 전부 동일한 상태에서 시작한다.

Docker Hub에 공개된 이미지가 수십만 개 있다. node, python, postgres, redis 같은 공식 이미지를 베이스로 쓰고, 거기에 자기 앱 코드를 얹는 게 일반적인 방법이다.

컨테이너

이미지를 실행한 인스턴스다. 이미지가 클래스라면 컨테이너는 객체라고 비유할 수 있다. 같은 이미지에서 여러 컨테이너를 만들 수 있고, 각 컨테이너는 독립적으로 동작한다.

컨테이너는 기본적으로 **일시적(ephemeral)**이다. 컨테이너를 삭제하면 안에서 변경한 내용도 사라진다. 데이터를 유지하고 싶으면 볼륨(Volume)을 사용해야 한다.

Dockerfile

이미지를 어떻게 만들지 정의하는 텍스트 파일이다. 레시피라고 생각하면 된다.

# Node.js 앱의 Dockerfile 예시
FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

EXPOSE 3000
CMD ["npm", "start"]

한 줄씩 보면:

  • FROM — 베이스 이미지 지정. node:20-alpine은 Node.js 20이 설치된 경량 리눅스
  • WORKDIR — 작업 디렉토리 설정
  • COPY + RUN — 먼저 package.json만 복사해서 의존성 설치, 그다음 나머지 코드 복사. 이렇게 순서를 나누면 코드만 바꿨을 때 의존성 설치 레이어를 캐시에서 재사용할 수 있다
  • EXPOSE — 컨테이너가 사용하는 포트 명시 (문서화 목적)
  • CMD — 컨테이너 시작 시 실행할 명령어

자주 쓰는 명령어

처음에는 이것만 알아도 충분하다.

# 이미지 빌드
docker build -t my-app .

# 컨테이너 실행
docker run -d -p 3000:3000 --name my-app my-app

# 실행 중인 컨테이너 확인
docker ps

# 컨테이너 로그 보기
docker logs my-app

# 컨테이너 안에서 명령어 실행
docker exec -it my-app sh

# 컨테이너 중지 & 삭제
docker stop my-app
docker rm my-app

# 이미지 목록
docker images

# 안 쓰는 이미지/컨테이너 정리
docker system prune

-d는 백그라운드 실행, -p 3000:3000은 호스트의 3000번 포트를 컨테이너의 3000번 포트에 연결, --name은 컨테이너에 이름 부여. -it는 인터랙티브 터미널.

Docker Compose — 여러 컨테이너를 한번에

실제 프로젝트에서는 앱 하나만 돌리는 경우가 드물다. 웹 서버 + 데이터베이스 + 캐시 서버 조합이 흔하다. 이걸 매번 docker run으로 하나씩 띄우는 건 번거로우니까, Docker Compose를 쓴다.

# docker-compose.yml
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgres://user:pass@db:5432/mydb
    depends_on:
      - db
      - redis

  db:
    image: postgres:16
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
      - POSTGRES_DB=mydb
    volumes:
      - db-data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine

volumes:
  db-data:

이 파일 하나로 docker compose up을 실행하면 앱, PostgreSQL, Redis가 동시에 뜬다. docker compose down으로 전부 내리고. 개발 환경 셋업이 이 파일 하나로 끝나는 거다.

volumesdb-data를 정의한 건 데이터베이스 데이터를 컨테이너가 삭제되어도 유지하기 위해서다. 이걸 안 하면 docker compose down 할 때마다 DB가 초기화된다.

자주 하는 실수들

이미지가 너무 크다FROM node:20을 쓰면 이미지가 1GB 가까이 된다. node:20-alpine을 쓰면 150MB 이하로 줄어든다. -alpine 태그는 경량 리눅스 기반이라 프로덕션 이미지에 적합하다.

.dockerignore를 안 만든다COPY . . 할 때 node_modules, .git, .env 같은 파일까지 전부 복사된다. .dockerignore 파일로 제외 목록을 지정해야 한다. .gitignore랑 비슷한 문법이다.

node_modules
.git
.env
*.md

레이어 캐싱을 활용하지 않는다 — Dockerfile에서 자주 바뀌는 부분을 뒤쪽에 배치해야 캐싱 효과가 높다. package.json이 안 바뀌었으면 npm ci 단계를 다시 실행할 필요가 없으니까, 코드 복사(COPY . .)보다 먼저 의존성 설치를 하는 게 맞다.

어디서부터 시작하면 되나

Docker Desktop을 설치하고, 자기가 진행 중인 프로젝트에 Dockerfile을 하나 만들어보는 게 가장 좋다. 공식 이미지 기반으로 간단한 Dockerfile을 작성하고, docker builddocker run으로 돌아가는 걸 확인하는 것만으로도 핵심 개념은 잡힌다.

그다음 Docker Compose로 DB를 추가해보고, 개발 환경 전체를 컨테이너화하는 단계로 넘어가면 된다. 이 두 단계를 거치면 Docker를 왜 쓰는지 몸으로 이해하게 된다.

#Docker#컨테이너#DevOps#배포#인프라

관련 글