본문 바로가기
docker

docker compose를 통해 배포해보자(springboot, react, redis)

by khds 2023. 11. 17.

 

들어가기

 
프로젝트를 진행하면서 실제 서비스를 위해 반드시 필요한 과정이 배포이다. 배포하는 과정을 통해 다른 사람들이 서비스에 접근할 수가 있다. 
나는 프로젝트를 배포한 경험이 여러 번 있지만 그때마다 aws 인스턴스를 새로 만들어서 사용했었다. 그리고 새 인스턴스마다 환경 설정을 하고 필요한 것들을 설치하고 shell 명령어를 입력하는 등 성가신 과정들을 밟았었다. 그리고 로컬 서버에서 동작하던 것이 배포 서버에서는 동작을 못하는 등 배포 서버에 추가적인 설정을 해줬어야 했다.
하지만 도커(docker)를 사용한다면 복잡한 환경설정을 하지 않아도 된다. 미리 만들어 놓은 이미지들을 컨테이너로서 실행하기만 하면 된다. 그리고 도커 컴포즈를 사용하면 다중 컨테이너(다중 서비스)를 하나의 파일로 한번에한 번에 통제할 수 있기에 훨씬 간편함을 가진다. 즉, React와 Springboot, mysql, redis 등 여러 서버를 한 번에 열고 닫을 수가 있는 것이다. 
이 글에서는 이러한 도커 컴포즈를 통해 실제 프로젝트에 React, Springboot, redis 서버를 담은 컨테이너를 생성하여 배포하는 과정을 담을 것이다. 참고로 데이터베이스 서버는 aws rds를 사용하고 있기 때문에 따로 서버를 열지는 않았다.
 
 

도커 이미지, 컨테이너에 기본 이해

 
우선 본격적으로 포르젝트에 도입하기 전에 내가 책으로 공부한 내용을 바탕으로 도커에 대해 간단하게 설명하고 넘어가고자 한다. '도커 컨테이너 빌드업 - 이현룡', '시작하세요! 도커/쿠버네티스-용찬호'을 참고하였다.
'도커' 하면 많이 언급되는 것은 이미지, 컨테이너이다. 이미지라는 읽기 전용 파일을 통해 컨테이너라는 것이 구현되고, 이 컨테이너가 서비스 프로세스이다. 컨테이너는 어떤 사물을 격리할 수 있는 공간을 의미하며 컨테이너에 서비스하고자 하는 애플리케이션 코드와 프로세스를 격리한다. 즉, 컨테이너를 실행한다는 것은 프로세스를 실행한다는 것과 같다.
컨테이너 서비스는 기존 환경과 다르게 애플리케이션 실행에 필요한 바이너리, 라이브러리 및 구성 파일 등을 패키지로 묶어 배포하는 방식이다. 즉, 어떤 환경에서든 동일한 컨테이너는 동일한 환경을 가진 동일한 패키지로 구성되어 있기에 따로 설정하는 것 없이도 항상 같은 실행을 하게 된다.
그리하여 애플리케이션이 가지고 있는 운영체제, 하드웨어(CPU, 메모리, 스토리지 등)에 대한 의존성 문제를 해결하는 것이다.
 
이러한 컨테이너로 인해 도커는 컨테이너 동작에 필요한 모든 내용을 사전에 코드로 작성하여 자동화하게 되면 기업이 필요할 때마다 애플리케인션 및 서버 환경을 적은 비용으로 빠르게 개발, 배포, 확장할 수 있다. 이렇게 코드를 통해 인프라를 관리하고 프로비저닝 하는 것을 IAC(Infrastructure as Code)라고 한다. 
이 기능을 통해 개발자는 애플리케이션 개발, 테스트, 배포 시마다 모든 인프라 구성 요소를 하나하나 수동적일 필요 없고, 변경 불가능한 인프라 환경에서 언제든 동일한 상태에서의 개발이 가능하다.
 
도커 컨테이너는 일반적으로 도커 허브에서 제공하는 이미지를 기반으로 실행된다. 
 
도커 이미지는 도커의 핵심 기술이며 코드로 개발된 컨테이너 내부 환경 정보를 고스란히 복제하여 사용할 수 있다. 이미지는 여러 개의 레이어가 겹겹이 쌓여서 구성되어 있는데 각각의 레이어마다 운영체제, 프로세스 등 주요 로직을 실행하기 위해 밑받침이 되는 부분을 담당한다. 아래의 사진을 봐보자.
 

