프론트엔드 개발을 하면서 가장 번거로운 작업 중 하나가 빌드와 배포이죠
수동으로 서버에 접속해 코드를 옮기고 컨테이너를 띄우는 과정은 시간이 많이 들고 사람마다 절차가 달라지면 안정성도 떨어져요.
그러기 때문에 React 프로젝트를 AWS EC2 서버에 Docker와 Nginx, GitHub Actions를 이용해 CI/CD 자동 배포 파이프라인으로 구축한 과정을 기록합니다.
dist/)를 Nginx 컨테이너 이미지로 패키징t2.smallAWS EC2에 접속하기 위해서는 SSH Key Pair를 생성하고 .pem 파일을 다운로드해야 해요.
왜냐하면 이 키는 EC2 서버에 원격으로 접속하거나 GitHub Actions에서 배포할 때 사용되기 때문이죠.
.pem 파일 자동 다운로드⚠️
.pem파일은 한 번만 다운로드 가능하므로 잘 보관해야 합니다...
.pem 파일은 다른 사용자에게 공유되면 안 되기 때문에 권한을 제한해야 해요.
chmod 400 my-ec2-key.pem
ssh -i <발급받은 key 이름.pem> ubuntu@<EC2_IP>
한 번 등록하면 이후에는 수정이 가능하지만 이전에 등록했던 값을 볼 수는 없습니다.
1. GitHub → Settings → Secrets and variables → Actions
2. Secret 이름: EC2_SSH_KEY
3. .pem 파일 내용 전체 붙여넣기
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;"]
프론트엔드 컨테이너는 3000 포트에서 동작하지만 http://<탄력적IP> (기본 80 포트)로 접속되도록 Nginx 리버스 프록시 설정
ssh -i <발급받은 key 이름.pem> ubuntu@<EC2_IP>
sudo nano /etc/nginx/conf.d/app.conf
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;
}
}
Ctrl + O → 저장Enter → 확인Ctrl + X → 종료sudo nginx -t && sudo systemctl reload nginx
이렇게 하면 Nginx가 80 → 3000 포트로 요청을 넘겨주니까 사용자는 단순히
http://<탄력적IP>만 입력하면 접근 가능
핵심은 두 개의 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를 실행시키면 돼요.
EC2_HOST = <EC2 탄력적 IP>
EC2_USER = ubuntu
EC2_SSH_KEY = <.pem 개인키 내용>
IMAGE_NAME = ghcr.io/<유저명>/<프로젝트명>
HOST_PORT = 사용할 포트 번호
호스트의 해당 포트(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
EC2_SSH_KEY 형식 오류로 GitHub Actions Secrets에 .pem 전체 내용(BEGIN/END 포함)을 그대로 붙여넣고 재배포하면 돼요.
컨테이너를 127.0.0.1:<port>:80 로 띄워 내부 전용 바인딩 했거나 AWS 보안그룹/방화벽에서 포트를 미허용 했을 때 발생해요.
이럴 경우에는 직접 노출하거나 프록시 운영으로 해결해야 해요.
-p ${HOST_PORT}:80 + 보안그룹에서 그 포트 허용.구분 Repo Visibility Package Visibility Pull 가능 여부
Public Public ✅ Public ✅ 로그인 불필요
Private Public ✅ Public ✅ 로그인 불필요
Public Private ❌ Private ❌ 로그인 필요
Private Private ❌ Private ❌ 로그인 필요
멋집니다