NGINX, React, FastAPI Docker-compose 구성(환경 변수 문제 해결)

Jaeyeon Kim·2023년 7월 16일
3

Docker

목록 보기
2/3

개요

NGINX-React 컨테이너와 FastAPI 컨테이너를 따로 분리를 하니
두 컨테이너 사이에서 CORS 이슈가 발생할 뿐더러 유지보수에서의 불편함이 생겨
하나의 docker-compose 파일로 구성하는 것을 시도했습니다.

또한 React, FastAPI에서 모두 환경변수를 사용하다보니
이 환경변수들을 어떻게 관리할 지에 대해 주로 설명할 예정입니다.

이런 저런 방법들 다 시도해보다가 직접 해보고 성공한 해결책입니다.

디렉토리 구조

project_root
 ┣ backend
 ┃ ┣ app
 ┃ ┃ ┣ main.py
 ┃ ┃ ┗ requiements.txt
 ┃ ┣ dockerfile
 ┃ ┗ .dockerignore
 ┣ frontend
 ┃ ┣ nginx  
 ┃ ┃ ┗ nginx.conf
 ┃ ┗ dockerfile
 ┃ ┗ .dockerignore
 ┣ .env
 ┣ .gitignore
 ┗ docker-compose.yml

이번 게시물에서 필요한 파일들만 디렉토리 구조에 남겨두었습니다.

docker-compose.yml

version: "3.7"
services:
  docker-fastapi:
    container_name: backend
    build:
      context: ./backend/
      dockerfile: Dockerfile
    environment:
      - MYSQL_HOST=${MYSQL_HOST}
      - MYSQL_USER=${MYSQL_USER}
      - MYSQL_PASSWORD=${MYSQL_PASSWORD}
      - MYSQL_DB=${MYSQL_DB}
      - MYSQL_CHARSET=${MYSQL_CHARSET}
    restart: always
    networks:
      - chosen-network

  nginx:
    depends_on:
      - docker-fastapi
    container_name: frontend
    build:
      context: ./frontend/
      args:
        - REACT_APP_DPR_ENDPOINT=${REACT_APP_DPR_ENDPOINT}
        - REACT_APP_SUMMARY_ENDPOINT=${REACT_APP_SUMMARY_ENDPOINT}
      dockerfile: Dockerfile
    ports:
      - "80:80"
    restart: always
    networks:
      - chosen-network

networks:
  chosen-network:
    driver: bridge

먼저, CORS 이슈를 해결하기 위해 같은 도커 네트워크 상에 두도록 네트워크를 정의하였습니다.
또한 환경 변수는 .env에 저장되어 있으며 예시는 아래와 같습니다.

# API 환경 변수
MYSQL_HOST=0.0.0.0
MYSQL_USER=root
MYSQL_PASSWORD=password
MYSQL_DB=db
MYSQL_CHARSET=utf8

# 프론트엔드 환경 변수
REACT_APP_DPR_ENDPOINT=http://IP:PORT
REACT_APP_SUMMARY_ENDPOINT=http://IP2:PORT2

.env 파일로 관리할 때는 .gitignore 파일에 .env를 꼭 추가해주세요.

이처럼 .env 파일로 관리할 수도 있고 커맨드라인에서 argument로 넘겨줄 수도 있습니다.

docker-compose -e KEY=VALUE -e KEY2=VALUE2 up

중요한 점은, fastapi에서는 environment로 환경 변수를 넘겨주고,
react에서는 args로 환경 변수를 넘겨준다는 점입니다.
뒤에서 설명할 dockerfile에서 그 이유를 알 수 있습니다.

docker-compose 설치

설치

sudo curl -L "https://github.com/docker/compose/releases/download/v2.19.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

실행권한 부여

sudo chmod +x /usr/local/bin/docker-compose

버전 확인

docker-compose --version

dockerfile

/frontend/dockerfile

FROM node:19.6.1-alpine3.17 as builder

ARG REACT_APP_DPR_ENDPOINT
ARG REACT_APP_SUMMARY_ENDPOINT

WORKDIR /app
COPY . /app


RUN echo "REACT_APP_DPR_ENDPOINT=${REACT_APP_DPR_ENDPOINT}" >> .env
RUN echo "REACT_APP_SUMMARY_ENDPOINT=${REACT_APP_SUMMARY_ENDPOINT}" >> .env

RUN npm install
RUN npm run build

RUN rm .env

FROM nginx:alpine

COPY --from=builder /app/build /usr/share/nginx/html

RUN rm /etc/nginx/conf.d/default.conf
COPY nginx/nginx.conf /etc/nginx/conf.d

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