출처: 도커 컨테이너 빌드업 - 이현룡

 
 도커 이미지 구조의 기본 운영체제 레이어 - 아파치 웹서버 레이아 - 서비스에 필요한 리소스 정보 및 환경 정보를 담은 레이어가 겹겹이 쌓여있다. 여러 레이어로 구성된 이미지는 몇 개의 컨테이너를 실행해도 별도의 읽고 쓰기가 가능한 컨테이너 레이어가 상위에 추가되므로 하위 이미지 레벨의 레이어에는 영향을 주지 않으면서 동작하는 것이 컨테이너 가상화의 특징 중 하나이다.
레이어를 구분하면 web source가 변경되더라도 전체이미지가 아닌, 기존 레이어를 제외한 변경된 웹 소스 레이어만 내려받아 사용하면 되기 때문에 효율적이라 할 수 있다.
이러한 이미지는 도커허브로부터 내려받을 수 있고, Dockerfile로 생성할 수 있는데, 생성된 이미지는 도커허브에 로그인을 한 후 업로드가 가능하다. 혹은 깃허브를 통해 Dockerfile 코드를 공유하여 관리할 수도 있다.
 
이렇게 간단하게 컨테이너와 이미지에 대해 알아보았다. 이제 본격적으로 배포 과정을 밟아보겠다.
 
 

도커 컴포즈를 이용한 배포

 
이제 본격적으로 도커 컴포즈를 이용하여 배포를 진행하겠다. 도커 컴포즈를 사용하는 이유는 위에서 언급했다시피 멀티컨테이너를 동시에 실행하기 위해서이다. 
 
아래는 Springboot 이미지를 기반으로 컨테이너를 실행하는 명령어이다. 
 

docker run -p 8080:8080 --name=backend --net=test-net sprinboot:latest

 
여러 컨테이너를 위해 위와 같은 실행을 컨테이너 별로 실행해야 하고, Dockerfile로 이미지를 빌드해야 할 때도 빌드 과정을 따로 밝아야 하고, 네트워크 설정도 해주는 등 여러 명령어를 입력해야 한다.
하지만 도커 컴포즈로는 위의 과정들을 하나의 파일로 한 번에 실행할 수가 있다.
처음에는 shellscipts로 미리 작성된 명령어 집합 파일을 실행하면 되지 않나? 생각을 하였다. 하지만 이는 명령어를 여러 번 실행하는 것임은 변함이 없기에 가독성 면에서도 도커 컴포즈 파일을 실행하는 것이 훨씬 나았다.
 
도커 컴포즈는 '야믈(yaml, yml)' 형식의 파일로 위 파일이 위치한 곳에서 'docker compose up'을 통해 실행시킬 수 있다.
선 코드를 봐보자. 아래는 docker-compose.yml 파일이다.

version: '3.8'
services:
  footprint-redis:
    image: redis:latest
    container_name: footprint-redis
    ports:
      - "6379:6379"
    networks:
      - footprint-net
    restart: always
  footprint-backend:
    depends_on:
      - footprint-redis
    build:
      context: ./backend/footprint
      dockerfile: Dockerfile
    container_name: footprint-backend
    ports:
      - "8080:8080"
    networks:
      - footprint-net
    restart: always
  footprint-frontend:
    depends_on:
      - footprint-backend
    build:
      context: ./frontend/footprint
      dockerfile: Dockerfile
    container_name: footprint-frontend
    ports:
      - "3000:3000"
    networks:
      - footprint-net
    restart: always
networks:
  footprint-net: {}

 
'version'은 도커 엔진의 버전마다 다른 버전을 적용해야 하며, 최신버전은 3.8이다.
'services'는 어떤 컨테이너들을 생성할지를 나타낸다. 나는 redis, backend, frontend 3개의 컨테이너를 생성하려고 한다. 이는 run 명령어를 옵션들과 함께 실행하는 것과 같다.
'image'는 어떤 이미지를 기반으로 할지, 'container_name'은 컨테이너의 이름을 어떻게 할지 ports는 외부의 어느 포트와 컨테이너의 포트를 연결할지를 나타낸 것이다. 해당 이미지가 서버에 존재한다면 그 이미지를 사용하고, 없다면 도커 허브로부터 pull 한 후 컨테이너로 생성한다.
'networks'는 컨테이너가 어느 네트워크에 속할지를 지정하는 것으로 파일 실행 시 구현되지 않은 네트워크를 지정할 경우 위와 같이 파일 아래에 추가적인 코드를 작성해야 한다. 네트워크에 속한 컨테이너들은 자동으로 alias가 컨테이너 이름으로 부여가 서로 다른 컨테이너까리 컨테이너 이름으로 접근할 수가 있다.
'restart'는 도커가 문제가 생겨서 도커 컴포즈가 종료되면 재실행을 할 것인지 미리 적어놓는 것이다.
'depends_on'은 등록된 컨테이너가 생성되기 전에는 먼저 생성될 수 없도록 하는 것이다.
 
