일반적인 웹 서비스를 Docker 기반의 환경에서 구동해보기


웹 서비스를 개발하고 서버에 세팅할때면 늘 backend, frontend, redis, nginx 등 각각 배포하고 설정해주는 작업이 필요했었다. 한 두개 서비스면 모르지만 여러가지 환경에 배포할 때는 하나하나 작업하는게 여간 번거로운 일이 아니었다. 그래서 웹 서비스를 한번 docker 기반의 환경으로 구동해보고 나아가서는 kubernetes를 통해 배포/관리 하는 것을 해보는 것을 목표로 삼아보려고 한다.

우선, 기존에도 도커 자체는 종종 자주 사용해왔다. 주로 테스트환경을 구성하는 용도로 사용하긴 해서(ex. 이런거 구성해볼 때) 지식이 0인 상태는 아니다. 다만 그 이외에 것은 본인도 초보수준이라 메모의 목적으로 수정해갈 것이다.


우선적으로 해본 것은 Spring 기반의 Backend, Vuejs 기반의 Frontend, 이를 hosting하고 proxy 처리하는 Nginx, Backend 캐싱용 Redis로 구성되어 있는 앱을 docker-compose를 통해 간단하게 구동할 수 있게 하는 것이다. 네가지의 서비스를 관리해야 해서 각각 설정해주어야 하는데 도커 이미지화 시켜 설정을 좀 편하게 해보려는 목적이 있다.

Docker 설치 설정 등은 생략한다.

1. 프로젝트 구조

ROOT
├─backend
│   ├─src
│   │  └─ ...               # 백엔드 소스
│   ├─target
│   │  └─ ...               # 백엔드 빌드 (jar)
│   └─backend.Dockerfile    # 백엔드 도커파일
├─frontend
│   ├─node_modules
│   ├─src 
│   │  └─ ...               # 프론트엔드 소스
│   ├─package.json
│   ├─nginx.conf            # nginx 설정 파일(백엔드 api reverse_proxy 설정)
│   └─frontend.Dockerfile   # 프론트엔드 도커파일
├─.dockerignore
└─docker-compose.yml

일반적인 구조의 웹 서비스 개발소스를 백엔드, 프론트엔드 폴더로 구분한 상태이고, 루트 레벨에 .dockerignore, docker-compose.yml 파일이 구성되어있다. 각각 구성과 경로는 사용하는 환경에 맞게 편한대로 수정해도 된다.

하위 폴더나 파일은 대부분 생략된 상태다. 사전 준비로는 정상빌드/동작 하는 frontend소스, backend소스가 필요하다.

2. 주요 설정 파일

backend.Dockerfile

FROM openjdk:11-jre-slim
WORKDIR /backend

