AWS EC2로 풀스택 앱 호스팅하고 GitHub Actions로 자동 배포하기 — NestJS + Docker + CI/CD 완전 가이드
AWS EC2로 풀스택 앱 호스팅하고 GitHub Actions로 자동 배포하기 — NestJS + Docker + CI/CD 완전 가이드
NestJS API, PostgreSQL, Redis를 Docker Compose로 묶어 EC2에 올리고, main 브랜치 머지만으로 자동 배포가 돌아가는 파이프라인을 구성한 전 과정을 기록한다
EC2 인스턴스 하나에 GitHub Actions의
appleboy/ssh-action으로 SSH 접속해git pull→docker-compose up --build를 실행하는 구조. 복잡한 오케스트레이션 없이 소규모 프로젝트에서 빠르게 자동 배포를 붙이기에 충분하다.
Kubernetes나 ECS 없이도 소규모 풀스택 앱을 안정적으로 운영할 수 있는 조합이 있다. EC2 + Docker Compose + GitHub Actions 트리오다. 특히 appleboy/ssh-action은 별도 배포 서버 없이 GitHub Secrets만으로 SSH 배포를 구성할 수 있어서 초기 세팅 비용이 낮다. 이 글은 NestJS + PostgreSQL + Redis 스택을 EC2에 올리고 자동 배포까지 붙이는 전 과정을 단계별로 정리한다.
스택 구성
| 레이어 | 기술 |
|---|---|
| 클라우드 | AWS EC2 t3.micro (서울 ap-northeast-2) |
| API 서버 | NestJS + TypeScript |
| DB | PostgreSQL 16 |
| 캐시 | Redis 7 |
| 컨테이너 | Docker + Docker Compose |
| CI/CD | GitHub Actions |
Part 1 — AWS EC2 인스턴스 생성
1. 리전 선택
AWS 콘솔 우측 상단에서 아시아 태평양(서울) ap-northeast-2 선택.
💡 서울 리전을 고른 이유: 국내 사용자 대상 앱이라 레이턴시가 가장 낮다. 버지니아(
us-east-1) 대비 응답 시간 약 150ms 이상 차이.
2. 인스턴스 생성
EC2 → 인스턴스 시작 → 아래 설정으로 구성했다.
| 항목 | 선택값 |
|---|---|
| AMI | Amazon Linux 2023 |
| 인스턴스 유형 | t3.micro (vCPU 2, 메모리 1GiB) |
| 키 페어 | 새로 생성 → .pem 파일 안전하게 보관 |
| 스토리지 | gp3 20GB |
| 보안 그룹 | 아래 참고 |
3. 보안 그룹 설정
인바운드 규칙을 최소한으로 열었다.
| 포트 | 프로토콜 | 소스 | 용도 |
|---|---|---|---|
| 22 | TCP | 내 IP | SSH 접속 |
| 3001 | TCP | 0.0.0.0/0 | NestJS API |
| 8000 | TCP | 0.0.0.0/0 | AI 서비스 |
⚠️ SSH 포트(22)는 반드시 내 IP만 허용.
0.0.0.0/0으로 열면 브루트포스 공격 대상이 된다. IP가 바뀌는 환경이라면 VPN을 쓰거나, 배포 시에만 임시로 GitHub Actions IP 대역을 추가하는 방법도 있다.
DB(5432)와 Redis(6379)는 외부 미노출 — Docker 내부 네트워크로만 통신한다.
Part 2 — 서버 초기 설정
4. SSH 접속
chmod 400 ~/.ssh/healthmate-key.pem
ssh -i ~/.ssh/healthmate-key.pem ec2-user@43.201.67.1
5. Docker 설치
Amazon Linux 2023 기준:
sudo dnf update -y
sudo dnf install -y docker
sudo systemctl enable --now docker
sudo usermod -aG docker ec2-user
재로그인 후 권한 적용 확인:
docker ps # sudo 없이 실행되면 OK
6. Docker Compose 설치
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" \
-o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version
7. 프로젝트 클론
cd ~
git clone https://github.com/health-mate-team/health-mate.git
cd health-mate
8. 환경변수 설정
.env 파일은 저장소에 커밋하지 않으므로 서버에서 직접 생성한다.
cat > .env << 'EOF'
DB_NAME=health_mate
DB_USER=healthmate
DB_PASSWORD=<strong-password>
JWT_SECRET=<strong-secret>
ALLOWED_ORIGINS=http://43.201.67.1:3001
EOF
⚠️
.env는 절대git add하지 않는다..gitignore에 포함 여부를 먼저 확인할 것.
9. 첫 배포 실행
docker-compose up -d --build
docker-compose ps
정상 실행 시:
NAME STATUS
health-mate-db-1 running (healthy)
health-mate-redis-1 running (healthy)
health-mate-backend-1 running (healthy)
health-mate-ai-service-1 running (healthy)
⏱️ 첫 빌드 소요 시간: Docker 이미지 pull +
npm ci포함 약 3~5분.
Part 3 — GitHub Actions CI/CD 파이프라인
10. GitHub Secrets 등록
Repository → Settings → Secrets and variables → Actions → New repository secret
| 시크릿 이름 | 값 |
|---|---|
EC2_HOST | 43.201.67.1 |
EC2_USER | ec2-user |
EC2_SSH_KEY | .pem 파일 전체 내용 (-----BEGIN RSA PRIVATE KEY----- 포함) |
💡
.pem파일 내용을 복사할 때 개행 포함 전체를 그대로 붙여야 한다. 잘리면 SSH 연결 실패.
11. 배포 워크플로우 — deploy.yml
name: Deploy to AWS EC2
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy to EC2
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USER }}
key: ${{ secrets.EC2_SSH_KEY }}
command_timeout: 30m
script: |
cd ~/health-mate
git fetch origin
git checkout main
git pull origin main
docker-compose up -d --build
main 브랜치에 push가 발생하면:
- GitHub Actions 러너가 EC2에 SSH 접속
- 최신 코드를 pull
docker-compose up -d --build로 변경된 컨테이너만 재빌드- dangling 이미지 자동 정리
⏱️ 이후 배포는 약 13~20초. 첫 빌드 대비 캐시 활용으로 대폭 단축된다.
12. 검증 워크플로우 — verify.yml
PR 단계에서 코드 품질을 검증하는 파이프라인도 별도로 구성했다.
name: Verify
on:
push:
branches: [main, develop]
paths:
- 'backend/**'
pull_request:
branches: [main, develop]
paths:
- 'backend/**'
jobs:
jest:
name: Jest 단위테스트
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
💡
backend/**경로에 변경이 있을 때만 트리거되어 불필요한 실행을 막는다.
13. 실제 파이프라인 실행 결과
테스트 PR로 측정한 실측 소요 시간:
| Job | 결과 | 소요 시간 |
|---|---|---|
| Jest 단위테스트 | ✅ | 22초 |
| run-cases (9개 엔드포인트) | ✅ | 60초 |
| diff-results 회귀 감지 | ✅ | 22초 |
| verify.yml 전체 (병렬) | ✅ | 85초 |
| deploy.yml (main 머지 후) | ✅ | 13~20초 |
Jest와 diff-results는 병렬로 실행되고, run-cases가 병목 포인트다. 9개 엔드포인트를 순차 실행하기 때문에 병렬화하면 추가 단축 가능하다.
Part 4 — 브랜치 전략과 배포 흐름
14. 브랜치 규칙
main ← 프로덕션 (머지 즉시 EC2 자동 배포)
develop ← 통합 브랜치
feature/* ← 기능 개발
hotfix/* ← 긴급 수정
15. 전체 흐름 요약
feature/기능명
│
▼ PR → develop
검증 없음 (개발 통합용)
develop
│
▼ PR → main
verify.yml 실행 (Jest + run-cases + diff-check)
리뷰 & 승인
main (머지)
│
▼ deploy.yml 자동 실행
EC2 SSH → git pull → docker-compose up --build
약 15초 후 반영 완료
16. 트러블슈팅
| 에러 | 원인 | 해결 |
|---|---|---|
Permission denied (publickey) | .pem 내용 잘림 또는 개행 문제 | Secrets에 파일 전체 내용 재입력 |
docker-compose: command not found | Compose v2로 변경됨 | docker compose(공백) 또는 별도 바이너리 재설치 |
port already in use | 이전 컨테이너가 남아 있음 | docker-compose down 후 재실행 |
npm ci 실패 | package-lock.json 없음 | 로컬에서 npm install 후 lock 파일 커밋 |
| 초과 |
마치며
EC2 + Docker Compose + GitHub Actions 조합은 Kubernetes나 ECS 없이도 소규모 풀스택 앱을 안정적으로 운영하기에 충분하다. 특히 appleboy/ssh-action은 별도 배포 서버 없이 GitHub Secrets만으로 SSH 배포를 구성할 수 있어서 초기 세팅 비용이 낮다.
운영 중 가장 유효했던 결정은 DB와 Redis를 외부 포트에 노출하지 않은 것이다. Docker 내부 네트워크로 격리해두면 보안 그룹 설정이 단순해지고, 외부 포트 스캔으로 인한 위협을 원천 차단할 수 있다.
규모가 더 커지면 ECS Fargate나 EKS로 옮겨가야 하겠지만, 초기 MVP 단계에서는 이 정도 구성으로도 충분히 안정적으로 굴러간다. 한 번 셋업해두면 main 머지 한 번으로 배포가 끝나니, 작은 팀일수록 이 조합의 가성비가 좋다.