새벽 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
심화 — '커널을 공유한다'는 말이 데려오는 현실
심화: '호스트 커널을 공유한다'의 진짜 의미 — Mac·Windows의 숨은 리눅스 VM과 CPU 아키텍처
앞에서 컨테이너는 호스트 커널을 공유한다고 했습니다. 이 한 문장을 곧이곧대로 따라가면, 초보자가 자주 부딪히는 두 가지 현실이 설명됩니다.
- Mac·Windows에는 리눅스 커널이 없습니다: 리눅스 컨테이너는 리눅스 커널이 있어야 돕니다. 그런데 macOS·Windows에는 리눅스 커널이 없으므로, Docker Desktop은 속으로 경량 리눅스 VM을 하나 돌리고, 여러분이 띄우는 컨테이너는 사실 그 VM의 커널을 공유합니다. "컨테이너는 VM보다 가볍다"는 말은 맞지만, Mac에서는 그 가벼운 컨테이너들이 숨은 VM의 자원 예산 안에서 돕니다. Docker Desktop 설정의 CPU·메모리 슬라이더가 바로 그 VM 크기이고, Mac에서 bind mount가 느린 것도 파일이 VM 경계를 넘나들기 때문입니다.
- 커널만이 아니라 CPU 아키텍처에도 묶입니다: 컨테이너 안의 바이너리는 특정 CPU 명령어 집합(amd64/arm64 등)으로 컴파일돼 있습니다. 호스트 커널을 직접 쓰는 만큼 호스트의 CPU 아키텍처와도 맞아야 합니다. Apple Silicon(arm64) 맥이 늘어나면서, amd64 전용 이미지를 그대로 못 돌리는 상황이 흔해졌습니다(아래 트러블케이스).
한계도 여기서 나옵니다 — 커널을 공유하므로 리눅스 호스트에서 윈도우 컨테이너를, 혹은 그 반대를 네이티브로 돌릴 수 없고, 아키텍처가 다른 이미지도 그대로는 실행되지 않습니다. "가볍다"는 이점과 "호스트 커널·아키텍처에 묶인다"는 제약은 같은 동전의 양면입니다.
동료가 쓰던 이미지를 그대로 받아 실행했는데, 인텔 맥·인텔 서버에서는 멀쩡하던 것이 새 M1/M2 맥이나 ARM 클라우드 인스턴스에서만 실패합니다.
원인: 컨테이너 안 바이너리는 특정 CPU 아키텍처용으로 컴파일돼 있습니다. amd64(x86_64) 전용으로 빌드된 이미지를 arm64 호스트에서 실행하면 커널이 그 바이너리를 해석하지 못해 exec format error가 납니다. no matching manifest는 한 발 더 앞 단계로, 그 레지스트리 이미지에 현재 아키텍처용 빌드 자체가 없다는 뜻입니다.
진단:
# 내 호스트(도커) 아키텍처 확인
docker version --format '{{.Server.Arch}}' # arm64 / amd64
# 이미지가 어떤 아키텍처용인지 확인
docker image inspect myimage:tag --format '{{.Architecture}}'
호스트 Arch와 이미지 Architecture가 다르면 이 문제입니다.
해결:
# 1) 여러 아키텍처를 담은 멀티아키 이미지를 쓰면 자동으로 맞는 것이 받아진다
docker pull node:20 # 공식 이미지는 대개 multi-arch
# 2) 급하면 플랫폼을 지정해 에뮬레이션으로 실행 (느릴 수 있음)
docker run --platform linux/amd64 myimage:tag
# 3) 내가 빌드하는 이미지라면 buildx로 대상 아키텍처를 맞춰 빌드
docker buildx build --platform linux/amd64,linux/arm64 -t myimage:tag --push .
근본 해결은 팀이 쓰는 이미지를 처음부터 멀티아키로 배포하는 것입니다. 그러면 인텔이든 애플 실리콘이든 각자 맞는 아키텍처가 알아서 내려받아집니다.
팀에 Docker가 없을 때와 있을 때
5명 백엔드 팀에 신입이 합류했습니다.
Docker 없는 팀에서는 이렇게 됩니다:
- README를 보고 Python 버전 설치 → 버전이 맞지 않으면 선임에게 질문
- 라이브러리 설치 → OS에 따라 다르게 설치되는 패키지 때문에 막힘
- 환경 세팅에만 반나절 ~ 하루 소요
Docker를 도입한 팀은 이렇게 됩니다:
git clone https://github.com/team/project
docker compose up
신입이 첫날 한 시간 안에 로컬에서 서비스 전체를 실행합니다. "우리 팀에서만 재현되는 버그"가 사라집니다. Docker Compose는 다음 모듈에서 다룹니다.
명령어·단축키 빠른 참조
이 첫 모듈에서 다룬 기본 명령과 아키텍처 진단 명령을 모았습니다. 대부분 이후 모듈에서 더 깊이 다룹니다.
| 명령어/단축키 | 용도 | 자주 쓰는 예 |
|---|---|---|
docker run hello-world | Docker 설치·동작 확인(첫 컨테이너) | 이미지 pull→컨테이너 생성→실행→종료 4단계 확인 |
docker ps / docker ps -a | 실행 중 / 전체(종료 포함) 컨테이너 목록 | hello-world는 종료돼 ps엔 없고 ps -a에 Exited로 남음 |
docker version --format '{{.Server.Arch}}' | 호스트(도커) CPU 아키텍처 확인 | arm64 / amd64 — exec format error 진단 1단계 |
docker image inspect --format '{{.Architecture}}' | 이미지의 대상 아키텍처 확인 | 호스트 Arch와 다르면 exec format error 발생 |
docker run --platform | 다른 아키텍처를 에뮬레이션 실행 | docker run --platform linux/amd64 myimage:tag (느릴 수 있음) |
docker buildx build --platform | 멀티아키 이미지 빌드·푸시 | docker buildx build --platform linux/amd64,linux/arm64 -t img --push . |
sudo systemctl start docker | 리눅스에서 도커 데몬 시작 | Cannot connect to the Docker daemon 해결 |
sudo usermod -aG docker $USER | sudo 없이 docker 사용(그룹 추가) | 추가 후 터미널을 닫고 새로 열어야 적용 |
관련 모듈로 더 깊이:
- 가상화의 혁신과 Docker 엔진의 핵심 구조 — VM과 컨테이너의 구조적 차이, Docker 엔진이 격리를 만드는 원리
- 컨테이너 실행 옵션과 네임스페이스 작동 원리 — 이 모듈의 동기를 실제
docker run명령과 옵션으로 실습하는 첫걸음
다음 모듈에서는 hello-world를 넘어서 실제로 의미 있는 컨테이너를 실행합니다. docker run의 구조와 자주 쓰는 옵션들 — 포트 연결, 볼륨 마운트, 환경변수 주입을 직접 실습합니다.