[토익 시험장 찾기 3] EC2 인스턴스에 React 도커로 배포하기

Choi Wontak·2025년 4월 16일

비바이빙

목록 보기
3/8
post-thumbnail

비바이빙 - 빠르게 개발하기

AI 시대에 맞추어 2주에 한 프로덕트를 만들어내는, 작지만 빠른 개발을 지향하는 프로젝트입니다.

결과물 보러가기! >> 내 근처 토익 시험장 찾기


사이트가 너무 느리다 (문제 인식 단계)

리버스 프록시 서버를 구현하여 서버 도메인을 성공적으로 가릴 수 있었다.
Let's Encrypt도 함께 사용하여 HTTPS까지 적용하였다.

그런데 문제는, 리소스를 받아오는 과정에서 리버스 프록시를 거치기 때문에 처음 페이지에 접속하는 경우 4-5초 정도의 시간이 걸린다는 문제가 있었다.


무엇이 필요할까 (기획 단계)

문제점

지금 방식은 너무 느리다!

이번 포스팅에서 원하는 목표는 다음과 같다.
1. 속도를 개선할 것
2. Github Pages를 포기하되, 배포에 어려움은 없어야 한다.
3. 얇은 내 지갑을 지켜라!

해결방안

찾아보니 두 방식 정도로 좁혀지는 것 같았다.

  1. S3 + CloudFront로 배포한다
  2. EC2에 도커로 배포한다.

구체화
1번 방식이 간단한 것 같았으나,

  • AWS의 다른 클라우드 서비스를 설정하면서 비용적인 부담이 커지는 것
  • 어차피 다른 프로젝트를 위해 EC2는 사용하게 될 것

을 고려하여 2번으로 결정하였다!

Github Main 브랜치에서 push 감지
-> Actions로 도커 이미지를 만들어 서버에 배포

이 단계로 진행하면 될 것 같다.


어떻게 만들까? (설계 단계)

  1. 인스턴스에 도커를 설치
  2. Actions 워크플로우 설정
  3. 도커 배포

만들어보자! (개발 단계)

[ChatGPT와 함께 진행하였습니다.]

도커를 설치한다.

# 도커 설치
sudo apt update
sudo apt install -y docker.io
sudo systemctl enable docker
sudo systemctl start docker
# 나중을 위해 compose도 설치
sudo apt install -y docker-compose

이제 깃허브 레포지토리로 이동해서 설정 > Secrets > Actions

레포지토리 Secrets를 추가한다.
.env에 포함되어 있던 Google Map API Key랑
ec2에 접속하기 위해

EC2_HOST (IPv4 public)
EC2_USER ubuntu (EC2에 접속할 유저 이름)
EC2_SSH_KEY EC2 .pem 파일의 내용 전체

이렇게 3개의 Secret key를 추가했다.

이제 프로젝트 루트에 Dockerfile을 추가한다!

FROM node:18 as build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
ARG REACT_APP_GOOGLE_MAPS_API_KEY
ENV REACT_APP_GOOGLE_MAPS_API_KEY=$REACT_APP_GOOGLE_MAPS_API_KEY
RUN npm run build

FROM nginx:alpine
COPY --from=build /app/build /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

도커 이미지를 만드는 데 필요한 파일들을 빌드하고, 정적 파일들만 이미지에 담아 저장한다.
이미지에는 nginx도 같이 올라가는데, 컨테이너 내부 80번 포트에서 실행된다. (React는 3000번)

루트 파일에 nginx 설정 파일을 추가한다.

# nginx.conf
server {
    listen 80;
    server_name bevibing.duckdns.org;

    location /location-map-app/ {
        root /usr/share/nginx/html;
        index index.html;

        try_files $uri $uri/ /location-map-app/index.html;
    }
}

컨테이너 내부의 nginx는 80번에서 요청을 받아
정적 파일들이 들어있는 디렉토리에서 리소스를 반환한다.

.github/workflows/deploy.yml 을 생성한다.

name: Deploy React App to EC2 with Docker

