같은 코드를 받았는데 내 컴퓨터에서만 안 되는 경험은 개발자라면 다 있다. 원인은 대개 코드가 아니다. 파이썬 버전이 다르거나, 시스템 라이브러리가 없거나, 환경변수가 안 잡혀 있다.
지금까지의 해법은 README에 설치 순서를 적는 것이었다. 그런데 문서는 낡고, 사람마다 운영체제가 다르고, “저는 그 단계에서 에러가 나요”가 반복된다.
도커는 이 문제를 다른 방식으로 푼다. 환경을 문서로 설명하는 대신 통째로 포장해서 같이 보낸다.
가상머신과 무엇이 다른가
한 줄로 정리하면 이렇다.
| 가상머신 | 도커 컨테이너 | |
|---|---|---|
| 포함하는 것 | 게스트 OS 전체 + 앱 | 앱 + 필요한 라이브러리만 |
| 커널 | 각자 따로 | 호스트 커널 공유 |
| 크기 | 수 기가바이트 | 수십~수백 메가바이트 |
| 시작 시간 | 수십 초 | 1초 안팎 |
핵심은 커널 공유다. 가상머신은 운영체제를 통째로 하나 더 돌리지만, 컨테이너는 호스트의 커널을 빌려 쓰고 그 위에 필요한 것만 얹는다. 그래서 가볍고 빠르다. 대신 커널을 공유하므로 호스트와 다른 종류의 커널이 필요한 것은 못 돌린다.
알아야 할 개념 4개
이미지 — 실행에 필요한 것을 담은 읽기 전용 템플릿. 설치 CD나 클래스에 비유할 수 있다.
컨테이너 — 이미지를 실제로 실행한 상태. 인스턴스다. 같은 이미지로 컨테이너를 열 개 띄울 수 있고, 각각 독립적이다. 이미지↔컨테이너 관계가 처음에 가장 헷갈리는 부분인데, “붕어빵 틀과 붕어빵”으로 기억하면 대체로 맞다.
볼륨 — 컨테이너 밖에 데이터를 저장하는 공간. 컨테이너는 지우면 안의 데이터가 같이 사라지므로, 살아남아야 하는 것(데이터베이스 파일, 업로드된 이미지)은 볼륨에 둔다.
네트워크 — 컨테이너끼리 통신하는 통로. 같은 네트워크에 있으면 컨테이너 이름으로 서로를 부를 수 있다. 웹 컨테이너에서 DB 컨테이너를 db:5432로 접근하는 식이다.
첫 컨테이너 띄우기
설치 후 확인부터 한다.
docker --version # 설치 확인
docker run hello-world # 동작 확인
이제 실제로 쓸 만한 것을 띄워본다. nginx 웹서버다.
# 이미지를 받아 컨테이너로 실행. -d 는 백그라운드, -p 는 포트 연결
docker run -d -p 8080:80 --name web nginx
docker ps # 실행 중인 컨테이너 목록
docker logs web # 로그 확인
docker exec -it web bash # 컨테이너 안으로 들어가기
docker stop web # 정지
docker rm web # 삭제
-p 8080:80은 내 컴퓨터의 8080 포트를 컨테이너의 80 포트에 연결한다는 뜻이다. 브라우저에서 localhost:8080을 열면 nginx 기본 페이지가 뜬다. 앞이 내 쪽, 뒤가 컨테이너 쪽 — 이 순서를 헷갈리는 것이 초보의 첫 번째 관문이다.
Dockerfile 작성
남의 이미지를 쓰는 단계에서 내 앱을 이미지로 만드는 단계로 넘어간다.
FROM python:3.12-slim
WORKDIR /app
# 1) 의존성 목록만 먼저 복사해서 설치
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 2) 그다음에 소스 복사
COPY . .
CMD ["python", "main.py"]
의존성 설치와 소스 복사의 순서가 이 파일의 핵심이다. 도커는 각 명령의 결과를 층(layer)으로 캐시한다. 어떤 층이 바뀌면 그 아래 층은 전부 다시 실행된다.
소스를 먼저 복사하고 의존성을 나중에 설치하면, 코드 한 줄만 고쳐도 패키지를 전부 다시 설치한다. 빌드가 매번 몇 분씩 걸린다. 반대로 위처럼 쓰면 requirements.txt가 안 바뀌는 한 설치 층은 캐시에서 재사용되고 빌드가 몇 초로 줄어든다. 초보가 가장 많이 당하는 지점이라 순서를 외워두는 편이 낫다.
빌드와 실행은 이렇다.
docker build -t myapp .
docker run -d -p 8000:8000 myapp
.dockerignore 파일도 같이 만든다. .git, node_modules, 로컬 설정 파일을 제외해야 이미지가 쓸데없이 커지지 않는다.
docker compose로 넘어가기
컨테이너가 하나면 위 명령으로 충분하다. 그런데 앱 + 데이터베이스 + 캐시처럼 여러 개가 필요해지는 순간 명령이 길어지고 순서를 기억해야 한다. 그때 compose로 넘어간다.
services:
web:
build: .
ports:
- "8000:8000"
environment:
DATABASE_URL: postgresql://user:pass@db:5432/app
depends_on:
- db
db:
image: postgres:16
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: app
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
docker compose up -d 한 줄이면 둘 다 뜬다. 주목할 곳은 두 군데다. DATABASE_URL의 호스트가 IP가 아니라 db라는 서비스 이름이라는 점(같은 네트워크에 있어 이름으로 통한다), 그리고 DB 데이터가 볼륨에 저장된다는 점(컨테이너를 지워도 데이터가 남는다).
이 파일 하나를 저장소에 넣어두면 새 팀원의 환경 설정이 docker compose up으로 끝난다. 도커를 쓰는 이유의 대부분이 여기에 있다.
자주 묻는 질문
윈도우에서도 되나요?
됩니다. Docker Desktop을 설치하면 되고, 내부적으로 WSL2를 사용합니다. 설치 시 WSL2를 함께 활성화해야 하며, 윈도우 파일 시스템의 폴더를 컨테이너에 연결하면 입출력이 느려질 수 있으니 프로젝트를 WSL2 내부 경로에 두는 편이 성능에 유리합니다.
도커를 쓰면 느려지나요?
리눅스에서는 오버헤드가 거의 없습니다. 커널을 공유하기 때문에 가상머신과 다릅니다. 다만 맥과 윈도우에서는 내부적으로 리눅스 가상머신을 거치므로 파일 입출력에서 체감 지연이 있을 수 있고, 이것이 개발 환경에서 느껴지는 대부분의 느림입니다.
컨테이너를 지우면 데이터가 사라지나요?
컨테이너 안에 저장한 데이터는 사라집니다. 그래서 데이터베이스 파일이나 업로드된 파일처럼 살아남아야 하는 것은 볼륨에 저장합니다. 볼륨은 컨테이너와 수명이 분리되어 있어 컨테이너를 지우고 다시 만들어도 데이터가 유지됩니다.
실무에서 꼭 필요한가요?
여러 사람이 함께 개발하거나, 로컬과 서버 환경을 맞춰야 하거나, 서비스가 여러 개로 나뉘어 있다면 사실상 필수입니다. 반대로 혼자 만드는 간단한 스크립트나 정적 사이트라면 도커가 없어도 문제가 없고, 오히려 학습 비용이 더 큽니다.

Leave a Reply