'builds'는 Dockerfile을 이미지로 빌드한 후 컨테이너로 생성하는 것이고 'context'와 'dockerfile'를 통해 해당 위치에 있는 Dockerfile을 실행한다. 
즉, 위의 도커 컴포즈 파일은 Redis는 도커 허브의 이미지를 통해 컨테이너를 생성하고, Springboot와 React는 도커 파일을 통해 이미지를 생성 후 컨테이너를 생성하는 것이다.
도커 파일이 있는 위치의 하위 디렉터리를 포함시켜 이미지가 생성되는 것이므로 도커 파일의 위치는 프로젝트의 최상단에 위치해야 한다. 혹은 docker build 명령어 실행 시 경로를 설정할 수도 있지만, 경로는 이미 도커 컴포즈 파일에 작성해 두었으므로 도커파일 위치를 따로 조정할 필요는 없다. 
 
아래는 스프링부트 이미지 빌드를 위한 도커파일이다. 
https://da2uns2.tistory.com/entry/Docker-%EB%8F%84%EC%BB%A4%EC%97%90-Spring-Boot-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0를 참고하였다.

 

FROM openjdk:11
MAINTAINER khds <khdscor@gmail.com>
ENV APP_HOME=/apps

ARG JAR_FILE_PATH=build/libs/footprint-0.0.1-SNAPSHOT.jar

WORKDIR $APP_HOME

COPY $JAR_FILE_PATH app.jar

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "app.jar"]

 
FROM으로 이미 만들어진 jdk 이미지에 ENTRYPOINT 명령어를 실행함으로써 1층의 레이어가 추가된 이미지가 생성될 것이다. 
위 파일로 만든 이미지 내부에는 하위 디렉터리 즉, 스프링부트 프로젝트 폴더가 들어있고, 이미지를 컨테이너로 실행하면 ENTRYPOINT에 작성한 명령어가 자동으로 실행된다. 그래서 이미지를 컨테이너로 실행하면 8080 포트가 열려있고 빌드가 미리 완료된 jar 파일을 실행하여 서버가 배포될 것이다. 
 
한 가지 알아야 할 점은 위 도커 파일은 이미 빌드된 jar 파일을 실행하는 것이기에 빌드하는 과정은 따로 거쳐야 한다. 이는 CI CD 과정에서 미리 build를 해두는 것이 낫다고 생각하여 필자는 GitLab CI CD를 통해 빌드를 미리 하고 위의 도커 컴포즈가 실행되도록 하였다.
 
이제 React를 실행하는 이미지를 만드는 도커파일을 봐보자.
https://velog.io/@oneook/Docker% EB% A1%9C-React-%EA% B0% 9C% EB% B0%9C-%EB% B0%8F-%EB% B0% B0% ED% 8F% AC% ED%95%98% EA% B8% B0 참고하였다.
 

FROM node:14 
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .

EXPOSE 3000

CMD ["npm", "start"]

 
react 실행에 필요한 node 이미지를 기반으로 RUN을 통해 추가적인 install을 진행하고 CMD를 통해 명령어를 실행함으로써  2층의 레이어가 추가된 이미지가 생성될 것이다.
위 파일로 만든 이미지 내부에도 하위 디렉터리 즉, 리엑트 프로젝트 폴더가 들어있고, 이미지를 컨테이너로 실행하면 CMD에 작성한 명령어가 자동으로 실행된다. 그래서 이미지를 컨테이너로 실행하면 3000포트가 열린 채 리엑트가 실행되어 서버에 배포될 것이다. 
 
디렉토리 구조를 간단하게 표현하면 아래와 같다.