docker-compose에서 environment로 환경 변수를 넘겨주려고 했으나,
여기서 넘겨주는 환경 변수는 빌드 시점이 아닌 실행 시점에서 적용이 됩니다.
그렇기 때문에 server-side rendering이 지원되지 않는 react에서는
빌드 시점에서 환경 변수가 전달되어야 올바르게 빌드가 되고 앱이 실행됩니다.

ARG로 docker-compose에서 넘겨준 인자를 받은 뒤에,
받은 인자와 echo를 사용하여 .env 파일을 만들어줍니다.
이렇게 진행할 시 빌드 시점에 .env 파일이 생성된 상태이기 때문에
함께 빌드가 되게 됩니다.
빌드가 끝나면 .env 파일을 삭제함으로써 환경 변수를 보호합니다.

react에서 환경 변수를 사용할 때는 아래와 같이 사용할 수 있습니다.

const dpr_endpoint = process.env.REACT_APP_DPR_ENDPOINT

/frontend/nginx/nginx.conf

upstream docker_fastapi {
    server docker-fastapi:8080;
}

server {
    listen 80;

    location ~ /api/ {
        proxy_pass http://docker_fastapi;
        proxy_set_header   Host $host;
        proxy_set_header   X-Real-IP $remote_addr;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Host $server_name;
    }

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

    error_page   500 502 503 504  /50x.html;

    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

react 이미지는 nginx 웹 서버를 사용하여 접근할 수 있도록 하였습니다.
nginx를 사용하면 nginx 아래에 있는 어플리케이션들만 localhost로 인식하므로
fastapi에 접근할 수 있도록 upstream을 지정해주었습니다.

react -> fastapi 요청 url

같은 도커 네트워크 내에 위치해있기 때문에 localhost로 볼 수 있습니다.
그렇기 때문에 react에서 axios로 http 요청을 보낼 때,
요청할 url에서 IP를 제외한 경로만 적어주어야 합니다.

useEffect(() => {
  const FetchData = async () => {
    await axios({
      method: "get",
      url: "/api/test_path/",
    }).then((response) => {
      console.log(response.data);
    });
  };
  
  FetchData();

}, []);

/frontend/.dockerignore

/node_modules
yarn.lock

dockerfile에서 COPY를 진행할 시 node_modules 디렉토리까지 복사하는 것은
시간 낭비이므로 .dockerignore 파일에 추가해줍니다.

/backend/dockerfile

FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7

COPY ./app /app
WORKDIR /app

RUN pip install -r requirements.txt

ENV PORT=8080

fastapi의 dockerfile은 크게 설명할 부분이 없습니다.

main.py에서 환경변수를 사용할 때는 아래와 같이 사용합니다.

host = os.getenv("MYSQL_HOST")

배포

배포하고자 하는 서버에서 아래 명령어들로 레포지토리를 clone한 후, 배포를 진행할 수 있습니다.

git clone {프로젝트 레포지토리}
cd {프로젝트 레포지토리}
sudo apt install docker-compose
vim .env
# i로 Insert 모드 켠 후 환경 변수 복사
# :wq 로 저장 후 나가기
docker-compose -f docker-compose.yml up -d --build

AWS EC2 프리티어 이슈

프리티어 인스턴스에서는 CPU 자원이 부족해서 빌드하는 동안 서버가 터지거나,
빌드에 오랜 시간이 걸릴 수도 있습니다. (참고로 전 터졌습니다 🥲)
리액트 이미지는 로컬에서 빌드하고 docker hub에 push한 후,
docker-compose에서는 image:로 불러오는 것을 추천합니다.

이미지 빌드하기

cd frontend
docker build \
--build-arg KEY1=VALUE1 \
--build-arg KEY2=VALUE2 \
-t {docker hub 아이디}/{이미지 이름} .
docker push {docker hub 아이디}/{이미지 이름}

M1/M2칩의 경우 인자에 --platform linux/amd64를 추가해주세요.

docker-compose.yml 또한 수정해야합니다.

...
nginx:
    depends_on:
      - docker-fastapi
    container_name: frontend
	image: {docker hub 아이디}/{이미지 이름}
    ports:
      - "80:80"
    restart: always
    networks:
      - chosen-network
...

nginx의 빌드 부분을 없애고 image로 바꿔주세요.

Gitgub Action으로 CI/CD를 적용하여 배포를 자동화하는 방법도 있습니다.
이 방법은 다른 게시물에서 설명하도록 하겠습니다.

profile
낭만과 열정으로 뭉친 개발자 🔥

3개의 댓글

comment-user-thumbnail
2023년 7월 17일

저도 개발자인데 같이 교류 많이 해봐요 ㅎㅎ! 서로 화이팅합시다!

답글 달기
comment-user-thumbnail
2023년 7월 27일

CI/CD 부분 너무 기대되네요!

답글 달기
comment-user-thumbnail
2023년 7월 27일

와 너무 좋은 글입니다. 다른 게시물이 기대되네요

답글 달기