배포경험은 처음이기에 초보자가 접근하기 쉽다는 AWS EC2로 개인프로젝트를 배포했다.
Amazon EC2 - 클라우드 컴퓨팅 용량 - AWS
Amazon EC2는 프로세서, 스토리지, 네트워킹, OS 및 구매 모델의 다양한 옵션을 제공하며, 클라우드에서 안전하고 크기 조정 가능한 컴퓨팅을 제공합니다.
aws.amazon.com
1. AWS EC2 인스턴스 생성
- AWS EC2 인스턴스 생성 (t2.micro, Ubuntu, 서울 리전)
- 보안 그룹 포트 오픈: 22(SSH), 80(HTTP), 443(HTTPS), 8080(Spring Boot)
1) AWS EC2 인스턴스 생성
6개월간은 Free plan이 가능하여 새로 가입을 했는데 region을 잘못보고 Sydney 로 설정하여
다시 Seoul로 설정한 뒤 재포를 했다. 헛수고 하지 않기 위해 꼭 Region 확인 후 'Launch instance'

Instance Luanch 과정에서 생성된 .pem키는 꼭 잘 저장해놓도록 하자.
EC2 환경에 빌드할때 필요하다.
Public IP 또한 EC2에 올릴때 필요하다.
2) 인바운드 규칙 탭 추가하기
- HTTPS 포트 443 → 0.0.0.0/0
- HTTP 포트 80 → 0.0.0.0/0
- Custom TCP 포트 8080 → 0.0.0.0/0
2. EC2 환경 설정
- SWAP 메모리 설정 (t2.micro 메모리 부족 대비)
- Java 17 설치
- MySQL 설치 및 DB 생성, 비밀번호 설정
최대한 free plan 한도에서 진행하려 하니 터미널이 자주 멈추는 현상이 발생했다.
찾아보니 t2.micro 메모리가 1GB밖에 안 돼서 Spring Boot를 띄우는 것만으로도 벅찬 것 같아
SWAP 메모리를 띠로 설정해 주었다.
*SWAP은 EC2 안에 있는 디스크를 메모리처럼 쓰는 것.
1) SWAP 메모리 설정
sudo fallocate -l 1G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
2) 프로젝트는 Java17 과 MySQL 로 설정 했기때문에 클라우드 환경에도 동일하게 설치해준다.
sudo apt update
sudo apt install -y openjdk-17-jdk
sudo apt install -y mysql-server
3) MySQL 접속하여 DB와 유저 설정을 한다.
sudo mysql
접속되면, root 비밀번호 설정
CREATE DATABASE workout_diary;
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'yourpassword';
FLUSH PRIVILEGES;
EXIT;
3. DB 마이그레이션
- MySQL Workbench로 로컬 DB export
- scp로 EC2에 업로드
- EC2에서 import
Windows에서 SQL 파일을 export하면 파일 앞에 보이지 않는 특수문자가 붙는다. MySQL이 이걸 읽으면 에러가 난다. mysqldump 명령어로 해결하려 했지만 계속 실패해서 결국 MySQL Workbench로 export하니 해결됐다.
1) DB export -> 파일을 export 해서 EC2 환경에 올린다. (MySQL Workbench 접속)
- Dump Data Only → Dump Structure and Data 로 변경
- Export to Self-Contained File 선택 (단일 파일로)
- Include Create Schema 체크
- Start Export 클릭!
2) scp로 EC2에 올리기
scp -i "C:\Users\dhwkd\workout server.pem" "C:\Users\dhwkd\OneDrive\문서\dumps\Dump20260511.sql" ubuntu@your-ec2-ip:~
3) EC2 터미널에서 DB import
mysql -u root -p'your-password' workout_diary < ~/Dump20260512.sql
4. Spring Boot 배포
- application.properties 수정 (SSL 비활성화, 포트 8080)
- CORS 설정 (배포 URL 추가)
- mvnw clean package -DskipTests 빌드
- scp로 jar 파일 업로드
- nohup java -jar 백그라운드 실행
Spring Boot 배포전에 application.properties 포트번호 변경, CORS 설정에 배포 URL을 추가한다.
*도메인은 Duck DNS 의 무료 도메인을 이용했다. (계정 한개당 1개 무료 도메인 가능)
1) 포트번호, 배포 URL 추가
# Server web port
server.port=8080
configuration.setAllowedOrigins(Arrays.asList(
"https://localhost:3000",
"https://dailylift.duckdns.org"
));
private String[] theAllowedOrigins = {
"https://localhost:3000",
"https://dailylift.duckdns.org"
};
2) Spring Boot 빌드 > jar파일 EC2에 업로드
cd C:\exercise-library\02-backend\workout-diary\workout-diary
.\mvnw.cmd clean package -DskipTests
scp -i "C:\Users\dhwkd\gymrat server.pem" "C:\exercise-library\02-backend\workout-diary\workout-diary\target\diary-0.0.1-SNAPSHOT.jar" ubuntu@your-ec2-ip:~
3) nohup java -jar 백그라운드 실행
sudo cp -r ~/build/* /var/www/html/
nohup java -jar ~/diary-0.0.1-SNAPSHOT.jar > ~/app.log 2>&1 &
5. React 배포
- .env.production 생성 (배포 API URL)
- npm run build 빌드
- scp로 build 폴더 업로드
- Nginx로 정적 파일 서빙
1) .env.production 생성
기존에 .env 파일만 존재했는데, 이럴 경우 배포 주소와 local 환경 테스트를 분리할 수가 없어서
매번 포트번호를 수정하는게 불편하여 .env.production 과 .env 파일로 나누었다.
[React 환경 변수 우선순위]
- npm start (로컬) → .env.local > .env
- npm run build (빌드) → .env.production > .env
따라서 파일을 나눠서 관리한다
- .env → REACT_APP_API='http://localhost:8080/api' (로컬용)
- .env.production → REACT_APP_API='https://dailylift.duckdns.org/api' (배포용)
2) npm run build 빌드 > PowerShell-EC2에 업로드
cd C:\exercise-library\03-frontend\react-library
npm run build
scp -i "C:\Users\dhwkd\workout server.pem" -r "C:\exercise-library\03-frontend\react-library\build" ubuntu@52.64.44.192:~
3) Nginx 설치 > React 파일 복사 > Nginx 설정
sudo apt install -y nginx
sudo cp -r ~/build/* /var/www/html/
sudo tee /etc/nginx/sites-available/default << 'EOF'
server {
listen 80;
server_name gymrat.duckdns.org;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name gymrat.duckdns.org;
ssl_certificate /etc/letsencrypt/live/gymrat.duckdns.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/gymrat.duckdns.org/privkey.pem;
location / {
root /var/www/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://localhost:8080;
}
}
EOF
6. 도메인 & HTTPS
- DuckDNS 무료 도메인 생성
- Let's Encrypt SSL 인증서 발급 (certbot)
- Nginx 설정 (HTTP → HTTPS 리다이렉트, API 프록시)
1) DuckDNS 무료 도메인 생성
-> 도메인 설정 후, EC2 IP 주소를 입력한다.
DuckDNS는 certbot 자동 인증이 안 돼서 수동으로 TXT 레코드를 등록해서 인증했다.
certbot이 알려주는 값을 DuckDNS URL에 직접 입력하는 방식이다.
2) SSL 인증서 받기 > certbot이 TXT레코드 값 알려주면 이메일 주소 입력 > Y
sudo apt install -y certbot python3-certbot-nginx
sudo systemctl stop nginx
sudo certbot certonly --manual --preferred-challenges dns -d dailylift.duckdns.org
3) DuckDNS에 TXT 레코드 추가하기 > URL에 입력 후 OK 뜨면 bash Enter 누르기
https://www.duckdns.org/update?domains=dailylift&token=여기에토큰&txt=여기에TXT레코드
7. Auth0 설정
- Allowed Callback URLs 추가
- Allowed Logout URLs 추가
- Allowed Web Origins 추가
배포하면서 okta.oauth2.audience 를 배포 도메인으로 바꿨더니 401 에러가 발생했다. Auth0 API의 Identifier(http://localhost:8080)와 설정값이 달라서 토큰 검증이 실패한 것이었다. Identifier는 변경이 불가능하므로 Spring Boot와 React 설정을 다시 http://localhost:8080 으로 맞춰서 해결했다.
8. 트러블슈팅
위 과정에서 겪은 주요 이슈들은 각 단계에 기재했으나 한눈에 보기 위해 정리했다.
- SWAP 없으면 Spring Boot 실행 중 멈춤 (2번 참고)
- BOM 문제로 SQL import 실패 → Workbench export로 해결 (3번 참고)
- .env.production vs .env.local 우선순위 문제 (5번 참고)
- SSL 인증서는 DuckDNS CAA 문제로 manual DNS 방식 사용 (6번 참고)
- Auth0 audience 불일치 → http://localhost:8080 으로 맞춤 (7번 참고)
