Docker의 컨테이너는 이미지 레이어, 컨테이너 레이어 총 2가지로 나눠볼수 있다.
이미지 레이어 안에서 생성된 이미지들은 read-only이다.
즉 안의 내용물들은 수정 불가능
하다.
만약 이미지 안에서 변경될 부분이 존재한다면, 수정 후에 다시 이미지를 빌드해주어야한다.
하지만 이는 당연하다.
실행중인 어플리케이션에서 수정사항이 바로바로 반영 된다면 에러를 발생시킬 확률이 높기 때문에 이를 이미지로 관리해주는 것이다.
예를 들어서 어플리케이션 내에서 노드 17버전을 사용하다가, 노드 18버전으로 수정후 이것이 바로 반영 된다면 해당 어플리케이션은 오류가 날 확률이 지극히 높다.
하지만 이런 이미지내에서 바로 변경 되면 안되는 데이터들 뿐만 아니라, 2가지의 데이터 유형이 더 존재한다.
하나는 임시 데이터
인데, 이는 이미 저장된 소스코드가 아니라 어플리케이션이 실행 되는동안 생성되는 데이터를 의미한다. (Ex. 유저 인풋값)
즉 컨테이너가 종료 될때, 삭제가 되어도 상관없는 데이터를 의미한다.
또 다른 데이터는 영구적인 데이터
를 의미한다.(Ex.유저 계정)
만약 이 영구적인 데이터가, 컨테이너가 종료가 되거나 삭제 될때 동시에 삭제된다면 문제가 발생할 것이다.
예를 들어서 어플리케이션의 수정사항을 반영해서 컨테이너화 해서 재배포를 했는데, 사용자 계정이 전부 다 사라지는 문제가 발생하는 것이다.
실질적인 예시를 들어보자.
const fs = require('fs').promises;
const exists = require('fs').exists;
const path = require('path');
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.urlencoded({ extended: false }));
app.use(express.static('public'));
app.use('/feedback', express.static('feedback'));
app.get('/', (req, res) => {
const filePath = path.join(__dirname, 'pages', 'feedback.html');
res.sendFile(filePath);
});
app.get('/exists', (req, res) => {
const filePath = path.join(__dirname, 'pages', 'exists.html');
res.sendFile(filePath);
});
app.post('/create', async (req, res) => {
const title = req.body.title;
const content = req.body.text;
const adjTitle = title.toLowerCase();
const tempFilePath = path.join(__dirname, 'temp', adjTitle + '.txt');
const finalFilePath = path.join(__dirname, 'feedback', adjTitle + '.txt');
await fs.writeFile(tempFilePath, content);
exists(finalFilePath, async (exists) => {
if (exists) {
res.redirect('/exists');
} else {
await fs.rename(tempFilePath, finalFilePath);
res.redirect('/');
}
});
});
app.listen(80);
이런 백엔드 코드가 존재하고, 이는 form 데이터를 받아서 txt파일을 만들어주고 있다.
FROM node:18-alpine AS base
WORKDIR /app
COPY package.json .
RUN npm install
COPY . /app
EXPOSE 80
CMD ["node", "server.js"]
docker build -t feedback-node
feedback-node라는 태그 이름을 가진 이미지를 만들어준다.
docker run --rm -p 3000:80 --name feedback-app -d feedback-node
빌드한 이미지를 컨테이너화 해서 실행시켜준다.
그 후에 3000번 포트를 접속하면
이런 페이지가 나온다.
저 안에 "awesome" 이라는 입력어를 입력한 후에
/feedback/awesome.txt라는 url로 접속하면
브라우저 상에는 제대로 텍스트값이 나오지만, 로컬 폴더 내에서는 txt파일이 생성이 되지 않은걸 확인할 수 있다. (예상대로라면 feedback 폴더에 awesome.txt 파일이 생성되어야한다.)
그 이유는 작성한 Dockerfile에서 로컬 폴더를 이미지에 복사하고 컨테이너는 해당 이미지를 기반으로 하기 때문이다.
즉 만들어진 이미지나 컨테이너는 로컬 폴더와 연결된 것이 아니다.
저 해당 파일은 만들어진 컨테이너안에서 생성이 된것이기 때문에, 로컬폴더에선 awesome.txt를 찾을수 없지만, 직접 awesome.txt에 관련된 url에 접속하면 해당 파일을 볼수 있는것이다.
만약 해당 컨테이너를 중지하고 --rm 명령어를 뺀후에,
컨테이너를 재시작 한다면, 컨테이너는 종료되어도, 컨테이너는 삭제 된것이 아니기 때문에 입력값은 그대로 남아있다.
만약 --rm 명령어를 추가해서 실행한다면, 컨테이너 종료와 동시에 컨테이너를 제거한다면 맨위의 사진에 있는 이미지를 제외한 레이어가 삭제되는 것이기 때문에 입력했던 데이터들은 동시에 삭제된다.
하지만 이런 순수성을 보장해주는 흐름은 어플리케이션 운영에서 문제가 될 때가 있다.
만약 사용자 계정을 입력받아서 회원가입을 진행했거나, 꾸준히 저장이 되어야할 데이터들을 입력받은 상황에서
어플리케이션의 수정사항을 반영한 후에 다시 빌드를 하게 되면 해당 데이터는 사라지는 것이다.
이럴때 필요한것이 볼륨이다.
볼륨
은 호스트 머신의 폴더이다.
여기서 호스트 머신은 우리가 사용하는 가상이나 실제의 컴퓨터를 의미힌다.
즉 로컬 폴더라고 볼수 있는데, 이 볼륨은 도커에 의해 생성되고 관리 되기 때문에 단순히 로컬폴더라고 이야기할 수는 없다.
이 볼륨을 사용한다면, 컨테이너가 삭제 되거나 종료되더라도 해당 어플리케이션의 데이터는 그대로 유지를 시킬수 있다.
또한 볼륨에는 2가지 종류의 볼륨이 존재한다.
1개는 익명 볼륨(Anonymous Volume) 이고, 또 다른 하나는 명명 된 볼륨(Named Volume) 이다.
익명 볼륨은 말그대로 네이밍이 되어있지 않은 볼륨이고, 명명 된 볼륨은 말그대로 이름이 지어진 볼륨이다.
또한 익명볼륨은 해당 컨테이너가 삭제 될때 동시에 삭제가 되지만, 명명 볼륨은 그대로 데이터를 보존한다.
익명 볼륨은 하나의 컨테이너에 의존적이고, 명명 된 볼륨은 하나의 컨테이너에만 의존하지 않는다.
-v 이라는 명령어를 추가해서 명명 된 볼륨을 만들어줄수있다.
위의 순서대로 똑같이 이미지를 빌드 후, 실행시켜줄때
-v 명명된볼륨이름:컨테이너내경로
식으로 명령어를 추가해준다면, 명명 된 볼륨이 만들어져서 해당 컨테이너가 삭제 된후, 다시 빌드 되더라도 데이터는 그대로 보존이 되어있는것이다.
docker run -d -p 3000:80 --rm --name feedback-app -v feedback:/app/feedback feedback-node:volume
이렇게 된다면 docker stop을 통해 해당 컨테이너를 종료시켜주고 다시 실행시켜도 이전에 입력시켰던 값이 그대로 남아있는걸 확인할 수 있다.
바인드 마운트와 볼륨은 비슷한 기능을 하지만 다른 기능을 한다.
예를 들어서
<h1>안녕하세요</h1>
이라는 태그 구문을 가진 어플리케이션의 소스코드에 "!"를 붙여서 이를 반영시키고 싶다고 하자.
"!" 를 추가하면 소스코드는 수정되지만, 도커내에서는 이미 빌드 됐으므로 수정사항이 반영이 되지 않는다.
이러한 수정이 가능한 데이터들을 관리하는 역할을 해주는 곳이 바인드 마운트이다. (반대로 볼륨은 데이터가 저장만 될뿐, 수정은 불가능하다.)
-v "바인드 마운트 하고 싶은 로컬호스트 내의 절대경로":컨테이너내의파일경로
절대경로 안에 폴더가 공백이나 대문자를 포함할경우 에러를 발생시킬수도 있으므로 ""로 감싸주는것이 좋다.
그리고 항상 전체 경로를 사용하고 싶다면
-v $(pwd):/app
이런식으로 입력해준다면 일일히 전체경로를 복사해올 필요가 없다.
만약 이렇게 실행시켜준다면, 저 전체경로의 소스코드가 컨테이너의 파일 시스템과 직접 연결이 되기때문에, 소스 코드가 변경되면 컨테이너 내에서도 수정 사항이 바로 반영이 된다.
docker run -d -p 3000:80 --rm --name feedback-app -v feedback:/app/feedback -v "작업폴더전체경로":/app" -v /app/node_modules feedback-node:volume
그리고 이런식으로 /app/node_modules라는 익명 볼륨을 추가해줌으로써 로컬 폴더내의 node_modules와는 별개로 컨테이너 내에 별도의 node_modules를 주입시킬수 있다.
이렇게 되면 호스트 환경과 컨테이너 환경이 분리 되므로, 독립된 패키지 환경을 가질수 있고, 컨테이너 내에서 생성된 node_modules가 바인드 마운트의 폴더로 덮어씌워지지 않는다.(컨테이너 내의 경로를 더 우선순위로 둔다고 생각하면 된다.)
하지만 이는 단순히 백엔드 내의 html파일의 내용을 수정할때이다. (즉, 정적인 소스코드)
만약 서버 코드를 수정하면서 개발을 진행한다면, nodemon을 사용해서 서버를 꾸준히 재시작을 시켜주어야하는데, 이런 경우에는 위의 방법이 적용되지 않는다.
이런 경우엔 따로 필요한 패키지를 설치해주고 패키지 스크립트를 수정해준후에 이미지를 다시 빌드해주어야한다.
또한 Dockerfile의 내용도
FROM node:18-alpine
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
EXPOSE 80
CMD ["npm", "start"]
CMD 부분을 npm start로 바꾸어주었다.
COPY 명령어는 이미지를 빌드할때 사용하는 명령어이고, 바인드 마운트는 수정 사항을 즉각 해당 컨테이너에 적용시키고 싶을때 사용한다.
즉 배포 환경에서는 바인드마운트를 사용할 필요가 없으므로 COPY를 통해서 해당 모든 코드를 복사해주어야한다. 둘의 용도는 다르다.