프론트엔드 자동 배포 파이프라인 구축기 (React · Docker · Nginx · GitHub Actions · AWS EC2)

장예성·2025년 8월 19일

프론트엔드 개발을 하면서 가장 번거로운 작업 중 하나가 빌드와 배포이죠
수동으로 서버에 접속해 코드를 옮기고 컨테이너를 띄우는 과정은 시간이 많이 들고 사람마다 절차가 달라지면 안정성도 떨어져요.

그러기 때문에 React 프로젝트를 AWS EC2 서버에 Docker와 Nginx, GitHub Actions를 이용해 CI/CD 자동 배포 파이프라인으로 구축한 과정을 기록합니다.


⚙️ 사용한 스펙

  • Frontend: React (Vite)
  • CI/CD: GitHub Actions
  • Container: Docker, Docker Buildx
  • Registry: GitHub Container Registry (GHCR)
  • Infra: AWS EC2 (Ubuntu 22.04)
  • Reverse Proxy: Nginx
  • 배포 포트: 컨테이너 내부 80 → 호스트 3000 → Nginx(80) 프록시

📦 전체 아키텍처

  1. GitHub Actions
    • main 브랜치 push → 빌드 & 도커 이미지 생성 → GHCR 푸시 → EC2 배포 스크립트 실행
  2. Docker
    • 프론트 빌드 결과(dist/)를 Nginx 컨테이너 이미지로 패키징
  3. EC2
    • CI/CD로 전달된 이미지를 pull → 컨테이너 run
  4. Nginx
    • 80 포트에서 외부 요청 수신 → 내부 3000 포트 컨테이너로 프록시 전달

☁️ AWS EC2 인스턴스 생성하기

1. AWS 콘솔 접속 후 EC2 선택

2. 인스턴스 시작 클릭

3. 기본설정

  • AMI(운영체제): ubuntu Server 22.04 LTS
  • 인스턴스 유형 t2.small
  • 스토리지: 기본 8GB SSD

🔑 EC2 SSH Key 발급 및 접속 방법

AWS EC2에 접속하기 위해서는 SSH Key Pair를 생성하고 .pem 파일을 다운로드해야 해요.
왜냐하면 이 키는 EC2 서버에 원격으로 접속하거나 GitHub Actions에서 배포할 때 사용되기 때문이죠.

1. Key Pair 생성

  1. AWS Management Console → EC2 → Key Pairs 이동
  2. Create key pair 버튼 클릭
  3. Key name 입력
  4. Key pair type: RSA
  5. Private key file format: .pem
  6. Create key pair 클릭 → 로컬 PC로 .pem 파일 자동 다운로드

⚠️ .pem 파일은 한 번만 다운로드 가능하므로 잘 보관해야 합니다...


2. 파일 권한 설정

.pem 파일은 다른 사용자에게 공유되면 안 되기 때문에 권한을 제한해야 해요.

chmod 400 my-ec2-key.pem

3. SSH로 EC2 접속

ssh -i <발급받은 key 이름.pem> ubuntu@<EC2_IP>

4. GitHub Actions 등록

한 번 등록하면 이후에는 수정이 가능하지만 이전에 등록했던 값을 볼 수는 없습니다.
1. GitHub → Settings → Secrets and variables → Actions
2. Secret 이름: EC2_SSH_KEY
3. .pem 파일 내용 전체 붙여넣기


🛠️ 설정 과정

1. Dockerfile 작성

FROM nginx:1.27-alpine

# Nginx 설정 복사
COPY nginx/default.conf /etc/nginx/conf.d/default.conf

# 빌드 산출물 복사
COPY dist/ /usr/share/nginx/html

HEALTHCHECK CMD wget -qO- http://127.0.0.1/ || exit 1

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

2. Nginx 리버스 프록시

프론트엔드 컨테이너는 3000 포트에서 동작하지만 http://<탄력적IP> (기본 80 포트)로 접속되도록 Nginx 리버스 프록시 설정

  1. EC2 접속
ssh -i <발급받은 key 이름.pem> ubuntu@<EC2_IP>
  1. Nginx 설정 파일 만들기
sudo nano /etc/nginx/conf.d/app.conf
  1. nano 에디터에 아래 코드 넣기
server {
  listen 80 default_server;
  server_name _;
  location / {
    proxy_pass http://127.0.0.1:3000;
    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-Proto $scheme;
  }
}
  1. 설정 저장 후 나가기
  • Ctrl + O → 저장
  • Enter → 확인
  • Ctrl + X → 종료
  1. Nginx 설정 문법 확인 후 reload
