Docker는 기본적으로 컨테이너 기반으로 이미지를 저장해서 사용한다.
그럼 이 이미지는 무엇일까 ?
Docker는 컨테이너 기반으로 하고, 이 컨테이너 안에는 어플리케이션의 필요한 모든 파일들이 들어가있다.
이미지는 이 컨테이너의 청사진이라고 생각하면 되는데, 컨테이너 안에 들어간 어플리케이션의 실제 코드와 해당 코드를 실행시키기 위한 도구들을 포함하고 있다.
그렇기에 컨테이너는 이미지 없이 존재할수 없지만, 이미지는 컨테이너 없이도 존재할수 있다.
즉 Docker의 컨테이너는 이미지를 기반으로 만들어진다.
Dockerfile 생성 -- 생성된 Dockerfile에서 빌드--> Image --(Create)--> Container
실제로 이미지를 터미널을 통해서 만들어보자.
Docker hub에서는 많은 이미지들이 라이브러리화 해서 업로드가 되어있는데, 대표적으로 node를 한번 사용해보자.
Docker가 켜져있는 상태에서 터미널에서
docker run node
를 실행시켜주면 저 Docker hub에 업로드 되어있는 Node가 이미지화 되어서 새로운 컨테이너를 갖게 되는것이다.
docker ps -a
여기서 ps는 process, -a 는 all을 의미하는데, 도커가 생성한 모든 이미지가 나타난다.
살펴보면, Name이 임의로 생성이되고, 생성된 이미지는 node 등 해당 이미지에 대한 정보들을 볼수있다.
docker run -it node
그리고 도커에서 -it 명령어를 추가해서 node 이미지를 실행시키면
터미널 내에서 이미지화된 노드를 사용할수 있게 된다.
지금 실행되고 있는 노드는 현재 우리의 컴퓨터에서 실행되고 있는것이 아니라, 격리 된 컨테이너 안에서 실행되고 있는것인데 이를 어떻게 알아볼수 있을까 ?
터미널에서 컨테이너를 종료하고 새롭게 node 버전을 확인해보면 18.17.1 버전이 설치되는 것을 확인할 수 있다.
이렇게 컨테이너 안에서는 각 컨테이너의 이미지들을 설정해줌으로써 격리된 환경을 가질수 있는것이다.
그리고 이 이미지를 기반으로 새로운 본인만의 이미지를 빌드할수도 있다.
const express = require("express");
const bodyParser = require("body-parser");
const app = express();
app.use(
bodyParser.urlencoded({
extended: false,
})
);
app.use(express.static("public"));
app.get("/", (req, res) => {
res.send(`
<html>
<head>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div>Docker 실습입니다!</div>
</body>
</html>
`);
});
app.listen(80);
간단한 express 코드가 있다고 가정해보자.
Dockerfile 을 생성해주면, 간단하게 프로젝트 내에서 도커이미지를 형성해줄수 있도록해준다.
(익스텐션에서 docker 를 설치해주면 도커 관련 설정을 손쉽게 해줄수 있다)
그럼 Dockerfile 안에는 그럼 어떠한 내용들을 담아야할까 ?
FROM 기본적으로 사용할 이미지명
첫번쨰로 FROM 명령어인데, FROM 이미지명은, 이미지를 생성해줄때 기본적으로 사용할 이미지를 의미한다.
여기서는 node를 기반으로 이미지를 만들어줄것이기때문에 node를 작성했다.
WORKDIR는 이미지 내부의 경로를 설정해주는 역할을 한다.
모든 이미지와 컨테이너들은 로컬 머신의 파일시스템 경로에서 분리된 자체 내부 파일시스템이 따로 존재한다.
이는 도커 컨테이너 내부에 존재하는데, 만약 /app 이라고 경로를 설정해준다면, 이 이후의 모든 명령어가 /app 경로를 기준으로 실행 된다고 생각하면 된다.
4번에 기술한 RUN을 예로 들면, 만약 WORKDIR 설정이 없다면 컨테이너 내부의 루트 경로에서 해당 명령어가 실행이 되는데, 이렇게 경로를 설정해주었다면 /app 폴더를 기준으로 실행이 되는것이다.
COPY 경로 경로
두번째는 COPY이다. COPY는 기본적으로 두개의 경로값을 인자로 받는데, 첫번째 경로는 컨테이너의 외부 이미지의 경로이며, 이미지로 복사되어야할 코드가 있는 경로이다.
기본적으로 .을 설정하면 현재 루트경로로 설정된다.
또한 두번째 경로는 저 복사되어야할 이미지가 저장될 이미지의 내부 경로이다.
2번의 WORKDIR에서 /app으로 설정해주었다면 이미 기준 경로는 /app으로 설정어 된 상태이다.
그렇기 때문에 복사되어야할 경로는 ./ 으로 해준다면 자동으로 app 폴더를 가리키게 된다. (/app으로 지정해도 상관 없다.)
1번째 인자로 들어가는 경로 하위에 있는 모든 폴더들이 2번째 인자 폴더 내부로 복사된다.
3번의 COPY 명령어를 통해 이미지 내부경로로 필요한 파일들을 복사 한후에, 해당 이미지 경로에서 수행할 명령어를 설정할 수 있다.
만약 RUN npm install 로 설정을 해준다면, 이미지가 빌드 되면서 필요한 패키지들을 설치해준다.
여기서 중요한 점은 위의 명령어들은 이미지 빌드를 위한 명령어이다.
이미지는 컨테이너의 템플릿이어야한다.
Docker를 실행시키는 것은 이미지 그 자체를 실행시키는 것이 아니라, 이미지를 기반으로 한 컨테이너를 실행시키는 것이다.
만약
RUN node server.js
이라는 코드를 추가했다고하면 이미지가 빌드 될때마다 저 node server.js 명령어가 실행이 된다.
원래 원하는 것은 이미지를 기반으로 한 컨테이너를 시작하는 경우에만 서버를 시작하고 싶은것인데, 이렇게 된다면 하나의 이미지를 여러 컨테이너에서 사용하는 경우에도 여러개의 node 서버가 띄워지게 되는 것이다.
CMD는 RUN 과 다르게 컨테이너가 시작 될때 실행할 명령어를 작성할 수 있다.
CMD ["node", "server.js"]
CMD는 대신 RUN과는 다르게 2개의 배열 인자를 받는다.
컨테이너가 실행 될때마다 해당 컨테이너 내부에 있는 node 이미지를 이용해서 server.js를 실행하도록 한다.
EXPOSE 는 컨테이너가 실행될때, 컨테이너로 들어오는 트래픽을 특정 포트에서 받아들일수 있도록 해주는 역할을 한다.
위의 server.js 코드에서 80번 포트를 연결을 했는데, 이는 컨테이너의 포트가 아니라 우리의 로컬 환경에서의 포트 번호이다.
컨테이너는 격리 되어있으므로 컨테이너 자체의 포트번호가 존재한다.
해당 컨테이너를 실행할 우리의 로컬 환경에 포트번호를 노출해줌으로써 포트를 연결할수 있도록 해주는 것이다.
FROM node
WORKDIR /app
COPY . /app
RUN npm install
EXPOSE 80
CMD ["node", "server.js"]
docker build .
해당 명령어를 실행하면 설정해놓은 Dockerfile이 실행되면서 이미지가 만들어진다.
.는 해당 폴더의 루트경로를 의미한다.
그 후 해당 빌드된 이미지의 이름을 확인해볼수 있는데 이것이 아니더라도
docker ps -a
명령어를 이용하면 빌드된 이미지 목록을 확인해볼수 있다.
docker run -p 접근하려는로컬포트:내부도커노출포트 이미지이름
여기서 -p는 publish의 약자인데, 이를 통해 도커에게 어떤 로컬포트가 있는지 알려줄수 있다.
도커에게 우리의 어떤 로컬 포트가 도커 포트에 접근할수 있는지 알려주는 것이다.
현재 도커의 내부 포트를 80번을 EXPOSE해준 상태이고, 로컬 포트는 3000번으로 설정해주고 싶다면
docker run -p 3000:80 이미지이름
으로 해주면 된다.
이는 'node'이미지를 기반으로 컨테이너를 실행해주는 것이다.
그후에 localhost:3000 번으로 접속하면
저 server.js가 정상적으로 실행된다.
그리고 컨테이너 실행을 멈추고 싶다면
docker ps 로 해당 이미지의 이름을 찾아서
docker stop 이미지 이름
을 해주면 된다.
모든 도커의 이미지는 레이어 형식으로 작동한다.
❗️ 또한 읽기 전용이기 때문에, 한번 빌드가 되면 그 안의 내용물은 수정 불가능하다 ❗️
해당 레포를 다시 build 해보면 0.1초만에 작업이 완료가 되는데,
CACHED라는 문구로 캐싱된 명령어를 사용하는것을 볼수 있다.
즉 Dockerfile에 작성했던 각각의 명령어가 각 레이어이며 이 명령어는 캐싱된다는 뜻이다.
저 컨테이너 레이어 큰 틀 자체를 컨테이너라고 볼수있고, 각 명령어를 모두 실행시켜서 이미지를 생성하는 것이다.
그래서 각 레이어를 실행시킬때 변경사항이 존재하지 않고 이미 전에 저장되어있는 값이 있다면, 캐싱된 값을 사용함으로써 효율적으로 작동하게 하는것이다.