도커 기초 강좌 #1 컨테이너란 — VM과 차이, 도커 생태계
이 블로그의 백엔드 트랙(FastAPI, Django, Go 실전)은 마지막 화에서 모두 같은 질문에 부딪힙니다 — “이걸 어떻게 배포하지?”. 답은 한결같이 도커였습니다. 그래서 도커 트랙을 따로 떼어 4 시리즈 24편으로 정리합니다.
이번 시리즈는 도커 기초 6편입니다.
- #1 컨테이너란 — VM과 차이, 도커 생태계 ← 이번 글
- #2 Dockerfile 첫 작성 — RUN, COPY, CMD
- #3 이미지와 컨테이너 — build, run, ps, logs, exec
- #4 볼륨과 네트워크
- #5 레지스트리 — Docker Hub, GHCR, push/pull
- #6
.dockerignore와 빌드 컨텍스트
이번 글은 컨테이너가 무엇인가, 그리고 도커가 어떤 도구들의 묶음인가를 정리하고, docker run hello-world까지 직접 실행해 보며 마무리하겠습니다.
“내 컴퓨터에서는 되는데요” #
배포 이야기를 시작하면 가장 먼저 마주치는 문장입니다. 개발자 머신에서는 잘 도는데 서버에 올리면 깨지는 상황이 자주 나옵니다. 원인을 풀어보면 거의 매번 같은 그림입니다.
- OS가 다름 (macOS / Ubuntu / Amazon Linux)
- 파이썬,노드,Go의 버전이 다름
- 시스템에 설치된 라이브러리(
libssl,libpq,imagemagick)가 다름 - 환경변수, 파일 권한, 타임존 같은 세부 환경이 다름
“앱 + 그 앱이 의존하는 모든 것"을 한 덩어리로 묶어서 어디서든 똑같이 돌릴 방법이 필요합니다. 그 방법이 컨테이너입니다.
가상머신과 무엇이 다른가 #
컨테이너 이전에 같은 문제를 풀던 대표 도구가 가상머신(VM) 입니다. VirtualBox, VMware, 그리고 클라우드의 EC2 같은 서비스가 이 방식을 씁니다. 둘 다 “독립된 환경에서 앱을 돌린다"는 점은 같지만, 격리되는 층(layer)이 다릅니다.
가상머신 컨테이너
┌─────────────────────────┐ ┌─────────────────────────┐
│ App A │ App B │ │ App A │ App B │
├────────────┼────────────┤ ├────────────┼────────────┤
│ Bin/Lib │ Bin/Lib │ │ Bin/Lib │ Bin/Lib │
├────────────┼────────────┤ ├────────────┴────────────┤
│ Guest OS │ Guest OS │ │ Docker Engine │
├────────────┴────────────┤ ├─────────────────────────┤
│ Hypervisor │ │ Host OS │
├─────────────────────────┤ ├─────────────────────────┤
│ Host OS │ │ Hardware │
├─────────────────────────┤ └─────────────────────────┘
│ Hardware │
└─────────────────────────┘- VM은 하이퍼바이저 위에 게스트 OS 전체를 띄웁니다. 커널까지 따로. 기동에 분 단위가 걸리고, 빈 우분투 한 대만 띄워도 디스크 수 GB,메모리 수백 MB가 기본으로 쓰입니다.
- 컨테이너는 호스트 OS의 커널을 공유합니다. 격리는 리눅스 커널 기능(namespace, cgroup) 으로. 그래서 한 컨테이너는 사실상 “격리된 프로세스” 수준 — 시작이 ms 단위, 메모리 오버헤드도 거의 없습니다.
가벼운 만큼 한 호스트에 컨테이너를 수십~수백 개 올릴 수 있고, 빌드,배포 사이클이 대폭 짧아집니다. 대신 한 가지 제약 — 호스트와 다른 커널은 못 씁니다. macOS나 Windows에서 도커를 쓰면 내부적으로는 작은 리눅스 VM이 깔리고, 그 위에서 컨테이너가 돕니다. (이게 Docker Desktop의 정체입니다.)
컨테이너가 어디서 왔나 #
도커가 컨테이너를 발명한 건 아닙니다. 리눅스 커널에는 이미 격리 기능이 오래 전부터 있었습니다.
- chroot (1979) — 프로세스의 루트 디렉터리를 바꿔 파일시스템을 격리
- cgroups (2007) — CPU,메모리,IO 같은 자원을 그룹 단위로 제한
- namespaces (2002~) — PID, 네트워크, 마운트, 사용자 등을 프로세스별로 격리
- LXC (2008) — 위 기능들을 묶은 첫 컨테이너 도구
이 부품들은 강력했지만 다루기가 까다로웠습니다. **“앱을 컨테이너로 묶는 표준 방법”**이 없었습니다. 사람마다, 회사마다 다른 방식을 썼습니다.
2013년 도커가 등장하면서 그게 정리됩니다.
- Dockerfile 이라는 통일된 빌드 정의
- 이미지 라는 휴대 가능한 결과물
- 레지스트리로 이미지를 공유
- Docker CLI로 한 번에 다루기
컨테이너 자체는 옛날 기술이지만, 앱을 패키징하고 옮기는 공통 규약을 만든 게 도커의 공입니다. 지금은 OCI(Open Container Initiative)라는 표준으로 정착해 있고, 도커 외에도 podman, containerd, Kubernetes의 cri-o 같은 구현체들이 같은 이미지 포맷을 공유합니다.
이미지와 컨테이너 — 둘은 다른 것 #
도커를 쓰기 시작하면 이 두 단어가 헷갈립니다. 짧게 정리하면:
| 이미지 (image) | 컨테이너 (container) | |
|---|---|---|
| 비유 | 클래스 / 설계도 | 인스턴스 / 실체 |
| 상태 | 읽기 전용, 변하지 않음 | 실행 중,중지,삭제 가능 |
| 만들어지는 방법 | docker build (Dockerfile에서) | docker run (이미지에서) |
| 보관 위치 | 로컬 캐시 또는 레지스트리(Hub, GHCR, ECR …) | 호스트 디스크 |
같은 이미지로 컨테이너를 100 개 띄울 수 있고, 그 100 개는 서로 영향을 주지 않습니다. 이미지가 변하지 않는다는 점이 컨테이너 시스템의 **재현성(reproducibility)**을 만듭니다.
도커 생태계 개요 #
“도커” 라는 이름 아래에는 사실 여러 도구가 모여 있습니다. 처음 익히기 전에 큰 그림을 잡아두면 글을 따라가기 편합니다.
┌────────────────────────────┐
│ Docker CLI │ ← 우리가 치는 명령
│ (docker run / build ...) │
└─────────────┬──────────────┘
│ REST API
┌─────────────▼──────────────┐
│ Docker daemon │ ← 실제 일을 하는 프로세스
│ (dockerd -> containerd) │
└──┬──────┬────────┬─────────┘
│ │ │
┌──────▼─┐ ┌─▼────┐ ┌▼────────────┐
│ Images │ │ Net │ │ Containers │
│ (cache)│ │ Vol │ │ (runtime) │
└────────┘ └──────┘ └─────────────┘- Docker Engine — 컨테이너를 실제로 띄우는 데몬(
dockerd)과 그걸 다루는 CLI(docker). 내부적으로는containerd같은 런타임 계층을 사용합니다. - Docker CLI —
docker run,docker build,docker ps같은 명령. 사실 이 명령들은 데몬에게 REST API로 요청을 보내는 얇은 클라이언트입니다. - Dockerfile — “이미지를 어떻게 만들지"를 적은 텍스트 파일. (#2에서 본격적으로 다룹니다.)
- Docker Compose — 여러 컨테이너를 한 묶음으로 정의,실행하는 도구. 보통
compose.yaml(또는docker-compose.yml) 한 파일로 web + db + cache를 동시에 띄울 수 있습니다. (중급 시리즈 주제) - Docker Hub / GHCR / ECR — 이미지를 보관,공유하는 레지스트리(registry). GitHub와 GitHub Container Registry의 관계와 같습니다. (#5에서 다룹니다.)
- Docker Desktop — macOS / Windows 용 통합 패키지입니다. 내부에 작은 리눅스 VM과 위 도구들을 함께 묶어둔 패키지입니다.
이번 시리즈는 CLI + Dockerfile + 레지스트리까지, 즉 한 컨테이너를 만들고 실행하는 데 필요한 핵심만 다룹니다. Compose와 운영은 다음 시리즈들입니다.
설치 — Docker Desktop #
macOS / Windows 사용자는 Docker Desktop 한 번 설치로 끝납니다.
- 다운로드: docker.com/products/docker-desktop
- macOS 라면 Apple Silicon / Intel 둘 중 본인 칩에 맞는 빌드를 받으세요.
설치 후 Docker Desktop을 실행해 두면, 터미널에서 docker 명령이 곧장 됩니다. 별도의 PATH 설정은 필요하지 않습니다.
리눅스(우분투)라면 Desktop 대신 Engine을 직접 설치합니다.
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER # sudo 없이 docker 명령 쓰려면usermod 후에는 한 번 로그아웃했다가 다시 들어와야 그룹 변경이 적용됩니다.
설치가 끝났으면 버전을 찍어보세요.
docker --version
# Docker version 27.x.x, build ...
docker info
# 데몬 상태, 컨테이너 수, 이미지 수, 스토리지 드라이버 등docker info에서 에러가 나면 데몬이 안 떠 있는 겁니다. macOS / Windows는 Docker Desktop이 켜져 있는지, 리눅스라면 sudo systemctl start docker를 확인하세요.
첫 컨테이너 — hello-world
#
도커가 잘 설치됐는지 확인하는 의례 명령이 있습니다.
docker run hello-world처음 실행하면 출력이 이렇게 흐릅니다.
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
...
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
This message shows that your installation appears to be working correctly.
...한 줄짜리 명령 뒤에서 일어난 일을 풀어보면 이렇습니다.
dockerCLI가 데몬에게 “hello-world이미지를 실행해줘” 라고 요청- 데몬이 로컬 캐시를 뒤짐 → 없음
- Docker Hub에서
hello-world:latest를 받아옴 (Pulling from library/hello-world) - 받은 이미지로 컨테이너를 만들고 실행
- 컨테이너 안의 프로그램이 “Hello from Docker!” 를 찍고 종료
- 컨테이너는 종료됐지만 사라지진 않음(아직 디스크에 있음)
방금 만들어진 것들을 직접 봅시다.
docker images
# REPOSITORY TAG IMAGE ID SIZE
# hello-world latest abc123... 13.3kB
docker ps -a
# CONTAINER ID IMAGE COMMAND STATUS NAMES
# d3f4... hello-world "/hello" Exited (0) 10 seconds ago bold_curiedocker images는 호스트 디스크에 캐시된 이미지를 보여줍니다.docker ps는 실행 중인 컨테이너만,docker ps -a는 종료된 것까지 모두 보여 줍니다.
치워두려면:
docker rm bold_curie # 컨테이너 삭제 (이름은 본인 것으로)
docker rmi hello-world # 이미지 삭제한 발만 더 — docker run -it ubuntu
#
hello-world는 한 줄 찍고 끝나는 컨테이너라 감이 잘 안 옵니다. 실제 리눅스 안에 들어가 보면 “이게 그렇게 가벼운 거였어?” 가 체감됩니다.
docker run -it ubuntu:24.04 bash처음 실행하면 우분투 24.04 이미지를 받아오고 (~80MB), 곧장 컨테이너 안의 셸 프롬프트가 떠요.
root@a1b2c3d4e5f6:/# cat /etc/os-release
PRETTY_NAME="Ubuntu 24.04 LTS"
...
root@a1b2c3d4e5f6:/# uname -r
6.10.0-... # 호스트의 커널 버전
root@a1b2c3d4e5f6:/# exit여기서 한 가지 흥미로운 사실 — cat /etc/os-release는 우분투라고 나오지만, uname -r의 커널 버전은 컨테이너 바깥에서 공유하는 커널의 것입니다. 리눅스 호스트에서는 호스트 커널이 보이고, Docker Desktop에서는 내부 Linux VM의 커널이 보일 수 있습니다. 컨테이너가 자기만의 커널을 들고 있지 않다는 점이 여기서 드러납니다.
-it 플래그는 두 가지 의미입니다.
-i(--interactive): 컨테이너의 stdin을 열어둠 (입력을 받을 수 있게)-t(--tty): 가상 터미널을 붙임 (셸이 자연스럽게 동작하게)
대화형(인터랙티브) 컨테이너를 띄울 땐 거의 항상 같이 옵니다. exit로 나오면 셸이 종료되고, 셸이 종료되면 컨테이너의 메인 프로세스가 종료된 셈이라 컨테이너도 멈춥니다.
자주 쓰는 명령 한 표 #
이번 시리즈에서 두루 쓸 명령을 미리 한곳에 모아둡니다. 외울 필요는 없고, 막히면 돌아오세요.
| 명령 | 하는 일 |
|---|---|
docker run <image> | 이미지로 컨테이너를 만들고 실행 |
docker run -it <image> <cmd> | 인터랙티브 모드 (셸 진입에 자주 씀) |
docker run -d <image> | 백그라운드 실행 (detached) |
docker ps / docker ps -a | 실행 중 / 모든 컨테이너 |
docker images | 캐시된 이미지 |
docker logs <container> | 컨테이너의 stdout/stderr |
docker exec -it <container> bash | 실행 중 컨테이너에 셸로 들어가기 |
docker stop <container> | 그레이스풀 종료 (SIGTERM → SIGKILL) |
docker rm <container> | 종료된 컨테이너 삭제 |
docker rmi <image> | 이미지 삭제 |
docker build -t <name> . | 현재 디렉터리의 Dockerfile로 이미지 빌드 |
docker pull <image> | 레지스트리에서 이미지 받기 |
docker push <image> | 레지스트리로 이미지 올리기 |
정리 #
이번 글에서 잡은 그림:
- 컨테이너는 앱과 의존성 전체를 한 덩어리로 묶는 휴대 가능한 단위입니다. “내 컴퓨터에서는 되는데” 문제를 해결합니다.
- 가상머신이 게스트 OS까지 가상화한다면, 컨테이너는 호스트 커널을 공유해 가볍게 격리합니다.
- 컨테이너 자체는 chroot/cgroups/namespaces 같은 옛 기술 위에 서 있습니다. 도커가 한 일은 표준 빌드,운반 규약을 만든 것입니다.
- 이미지 = 변하지 않는 설계도, 컨테이너 = 그 설계도로 만든 실행 인스턴스.
- 도커는 한 도구가 아니라 Engine + CLI + Compose + Hub의 생태계입니다. 이번 시리즈는 그중 CLI + Dockerfile + 레지스트리까지 다룹니다.
docker run hello-world/docker run -it ubuntu로 첫 컨테이너를 실행해 봤습니다.
다음 글(#2 Dockerfile 첫 작성)에서는 남이 만든 이미지를 받아 쓰는 데서 한 걸음 더 나아가, 내 앱을 위한 이미지를 직접 만드는 방법을 다룹니다. FROM, RUN, COPY, CMD 네 명령으로 가장 단순한 Dockerfile을 짜보고, docker build로 이미지를 굽는 흐름을 잡습니다.