AWS 배포 완벽 가이드 | Section 7, 8

김민지·2024년 12월 21일

Section 7 | Docker Container 101

Container의 장점

  1. snapshot 관리 가능
  2. resorce 관리 가능
  3. docker를 설치한 환경이라면 어떤 OS에서건 상관없이 실행 가능
  4. 초기세팅이 불필요하고 가볍다
  5. 모든 언어에 대해 PM2

Docker 개념: Dockerfile, Image, Container

  • Dockerfile: 설계도(명세서)의 개념으로, Node 18 등의 버전과 dependency, 그리고 시작 명령어 (npm run start 등) 등을 명시
  • DockerImage: Dockerfile에서 docker build를 해서 온전히 설치도 다 되어있는 버전
    예: npm에서 package.json은 어떤 패키지가 설치될 지에 대한 설계도이고 node_modules는 실제 설치된 모듈들인 것과 비슷한 개념
  • Container: DockerImage에서 control groups (docker run -it --memory 300m --cpus=0.5 myapp) 로 DockerImage를 실행한 결과. 실제로는 프로세스도 여러개이고, 이런 여러개의 프로세스가 커널도 파일시스템도 OS도 공유하겠지만 컨테이너에서는 마치 프로세스 하나가 하나의 커널을 사용하고 OS를 사용하는 것처럼 보이도록 함

Docker 설치

  • Docker 설치 시, 다음 세가지가 설치됨
  1. Docker Client (Docker CLI)
  2. Docker Server (Daemon): container들 관리하는 애
  3. Linux VM

Docker hello-world

  • Docker 설치 후에는 단순히 docker run redis라는 명령어만으로 곧바로 redis 서버를 설치하고 실행할 수 있음
  • 다른 터미널에서 접속하면 접속되지 않음. 왜? 하나의 container 안에서만 실행되는 중이니까!
  • 그렇다면 현재 실행중인 container에게 접속하기 위해 docker ps로 현재 실행중인 container 조회하기
  • 조회한 container로 접속하기: docker exec -it <containerId앞부분> bash

    bin, boot, data, ... 는 container 내부에 있는 완전히 독립된 파일시스템임.
  • container 내부에서 redis-cli를 하면 앞서 container 내부에 대해 redis를 설치했으므로 아래와 같이 잘 실행됨

Docker 명령어 더 알아보기

  • docker run <imageName> = docker create <imageName> + docker start <imageName>
    만약 imageName이 설치되어있으면 바로 start, 설치되어있지 않으면 create + start
  • docker ps --all: --all 옵션 없이 실행하면 현재 실행중인 container들만 보이고, --all 옵션을 주면서 실행하면 현재 다운되어있는 container들도 보임. 이 때 다운되어있는 container에 docker start를 해주면 다시 살릴 수 있음.
  • docker system prune: 다운된 container를 날리기
  • docker run -d <containerId>: container 안에서만 실행해라 (외부 터미널에서는 보이지 않도록 해라)
  • docker logs <containerId>: container 안에서만 실행중인 로그를 확인함
  • 예시: docker run alpine, docker run alpine ping google.com

  • -it 옵션: interactive run, 즉 해당 옵션을 붙여서 실행하면 bash 창이 나옴.

Docker container 멈추기: SIGTERM, SIGINT, SIGKILL

  1. docker stop <dockerId>: 이미 받은 요청들은 처리하고 shutdown하는 gracefull shutdown이다. 이 때 디폴트로는 10초 가량 기다린다. (SIGTERM, SIGINT)
  2. docker kill <dockerId>: 바로 종료함 (SIGKILL)
  • docker stop에 대한 처리가 되어있는 여부
  1. 처리 X: 터미널을 두개 열어서 하나에서는 docker run -it alpine 실행 후 ping google.com을 실행하고, 다른 터미널에서 docker ps로 직전 터미널에서 실행한 container의 id를 파악한 후 docker stop <dockerId>를 입력하면 10초 가량 더 실행한 후 멈춤.
    (ping google.com은 워낙 단순한 명령어이므로 이런 처리가 되어있지 않음)
  2. 처리 O: docker run -it redis로 redis 실행 후 docker ps로 실행중인 container id 확인 후 docker stop
  • 지금까지 실행한 container 들 docker UI로도 확인가능
  • docker kill <containerID>로 하면 즉시 종료

