Docker 이미지 & 컨테이너 이해하고 사용하기
[Infra/Docker(도커)] - Docker - 도커란
이전 글에서 Docker의 개념에대해 살짝 정리해보았습니다.
이번 글에서는 Docker 실질적으로 사용하기위한 개념인 컨테이너와, 이미지에 대해 학습 정리를 하고자 합니다.
[목차]
- Docker Images, Container 차이점
- Contanier 이미지 사용하기 (이미지 만들기)
- 외부 빌드된 이미지 - Docker Hub 이미지 가져오기
- 커스텀화된 이미지 - DockerFile 로 이미지 빌드하기
- 도커 이미지 레이어 이해하기
- 도커 이미지 지우기
- Docker hub 에 커스텀 이미지 올리기
- Docker 로 EC2에 배포하기
1. Images vs Containers
Docker 환경에서의 이미지와 컨테이너의 차이점
> 도커는 컨테이너 격리 프로그램입니다
> 따라서 컨테이너라는 개념을 적용하고, 이를 구성하는 이미지가 존재합니다.
✔️ Docker Containers
- 컨테이너는 코드, 애플리케이션을 실행하는 전체 환경 등, 무엇이든 포함하는 작은 패키지라고 생각할 수 있습니다.
- 컨테이너에는 소프트웨어 실행 유닛이 존재하고, 컨테이너에서는 이 유닛을 실행시킵니다.
- 컨테이너는 실제로 실행되는 프로세스 입니다.
✔️ Docker Images
- 도커 이미지란, 컨테이너의 템플릿 혹은 블루포인트(시작점) 입니다.
- 이미지는 실제로 코드와 코드를 실행하는데 필요한 도구를 포함하고 → 그런 다음 컨테이너가 실행되어 코드가 실행됩니다.
- 이렇게 이미지와, 컨테이너의 개념을 분리함으로써 이미지를 통해 여러대의 컨테이너를 손쉽게 만들 수 있게 됩니다.
📌 즉, 이미지는 모든 설정 명령과 모든 코드가 포함된 공유 가능한 패키지 이며 컨테이너는 그러한 이미지의 구체적인 실행 인스턴스(어플리케이션) 입니다
📌 Dokcer 를 사용한다는 것은 이미지를 기반으로하는 컨테이너를 실행하는 것이고, 이것이 도커의 핵심 기본 개념이며 모든 것 입니다.
2. Docker Image 사용하기 (Docker Build Image)
실제로 컨테이너를 실행할 수 있도록 이미지를 생성하거나 가져오는 2가지 방법이 존재합니다.
1) 외부 이미지 사용 - "Docker Hub"
- 이미 존재하는 이미지를 사용하는 것
- 동료, 혹은 미리 구축된 공식 이미지나 커뮤니티에서 공유한 이미지를 사용하는 방법입니다.
- Docker Hub 에서 공식 이미지들을 찾을 수 있습니다.
Docke hub에서 이미지 내려받기
- 로컬환경 shell script(명령 프롬프트) 환경에서 Docker HUB에 올라가있는 공식 이미지의 이름을 사용합니다,
- `docker pull - `를 사용해서 이미지를 받기만해도 되고, `docker run - `을 사용하여 컨테이너를 바로 실행시킬 수 도 있습니다.
- 맥(mac)환경이라면 Docker desktop 을 실행시켜 Docker 를 활성화 시켜주어야 합니다.
- `docker run ` 시 로컬환경에 해당 이미지가 없다면 자동으로 Docker hub에서 이미지를 pull 받습니다.
- `docker run `으로 실행시켰기 때문에 contatner name 도 자동으로 설정되어 실행되어집니다.
//기본적인 Docker run 포맷
docker run [OPTIONS] IMAGE[:TAG|@DIGEST] [COMMAND] [ARG...]
✔️ docker run 시 status 가 exit 인 이유
- 하지만 위의 사진처럼 단순하게 docker run [image name] 로 실행한다면 컨테이너는 EXIT 상태가 되어집니다.
- 그 이유는, 컨테이너는 격리되어 실행되기 때문에 노드에 의해 노출된 인터렉티브 쉘이 우리(Host)에게는 노출되지 않기 때문입니다.
✔️ docker run 시 컨테이너 내부 노출시키기
- Docker run options 으로 어떻게 Host 에게 노출시킬것인지 정할 수 있습니다.
- -it 컨테이너 실행시 내부에서 호스팅으로 대화형 세션(명령 프롬프트)를 노출시킵니다.
- -p : 호스트에 연결된 컨테이너의 특정 포트를 외부에 노출시킵니다. 해당 포트를 이용하여 로컬에서 특정 포트를 바인딩해 컨테이너 내부로 연결시킬 수 있습니다.
// -it 옵션
$ docker run -it ubuntu:20.04
// -p 옵션
$ docker run -p 127.0.0.1:80:8080 ubuntu:20.04 bash
하자만 중요한 것은 나의 어플리케이션을 Docker 컨테이너로 실행시키는 것!
이렇게 컨테이너를 띄운 후, 노출된 컨테이너 내부에서 깃허브와 같은 외부 저장소에서 코드를 끌어다 쓸 수도 있지만
이미지의 "쉬운 공유" 장점을 살리기 위해선 우리의 어플리케이션까지 같이 빌드된 커스텀화된 이미지가 있으면 좋겠죠
2) Dockerfile 을 사용하여 자체 이미지 빌드하기
> DockerFile을 이용하여 커스텀화된 이미지를 빌드할 수 있습니다.
1. Dockrfile 만들기
- DockerFile이란 도커에의해 식별되는 특별한 이름을 가진 파일입니다.
- 확장자 없이 "DockerFile"이란 이름의 파일을 생성하면 도커에서 이미지를 빌드하기위한 기준파일이구나~ 인식하게됩니다.
- IDE를 사용하시다면 각 IDE에서 지원하는 "도커 플러그인"을 사용하시다면 조금 더 편하게 작성할 수 있습니다.
2. Dockerfile 문법 (Dockerfile 구조)
Dockerfile 작성법에 대해 기본적인 개념만 정리해보고자 합니다.
//node 서버를 실행하는 Dockerfile
FROM node //(1)
WORKDIR /app //(3)
COPY . /app //(2)
RUN npm install //(4)
EXPOSE 80 //(5)
CMD ["node", "server.js"] //(6)
- FROM
- 베이스 이미지를 불러옵니다. 베이스 이미지는 외부 빌드된 이미지 - 즉, Docker Hub 에서 가져옵니다.
- 이론적으로 처음부터 도커 이미지 레이어를 구축 할 수도 있지만, 그건 너무 손이 많이가는일이니 미리 구축되어있는 공식 운영체제 레이어를 사용하는 걸 추천한다고 합니다.
( → Docker Hub 에 등록된 공식 이미지들도 보통 Linux의 이미지인 'alpine' 이나 'debian'을 사용합니다. docker node repo) - Docker 이미지는 base 이미지부터 시작해 기존 이미지위에 새로운 이미지를 중첩해서 여러 단계의 이미지 층( layer)를 쌓아가며 만들어집니다.
- COPY
- 이미지를 만들 때 "어떤 파일"을 복사해서 "어디에" 저장할 것인지를 의미 합니다.
- COPY [복사할 로컬 파일경로] [붙여넣을 컨테이너 내부 경로]
- 이때 첫번째 경로에 Dockerfile이 존재한다면 복사 시 제거됩니다.
- 두번째 경로로 루트 폴더 즉, 도커 컨테이너의 루트 엔트리(/.)를 사용하지 않고 전적으로 사용자가 선택한 "/app 경로를 사용한다면 컨테이너 내부의 app 폴더 에 저장되고, 폴더가 없으면 생성됩니다.
- WORKDIR
- 상대경로를 적었다면 이 폴더 내부에서 명령어를 실행하라고 알리는 것 입니다.
- 그렇지않다면 이미지와 컨테이너로 인해 실행된 인스턴스는 루트에서 아래 정의된 모든 명령어를 실행할 것 입니다.
- Run
- RUN 명령어는 도커파일로부터 도커 이미지를 빌드하는 순간에 실행이 되는 명령어입니다.
- RUN 명령어를 이용하여 어플리케이션을 실행시킬 수도 있지만, 이는 권장되는 방식입니다.
- 도커 이미지는 실행환경을 세팅하는 템플릿이고, 어플리케이션은 컨테이너가 실행될때 같이 실행되는게 권장되는 방식이기 때문입니다.
- 따라서 RUN 명령어는 라이브러리를 설치하는 용도로 주로 사용됩니다.
- EXPOSE 80
- EXPOSE 명령어는 해당 Dockerfile로 빌드되는 이미지가 어떤 포트를 노출할 것인지 명시적으로 알리기위한 용도라고 합니다.
- 따라서 Document 에 가까워서 사실 안해줘도 되지만, 공식문서에서는 Dockerfiledp 'EXPOSE'를 추가하여 이 동작을 문서화하는 것을 모범적인 사용법이라고 명시하고 있습니다.
- EXPOSE 명령어는 문서화이기 때문에, docker run [이미지] 실행시 -p 옵션(publish) 을 주어 실질적으로 EXPOSE 에 명시된 포트를 노출시켜주어야 합니다.
- CMD
- 이미지가 생성될 때 실행되지 않고 이미지를 기반으로 컨테이너가 시작할때 해당 명령어가 실행 됩니다.
- 문법이 조금 다른데 배열을 이용해 전달합니다.
ex) CMD ["node", "server.js"]
→ 명령을 두 개의 문자열로 분열
→ 도커에게 이미지를 기반으로 컨테이너가 생성될 때마다 그 컨테이너 내부에 있는 node 명령어를 사용하여 server.js 파일을 실행하도록 지시
❗️이미지를 실행 명령어들 (RUN, ENTRYPOINT, CMD 차이점)
→ 해당 사진은, 과거 저의 포스팅에서 가져온건데, ENTRYPOINT를 사용했군요
- RUN : 위에서 말했다시피, 이미지가 빌드될 떄 실행합니다. 따라서 어플리케이션 실행으로는 권장되지 않는 명령어입니다.
- CMD, ENTRYPOINT : 2개 명령어 모두 컨테이너가 실행시 동작할 명령어를 정의 합니다.
- 차이점
- ENTRYPOINT 명령어는 오버라이딩이 어렵고 CMD 명령어는 오버라이딩이 쉽다.
- docker run 시에 다른 실행 명령어가 있으면 CMD 명령어에 써준 내용은 무시된다. 반면에 ENTRYPOINT 명령어는 무시되지 않고 docker run에 붙여준 실행 명령어를 인자로 받아서 컨테이너를 실행한다.
- CMD 명령어는 docker run 명령 내에 명시된 매개변수가 있는 경우 Daemon에서 무시된다.
- ENTRYPOINT 명령어는 무시되지 않고 대신 명령의 인수로 취급하여 매개변수로 추가된다.
- 차이점
3. 이미지 빌드하기
- 자 다왔습니다! 이제 Dockerfile이 있는 디렉토리 경로에서 빌드만 하면됩니다!
- 태그 : 태그는 도커 레포지토리(이미지)의 버전을 관리하기 위함입니다. default는 lateast입니다.
//docker build -t {이미지명}:{태그} {Dockerfile 파일 위치}
//-t : 이름을 나타냄
docker build -t 이미지이름 .
4. 도커 이미지 생성 확인
> 아래의 명령어로 이미지 생성 목록을 확인할 수 있습니다.
docker images
5. 도커 이미지를 컨테이너화 시키기
이미지를 만들어주었으니 Docker Hub 이미지를 사용할 때와 마찬가지로 실행시키면 됩니다.
- -p : 로컬포트에서 접속시 어떤 도커포트랑 연결할 것인지를 의미하는 명령문입니다.
- -d : 백그라운드 실행을 의미합니다. (detach : 종료하지 않고 분리시킴)
docker run --name maru-spring -p 8080:8080 -d maru-spring
docker run --name '이미지 이름' -p '로컬포트:도커포트' -d 이미지이름 (-d는 백그라운드 실행을 의미)
도커가 실행이 완료되면 요상한 문자가 나타납니다. 어떤 도커 이미지가 실행중인지 확인해 봅니다.
docker ps
사진의 PORTS를 보면 로컬의 어떤 포트가, 도커의 포트랑 연결되어있는지 볼 수 있습니다.
3. Docker Image 레이어 이해하기
👏🏻 Docker image 는 레이어 기반으로 이루어져있고, 그렇기에 Docker는 레이어 기반 아키텍처를 가진다라고도 이야기합니다.
- 실제로 이미지를 빌드해보면 각각의 명령어별로 무언가 수행됨을 볼 수 있습니다.
(EXPOSE는 문서화, CMD 는 컨테이너가 실행될 때 수행)
✔️ Docker Image build 동작과정
- 이미지가 빌드될 떄 명령어가 레이어에 쌓여가는 과정은 아래 사진과 같이 명령어 별로 Read Only(읽기 전용) 쌓아가는 것 입니다.
- 이미지의 모든 명령어는 도커에서 관리하는 "읽기 전용" 레이어가 되고, 해당 이미지를 컨테이로 실행시킬 때 "쓰기 가능한" 최종 레이어가 생성됩니다.
이러한 특성 때문에 도커의 이미지는 "불변"한다고 이야기 합니다.
만약 아래와 같이 3가지 명령어를 가지는 Dockerfile 을 && 명령어를 사용해 2가지 명령어로 수정한다면 레이어의 개수 또한 줄어듭니다.
FROM ubuntu:18.04
RUN apt update
RUN apt install -y git
↓
FROM ubuntu:18.04
RUN apt update && apt install -y git
"docker inspect [이미지]" 명령어를 사용하면 아래와같이 어떤 레이어로 구성되어있는지 확인할 수 있는데, 실제 Dockerfile 의 명령어 개수보다 많은 레이어로 구성되는 이유는 "FROM node" 라는 베이스 이미지에서 구축된 레이어를 포함하기 때문입니다.
✔️ Docker Cache를 통해 build 최적화 하기
→ "읽기 전용 레이어" 라는 뜻은 이미지를 다시 빌드할 때 변경된 부분의 명령과 그 이후의 모든 명령이 재평가된다는 의미입니다.
→ 아무런 변경도 없이 Dockerfile 을 이용해 새롭게 이미지를 빌드하면 이전과는 다르게 매우 빠르게 빌드가 되는 것을 확인할 수 있었습니다. (3.5s → 1.5s)
📌 그 이유는 Docker Cache 를 사용하기 때문입니다.
- 동일한 작업 디렉토리에
- 복사한 코드(COPY로 불러오는 어플리케이션 코드) 는 전혀 변경되지 않았고
- 새 파일도 없고 변경된 파일도 없다면
도커는 실제로 그 명령을 다시 거칠 필요가 없다고 추론합니다. → 도커는 이미지를 빌드할 때 마다 모든 명령 결과를 캐시하고 다시 빌드할때 명령을 다시 실행할 필요가 없다고 판단되면 캐시된 결과를 사용합니다.
📌 이렇기때문에 도커는 레이어 기반 아키텍처라고 합니다.
- 모든 명령은 Dockerfile의 레이어를 나타냅니다.
- 즉, 이미지의 모든 명령은 캐시 가능한 레이어를 생성하고 이러한 캐시는 이미지의 재구축 및 공유를 돕습니다.
📌 하지만 여기서 하위 레이어가 변경이 일어난다면, 도커는 변경점이 일어났다고 판단하기 때문에 그 위에 쌓여진 모든 레이어를 다시 빌드합니다.
- 따라서 Dockerfile을 아래처럼 변경하여 최적화 할 수 있습니다.
- COPY . /app 으로 불러오는 어플리케이션 코드가 변경되더라도, 다운받아야하는 종속성이 변경되지 않았다면 추가적인 작업을 하지 않도록 하위레이어로 쌓는 것 입니다.
//node 서버를 실행하는 Dockerfile
FROM node //(1)
WORKDIR /app //(3)
COPY . /app //(2)
RUN npm install //(4)
EXPOSE 80 //(5)
CMD ["node", "server.js"] //(6)
↓
FROM node //(1)
WORKDIR /app //(3)
COPY package.json /app // 종속성이 추가되거나 변경됨을 감지하기위한 COPY 명령어
RUN npm install // 종속성파일이 변경되지않으면 npm install을 또 할 필요가 없음
COPY . /app // 어플리케이션이 변경되면 그 위의 레이어만 영향이 가도록
EXPOSE 80 //(5)
CMD ["node", "server.js"] //(6)
4. Docker 이미지 지우기
이미지를 빌드해서 만들고, 컨테이너를 생성해보았으니 이제 지워볼까 합니다.
✔️ 컨테이너 지우기 or 종료
- 컨테이너는 Stop 명령어 중지 후에
- rm 명령어로 간단하게 지울 수 있습니다.
- ps -a : 실행중인 프로세스와 종료중인 프로세스 모두 보여줍니다.
✔️ 이미지 지우기
- docker rmi
- 이미지는 rmi 명령어를 이용하여 개별 이미지를 선택해 삭제할 수 있습니다.
- 단, 해당 이미지를 사용중인 컨테이너가 존재한다면(시작 or 중지) 그 컨테이너가 속한 이미지를 지울 수 없습니다.
- ex) docker rmi [이미지 id] [이미지 id] [이미지 id] (복수 삭제 가능)
- docker image prune
- 현재 실행중인 컨테이너에서 사용중이지 않은 모든 이미지를 지우는 명령어 입니다.
- docker image prune : 사용되지 않는 이미지 모두 삭제
- docker run "--rm"
- 중지된 컨테이너 자동으로 제거하는 docker run 옵션입니다.
- 보통 중지된 컨테이너를 새로 시작할때는 → 코드에 변경이 일어나 이미지를 재빌드 해야할 때가 많습니다.
- 그렇기 때문에 컨테이너를 중지시킬때 아예 지워버리는게 경험상 유용하다고 합니다
- 하지만... 음... 해당 옵션을 활성화하기 위해서는 에러 로그나, 덤프파일 같이 비정상적인 다운 시 모니터링을 위한 파일은 따로 빼둘 수있는 작업이 선행되어지는게 좋아보입니다.
5. Docker hub에 이미지 업로드 (push)
1. 먼저 도커허브에 레포지토리를 만들어 둡니다.(https://hub.docker.com/)
2. 터미널에서 도커에 로그인을 해야합니다.
docker login
3. 그 다음 업로드할 이미지를 태깅해 주어야합니다.
docker tag maru-spring thalals/maru-spring
docker push {리파지토리 이미지명}:{태그}
4. 태깅한 이미를 도커허브의 레포지토리에 push해 줍니다.
docker push thalals/maru-spring
docker push {리파지토리 이미지명}:{태그}
6. Docker 이용해서 EC2에 배포하기
- 사실 별건 아닙니다. Docker hub 에 이미지를 올렸기 때문에 pull 받아서 손쉽게 배포가 가능함을 보이기 위함입니다.
- 먼저 EC2를 하나 생성하고, 터미널에서 EC2로 접속합니다.
- EC2 프리티어 생성 및 접속 : https://thalals.tistory.com/120
1) EC2에 도커 설치하기
vi install.sh
install.sh
sudo apt-get update
sudo apt-get -y install \
apt-transport-https \
ca-certificates \
curl \
gnupg2 \
software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository \
"deb [arch=amd64] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) \
stable"
sudo apt-get update
sudo apt-get install -y docker-ce
sudo usermod -aG docker $USER
install 파일에 실행권한을 준다음(chmod 777) 파일을 실행시킵니다.
chmod 777 install.sh
./install.sh
2) Docker image pull
- ec2에서 도커허브에 올라가있는 이미지를 pull 받아서 외부 빌드된 이미지랑 똑같이 사용하면 됩니다.
docker pull {도커 허브 이미지명}:{태그}
docker run --name {컨테이너 이름} -p 5000:5000 -d {이미지 이름}:{태그}
그러면 인자 localhost:8080으로 접속할 수 있는데 → 그 전에 AWS에서 8080포트를 열어주어야합니다.
참고
- docker docs CMD vs ENTRYPOINT: https://docs.docker.com/engine/reference/builder/#understand-how-cmd-and-entrypoint-interact
- 유데미 Docker & Kubernetes : 실전가이드