
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
이번 게시물에서 필요한 파일들만 디렉토리 구조에 남겨두었습니다.
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에서 그 이유를 알 수 있습니다.
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
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
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을 지정해주었습니다.
같은 도커 네트워크 내에 위치해있기 때문에 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();
}, []);
/node_modules
yarn.lock
dockerfile에서 COPY를 진행할 시 node_modules 디렉토리까지 복사하는 것은
시간 낭비이므로 .dockerignore 파일에 추가해줍니다.
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
프리티어 인스턴스에서는 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를 적용하여 배포를 자동화하는 방법도 있습니다.
이 방법은 다른 게시물에서 설명하도록 하겠습니다.
저도 개발자인데 같이 교류 많이 해봐요 ㅎㅎ! 서로 화이팅합시다!