. // 간략화한 전체 프로젝트 폴더
├── frontend
│   ├─ public
│   ├─ src
│   ├─ package.json
│   ├─ ...
│   ├─ Dockerfile
├── backend 
│   ├─ gradle
│   ├─ src
│   ├─ build.gradle
│   ├─ ...
│   ├─ Dockerfile 
├── docker-compose.yml

 
docker-compose.yml 파일이 있는 위치에서 'docker compose up' 명령어를 통해 서버에 배포할 수가 있다.
아래의 사진을 봐보자.

 
표시된 부분을 보면 알 수 있듯이 3개의 service를 실행했을 때의 로그가 전부 보인다. 만약 백그라운드로 실행하고 싶으면 -d 옵션을 사용하면 되고, 로그를 보고 싶으면 'docker compose logs'를 통해 확인할 수 있다.
서버를 종료하고 싶으면 'docker compose down'을 통해 종료하고 실행된 컨테이너들은 모두 삭제가 된다. build 파일을 통해 생성한 이미지는 그대로 남고 다음에 컴포즈 파일을 실행 시 생성된 이미지가 그대로 있다면 build를 진행하지 않고 바로 해당 이미지를 사용한다. 
처음 실행할 시 시간이 오래 걸릴 것이다. 하지만 두 번째 실행부턴 캐시가 저장되어 있기 때문에 더 빨리 실행이 된다.
 
도커 컴포즈 안에 생성한 'footprint-net'라는 네트워크를 기억하는가? 도커 컴포즈에서 지정한 네트워크 내의 서비스들은 모두 별칭이 자동 부여 되어 컨테이너 이름을 지정하는 것으로 연결을 할 수 있다. 아래의 이미지를 봐보자.
 

 
위의 이미지처럼 외부로 호출을 한번 더 하는 것이 아닌 내부에서 바로 접근을 할 수 있다. 
아래는 Springboot에서 redis를 컨테이너 이름으로 지정한 것이다. 

spring.redis.host=footprint-redis
spring.redis.port=6379
spring.cache.type=redis

 
 
아쉽게도 react는 바로 접근이 되지 않았다... 찾아보니 springboot는 내부 엔진에 의해 코드가 변환되지만 react 파일들은 브라우저 상에서 실행되기에 컨테이너 이름을 쓰면 그대로 String 값으로 해당 이름이 지정되는 것이다. 그렇기에 ip 주소를 그대로 사용할 수밖에 없다.
 
 
참고로 도커 컴포즈를 통해 생성한 네트워크는 컴포즈를 종료하면 같이 삭제된다.
 
 

결론

 
이렇게 간단하게 도커 컴포즈를 통해 배포 과정을 밟아봤다.
나는 위의 과정을 밝으면서 배포를 할 때 바로 성공하지는 못했다. 결국 해결은 하였는데, 주된 문제 중 첫 번째는 경로 작성 문제였다. 도커 컴포즈 파일이나 빌드 파일에 작성된 경로가 잘못됐었 기에 해당하는 이름을 찾을 수 없다는 에러가 발생했었다. 
두 번째는 yml 형식에 맞지 않게 작성을 한 것이었다. yml은 탭이 아닌 스페이스 바를 통해 두 칸을 뛰어야 하고 형식 또한 준수하지 않으면 제대로 실행이 되지가 않았다.
코드로서 인프라를 구현하는 만큼 코드 하나하나에 크게 달라질 수 있다는 것을 확실히 알게 되었다..!
사실 도커 컴포즈를 통해 무중단 배포, 로드 밸런스도 이어서 진행하고 싶었지만 이는 오케스트레이션 도구를 사용하는 것이 훨씬 좋은 방법이라고 한다. 차후 오케스트레이션 도구를 사용해서 구현해 봐야겠다.
 

참고

 
도커, 컨테이너 빌드업! - 이현룡
시작하세요! 도커/쿠버네티스 - 용천호
https://da2uns2.tistory.com/entry/Docker-%EB%8F%84%EC%BB%A4%EC%97%90-Spring-Boot-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0

 

[Docker] 도커에 Spring Boot 구축하기

목차 1. Spring Boot 코드 작성 2. jar 파일 생성 3. Dockerfile 만들기 4. 컨테이너 실행하기 1. Spring Boot 코드 작성 간단한 Hello World 코드를 구현했다. 소스코드는 다음과 같다. package hello.hellospring; import org

da2uns2.tistory.com

 
https://velog.io/@oneook/Docker%EB%A1%9C-React-%EA%B0%9C%EB%B0%9C-%EB%B0%8F-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0

 

프론트엔드 개발자를 위한 Docker로 React 개발 및 배포하기

리액트 앱을 도커 컨테이너에 쉽게 띄워보자! 이 포스트는 Youtube의 Sanjeev Thiyagarajan라는 분이 올려주신 Docker + ReactJS tutorial 영상을 따라 쉽게 도커를 이해할 수 있도록 정리한 내용이다.

velog.io