새벽 2시. 분명 로컬에서 통과하던 배포가 운영 서버에서 SyntaxError를 냈습니다.
확인해보니 개발 PC는 Python 3.11, 운영 서버는 Python 3.6이었습니다. 배포 체크리스트에 버전 확인 항목이 없었고, 아무도 그 차이를 배포 전에 알지 못했습니다.
README를 더 꼼꼼히 쓴다고 해결되지 않습니다. 환경 자체가 코드와 분리되어 있는 게 문제이기 때문입니다. 컨테이너는 이 구조를 바꿉니다.
- 1"내 PC에서는 됐는데" 문제가 왜 발생하는지 설명할 수 있다
- 2컨테이너가 환경 불일치를 어떻게 구조적으로 해결하는지 이해한다
- 3VM이 무엇이고 컨테이너와 어떻게 다른지 말할 수 있다
- 4상황에 따라 컨테이너와 VM 중 어느 쪽이 적합한지 판단할 수 있다
문제: 환경이 다르면 코드가 달리 동작한다
같은 코드를 서로 다른 세 환경에 배포했을 때 실제로 발생하는 불일치입니다.

이 차이 때문에 팀들이 시도하는 해결책들이 있습니다:
- README 의존성 가이드 작성 → 사람마다 읽는 방식이 다르고, 문서는 코드와 함께 최신화되지 않습니다
- 배포 자동화 스크립트 → 서버마다 환경이 조금씩 달라서 결국 예외 처리가 쌓입니다
- "이 서버에서는 이렇게" 위키 → 위키가 실제 서버 상태를 따라가지 못합니다
어느 것도 근본 원인을 해결하지 못합니다. 환경 자체가 코드와 분리되어 있기 때문입니다.
컨테이너: 실행 환경을 코드와 함께 패키징한다
컨테이너의 핵심 아이디어는 단순합니다. 애플리케이션과 그 앱이 실행되는 환경 전체를 하나의 패키지로 묶는 것입니다.
이 패키지(이미지)를 어느 서버에 가져다 놓아도, 서버에 무엇이 설치되어 있든 상관없이 항상 동일한 환경에서 실행됩니다.

이미지 하나에 담기는 것:
| 항목 | 예시 |
|---|---|
| 앱 코드 | src/, main.py 전체 |
| 런타임 | Python 3.11.4 (버전 고정) |
| 의존성 | requirements.txt 기준 설치된 패키지 |
| 실행 명령 | CMD ["uvicorn", "main:app", "--host", "0.0.0.0"] |
로컬과 CI 서버와 운영 서버가 정확히 같은 이미지를 실행합니다. "내 로컬에서는 됐는데"라는 말이 의미를 잃습니다.
VM이 먼저 이 문제를 풀었다 — 그런데 무겁다
Docker를 배우면서 VM을 이해해야 하는 실용적인 이유가 있습니다. 현업에서 컨테이너는 대부분 VM 위에서 실행됩니다. AWS EC2, GCP Compute Engine이 VM이고, 그 위에서 Docker를 실행하는 것이 가장 일반적인 패턴입니다.
VM이 어떻게 격리를 구현하는지 알아야, 컨테이너가 VM과 무엇이 다른지 — 무엇을 얻고 무엇을 포기했는지 이해할 수 있습니다.
VM은 물리 서버 위에 하이퍼바이저라는 소프트웨어 레이어를 올리고, 그 위에 완전한 Guest OS를 실행합니다. 각 VM은 독립된 커널을 가집니다.
VM의 강점:
- 하드웨어 수준의 강력한 격리 — 한 VM이 침해돼도 다른 VM은 안전
- 서로 다른 OS를 같은 서버에서 실행 가능 (Linux 위에 Windows 등)
- 스냅샷으로 전체 상태를 저장하고 복원 가능
VM의 한계:
- 각 VM마다 Guest OS 전체(수 GB)가 필요 → 높은 메모리 오버헤드
- 부팅 시간 30초 ~ 수 분 → 빠른 스케일링에 불리
- 서버 한 대에 수십 개 수준이 한계
컨테이너: Guest OS를 포기하고 속도를 얻는다
VM의 무거움을 이해하면 컨테이너가 왜 등장했는지 납득이 됩니다.
CI/CD 파이프라인에서 테스트를 실행할 때마다 VM을 새로 부팅하면 수 분이 걸립니다. 하루에 수십 번 배포하는 팀에서 이 대기 시간은 개발 속도를 직접 제한합니다. 마이크로서비스 아키텍처에서 서비스가 수십 개라면, 서비스마다 VM을 하나씩 주면 서버 비용이 기하급수적으로 늘어납니다.
컨테이너는 Guest OS를 포기하는 대신 밀리초 안에 시작되는 격리된 실행 환경을 제공합니다. 하이퍼바이저 없이 호스트 OS의 커널을 직접 공유하며, 리눅스 커널의 두 가지 기능으로 격리를 구현합니다.
- 네임스페이스 — 컨테이너가 "보는 세계"를 분리합니다. 컨테이너 안에서는 다른 컨테이너의 프로세스, 네트워크, 파일시스템이 보이지 않습니다
- cgroups — CPU, 메모리, 디스크 I/O 사용량을 제한합니다.
--memory 512m같은 옵션이 이것으로 구현됩니다
지금은 이름만 기억하면 됩니다. 동작 원리는 이후 모듈에서 실제로 확인합니다.
VM vs 컨테이너 — 한눈에 비교

