Docker는 애플리케이션 실행 환경을 통째로 패키징해서 어디서든 동일하게 실행하게 만드는 컨테이너 기술이다.
1. 서비스의 추상화: 하드웨어나 OS 환경에 종속되지 않고 서비스를 어디서든 실행할 수 있는 단위로 만든다. 코드, 라이브러리, 설정, 런타임을 하나의 실행 단위로 묶어서 "내 PC에서는 되는데 서버에서는 안 돼" 문제를 제거한다.
2. 격리: 각 서비스가 독립된 환경을 가지면서 종속성 문제를 해결한다. 라이브러리 충돌, 버전 꼬임을 방지한다.
Docker는 Dockerfile이라는 설계도를 통해 Image를 만들고, Image를 통해 Container를 만든다.
1) Dockerfile (설계도)
→ 어떤 OS, 어떤 패키지, 어떤 명령을 실행할지 정의
2) Image (실행 가능한 패키지)
→ 여러 개의 Read-Only Layer로 구성
→ FROM, RUN, COPY 한 줄마다 Layer가 추가됨
3) Container (Image를 실행한 실제 프로세스)
→ 기존 Layer 위에 Writable Layer 하나가 추가됨

Dockerfile로 이미지를 직접 만드는 것 말고, Docker Hub(https://hub.docker.com/)에서 이미지를 받아오는 것부터 시작해보자.
docker pull [OPTIONS] REPOSITORY[:TAG|@DIGEST]
# 기본값
docker pull <REPOSITORY>:latest
# 태그로
docker pull <REPOSITORY>:<TAG>
# 다이제스트로
docker pull <REPOSITORY>@<DIGEST>
# 예시
docker pull ubuntu:22.04
docker pull은 Docker Hub에 있는 수많은 이미지 중에서 받아오는 명령이다.
주요 옵션
| Option | Short | Description |
|---|---|---|
--all-tags | -a | 태그된 모든 이미지를 다운로드 |
--platform | 이미지의 platform 설정 |
Tag와 Digest의 차이
Tag는 사용자가 특정 이미지에 부여한 값이라 같은 Tag라도 다운로드 시점에 따라 다른 이미지를 나타낼 수 있다. 사용자가 붙인 "이름표" 느낌이다. latest 태그도 "최신"이라는 뜻이지 고정 버전은 아니다.
Digest는 변경이 불가능한 고유 값이며, 동일한 방식으로 생성된 이미지라면 동일한 Digest가 생성된다. "정확히 동일한 이미지를 받았다"를 보장하는 장치다.
Manifest는 해당 이미지가 어떤 파일 시스템 레이어들로 구성되어 있는지, 어떤 CPU 아키텍처(amd64, arm64 등)를 지원하는지 설명하는 메타데이터다.
ARM용 Ubuntu를 다운로드하는 예시는 다음과 같다.
docker pull --platform linux/arm64 ubuntu:22.04
docker images [OPTIONS] [REPOSITORY[:TAG]]
# Alias
docker image list
docker image ls
주요 옵션
| Option | Short | Description |
|---|---|---|
--all | -a | 중간 이미지(intermediate image)도 표시 |
--digests | Digest 표시 | |
--filter | -f | 필터 조건에 맞는 이미지만 표시 |
--no-trunc | sha256 형식의 전체 이미지 ID 표시 | |
--quiet | -q | 이미지 ID만 표시 |
docker rmi [OPTIONS] IMAGE [IMAGE...]
# Alias
docker image rm
| Option | Short | Description |
|---|---|---|
--force | -f | 컨테이너가 존재하더라도 이미지를 삭제 |
주의사항 — 동일한 Image ID를 가진 이미지가 2개 이상이면 Image ID로 삭제할 수 없다 (-f 옵션을 쓰면 모두 삭제된다). 다른 이미지가 베이스 이미지로 사용하고 있으면 삭제할 수 없다.
docker image prune [OPTIONS]
| Option | Short | Description |
|---|---|---|
--all | -a | dangling 이미지를 포함한 사용하지 않는 모든 이미지를 삭제 |
--filter | 필터 값 지정 (예: until=<timestamp>) |
docker inspect [OPTIONS] NAME|ID [NAME|ID...]
이미지의 주요 정보를 확인할 수 있다. 예를 들어 PostgreSQL 이미지의 레이어 개수를 확인하려면 다음과 같이 한다.
docker image inspect postgres@sha256:a2282ad0db623c27f03bab803975c9e3942a24e974f07142d5d69b6b8eaaf9e2
컨테이너는 Docker 이미지로부터 생성된 하나의 실행 중인 프로세스다. 프로세스(PID=1)가 종료되면 컨테이너도 종료된다. 컨테이너의 생명주기는 결국 그 메인 프로세스의 생명주기와 같다.
docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
주요 옵션
| Option | Short | Description |
|---|---|---|
--name | 컨테이너 이름 지정 | |
--detach | -d | 백그라운드에서 실행 |
--env | -e | 환경 변수 설정 |
--env-file | 환경 변수 파일 설정 | |
--expose | 포트 또는 포트 범위 노출 | |
--publish | -p | 컨테이너 포트 공개 (-p HOST_PORT:CONTAINER_PORT) |
--rm | 종료 시 자동 삭제 | |
--interactive | -i | STDIN 활성화 |
--tty | -t | pseudo-TTY 할당 |
--volume | -v | 볼륨 설정 |
Expose와 Publish의 차이 — expose는 주석 역할만 한다. 실제로 포트를 열어주지 않고 "이 포트 쓴다"는 표시일 뿐이다. publish는 호스트의 포트와 바인드해서 호스트 또는 외부에서 접근 가능하게 한다. "이 포트 열어라"에 해당한다.
-i(interactive)와 -t(tty) — -i는 컨테이너의 표준 입력(STDIN)을 열어두는 역할로, 키보드 입력을 컨테이너가 계속 받아들일 수 있게 한다. 이게 없으면 컨테이너에 접속해도 입력한 명령이 전달되지 않고 무시된다. -t는 가상 터미널(Pseudo-TTY)을 할당해서 리눅스 터미널 같은 환경(쉘 프롬프트, 줄바꿈 등)을 만들어준다. 이게 없으면 root@container_id:/# 같은 프롬프트가 나타나지 않아 매우 불편하다.
-it를 함께 쓰면 사람이 직접 조작할 수 있는 컨테이너가 된다. 쉘을 쓸 때는 -it가 표준 세트다.
docker run -d --name server ubuntu:22.04만 실행하면 어떻게 될까. -it가 없이 bash를 실행하면 입력도 없고 터미널도 없어서 bash가 바로 종료된다.
이걸 이해하려면 터미널과 프로세스의 연결 구조를 알아야 한다. 터미널은 명령을 입력하면 그 명령이 끝날 때까지 기다리는 창이다. 예를 들어 ping google.com을 치면 그 명령이 끝나기 전까지 새 명령을 칠 수 없다. 이 상태를 "터미널이 그 프로그램에 붙어 있다"고 말한다.
이 말은 구조적인 사실이다. 터미널은 항상 STDIN(키보드 입력), STDOUT(화면 출력), STDERR(에러 출력) 세 가지를 프로그램과 연결한다.
터미널 ↔ 프로그램 (STDIN/STDOUT/STDERR 연결)
프로그램이 실행되는 동안 터미널은 그 프로그램의 입출력 통로가 되고, 프로그램이 끝나기 전까지는 다른 일을 못 한다.
"뒤에서 돌린다(-d)"는 이 연결을 끊는 것이다. 프로그램은 계속 실행되지만 터미널은 그 프로그램과 연결되지 않는다.
프로그램은 실행 중
터미널은 자유
전화 통화 중이면 다른 일을 못 하지만, 스피커폰을 켜두면 통화는 유지되면서 다른 일도 할 수 있는 것과 비슷하다.
docker run ubuntu는 터미널이 컨테이너 안의 메인 프로세스에 붙는 구조다.
터미널 ↔ 컨테이너 안의 메인 프로세스
-d를 주면 이렇게 바뀐다.
컨테이너 실행
터미널 ↔ 연결 안 함
bash는 리눅스에서 명령어를 받아 해석하고 실행해주는 쉘 프로그램이다. bash가 없으면 키보드는 있지만 명령을 전달할 통역사가 없는 상태가 된다.
컨테이너는 "프로그램 하나"를 중심으로 산다. 그 프로그램이 메인 프로세스(PID 1)다.
PID1 살아 있음 → 컨테이너 살아 있음
PID1 종료됨 → 컨테이너 종료
docker run -dit ubuntu:22.04 bash를 실행하면 ubuntu 컨테이너 안에서 bash가 메인 프로세스(PID 1)로 실행된다.
컨테이너
└─ PID 1 = bash
bash가 실행 중이면 컨테이너도 계속 실행되고, bash가 종료되면 컨테이너도 종료된다.
docker run ubuntu:22.04만 실행하면 실행할 프로그램이 없어서 컨테이너가 즉시 종료된다. docker run ubuntu:22.04 bash로 실행하면 bash가 계속 실행되므로 컨테이너도 계속 살아있다.
bash가 필요한 이유는 두 가지다.
apt-get update, ps -aux 같은 명령은 사람이 직접 입력해야 하는 작업이다.bash 대신 다른 프로그램을 써도 된다. 조건은 "계속 실행되는 프로그램"이면 된다.
docker run -dit ubuntu sleep infinity
docker run -dit ubuntu tail -f /dev/null
이것도 컨테이너를 살려두는 역할은 하지만, 명령을 치기 불편해서 실습과 학습에서는 bash가 가장 편하다.
요약하면 bash는 컨테이너를 살려두는 심장이자, 사람이 컨테이너와 대화하게 해주는 입이다.
80 포트를 이용해 nginx 서비스를 실행하는 예시다.
docker run -d --name web -p 80:80 nginx
docker ps [OPTIONS]
# 종료된 컨테이너 포함
docker ps -a
# 예시: server라는 이름의 꺼진 컨테이너 포함 다 출력
docker ps -a -f "name=server"
| Option | Short | Description |
|---|---|---|
--all | -a | 상태와 관계없이 모든 컨테이너 표시 |
--filter | -f | 지정된 조건에 맞는 컨테이너만 표시 |
--size | -s | 전체 파일 사이즈도 표시 |
docker logs [OPTIONS] <NAME_OR_ID>
| Option | Short | Default | Description |
|---|---|---|---|
--follow | -f | 계속 로그를 표시 | |
--tail | -n | all | 마지막 n개의 로그를 표시 |
--timestamps | -t | timestamp 표시 | |
--since | 특정 시간 이후 발생한 로그만 표시 | ||
--until | 특정 시간 이전 발생한 로그만 표시 |
컨테이너에 접속하는 방법은 두 가지다.
docker attach [OPTIONS] CONTAINER
컨테이너 안에서 실행 중인 메인 프로세스(PID=1)의 입출력(STDIN, STDOUT, STDERR)에 내 터미널을 그대로 연결한다.
내 터미널 ↔ 컨테이너 PID=1 프로세스
Ctrl+C로 프로세스를 종료하면 컨테이너가 종료된다. Ctrl+C는 SIGINT(Signal Interrupt) 신호를 현재 연결된 프로세스에 보내기 때문이다.
Ctrl+C → PID=1에게 SIGINT → 메인 프로세스 종료 → 컨테이너 종료
attach 상태에서 안전하게 빠져나오려면 Ctrl+P+Q를 써야 한다. 컨테이너는 살려두고 터미널 연결만 끊는 방법이다.
docker exec -it [OPTIONS] <ID_OR_NAME> /bin/bash|sh|...
이미 실행 중인 컨테이너 안에 새로운 프로세스 하나를 추가로 실행한다.
컨테이너
├─ PID 1 : 메인 프로세스
└─ PID 23 : 내가 exec로 띄운 bash
exec로 들어간 쉘은 메인 프로세스와 완전히 별개다. Ctrl+C를 눌러도 내가 띄운 쉘만 종료되고 컨테이너는 그대로 살아있다.
exec는 메인 프로세스가 실행되는 동안(컨테이너가 살아있을 때)만 실행할 수 있다. exec로 실행한 명령은 컨테이너 재실행(start) 시 다시 실행되지 않는다. exec는 컨테이너 설정이 아니라 실행 중에 추가한 임시 프로세스이기 때문이다.
-it 옵션이 없으면 bash는 실행되지만 입력도 안 되고 화면도 이상해진다.
| Attach | Exec | |
|---|---|---|
| 접근 터미널 | 컨테이너의 STDIN, STDOUT, STDERR | 새로운 process의 Terminal |
| 접속 종료 방법 | Ctrl + P + Q | Ctrl + D |
| PID | 1 (Main Process) | != 1 |
ubuntu 컨테이너를 종료 시 삭제, 백그라운드, -it 옵션으로 실행하고, PID는 ps -aux로 확인한다.
컨테이너 명령어는 항상 working directory를 기준으로 실행된다.
$ docker exec clock sh -c "pwd"
/
$ docker exec --workdir /bin clock sh -c "pwd"
/bin
왜 working directory 이야기가 나오나?
Docker는 프로그램을 다른 환경에서 실행해주는 도구다. docker run ubuntu는 결국 "리눅스 환경에서 프로그램 하나 실행해줘"라는 의미다. 그런데 프로그램은 항상 "어디를 기준으로 실행하지?"라는 질문에 답해야 한다. 그게 바로 working directory다. 이건 Docker의 개념이 아니라 운영체제의 기본 규칙이다.
도커로 컨테이너를 실행하면 그 안에서 프로그램이 실행되고, 프로그램은 실행되자마자 자기 기준 위치(working directory)를 정한다. 그래서 Docker를 쓰는 순간 운영체제의 기본 규칙(폴더, 경로, 위치)이 함께 따라온다.
실습에서 명령이 안 될 때 종종 "exec가 문제인가?"라고 생각하지만, 진짜 문제는 거의 항상 "지금 위치에 그 명령/파일이 없었던 것"이다. 그래서 "명령은 항상 working directory 기준이다"를 기억해두면 된다.
# 종료 / 시작
docker stop <NAME_OR_ID>
docker start <NAME_OR_ID>
# 중지 / 재실행
docker pause <NAME_OR_ID>
docker unpause <NAME_OR_ID>
stop/start와 pause/unpause의 차이는 프로세스의 생존 여부다. 노트북의 전원을 끄고 켜는 것과 절전모드를 하는 것의 차이와 비슷하다.
docker rm [OPTIONS] CONTAINER [CONTAINER...]
| Option | Short | Description |
|---|---|---|
--force | -f | SIGKILL 시그널로 실행 중인 컨테이너 삭제 |
--volumes | -v | 컨테이너의 anonymous volumes도 함께 삭제 |