# 스택
인프라: AWS EC2 (t3.small), AWS RDS (MySQL)
백엔드: Spring Boot (Java)
프론트엔드: React
배포/서빙: Docker, Docker Compose, Nginx
1단계: AWS 인프라 구축하기 (EC2, RDS, 보안 그룹)
1. RDS 데이터베이스 생성
- AWS 콘솔 > RDS > 데이터베이스 생성
- 엔진 유형: MySQL 선택
- 템플릿: 프리 티어 (테스트용으로 비용 최소화)
- 설정: DB 인스턴스 식별자, 마스터 사용자 이름(admin), 암호를 입력합니다. 이 암호는 나중에 .env 파일에 사용되니 기억
- 스토리지/연결: 기본값으로 두되, '퍼블릭 액세스'를 '예'로 설정. (배포 초기 단계에서 로컬 PC 접속 테스트를 위해 유용. 배포 완료 후에는 '아니요'로 바꾸는 것을 권장)
- 보안 그룹: '새로 생성'을 선택하고 이름을 shop-db-sg와 같이 지정
- 데이터베이스를 생성
2. EC2 인스턴스 생성
- AWS 콘솔 > EC2 > 인스턴스 시작으로 이동
- 이름: shop-server와 같이 식별하기 쉬운 이름을 지정
- AMI(OS): Ubuntu
- 인스턴스 유형: t3.micro (프리 티어)
- 키 페어: SSH 접속을 위한 키 페어를 생성하거나 기존 키를 선택하고, .pem 파일은 보관
- 네트워크 설정:
보안 그룹: '새 보안 그룹 생성'을 선택하고 이름을 shop-server-sg로 지정
인바운드 규칙을 아래와 같이 설정
SSH | TCP | 22 | 소스: 내 IP (내 컴퓨터에서만 접속)
HTTP | TCP | 80 | 소스: 위치 무관 (0.0.0.0/0)
HTTPS | TCP | 443| 소스: 위치 무관 (0.0.0.0/0)
- 인스턴스 시작
3. 보안 그룹 연동 - EC2(집)에서 RDS(창고)로 들어갈 수 있도록 허가 하기
- EC2 > 보안 그룹 메뉴로 이동
- shop-db-sg (RDS 보안 그룹)를 선택하고 [인바운드 규칙 편집]을 클릭
- 규칙 추가
유형: MYSQL/Aurora
소스: Custom을 선택하고, 검색창에 shop-server-sg (EC2 보안 그룹)을 입력하여 선택
- 규칙 저장
→ 이제 shop-server-sg 보안 그룹에 속한 EC2 인스턴스만이 shop-db-sg로 보호받는 RDS에 접근할 수 있다
2단계: 애플리케이션 Dockerize 하기
이제 코드를 서버에 올릴 준비. 각 프로젝트 루트에 Dockerfile 작성.
Backend (Spring Boot) Dockerfile
# backend/Dockerfile
# --- 1단계: 빌드 환경 설정 ---
# 존재하지 않는 이미지 이름 -> 실제 존재하는 공식 이미지 이름으로 변경합니다.
FROM maven:3.9.6-eclipse-temurin-21 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests
# --- 2단계: 최종 실행 환경 설정 ---
# 이 이미지(JRE 21)는 이름이 올바르므로 그대로 둡니다.
FROM eclipse-temurin:21-jre-jammy
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
Frontend (React) Dockerfile with Nginx
# frontend/Dockerfile
# --- 1단계: 빌드(Build) 스테이지 ---
# Node.js 20 버전을 'builder'라는 이름의 빌드 환경으로 사용합니다.
FROM node:20-alpine AS builder
WORKDIR /app
# package.json과 package-lock.json을 먼저 복사합니다.
COPY package*.json ./
# 의존성을 설치합니다.
RUN npm install
# 나머지 소스 코드를 복사합니다.
COPY . .
# 프로덕션용으로 React 앱을 빌드합니다. /app/build 폴더가 생성됩니다.
RUN npm run build
# --- 2단계: 실행(Runtime) 스테이지 ---
# 가볍고 빠른 Nginx 이미지를 최종 실행 환경으로 사용합니다.
FROM nginx:stable-alpine
# 'builder' 스테이지에서 생성된 빌드 결과물(/app/build)을
# Nginx의 기본 웹 루트 디렉토리(/usr/share/nginx/html)로 복사합니다.
# 기존에 있던 기본 Nginx 페이지는 삭제합니다.
COPY --from=builder /app/build /usr/share/nginx/html
# React Router가 정상적으로 동작하도록, 우리가 만든 Nginx 설정을 복사합니다.
COPY nginx.conf /etc/nginx/conf.d/default.conf
# 컨테이너 외부에서 80 포트로 접속할 수 있도록 포트를 노출합니다.
EXPOSE 80
# Nginx 서버를 실행합니다.
CMD ["nginx", "-g", "daemon off;"]
Nginx 설정 파일 (nginx.conf)
# frontend/nginx.conf
server {
listen 80;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
# 이 부분이 핵심: 요청된 경로에 파일이 없으면, index.html을 대신 반환
try_files $uri $uri/ /index.html;
}
# /api/로 시작하는 모든 요청은 백엔드 서버(backend:8080)로 전달하라는 규칙 (Proxy)
location /api/ {
proxy_pass http://backend:8080/;
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;
}
# Swagger UI
location /swagger-ui {
proxy_pass http://backend:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# OpenAPI 문서
location /v3/api-docs {
proxy_pass http://backend:8080;
proxy_set_header Host $host;
}
}
프론트엔드 프로젝트의 루트에 nginx.conf 파일을 생성한다. API 요청을 백엔드로 넘겨주는 proxy_pass 설정이 핵심이다
Frontend API 클라이언트 설정 (api.js)
// frontend/src/api.js (예시)
import axios from 'axios';
// ... 인터셉터 생략 ...
const api = axios.create({
baseURL: '/api', // 모든 요청에 '/api'를 자동으로 붙여줌
timeout: 5000,
//...
});
export default api;
React 코드에서 API를 호출하는 axios 인스턴스의 baseURL을 /api로 설정해야 Nginx 규칙과 일치한다
3단계: EC2에 배포하기 - EC2 서버에 접속해서 모든 것을 하나로 합치기
1. .pem 키를 사용하여 EC2에 SSH로 접속
2. 최신 Docker 공식 버전 설치:
# 1. 기존에 설치된 오래된 버전들을 깨끗하게 삭제합니다.
for pkg in docker.io docker-doc docker-compose podman-docker containerd runc; do sudo apt-get remove -y $pkg; done
# 2. Docker의 공식 저장소를 사용하기 위한 준비 작업을 합니다.
sudo apt-get update
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# 3. Docker 저장소를 apt 소스에 추가합니다.
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
# 4. 최신 버전의 Docker Engine과 Compose 플러그인을 설치합니다.
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker $USER
exit
3. Docker 네트워크 문제 예방 조치
# Docker 설정 파일 생성
echo '{"ip-masq": true, "dns": ["8.8.8.8", "8.8.4.4"]}' | sudo tee /etc/docker/daemon.json
# Docker 서비스 재시작
sudo systemctl restart docker
'컨테이너 인터넷 접속 불가' 및 'DNS 조회 실패' 문제 차단
4. Git 프로젝트 클론 및 .env 파일 생성
git clone <내_프로젝트_주소>
cd <프로젝트_폴더>
nano .env
5. docker-compose.yml 파일 작성
# 최종 동작 버전 docker-compose.yml
# EC2 내부에 MySQL 컨테이너를 직접 실행하는 docker-compose.yml
version: '3.8'
services:
# --- 1. MySQL 데이터베이스 서비스 (EC2 내부에서 직접 실행) ---
db:
image: mysql:8.0
ports:
- "3307:3306" # EC2 서버의 3307번 포트를 컨테이너의 3306 포트로 연결
container_name: shop-mysql-db-local
restart: always
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
MYSQL_DATABASE: shop
TZ: Asia/Seoul
volumes:
- db-data:/var/lib/mysql
# healthcheck를 추가하여 백엔드가 DB 준비 완료 후 시작되도록 보장합니다.
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${DB_PASSWORD}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
# --- 2. 백엔드 Spring Boot 애플리케이션 서비스 ---
backend:
build: ./backend
container_name: shop-backend-app
restart: always
env_file:
- .env
depends_on:
db:
condition: service_healthy # db 서비스가 완전히 준비된 후에 backend가 시작됩니다.
environment:
SPRING_PROFILES_ACTIVE: docker
# 데이터베이스 호스트를 외부 RDS가 아닌, 바로 위에서 정의한 'db' 서비스 이름으로 변경합니다.
SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/shop?serverTimezone=Asia/Seoul&characterEncoding=UTF-8
# EC2 내부의 MySQL이므로 사용자 이름을 'root'로 변경합니다.
SPRING_DATASOURCE_USERNAME: root
SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD}
APP_JWT_SECRET: ${APP_JWT_SECRET}
CLOUDINARY_CLOUD_NAME: ${CLOUDINARY_CLOUD_NAME}
CLOUDINARY_API_KEY: ${CLOUDINARY_API_KEY}
CLOUDINARY_API_SECRET: ${CLOUDINARY_API_SECRET}
# --- 3. 프론트엔드 React (Nginx) 웹 서버 서비스 ---
frontend:
build: ./frontend
container_name: shop-frontend-web
restart: always
depends_on:
- backend
ports:
- "80:80"
# MySQL 데이터베이스의 데이터를 영구적으로 보관하기 위한 볼륨 설정
volumes:
db-data:
EC2 내부에 직접 DB 컨테이너를 띄운다
6. 컨테이너 실행:
sudo docker-compose up -d --build
4단계: 트러블 슈팅
1. 백엔드 컨테이너가 계속 죽음 (Communications link failure)
증상: docker ps를 해보면 shop-backend-app이 보이지 않거나 Exited 상태. docker logs shop-backend-app을 보면 DB 연결 실패 에러가 지속 출력
해결: 대부분 1단계에서 설정한 보안 그룹 문제. RDS 보안 그룹(shop-db-sg) 인바운드 규칙에 EC2 보안 그룹(shop-server-sg)을 소스로 추가했는지 확인.
2. 컨테이너 안에서 인터넷 안됨 (apt-get update 타임아웃)
증상: 디버깅을 위해 docker exec -it shop-backend-app bash로 컨테이너에 들어가 apt-get update를 실행하면 connection timed out 에러가 발생한다. RDS 접속도 안된다.
원인 분석:
EC2 보안 그룹 아웃바운드 규칙: shop-server-sg의 아웃바운드 규칙이 모든 트래픽 | 0.0.0.0/0으로 열려있는지 확인
VPC 라우팅 테이블: EC2가 속한 서브넷의 라우팅 테이블에 인터넷 게이트웨이(igw-...)로 향하는 0.0.0.0/0 경로가 있는지 확인
네트워크 ACL: 서브넷의 네트워크 ACL 인바운드/아웃바운드 규칙이 ALLOW ALL로 되어있는지 확인
3. 위 모든 설정이 완벽한데도 인터넷 안됨
증상: 위 모든 AWS 네트워크 설정이 완벽하고, 심지어 EC2 서버 자체에서는 curl google.com이 성공하는데도, 유독 Docker 컨테이너 안에서만 인터넷 접속이 실패
최종 원인: Docker 데몬의 ip-masq(IP Masquerade) 기능이 비활성화된 경우. 이것은 Docker가 컨테이너의 내부 IP를 EC2의 공인 IP로 변환해주는 핵심 기능인데, 특정 OS 이미지나 설정에서 꺼져있을 수 있다
진단 방법 (EC2 터미널): docker network inspect bridge
→ 결과에서 "com.docker.network.bridge.enable_ip_masquerade": "false"라고 되어 있으면 이게 원인
영구적인 해결책: Docker 설정 파일을 만들어 이 옵션을 강제로 켠다
- 설정 파일 생성:
echo '{"ip-masq": true}' | sudo tee /etc/docker/daemon.json
- Docker 서비스 재시작:
sudo systemctl restart docker
- 컨테이너 재시작:
sudo docker-compose down
sudo docker-compose up -d --build
4. 프론트엔드-백엔드 통신 실패 (502 Bad Gateway, 403 Forbidden)
증상: 모든 컨테이너가 Up 상태인데도 브라우저에서 데이터를 불러오지 못함
진단: 브라우저의 개발자 도구(F12) > [Network] 탭으로 에러 코드를 확인
원인 A: 502 Bad Gateway
단서: Nginx가 백엔드를 찾지 못함. docker exec...로 프론트 컨테이너에 들어가 curl http://shop-backend-app... 시도 시 Could not resolve host 에러 발생
해결: nginx.conf 파일의 proxy_pass 주소를 container_name이 아닌 서비스 이름(http://backend:8080)으로 수정
원인 B: 403 Forbidden
단서: proxy_pass 수정 후 에러가 403으로 바뀜. 이는 백엔드의 Spring Security가 요청을 거부한다는 의미
해결 1 (CSRF): SecurityConfig.java에 .csrf(csrf -> csrf.disable())을 추가하여 Stateless한 API에 불필요한 CSRF 방어를 비활성화
해결 2 (CORS): 그래도 403이 발생. 원인은 CORS 설정에 있었음. setAllowedOriginPatterns에 http://localhost:3000만 허용되어 있었음. 실제 서버의 공인 IP 주소를 허용 목록에 추가하여 해결
해결 3 (API 경로): 최종적으로, React 코드에서 API를 호출하는 주소가 /products로 되어 있어 Nginx의 /api 규칙을 타지 않는 것을 발견. axios의 baseURL을 /api로 설정하여 모든 문제를 해결
# 인스턴스 중지 후 재시작 - EC2 서버에 다시 접속해서 Docker 컨테이너 켜주기
1. SSH로 EC2 서버에 접속
cd <.pem 파일이 위치한 디렉토리>
ssh -i shop-key-new.pem ubuntu@<퍼블릭 주소>
2. 프로젝트 폴더로 이동하여 Docker Compose 실행
cd ~/shop
sudo docker-compose up -d
# --build 옵션은 코드가 바뀐 게 아니므로 붙이지 않는다
3. 잠시 기다린 후, 새로운 퍼블릭 IP 주소로 접속
# 수정한 코드 서버 반영
1. SSH로 EC2 서버에 접속
cd <.pem 파일이 위치한 디렉토리>
ssh -i shop-key-new.pem ubuntu@<퍼블릭 주소>
2. 프로젝트 폴더로 이동하여 최신 코드를 내려 받기
cd ~/shop
git pull
3. 변경된 코드로 Docker 이미지를 다시 빌드하고 컨테이너를 재시작
docker-compose down
docker compose build --no-cache
docker compose up -d
# 에러 시 로그 확인
docker compose ps
docker compose logs backend
docker logs -f shop-backend-app
# gui에서 핸들링
sudo apt-get install ec2-instance-connect'Web Project > shop' 카테고리의 다른 글
| GitHub Actions로 CI/CD 구성 (0) | 2025.09.05 |
|---|---|
| 내 PC의 GUI 툴에서 EC2 내부 DB에 원격 접속 (0) | 2025.09.04 |
| Spring Boot + React + Nginx 풀스택 프로젝트 Docker 배포 (0) | 2025.09.01 |
| 내가 쓴 리뷰 조회 (Spring Boot + React) (0) | 2025.09.01 |
| 리뷰 수정/삭제 (Spring Boot + React) (0) | 2025.09.01 |