| VM | 컨테이너 | |
|---|---|---|
| 격리 단위 | 하드웨어 수준 (완전한 OS) | 프로세스 수준 (커널 공유) |
| 부팅 시간 | 30초 ~ 수 분 | 밀리초 ~ 수 초 |
| 이미지 크기 | 수 GB | 수십 ~ 수백 MB |
| 서버당 밀도 | 수십 개 | 수백 ~ 수천 개 |
| 보안 격리 | 강함 | 상대적으로 약함 |
| 다른 OS 실행 | 가능 | 불가 (호스트 커널 공유) |
Docker가 설치되어 있다면 지금 바로 실행할 수 있습니다.
이 명령 하나로 Docker는 네 가지를 순서대로 처리합니다:
- 로컬에
hello-world이미지가 없으면 Docker Hub에서 내려받습니다 - 이미지로부터 컨테이너를 생성합니다
- 컨테이너 안의 프로그램을 실행합니다
- 프로그램이 종료되면 컨테이너를 멈춥니다
docker run hello-world- "Hello from Docker!" 메시지가 출력됐는가?
- "This message shows that your installation appears to be working correctly." 문장이 있는가?
- docker ps 를 실행하면 목록이 비어있는가? — 컨테이너가 할 일을 마치고 종료된 것이 정상
- docker ps -a 를 실행하면 hello-world 컨테이너가 Exited 상태로 남아있는가?
Docker Desktop이 실행되지 않은 상태에서 명령을 입력하면 발생합니다.
확인할 것:
- macOS / Windows: 메뉴바(트레이) 아이콘에서 Docker Desktop이 실행 중인지 확인합니다
- Linux: 아래 명령으로 Docker 서비스 상태를 확인합니다
# 실습 디렉토리 준비
mkdir -p /tmp/docker/part1/exam_1 && cd /tmp/docker/part1/exam_1
sudo systemctl status docker
해결 — Linux에서 서비스 시작:
sudo systemctl start docker
해결 — sudo 없이 사용하려면 (Linux):
# 현재 사용자를 docker 그룹에 추가
sudo usermod -aG docker $USER
# 터미널을 닫고 새로 열어야 적용됩니다
Linux에서 sudo 없이 Docker를 실행하면 발생합니다.
# 현재 사용자가 docker 그룹에 속해 있는지 확인
groups $USER
# docker 그룹에 추가 후 새 터미널 열기
sudo usermod -aG docker $USER
팀에 Docker가 없을 때와 있을 때
5명 백엔드 팀에 신입이 합류했습니다.
Docker 없는 팀에서는 이렇게 됩니다:
- README를 보고 Python 버전 설치 → 버전이 맞지 않으면 선임에게 질문
- 라이브러리 설치 → OS에 따라 다르게 설치되는 패키지 때문에 막힘
- 환경 세팅에만 반나절 ~ 하루 소요
Docker를 도입한 팀은 이렇게 됩니다:
git clone https://github.com/team/project
docker compose up
신입이 첫날 한 시간 안에 로컬에서 서비스 전체를 실행합니다. "우리 팀에서만 재현되는 버그"가 사라집니다. Docker Compose는 다음 모듈에서 다룹니다.
다음 모듈에서는 hello-world를 넘어서 실제로 의미 있는 컨테이너를 실행합니다. docker run의 구조와 자주 쓰는 옵션들 — 포트 연결, 볼륨 마운트, 환경변수 주입을 직접 실습합니다.