Port mapping으로 host machine에서 container 접속하기

  • mapping: host ➡️ container
  • docker run -it redis 실행 후 다른 터미널에서 redis-cli 실행 시 접속 안됨
    ➡️ 새 터미널에서 docker ps로 컨테이너 아이디 확인 후 docker exec -it <컨테이너아이디> bash 로 접속한 후에 redis-cli 실행시 접속 가능
  • 외부 터미널에서도 container에 접속할 수 있도록 실행하기
    docker run -it -p 4000:6379 redis 로 접근 후 (6379는 redis의 기본 포트라서 작성한거고 4000은 임의의 포트번호를 정한 것임. 4000은 변경 가능. 6379도 만약 개발자가 redis 설정에 들어가서 실행할 포트번호를 바꾼 경우에는 변경 가능. 즉, redis 기본 포트 번호에서 임의의 포트번호로 redirect해주는 의도. 혼동을 줄이기 위해 6379:6379로도 많이 설정함), redis-cli -p 4000으로 접속

Docker image 생성하기

  • docker file: FROM (기본이 되는 base image), RUN(설치되어야 할 프로그램들과 dependency들), CMD(실행 명령어)가 적혀있는 파일
  • docker client는 docker file을 docker build . 명령어로 빌드함
  • docker server는 빌드된 결과를 이용해서 실제로 docker image를 만듦.
  • base image: 완전 백지에서 시작하는 게 아니라, container 환경에서 앞서 봤던 간단한 OS인 alpine 등의 기본 이미지를 가지고 시작함
  • comunity image: 지금까지 우리가 docker에서 설치해본 redis, alpine 등은 모두 comunity image이다.

Custom Redis Image 생성하기

  1. redis-image 폴더 생성
  2. 해당 폴더를 VSCode에서 연 후 Dockerfile 만들기 (뒤에 붙는 extention은 없음)
  3. 파일 내용 작성
FROM alpine

RUN apk add --update redis

CMD ["redis-server"]
  1. 터미널에서 docker build .로 빌드하기
  2. docker images로 이미지 아이디 확인하기
  3. docker run <imageid>로 실행하기
  4. docker run -it <imageid>로 실행하기
  5. (optional) 이미지 id 대신 이미지에 이름(tag)을 붙여서 redis, apline 처럼 이름으로 생성 하기: docker build . -t minji/redis:latest

Section 8 | Containerize our apps!

Express app image 생성하기

  1. Dockerfile 생성하기
  2. 파일 내용 작성
    추가되는 부분: COPY . . 명령어를 사용해서 host machine의 docker file 경로에 있는 모든 파일들을 container 상의 root 경로로 전부 복사해서 가져오기
FROM node:22

COPY . .

RUN npm install
RUN npm run build

CMD ["npm", "start"]
  1. 터미널에서 docker build -t my-express-app .로 빌드하기
  2. docker images로 이름과 id 확인하기
  3. docker run -d 6379:6379 redis
  4. docker run -it my-express-app

docker.internal.host 이용해서 부모 host 접속하기

  • mapping: host ➡️ container
  • docker.internal.host: container ➡️ host
  • .env 파일의 localhost 링크를 대체해줘야 함
PORT=4000
REDIS_URL=redis://host.docker.internal:6379
  • 새로 build: docker build -t my-express-app .
  • 새로 실행: docker run -it my-express-app

.dockerignore로 .env 분리하기

  • github에 업로드할 때 보안 문제로 .env 파일은 gitignore로 제외하고 업로드하듯이, docker에도 dockerignore가 있음
  • .dockerignore라는 이름의 파일을 만들고 제외하려는 파일의 이름 (.env)을 적기
  • 새로 build: docker build -t my-express-app .
  • 새로 실행: docker run -it --env-file .env my-express-app
  • 이미지와 중요한 환경변수들이 분리되어서 관리가 되도록 할 수 있음!

Build 속도 개선: docker cache를 활용할 수 있도록 Dockerfile 개선하기

  • 지금은 매번 실행할 때마다 npm install을 실행하고, npm run build를 실행하고 있음. 그러나 설치된 패키지의 변화가 없다면 npm install은 매번 실행되지 않아도 괜찮음. 따라서, package.json 내용의 변화가 있으면 npm install을 실행하고, 변화가 없으면 실행하지 않도록 설정해보자.
FROM node:22

COPY package*.json .

RUN npm install

COPY . .

RUN npm run build

CMD ["npm", "start"]
  • 추가로, dockerignore에 넣을 파일들 더 작성하기
.env
.github
build
node_modules

Docker image 크기 줄이기: build & production stage 구분

  • production 단계에서 패키지는 dependency만 있으면 되고, build 단계에서는 devDependencies까지 필요하므로 단계를 구분해서 필요한 패키지만 설치하는 것으로 docker image를 줄여보자.
# Build stage

FROM node:22 as build

