🚀 AWS EC2 배포 완료! - Docker + Nginx + HTTPS 풀스택 배포기
🎯 오늘의 목표
드디어... 배포다! 로컬에서만 돌리던 서비스를 진짜 인터넷에 올리는 날!
로컬호스트 감옥 탈출 🏃♂️
↓
https://idea-brainstorm.duckdns.org ✨✨ 완성된 것들
- ✅ AWS EC2 t3.medium 인스턴스 설정
- ✅ Docker Compose로 4개 컨테이너 배포
- ✅ DuckDNS 무료 도메인 연결
- ✅ Nginx 리버스 프록시 설정
- ✅ Let's Encrypt HTTPS 적용
- ✅ Google / Naver / Kakao OAuth 연동
- ✅ 브레인스토밍 기능 (Python API) 연동
결과: https://idea-brainstorm.duckdns.org 에서 실제 서비스 운영 중! 🎉
📚 최종 시스템 구조
┌─────────────────────────────────────────────────────────────┐
│ 사용자 브라우저 │
│ https://idea-brainstorm.duckdns.org │
└──────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ AWS EC2 (t3.medium) │
│ Ubuntu 24.04 LTS │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Docker Compose │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ Nginx (포트 80, 443) │ │ │
│ │ │ • SSL/TLS 인증서 (Let's Encrypt) │ │ │
│ │ │ • HTTP → HTTPS 리다이렉트 │ │ │
│ │ │ • 프론트엔드 정적 파일 서빙 │ │ │
│ │ │ • /api/ → Spring Boot 프록시 │ │ │
│ │ │ • /api/v1/brainstorming/ → Python 프록시 │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ ┌─────────────────┐ ┌─────────────────────┐ │ │
│ │ │ Spring Boot │ │ Python FastAPI │ │ │
│ │ │ (포트 8080) │ │ (포트 8000) │ │ │
│ │ │ • OAuth2 + JWT │ │ • OpenAI GPT-4 │ │ │
│ │ │ • User CRUD │ │ • Ephemeral RAG │ │ │
│ │ │ • Idea CRUD │ │ • 브레인스토밍 엔진 │ │ │
│ │ │ • Inquiry CRUD │ │ • SWOT 분석 │ │ │
│ │ └────────┬────────┘ └─────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ MySQL 8.0 │ │ │
│ │ │ (포트 3306) │ │ │
│ │ │ • users │ │ │
│ │ │ • ideas │ │ │
│ │ │ • inquiries │ │ │
│ │ └─────────────────┘ │ │
│ └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘🔥 1. AWS EC2 인스턴스 설정
첫 번째 삽질: t3.micro의 한계 💥
처음엔 프리티어 t3.micro (1GB RAM)로 시작했는데...
Docker 컨테이너 3개 실행
↓
메모리 부족
↓
서버 과부하 💀
↓
SSH 접속도 안 됨...EC2 콘솔에서 강제 재부팅해야 했음 😱
해결: t3.medium 업그레이드
t3.micro (1GB RAM) → 💀 메모리 부족
t3.medium (4GB RAM) → ✅ 쾌적!비용: 월 약 $33 (프리티어 아님... 😢)
하지만 4개 컨테이너 돌리려면 어쩔 수 없음!
인스턴스 유형 변경 방법
1. EC2 콘솔 → 인스턴스 선택
2. 인스턴스 상태 → 중지
3. 작업 → 인스턴스 설정 → 인스턴스 유형 변경
4. t3.medium 선택
5. 다시 시작⚠️ 주의: IP 주소가 바뀔 수 있음! (탄력적 IP 사용 권장)
🐳 2. Docker Compose 설정
docker-compose.yml
services:
mysql:
image: mysql:8.0
container_name: brainstorm-mysql
environment:
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MYSQL_DATABASE=brainstorm
- MYSQL_USER=${MYSQL_USER}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
networks:
- brainstorm-network
python-service:
build:
context: .
dockerfile: Dockerfile.python
container_name: brainstorm-python
ports:
- "8000:8000"
environment:
- OPENAI_API_KEY=${OPENAI_API_KEY}
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
networks:
- brainstorm-network
spring-boot:
build:
context: .
dockerfile: Dockerfile.spring
container_name: brainstorm-spring
ports:
- "8080:8080"
environment:
- SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/brainstorm?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul
- SPRING_DATASOURCE_USERNAME=${MYSQL_USER}
- SPRING_DATASOURCE_PASSWORD=${MYSQL_PASSWORD}
- JWT_SECRET=${JWT_SECRET}
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
- KAKAO_CLIENT_ID=${KAKAO_CLIENT_ID}
- KAKAO_CLIENT_SECRET=${KAKAO_CLIENT_SECRET}
- NAVER_CLIENT_ID=${NAVER_CLIENT_ID}
- NAVER_CLIENT_SECRET=${NAVER_CLIENT_SECRET}
- PYTHON_API_URL=http://python-service:8000
depends_on:
mysql:
condition: service_healthy
networks:
- brainstorm-network
nginx:
build:
context: .
dockerfile: Dockerfile.nginx
container_name: brainstorm-nginx
ports:
- "80:80"
- "443:443"
volumes:
- /etc/letsencrypt:/etc/letsencrypt:ro
depends_on:
- spring-boot
- python-service
networks:
- brainstorm-network
volumes:
mysql_data:
networks:
brainstorm-network:
driver: bridge
컨테이너 상태 확인
docker compose ps
NAME IMAGE STATUS PORTS
brainstorm-mysql mysql:8.0 Up (healthy) 0.0.0.0:3306->3306/tcp
brainstorm-python brainstorm-python Up (healthy) 0.0.0.0:8000->8000/tcp
brainstorm-spring brainstorm-spring Up 0.0.0.0:8080->8080/tcp
brainstorm-nginx brainstorm-nginx Up 0.0.0.0:80->80/tcp, 443->443/tcp4개 모두 Up! ✅
🌐 3. DuckDNS 무료 도메인
왜 DuckDNS?
유료 도메인: 연 1~2만원 💸
DuckDNS: 영구 무료! 🆓개인 프로젝트엔 이거면 충분!
설정 방법
- https://www.duckdns.org 접속
- Google 계정으로 로그인
- 도메인 이름 입력:
idea-brainstorm - EC2 Public IP 입력:
15.164.177.90 - add domain 클릭
결과: idea-brainstorm.duckdns.org 도메인 획득! 🎉
토큰 저장 (자동 갱신용)
토큰: 938e56a9-xxxx-xxxx-xxxx-xxxxxxxxxxxx나중에 IP 바뀌면 이 토큰으로 자동 업데이트 가능!
🔧 4. Nginx 리버스 프록시
왜 Nginx가 필요한가?
사용자 → Nginx → Spring Boot (API)
→ Python (브레인스토밍)
→ 정적 파일 (HTML/CSS/JS)하나의 도메인으로 여러 서비스 접근 가능!
nginx.conf
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
upstream spring-boot {
server brainstorm-spring:8080;
}
upstream python-api {
server brainstorm-python:8000;
}
# HTTP → HTTPS 리다이렉트
server {
listen 80;
server_name idea-brainstorm.duckdns.org;
return 301 https://$host$request_uri;
}
# HTTPS 서버
server {
listen 443 ssl;
server_name idea-brainstorm.duckdns.org;
ssl_certificate /etc/letsencrypt/live/idea-brainstorm.duckdns.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/idea-brainstorm.duckdns.org/privkey.pem;
# 프론트엔드 정적 파일
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
# Python 브레인스토밍 API (구체적인 경로 먼저!)
location /api/v1/brainstorming/ {
proxy_pass http://python-api;
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;
}
# Spring Boot API
location /api/ {
proxy_pass http://spring-boot;
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;
}
# OAuth2
location /oauth2/ {
proxy_pass http://spring-boot;
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;
}
# OAuth Callback
location /login/ {
proxy_pass http://spring-boot;
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;
}
}
}
⚠️ 중요: location 순서!
# ✅ 올바른 순서 (구체적인 것 먼저!)
location /api/v1/brainstorming/ { ... } # 1번
location /api/ { ... } # 2번
# ❌ 잘못된 순서
location /api/ { ... } # 이게 먼저 매칭되면
location /api/v1/brainstorming/ { ... } # 여기 안 옴!
Nginx는 먼저 매칭되는 location으로 라우팅함!
🔒 5. HTTPS 설정 (Let's Encrypt)
왜 HTTPS?
OAuth 로그인 → HTTPS 필수! (Google 정책)
보안 → 데이터 암호화
SEO → 검색 엔진 우대Certbot으로 인증서 발급
# 1. Certbot 설치
sudo apt update
sudo apt install -y certbot
# 2. Nginx 잠시 중지 (80 포트 필요)
docker compose stop nginx
# 3. 인증서 발급
sudo certbot certonly --standalone \
--preferred-challenges http \
-d idea-brainstorm.duckdns.org
# 4. 이메일 입력, 약관 동의
결과:
Certificate: /etc/letsencrypt/live/idea-brainstorm.duckdns.org/fullchain.pem
Key: /etc/letsencrypt/live/idea-brainstorm.duckdns.org/privkey.pem
만료일: 2026-03-04 (3개월 후)docker-compose.yml에 인증서 마운트
nginx:
volumes:
- /etc/letsencrypt:/etc/letsencrypt:ro # 읽기 전용!
이제 HTTPS 작동! 🔒
🔑 6. OAuth 설정 삽질기
문제 1: redirect_uri_mismatch 😱
오류 400: redirect_uri_mismatch
요청한 URI: http://idea-brainstorm.duckdns.org/login/oauth2/code/google원인: Google Console에 등록된 URI와 불일치!
해결:
Google Cloud Console → OAuth 클라이언트 ID
→ 승인된 리디렉션 URI 추가:
https://idea-brainstorm.duckdns.org/login/oauth2/code/google문제 2: http로 리다이렉트 됨 🤔
HTTPS 설정했는데 OAuth 리다이렉트가 http://로 감...
사용한 URI: http://idea-brainstorm.duckdns.org/...
^^^^
왜 http지?원인: Spring Boot가 원래 요청이 HTTPS인지 모름!
해결 1: application.yaml에 추가
server:
port: 8080
forward-headers-strategy: native # ✅ 프록시 헤더 인식!
해결 2: Nginx에서 헤더 전달
location /oauth2/ {
proxy_pass http://spring-boot;
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; # ✅ 이게 핵심!
}
X-Forwarded-Proto: https → Spring Boot가 HTTPS로 인식!
문제 3: Kakao 설정 오류 (KOE006)
앱 관리자 설정 오류 (KOE006)
등록하지 않은 리다이렉트 URI를 사용해결:
Kakao Developers → 앱 → 플랫폼 키 → 카카오 로그인 리다이렉트 URI
→ https://idea-brainstorm.duckdns.org/login/oauth2/code/kakao 추가최종 OAuth 설정 체크리스트
| 프로바이더 | Console URL | Redirect URI |
|---|---|---|
| console.cloud.google.com/apis/credentials | https://...duckdns.org/login/oauth2/code/google | |
| Naver | developers.naver.com/apps | https://...duckdns.org/login/oauth2/code/naver |
| Kakao | developers.kakao.com/console/app | https://...duckdns.org/login/oauth2/code/kakao |
3개 다 설정 완료! ✅
🧠 7. 브레인스토밍 연동 문제
문제: "세션 시작에 실패했습니다"
브라우저에서 브레인스토밍 시작하면 에러...
원인 분석
프론트엔드 코드:
// ❌ 로컬에서만 작동
const API_BASE_URL = 'http://localhost:8000/api/v1/brainstorming';
서버에서 localhost:8000은 접근 불가! 💥
해결
// ✅ Nginx 프록시 경유
const API_BASE_URL = '/api/v1/brainstorming';
그리고 Nginx에서 프록시 설정:
location /api/v1/brainstorming/ {
proxy_pass http://python-api;
...
}
흐름:
브라우저 → /api/v1/brainstorming/session
→ Nginx가 프록시
→ Python 컨테이너 (brainstorm-python:8000)테스트
curl http://localhost/api/v1/brainstorming/session -X POST
{
"session_id": "f2d9aeaf-d160-4aff-b402-3348cdb0a85b",
"message": "새로운 브레인스토밍 세션이 시작되었습니다."
}
브레인스토밍 작동! ✅
👑 8. 관리자 계정 설정
문제: OAuth로 로그인하면 일반 사용자
DB 확인:
SELECT user_id, email, role, provider FROM users
WHERE email='kimof4@gmail.com';
+---------+------------------+-------+----------+
| user_id | email | role | provider |
+---------+------------------+-------+----------+
| 1 | kimof4@gmail.com | ADMIN | LOCAL | ← 로컬 관리자
| 2 | kimof4@gmail.com | USER | GOOGLE | ← Google 로그인
+---------+------------------+-------+----------+같은 이메일인데 계정이 2개! 😅
해결: Google 계정을 ADMIN으로 변경
UPDATE users SET role='ADMIN' WHERE user_id=2;
로그아웃 → 재로그인 → 새 JWT 토큰에 ADMIN role 포함!
이제 /admin.html 접근 가능! 👑
🛡️ 9. 보안 설정
AWS 보안 그룹
| 포트 | 용도 | 상태 |
|---|---|---|
| 22 | SSH | ✅ 필요 |
| 80 | HTTP → HTTPS | ✅ 필요 |
| 443 | HTTPS | ✅ 필요 |
| 3306 | MySQL | ❌ 제거! (외부 차단) |
| 8080 | Spring Boot | ❌ 제거 권장 |
MySQL 외부 접근 차단 필수! DB 직접 접근되면 보안 끝남 💀
환경 변수로 비밀 관리
# .env 파일 (서버에만 존재, Git에 올리면 안 됨!)
MYSQL_ROOT_PASSWORD=강력한비밀번호
MYSQL_USER=brainstorm
MYSQL_PASSWORD=강력한비밀번호
JWT_SECRET=256비트이상시크릿키
GOOGLE_CLIENT_SECRET=구글시크릿
OPENAI_API_KEY=sk-xxxxx
Git Push Protection 발동! 😱
remote: - GITHUB PUSH PROTECTION
remote: Push cannot contain secrets
remote: - Google OAuth Client ID
remote: - Google OAuth Client Secret
remote: - OpenAI API Keynginx.conf.save 파일에 비밀키가 포함됨!
해결:
rm nginx/nginx.conf.save # nano 백업 파일 삭제
git reset HEAD~1
git add docker-compose.yml Dockerfile.nginx nginx/nginx.conf
git commit -m "Add nginx HTTPS config"
git push origin main
GitHub가 비밀키 푸시를 막아줌 → 오히려 고마웠음 😅
😵 10. 삽질 총정리
삽질 1: t3.micro 메모리 부족
증상: 서버 먹통, SSH 접속 불가
원인: 1GB RAM으로 Docker 4개 무리
해결: t3.medium (4GB) 업그레이드
교훈: 프리티어의 한계... 💸삽질 2: localhost 하드코딩
증상: 서버에서 API 호출 실패
원인: http://localhost:8000 하드코딩
해결: /api/v1/brainstorming 상대 경로로 변경
교훈: 배포 환경 고려해서 코드 작성!삽질 3: OAuth redirect_uri 불일치
증상: 400 redirect_uri_mismatch
원인: Console에 URI 미등록 / http vs https
해결: 각 프로바이더 콘솔에서 URI 추가
교훈: OAuth 설정은 정확하게!삽질 4: X-Forwarded-Proto 누락
증상: HTTPS인데 OAuth가 http로 리다이렉트
원인: Nginx에서 프록시 헤더 미전달
해결: proxy_set_header X-Forwarded-Proto $scheme; 추가
교훈: 프록시 환경에서는 헤더 전달 필수!삽질 5: Nginx location 순서
증상: Python API로 안 가고 Spring Boot로 감
원인: /api/가 /api/v1/brainstorming/보다 먼저 매칭
해결: 구체적인 경로를 위에 배치
교훈: Nginx location 순서 중요!💡 11. 배운 점
배포는 개발의 절반!
로컬 개발: 50%
배포 + 설정: 50%코드 다 짜도 배포 못 하면 의미 없음!
환경 차이 인식
로컬: localhost로 다 됨
서버: 컨테이너 이름, 프록시 경로처음부터 배포 환경 고려해서 설계하자!
보안은 기본
✅ HTTPS 필수
✅ 환경 변수로 비밀 관리
✅ 불필요한 포트 차단
✅ DB 외부 접근 차단로그는 친구
# 문제 생기면 바로 로그 확인!
docker compose logs -f spring-boot
docker compose logs -f nginx
docker compose logs -f python-service
📊 12. 최종 결과
서비스 URL
https://idea-brainstorm.duckdns.org기능 체크리스트
- ✅ Google 로그인
- ✅ Naver 로그인
- ✅ Kakao 로그인
- ✅ AI 브레인스토밍 (GPT-4)
- ✅ 아이디어 저장/조회
- ✅ 문의하기 시스템
- ✅ 관리자 페이지
서버 상태
docker compose ps
NAME STATUS PORTS
brainstorm-mysql Up (healthy) 3306/tcp
brainstorm-python Up (healthy) 8000/tcp
brainstorm-spring Up 8080/tcp
brainstorm-nginx Up 80/tcp, 443/tcp모든 컨테이너 정상 작동! ✅
🚀 13. 자주 쓰는 명령어 정리
서버 SSH 접속
ssh -i ~/Projects/PersonalProject/brainstorming/brainstorm-server-key.pem ubuntu@15.164.177.90
프로젝트 폴더 이동
cd ~/Braninstorming
컨테이너 관리
# 상태 확인
docker compose ps
# 로그 확인
docker compose logs -f spring-boot
docker compose logs -f python-service
docker compose logs -f nginx
# 재시작
docker compose restart
# 전체 재빌드
docker compose down
docker compose build
docker compose up -d
Git 작업
# 서버에서 최신 코드 받기
git pull origin main
# 로컬에서 푸시
git add .
git commit -m "커밋 메시지"
git push origin main
💬 14. 마무리
드디어... 첫 풀스택 배포 완료!!! 🎉🎉🎉
진짜 힘들었다... 😭
특히 힘들었던 것:
- EC2 메모리 부족으로 서버 먹통
- OAuth redirect_uri 삽질 (http vs https)
- Nginx 프록시 설정 순서 문제
- 각 프로바이더 콘솔 설정 찾아다니기
뿌듯한 것:
- 진짜 인터넷에서 접속 가능한 서비스!
- HTTPS 적용 완료 (자물쇠 뜸!)
- OAuth 3개 다 작동!
- AI 브레인스토밍도 정상 작동!
https://idea-brainstorm.duckdns.org이 URL로 누구나 접속할 수 있다는 게 신기함! ✨
다음 목표
- SSL 인증서 자동 갱신 (cron job)
- 모니터링 설정 (서버 상태 체크)
- CI/CD 파이프라인 (GitHub Actions)
- 비용 최적화 (필요할 때만 서버 켜기?)
작성일: 2024-12-04
개발 시간: 약 8시간 (삽질 포함 😅)
GitHub: https://github.com/magui-dev/Brainstorming
#AWS #EC2 #Docker #Nginx #HTTPS #LetsEncrypt #OAuth #SpringBoot #Python #배포 #DevOps
'그래서 일단 프로젝트 > 개인프로젝트-IdeaMaker(클로드 작성본 md 모음)' 카테고리의 다른 글
| 기록 8 . 관리자 기능 바이브코딩으로 만들고 준비작업 (0) | 2025.12.03 |
|---|---|
| 기록 7 . OAuth2.0 / JWT 구현...너무 어려운데 ?? (0) | 2025.12.02 |
| 기록 6 . 파이썬 모듈 JAVA 백엔드에 연결하기 .. (1) | 2025.12.01 |
| 기록 5 . 컨트롤러 계층 완성 및 테스트완료 (0) | 2025.11.27 |
| 기록 4 . DTO 작성 및 Test 코드 작성 단계 (0) | 2025.11.26 |