도커 엔진
- 2.1 도커 이미지와 컨테이너
- 2.2 컨테이너 다루기
- 2.3 컨테이너 볼륨
- 2.4 도커 네트워크
- 2.5 컨테이너 로깅
- 2.6 도커 이미지 생성과 구조
- 2.7 이미지 배포: Docker Hub
- 2.8 Dockerfile
- 2.9 도커 데몬
도커를 쓴다는 건 결국 이미지 만들고, 컨테이너 띄우고, 데이터랑 네트워크 관리하는 사이클을 반복하는 거야. 이 장이 그 모든 기초를 다루거든.
출발점은 이미지와 컨테이너의 관계를 확실히 잡는 거지. 이미지는 컨테이너를 만들기 위한 읽기 전용 템플릿이야. OS, 라이브러리, 애플리케이션 코드, 환경 변수 등이 레이어 형태로 쌓여 있어. 클래스에 비유하면 돼. 컨테이너는 그 이미지를 기반으로 생성된 실행 가능한 인스턴스야. 이미지 위에 읽기/쓰기가 가능한 레이어가 하나 추가되어 동작하지. 인스턴스에 비유할 수 있고. 하나의 이미지에서 여러 컨테이너를 만들 수 있고, 각 컨테이너는 독립적으로 동작해. 컨테이너 안에서 뭘 바꿔도 원본 이미지에는 영향이 없어. 이미지 이름은 [저장소이름/]이미지이름[:태그] 형식인데, 예를 들어 ubuntu:20.04에서 ubuntu는 이미지 이름이고 20.04는 태그야. 태그를 생략하면 latest가 기본값이 돼.
컨테이너를 실제로 다뤄보자면, 가장 기본이 되는 명령어가 이거야:
docker run -i -t ubuntu:20.04
-i는 표준 입력을 열어두고, -t는 TTY를 할당해. 이 둘을 합쳐서 -it로 많이 써. 컨테이너 안에서 셸을 쓰려면 이 옵션이 필요하거든. docker run은 사실 docker create + docker start + docker attach를 한 번에 하는 거야. 이미지가 없으면 docker pull까지 자동으로 해줘. 빠져나올 때 exit이나 Ctrl+D를 누르면 컨테이너가 정지되고, Ctrl+P, Q를 누르면 정지하지 않고 빠져나올 수 있어.
docker ps # 실행 중인 컨테이너
docker ps -a # 모든 컨테이너 (정지된 것 포함)
docker rm 컨테이너이름
docker rm -f 컨테이너이름 # 실행 중인 컨테이너 강제 삭제
docker container prune # 정지된 모든 컨테이너 삭제
웹 서비스를 띄울 때는 포트 매핑이 필수야:
docker run -d -p 8080:80 nginx
-d는 백그라운드(detached) 모드이고, -p 호스트포트:컨테이너포트로 포트를 매핑하지. 이제 호스트의 8080 포트로 접근하면 컨테이너의 80 포트(nginx)로 연결돼. -p 옵션 없이 실행하면 컨테이너가 외부에서 접근이 안 되니까 주의해야 해.
근데 컨테이너를 삭제하면 그 안의 데이터도 같이 사라지잖아. 그래서 볼륨이 필요한 거야. 호스트 디렉터리를 직접 마운트하는 bind mount 방식이 있어:
docker run -v /호스트/경로:/컨테이너/경로 이미지이름
호스트의 디렉터리를 컨테이너 내부에 직접 마운트하는 건데, 양쪽에서 모두 파일을 읽고 쓸 수 있지. 개발 환경에서 소스 코드를 마운트할 때 많이 써. 그리고 도커 볼륨이라는 게 있어:
docker volume create my-volume
docker run -v my-volume:/컨테이너/경로 이미지이름
도커가 관리하는 볼륨을 생성해서 사용하는 방식이야. 호스트 경로를 직접 지정하지 않아도 되고, 도커가 알아서 관리해주니까 프로덕션 환경에 더 적합하지. 핵심은 — 데이터베이스나 로그 같은 영속성이 필요한 데이터는 반드시 볼륨을 사용해야 한다는 거야.
컨테이너끼리 통신하려면 도커 네트워크를 알아야 해. 도커는 컨테이너를 생성할 때마다 veth(virtual ethernet) 인터페이스를 만들어서 docker0 브리지에 연결하거든. 기본 드라이버는 세 가지야 — bridge(기본값, docker0 브리지를 통해 컨테이너끼리 통신), host(호스트 네트워크를 그대로 사용, 포트 매핑 불필요), none(네트워크 없음, 완전 격리). 여기서 중요한 팁이 있는데, 사용자 정의 브리지 네트워크를 만들면 컨테이너 이름으로 DNS 해석이 가능해져:
docker network create my-net
docker run --name web --network my-net nginx
docker run --name app --network my-net my-app
이렇게 하면 app 컨테이너에서 web이라는 이름으로 nginx에 접근할 수 있어. 기본 docker0 브리지에서는 이 기능이 안 되니까, 여러 컨테이너를 연결할 때는 사용자 정의 네트워크를 쓰는 게 좋아.
로그는 컨테이너 환경에서 stdout/stderr로 출력하는 게 Best Practice야. 도커는 기본적으로 표준 출력과 표준 에러를 로그로 수집하거든. 애플리케이션이 파일에 로그를 쓰면 docker logs로 볼 수 없어.
docker logs 컨테이너이름
docker logs -f 컨테이너이름 # 실시간 로그 (tail -f처럼)
docker logs --tail 100 컨테이너이름 # 마지막 100줄
기본 로깅 드라이버는 json-file이고, 호스트의 /var/lib/docker/containers/ 디렉터리에 JSON 형태로 저장돼. syslog, fluentd, awslogs 등도 지원하지. 로그 파일이 무한히 커지는 걸 방지하려면 --log-opt max-size=10m --log-opt max-file=3 같은 옵션으로 크기와 개수를 제한할 수 있어.
이미지를 만드는 방법으로 docker commit이 있긴 한데, 어떤 변경이 들어갔는지 추적이 안 되니까 실제로는 Dockerfile을 사용해서 이미지를 만드는 게 표준이야. 도커 이미지는 레이어 구조로 되어 있어서, 각 레이어는 읽기 전용이고 새로운 변경이 생기면 그 위에 레이어가 하나씩 쌓여. 이 구조 덕분에 여러 이미지가 같은 베이스 레이어를 공유할 수 있고, 변경된 레이어만 다시 빌드하니까 빠르지.
Docker Hub는 도커의 공식 이미지 레지스트리야. docker tag로 이미지에 레지스트리 이름을 포함한 태그를 붙인 뒤 push하면 돼:
docker login
docker tag my-app:latest myname/my-app:1.0
docker push myname/my-app:1.0
Docker Hub 외에 AWS ECR, Google GCR, 사설 레지스트리 등을 사용할 수도 있어.
Dockerfile은 이미지를 빌드하기 위한 스크립트야. 어떤 베이스 이미지를 쓰고, 어떤 명령어를 실행하고, 어떤 파일을 복사하고, 어떤 포트를 열고, 어떤 명령어로 시작할지를 선언하지:
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
주요 명령어들을 보면 — FROM은 베이스 이미지 지정이고 모든 Dockerfile은 FROM으로 시작해. RUN은 이미지 빌드 시 실행할 명령어인데 각 RUN마다 새 레이어가 생겨. COPY/ADD는 호스트 파일을 이미지 안으로 복사하는 건데, 단순 복사는 COPY를 쓰는 게 권장돼. WORKDIR은 작업 디렉터리 설정이고, EXPOSE는 컨테이너가 사용할 포트를 문서화하는 건데 실제로 포트를 여는 건 아니야. CMD는 컨테이너 시작 시 기본 명령어이고 docker run 뒤에 명령어를 붙이면 덮어써져. ENTRYPOINT는 CMD와 비슷하지만 덮어써지지 않아.
빌드 캐시를 효율적으로 활용하려면 변경이 적은 레이어를 위에, 자주 변하는 레이어를 아래에 놓아야 해. 위 예시에서 package*.json을 먼저 복사하고 npm install한 뒤 소스 코드를 복사하는 이유가 바로 이거야. 소스 코드만 바뀌면 npm install 레이어는 캐시에서 재사용되거든.
docker build -t my-app:1.0 .
.은 빌드 컨텍스트의 경로야. 이 디렉터리의 모든 파일이 도커 데몬에 전송되니까, .dockerignore로 불필요한 파일(node_modules, .git 등)을 제외하는 게 좋아.
마지막으로 알아둘 건, 도커가 클라이언트-서버 아키텍처라는 거야. docker CLI가 클라이언트이고 dockerd(도커 데몬)가 서버지. 클라이언트가 REST API로 데몬에 명령을 보내고, 데몬이 실제로 컨테이너를 생성하고 관리해. 기본적으로 유닉스 소켓(/var/run/docker.sock)을 통해 통신하고, TCP 소켓으로 변경하면 원격에서도 접근할 수 있지만 보안 설정(TLS)이 필수야. 도커 데몬의 설정은 /etc/docker/daemon.json에서 할 수 있어:
{
"data-root": "/mnt/docker-data",
"storage-driver": "overlay2",
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
storage-driver는 이미지 레이어를 저장하는 방식을 결정하는데, 현재는 overlay2가 기본이자 권장값이야. 도커 데몬이 동작하지 않으면 모든 도커 명령어가 실패하니까 systemctl status docker로 상태를 확인할 수 있어.
정리
2장 읽고 기억할 거 네 가지:
- 이미지는 읽기 전용 템플릿이고, 컨테이너는 그 실행 인스턴스다. 하나의 이미지에서 여러 컨테이너를 만들 수 있고 각각 독립적.
- 데이터 영속성이 필요하면 볼륨을 써야 한다. 컨테이너가 삭제되면 내부 데이터도 사라진다.
- Dockerfile로 이미지를 빌드하는 게 표준이다. 레이어 순서를 잘 배치해서 빌드 캐시를 활용해라.
- 도커는 클라이언트-서버 구조다. CLI(클라이언트)가 dockerd(서버)에 API로 명령을 보내는 방식.