COPY package*.json .

RUN npm install

COPY . .

RUN npm run build

# Production stage

FROM node:22 as production

COPY --from=build ./build ./build
COPY --from=build ./package.json ./package.json
COPY --from=build ./package-lock.json ./package-lock.json

RUN npm install --only=production

CMD ["npm", "start"]

WORKDIR 이용해서 올바르게 파일 관리하기

  • image를 실행한 후 다른 터미널로 docker ps, docker exec -it 86f bash 순으로 접속한 후 ls를 통해 디렉토리 구조를 확인하면 다음과 같다. 이 때, 우리는 새로이 빌드되는 파일들을 모두 usr > src 파일 내에 저장하기 위해 working directory를 설정하자.
# Build stage

FROM node:22 as build

WORKDIR /usr/src/my-app

COPY package*.json .

RUN npm install

COPY . .

RUN npm run build

# Production stage

FROM node:22 as production

WORKDIR /usr/src/my-app

COPY --from=build ./usr/src/my-app/build ./build
COPY --from=build ./usr/src/my-app/package.json ./package.json
COPY --from=build ./usr/src/my-app/package-lock.json ./package-lock.json

RUN npm install --only=production

CMD ["npm", "start"]
  • 수정 후 container에 접속한 건 exit으로 나간 후, 다시 빌드, 실행, 그리고 접속을 해서 어떻게 저장되고 있는지 확인하자.

Graceful shutdown 적용하기

  • Graceful shutdown: docker stop 명령어를 받은 후 10초 정도동안은 새로운 요청을 받지 않고 기존 처리중이던 요청만 마무리한 후 스스로 shutdown하도록 하는 것
    무조건 10초를 기다린다면, Graceful shutdown이 설정되지 않은 것 (기존 처리중이던 요청을 마무리한 후에는 즉시 shutdown되어야 함)
  • index.ts의 하단에 아래와 같은 코드를 추가하고, npm run build로 빌드해준 후, docker build -t my-express-app .로 container image 생성하기
process.on("SIGTERM", () => {
  console.log("SIGTERM");
  process.exit();
})

process.on("SIGINT", () => {
  console.log("SIGINT");
  process.exit();
})

  • container 실행하기: docker run -it --env-file .env my-express-app
  • 다른 터미널에서 container로 접속하기: docker ps, docker exec -it dfe bash
    ➡️ 종료 시그널 보내기: docker stop dfe
  • 만약 성공적으로 시그널을 받았다면 콘솔에 앞서 작성한 출력문들이 찍혔어야 하는데 안 찍힘 ..
  • Dockerfile에서 현재는 npm을 이용해서 start를 하므로 npm이 부모 프로세스가 되어버려서 CMD ["node", "build/index.js"]로 수정해야 함
  • 그 후 docker build부터 새로 해서 잘 적용되었는지 확인: docker build -t my-express-app ., docker run -it --env-file .env my-express-app
  • 시그널을 받는 것을 확인했으니 드디어 최종적으로 graceful shutdown 코드 추가 후 재빌드, 재테스트
  const server = app.listen(PORT, () => {
    console.log(`App listening at port ${PORT}`);
  });

  return server;
};

const server = startServer();

const gracefulShutdown = async () => {
  const _server = await server
  _server.close(()=>{
    console.log("Graceful Shutdown . . ."); // 주로 여기서 DB 연결 등을 멈춰줌
    process.exit();
  }) 
  //(await server).close() 
}

process.on("SIGTERM", gracefulShutdown)
process.on("SIGINT", gracefulShutdown)

개발환경용 Dockerfile 만들기: Docker volume을 활용해서 개발환경 구축하기

  • host에 있는 소스코드가 수정되면 container에 있는 소스코드도 이를 반영하도록 해보자
  • 기존에 만든 Dockerfile은 production용(배포용) dockerfile이고, 지금 만들려는건 개발환경용! 이름은 Dockerfile.dev라고 짓자
  • Dockerfile.dev
FROM node:22 AS build

WORKDIR /usr/src/my-app

RUN npm install -g nodemon

COPY package*.json ./

RUN npm install

COPY . .

RUN npm run build

CMD ["npm", "run", "dev"]
  • 다음과 같이 빌드하자: docker build -t express-dev -f Dockerfile.dev .
  • Volumn을 넣어서 실행하자:
    docker run -it -p 4000:4000 --env-file .env -v ${PWD}/app:/usr/src/my-app/app express-dev
    💥 nodemon으로 모니터링 자체가 안됨,,, 같은 오류 발생한 사례 링크

docker-compose 이용해서 간편하게 개발하기

0개의 댓글