sudo nginx -t && sudo systemctl reload nginx
  1. 사이트 접속

    이렇게 하면 Nginx가 80 → 3000 포트로 요청을 넘겨주니까 사용자는 단순히 http://<탄력적IP> 만 입력하면 접근 가능

3. GitHub Actions Workflow 작성

핵심은 두 개의 Job이다

  • build-and-push: 빌드 후 GHCR에 이미지 푸시
jobs:
  build-and-push:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4.2.0

      - name: Build (Vite)
        run: yarn build

      - name: Build & Push Docker Image
        uses: docker/build-push-action@v6
        with:
          context: .
          file: ./Dockerfile
          push: true
          tags: |
            ghcr.io/<유저명>/<프로젝트명>:latest
            ghcr.io/<유저명>/<프로젝트명>:${{ github.sha }}
  • deploy: EC2에 접속해 새 이미지 실행
deploy:
    needs: build-and-push
    runs-on: ubuntu-latest
    steps:
      - name: Deploy on EC2
        uses: appleboy/ssh-action@v1.2.0
        env:
          IMAGE_NAME: ${{ secrets.IMAGE_NAME }}
          HOST_PORT: ${{ secrets.HOST_PORT }}
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USER }}
          key: ${{ secrets.EC2_SSH_KEY }}
          script: |
            APP_NAME="프로젝트명"
            docker pull "${IMAGE_NAME}:latest"
            docker rm -f $APP_NAME 2>/dev/null || true
            docker run -d --name $APP_NAME --restart unless-stopped \
              -p ${HOST_PORT}:80 "${IMAGE_NAME}:latest"

배포 script는 별도로 분리해서 EC2 서버에서 분리한 파일 경로를 찾고 script를 실행시키면 돼요.

  • Secrets 관리
EC2_HOST      = <EC2 탄력적 IP>
EC2_USER      = ubuntu
EC2_SSH_KEY   = <.pem 개인키 내용>
IMAGE_NAME    = ghcr.io/<유저명>/<프로젝트명>
HOST_PORT     = 사용할 포트 번호

🧩 배포 시 유의해야 할 점

port is already allocated

호스트의 해당 포트(80/443/3000 등)를 이미 어떤 프로세스가 점유 중이라 Docker가 -p로 바인딩을 못 했다는 의미예요.
쉽게 말해서 같은 포트를 다른 컨테이너에서 사용하면 컨테이너 이름과 상관 없이 포트가 동일하므로 충돌이 발생해요.

우선 컨테이너가 포트를 잡고 있는지 목록을 확인하면 좋아요.

# EC2 서버 접속 후 확인
docker ps --format 'table {{.ID}}\t{{.Image}}\t{{.Names}}\t{{.Ports}}'

케이스는 보통 4가지인데요.
1. nginx가 80/443을 잡고 있고 나의 앱 컨테이너도 80/443을 직접 열려 해서 충돌
2. 같은 포트를 다른 컨테이너가 이미 사용 중
3. 충돌 후 남은 docker-proxy가 점유
4. docker compose 프로젝트 중복 실행

보통 이런 케이스들은 컨테이너 포트 확인 후 충돌없는 포트로 변경하는 게 확실하고 빠르게 해결할 수 있어요.
또는 컨테이너를 정리하는 것이죠!
docker ps --filter "publish=8080" -q | xargs -r docker rm -f

Permission denied (publickey)

EC2_SSH_KEY 형식 오류로 GitHub Actions Secrets에 .pem 전체 내용(BEGIN/END 포함)을 그대로 붙여넣고 재배포하면 돼요.

배포는 성공했는데 외부에서 접속이 불가해요

컨테이너를 127.0.0.1:<port>:80 로 띄워 내부 전용 바인딩 했거나 AWS 보안그룹/방화벽에서 포트를 미허용 했을 때 발생해요.

이럴 경우에는 직접 노출하거나 프록시 운영으로 해결해야 해요.

  • 직접 노출: -p ${HOST_PORT}:80 + 보안그룹에서 그 포트 허용.
  • 프록시 운영(권장): 컨테이너는 내부(127.0.0.1:3000), 호스트 Nginx가 80/443 → 127.0.0.1:3000 프록시.

unauthorized

구분 Repo Visibility Package Visibility Pull 가능 여부
Public Public ✅ Public ✅ 로그인 불필요
Private Public ✅ Public ✅ 로그인 불필요
Public Private ❌ Private ❌ 로그인 필요
Private Private ❌ Private ❌ 로그인 필요

profile
Frontend Engineer

2개의 댓글

comment-user-thumbnail
2025년 8월 22일

멋집니다

1개의 답글