🚀 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/tcp

4개 모두 Up! ✅


🌐 3. DuckDNS 무료 도메인

왜 DuckDNS?

유료 도메인: 연 1~2만원 💸
DuckDNS: 영구 무료! 🆓

개인 프로젝트엔 이거면 충분!

설정 방법

  1. https://www.duckdns.org 접속
  2. Google 계정으로 로그인
  3. 도메인 이름 입력: idea-brainstorm
  4. EC2 Public IP 입력: 15.164.177.90
  5. 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
Google 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 Key

nginx.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

+ Recent posts