ARG JAR_FILE_PATH=target/*.jar
COPY ${JAR_FILE_PATH} apiserver.jar

ENV USE_PROFILE docker
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=${USE_PROFILE}", "apiserver.jar"]

해당 설정으로 사용하면 jar파일을 매번 package 해놓아야 한다는 단점이 있다. 아래의 설정으로 변경하면 패키징 또한 이미지내에서 실행해서 만들어준다. (다만 dependency 다운받느랴 훨씬 느리다.)

# Build stage
FROM maven:3.6.0-jdk-11-slim AS builder

WORKDIR /backend
COPY src .
COPY pom.xml .
RUN mvn -f pom.xml clean package

# Package stage
FROM openjdk:11-jre-slim
ARG JAR_FILE_PATH=backend/target/*.jar
COPY --from=builder ${JAR_FILE_PATH} apiserver.jar

ENV USE_PROFILE docker
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=${USE_PROFILE}", "apiserver.jar"]

COPY, RUN1, ENTRYPOINT 등의 설정을 볼 수 있다.

COPY {A} {B} 는 외부(A) 에서 이미지영역(B)으로의 COPY를 뜻한다.
WORKDIR는 실제 컨테이너 작업 경로로, 추후 컨테이너 내부로 접근해보면 해당 경로로 생성된 폴더를 확인할 수 있다.
ENTRYPOINT는 컨테이너가 최초에 꼭 실행해야만 하는 명령어가 있을 때 사용하고
CMD는 컨테이너 실행 시 시작되는 명령어지만 변경할 수 있을 때 사용한다.

frontend.Dockerfile

# Build stage
FROM node as builder

WORKDIR /frontend
COPY package.json .
RUN npm install
COPY . .
RUN npm run build

# Webserver stage
FROM nginx:stable-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /frontend/dist /usr/share/nginx/html # builder의 /fontend/dist(빌드결과물)을 복사

EXPOSE 8081
CMD ["nginx", "-g", "daemon off;"]

CMD2로 nginx 기본 실행 파라미터를 정의해준다. (Nginx가 포어그라운드에서 실행되게 한다)

프론트엔드 도커파일에는 Nginx도 같이 포함시켜놓았다. 또한 백엔드 서버를 proxy-pass 하여 서비스할 예정이라 설정 파일을 카피해서 사용한다. (볼륨 마운트 해도 될 것 같다.) 사용된 nginx 설정 파일은 다음과 같이 간단하게 프록시 설정이 되어있는 상태이다.

nginx.conf

server {
    listen 8081;
    server_name frontend

    resolver 127.0.0.11; # Docker 내장 DNS 서버를 사용

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
        try_files $uri $uri/ /index.html;
    }

    location ^~ /api {                      # fronthost:8081/api~ 의 요청은 backend로 호출되도록 지정했다.
        proxy_pass http://backend:8080/api; # 같은 network상의 컨테이너인 backend의 이름으로 지정되어있다.
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

resolver3설정을 통해 docker 내장 dns를 보도록 해주어야 정상적으로 동작하는 것 같다.

.dockerignore

.RData
.Rhistory
.git
.gitignore
dist/
manifest.json
rsconnect/
.Rproj.user
node_modules
npm-debug.log
Dockerfile*
docker-compose*
.dockerignore
README.md
LICENSE
.vscode
frontend/dist/
frontend/node_modules/

.gitignore와 비슷한 방식으로 사용되는 것 같다. image 생성시 copy 등등 작업에서 제외해줄 파일 또는 폴더 경로를 기입하면 된다. node_modules 같은 무거운 폴더가 포함되면 이미지 빌드 속도에도 영향이 갈 듯 하다.

docker-compose.yml

version: '3.4'

services:
  backend:
    image: tool/backend
    build:
      context: backend
      dockerfile: ./backend.Dockerfile
    container_name: backend
    hostname: backend
    ports:
      - 8080:8080
    expose:
      - 8080
    networks:
      - apiserver
    volumes:
      - D:/test/compose/backend:/backend/logs
  frontend:
    image: tool/frontend
    build:
      context: frontend
      dockerfile: ./frontend.Dockerfile
    container_name: frontend
    hostname: frontend
    ports:
      - 8081:8081
    expose:
      - 8081
    networks:
      - apiserver
  redis:
    image: redis:6.2.8-alpine
    command: redis-server --port 6377
    container_name: redis
    hostname: redis
    ports:
      - 6377:6377
    expose:
      - 6377
    networks:
      - apiserver

networks:
  apiserver:
    driver: bridge

각각 살펴보면 apiserver라는 이름의 network를 생성하여 backend, frontend, redis 3개의 컨테이너 이미지를 연동시키고 각각 dockerfile로 빌드하는 내용이다.
여기에선 테스트로 backend logs를 연결했는데 이처럼 로그나 내부 설정등을 volumes으로 외부에 연결해서 사용해도 된다.

VSCode를 사용한다면 Docker plugin을 설치해서 사용하면 문법체크 및 자동완성을 지원해준다.

구동

cd ROOT
docker-compose -f docker-compose.yml build                          # 전체 이미지 빌드 (--no-cache)
docker-compose -f docker-compose.yml build (backend|frontend|redis) # 이미지별로 빌드

docker-compose -f docker-compose.yml up                             # 전체 이미지 구동
docker-compose -f docker-compose.yml up (backend|frontend|redis)    # 이미지별로 구동

구동은 별 문제 없이 되는 것을 알 수 있는데 각각 컨테이너간 통신이 잘 안되는 경우가 있다. 이때는 network와 container name을 host로 잘 사용하고 있는지 체크해야 한다. (backend의 redis설정이 redis:6377로 되어있는지 확인한다던지, nginx가 backend:8080으로 되어있는지 확인한다던지..) 만약 production에서 도메인으로 서비스를 하는 경우 각각 환경별로 다르게 동작하도록 해야 할 텐데, 해당 부분은 여기에서는 별도로 찾아보지는 않았다.

기타

몇가지 명령어

docker network ls                                                   # Network 목록 확인
docker network inspect {name}                                       # Network 상세 확인
docker build -t frontend -f .\frontend\frontend.DockerFile .        # DockerFile 단독 빌드

ENTRYPOINT와 CMD

FROM ubuntu
ENTRYPOINT ["/bin/echo", "Hello"]
CMD ["world"]

실행하면 CMD에서 설정한 default 파라미터가 ENTRYPOINT에서 사용된다. docker run 명령 실행 시 파라미터를 주면 CMD에서 설정한 파라미터는 사용되지 않는다.

$ docker run -it --rm <image-name>
Hello world
$ docker run -it --rm <image-name> ME
Hello ME

TODO

환경별로 다른 docker-compose 설정하기..

  1. 보통 이미지 위에 다른 패키지(프로그램)를 설치하고 새로운 레이어를 생성할 때 사용 

  2. CMD는 docker run 실행 시 명령어를 주지 않았을 때 사용할 default 명령을 설정하거나, ENTRYPOINT의 default 파라미터를 설정할 때 사용한다. CMD 명령의 주용도는 컨테이너를 실행할 때 사용할 default를 설정하는 것 

  3. 업스트림 서버의 URL 이름을 IP 주소로 변경하는데 사용하는 DNS 서버를 지정하는 설정