on:
  push:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'

      - name: Build React app
        run: |
          echo "REACT_APP_GOOGLE_MAPS_API_KEY=${{ secrets.REACT_APP_GOOGLE_MAPS_API_KEY }}" > .env
          npm install
          npm run build

      - name: Build Docker image
        run: docker build -t my-react-app --build-arg REACT_APP_GOOGLE_MAPS_API_KEY=${{ secrets.REACT_APP_GOOGLE_MAPS_API_KEY }} .

      - name: Save private key
        run: |
          echo "${{ secrets.EC2_SSH_KEY }}" > private_key.pem
          chmod 600 private_key.pem

      - name: Save Docker image and send to EC2
        run: |
          docker save my-react-app | bzip2 | ssh -o StrictHostKeyChecking=no -i private_key.pem ${{ secrets.EC2_USER }}@${{ secrets.EC2_HOST }} 'bunzip2 | docker load'

      - name: Run Docker container on EC2
        run: |
          ssh -o StrictHostKeyChecking=no -i private_key.pem ${{ secrets.EC2_USER }}@${{ secrets.EC2_HOST }} << 'EOF'
            docker stop my-react-app || true
            docker rm my-react-app || true
            docker run -d --name my-react-app -p 3000:80 my-react-app
          EOF

Main에 Push를 감지하면,
Actions에서 Secret key를 넘겨 도커 이미지를 빌드
SSH 접속용 Private key를 생성해
빌드한 이미지를 압축해서 EC2로 전송한 후 컨테이너를 띄운다.

맨 마지막 '-p 3000:80' 이 부분에서 컨테이너 외부의 3000번 포트를 컨테이너 내부 80번 포트와 연결한다는 점!!

이제 EC2 nginx를 다음과 같이 설정한다.

location /location-map-app/ {
    proxy_pass http://localhost:3000;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
}

이제 /location-map-app으로 접속하면,
리버스 프록시로 localhost:3000으로 전환되고, 3000번 포트에는 컨테이너의 nginx 동작하고 있다.
컨테이너 내부의 nginx가 정적 파일을 가져오는 것으로 흐름이 종료된다.

리버스 프록시를 사용하는 이유

  • 리액트 정적 파일은 파일 그 자체로는 실행되지 않기 때문에 내부 서버인 nginx가 필요하다.
  • 만약에 외부 nginx가 없으면 내부에서 HTTPS를 처리해야 되는데, 컨테이너에서 이를 처리하기가 복잡하다.
  • 그래서 ec2 nginx는 HTTPS를 처리하고, HTTP로 돌려 컨테이너 내부로 전달, 컨테이너에서는 리소스만 반환한다.

이제 메인에 변경사항을 Push..!

deploy에 성공한다.

https://bevibing.duckdns.org/location-map-app/

이제 접속도 빠르게 진행된다!

Pages 브랜치를 None으로 바꿔 Github 퍼블리싱도 중단 완료!

트러블 슈팅

bunzip2: command not found

ec2에 bunzip이 안 깔려 있어서 발생한 문제

sudo apt-get install -y bzip2

이걸로 해결

permission denied while trying to connect to the Docker daemon socket

Docker에 ubuntu 유저에 대한 권한을 주지 않아서 생긴 문제다..

sudo usermod -aG docker ubuntu

이걸로 해결!


후기

처음의 목표 세 가지를 모두 지켜내었다!

  1. 속도를 개선할 것
    리버스 프록시 서버를 거쳐 Github Pages로 요청을 보내 리소스를 받아오던 복잡한 단계에서, 내부에서 바로 리소스를 반환하니 속도 문제가 해결되었다.
  2. Github Pages를 포기하되, 배포에 어려움은 없어야 한다.
    깃허브 Actions로 배포 자동화, Docker로 버전 충돌이나 환경 세팅에 생기는 어려움도 해결했다.
  3. 얇은 내 지갑을 지켜라!
    ec2 내에서 다 해결했으니 추가적인 비용은 없다 😊

속도 문제도 해결! 다음 이슈는 또 무엇이 있을까..? 😭

profile
백엔드 주니어 주니어 개발자

0개의 댓글