서론
시작하기에 앞서, 다른 서버를 배포하고 나니 이 방법은 별로 좋지 않다는 판단을 하여 이 글을 통해 배포하시는 것은 추천하지 않습니다!
이전 프로젝트에서 백엔드 담당자분이 서버를 내리시게 되었고, 해당 프로젝트가 다시 필요해졌습니다.
기왕 이렇게 된 김에 직접 배포해보고 싶다는 마음이 생겨, GPT와 Google의 힘을 빌려 백엔드 서버 배포에 도전했습니다.
하지만 결과는 계속 실패...
프론트엔드는 검색과 공식문서 등을 참고해 AWS를 통해 HTTPS로 배포한 경험이 있어서 ‘백엔드도 그냥 따라 하면 되겠지’ 싶었는데, 현실은 달랐습니다.
정작 제가 원하는 수준의 내 환경과 일치하는 실질적인 백엔드 배포 튜토리얼은 찾기 힘들었어요.
그래서 방향을 바꿔, GPT에게 구체적인 요구사항을 주고 단계별로 질문하며 하나씩 배워 나가는 방식으로 접근했습니다.
삽질도 많았지만, 결국 성공했고, 향후 재배포 시를 대비해 A to Z 가이드 형식으로 정리해두려고 합니다.
시작 전: 나의 상황 정리
- 백엔드 개발 경험은 거의 없음
- Java나 Spring Boot 문법은 잘 모르지만, 기능 구현 논의 과정은 기억함
- 서버 접속이나 EC2 초기 설정 정도는 익숙함
- GPT와 Google에 전적으로 의존하며 시도
왜 Docker를 도입했는가?
초기에는 Docker 없이 배포를 시도했습니다.
EC2에 직접 Spring Boot 프로젝트를 클론하고 빌드한 후, Nginx로 도메인 연결하는 방식이었는데...
계속해서 실패했습니다. 당시에는 이유를 몰랐는데 배포하면서 알아보니 환경 차이와 설정 누락이 계속 문제를 일으킨 것으로 보입니다.
그래서 이번에는 처음부터 Docker를 도입하여 환경을 컨테이너화하고, **Docker Compose로 의존 서비스(MySQL, Redis 등)**까지 한 번에 실행하는 전략으로 바꾸었습니다.
그 결과, 처음으로 DB 테이블이 잘 생성되고 서버가 꺼지지 않는 것을 봤습니다. (감격 🥹) 매번 바로 꺼지는 것만 봤지 DB 테이블 생성되는거 처음봤습니다..
물론 DB 테이블 생성된 뒤에도 계속 종료되었는데 그건 아래에서 설명하겠지만 설정 누락 때문이었습니다. ex. jwt 토큰, 리셋 패스워드를 보낼 email, email의 앱 비밀번호 등...
| 장점 | 설명 |
|---|---|
| ✅ 환경 일치 | 내 로컬 환경을 그대로 복제 가능 |
| ✅ 재사용성 | 컨테이너 하나 만들면 어디서든 실행 가능 |
| ✅ 간편한 배포 | docker-compose 한 줄로 여러 서비스 실행 |
| ✅ 유지보수 쉬움 | 설정이 코드로 남아 있어서 추적/수정 쉬움 |
| ✅ 의존성 격리 | 시스템에 직접 설치하지 않고도 사용 가능 |
🤖 GPT 사용 전략
GPT에게 "AWS 서버 배포 튜토리얼에 가장 적합한 엔진은 누구냐"고 물었고, 여러 엔진을 직접 비교해본 결과 gpt-4o가 가장 안정적으로 가이드를 제공했습니다. (처음엔 물어본 엔진이 계속 자기도 할 수 있다고 해서 모든 엔진을 나열하고 물어봤는데 4o를 추천하길래 4o로 진행했습니다)
특히, 모든 단계를 한 번에 알려달라는 식보다는 단계별로 하나씩 질문하고 진행하는 방식이라 백엔드 서버 배포의 지식이 거의 없는 저에게 좋았습니다.
🧱 백엔드 기술 스택
- Spring Boot 2.7.x / Java 17
- MySQL 8
- Redis
- Spring Security + JWT
- QueryDSL + JPA
- Swagger
- JUnit5 / H2 (Test용)
- Docker + GitHub Actions (CI/CD용)
- AWS EC2 (Ubuntu 24.04)
- 도메인: 구매한 도메인 (SSL 적용)
1. EC2 인스턴스 접속
ssh -i [your-key.pem] ubuntu@[EC2 퍼블릭 IP]2. EC2 서버 초기 세팅
🔧 2-1. 시스템 패키지 업데이트
sudo apt update && sudo apt upgrade -y문제 발생:
Ubuntu 24.04 (noble)에서는 certbot PPA가 아직 공식적으로 지원되지 않음 → Snap으로 설치 필요
✅ Certbot 설치 (Snap 방식)
# 기존 PPA 제거sudo add-apt-repository --remove ppa:certbot/certbot # Snap 기반 Certbot 설치sudo snap install coresudo snap refresh coresudo snap install --classic certbot # 명령어 심볼릭 링크 설정sudo ln -s /snap/bin/certbot /usr/bin/certbot정상 설치 확인:
certbot --version🔧 2-2. 필수 패키지 설치
sudo apt install -y git curl unzip wget gnupg lsb-release vim ufw🔧 2-3. Docker 설치
curl -fsSL https://get.docker.com -o get-docker.shsudo sh get-docker.shDocker Compose 설치:
DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker}mkdir -p $DOCKER_CONFIG/cli-pluginscurl -SL https://github.com/docker/compose/releases/download/v2.22.0/docker-compose-linux-x86_64 \ -o $DOCKER_CONFIG/cli-plugins/docker-composechmod +x $DOCKER_CONFIG/cli-plugins/docker-compose도커 권한 부여:
sudo usermod -aG docker $USER⚠️ 반드시 로그아웃 후 재접속 또는
newgrp docker실행 필요
설치 확인:
docker --versiondocker compose version🔧 2-4. Java 17 설치
sudo apt install openjdk-17-jdk -yjava -version3. Spring Boot 프로젝트 GitHub에서 클론
cd ~git clone https://github.com/your-username/your-repo.gitcd your-repo4. 환경 설정 파일 작성
작성할 파일 목록:
| 파일명 | 위치 | 설명 |
|---|---|---|
| Dockerfile | 루트 | JAR 파일을 기반으로 Spring Boot 애플리케이션 실행 |
| docker-compose.yml | 루트 | Spring, MySQL, Redis 컨테이너 통합 실행 |
| application-prod.yml | src/main/resources | 프로덕션 환경 전용 설정 |
| .env | 루트 | 민감한 설정 값 관리 (환경 변수화) |
❗ 주의:
소스 코드에서 @Value로 선언된 모든 항목을 .env와 application-prod.yml에서 반드시 설정해야 합니다.
또 EC2에 저장되는 이미지를 불러와야하기 때문에 이미지 경로 또한 설정합니다. 설정하지 않으면 이미지가 url 형식이 아닌 images/이미지이름.확장자로 데이터가 넘어오기 때문에 확인해보셔야합니다.
백엔드에서 이미지 처리하는 여러 방법이 있지만 기존 프로젝트에서는 이렇게 하는 것 같아서 이 방법으로 진행했습니다.
예시:
@Value("${jwt.secret}")@Value("${props.allowed-origin-url}")@Value("${props.allowed-origin-https-url}")@Value("${spring.mail.username}")@Value("${props.reset-password-url}")5. Dockerfile
FROM amazoncorretto:17ARG JAR_FILE=build/libs/프로젝트-0.0.1-SNAPSHOT.jarRUN mkdir -p /app/imagesCOPY ${JAR_FILE} app.jarENTRYPOINT ["java", "-Dspring.profiles.active=prod", "-jar", "/app.jar"]6. docker-compose.yml
version: "3.8" services: mysql: image: mysql:8.0 container_name: mysql restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} MYSQL_DATABASE: ${MYSQL_DATABASE} MYSQL_USER: ${MYSQL_USER} MYSQL_PASSWORD: ${MYSQL_PASSWORD} ports: - "3306:3306" volumes: - mysql-data:/var/lib/mysql redis: image: redis:latest container_name: redis restart: unless-stopped ports: - "6379:6379" app: build: context: . dockerfile: Dockerfile container_name: spring-app depends_on: - mysql - redis ports: - "8080:8080" environment: SPRING_DATASOURCE_URL: ${SPRING_DATASOURCE_URL} SPRING_DATASOURCE_USERNAME: ${SPRING_DATASOURCE_USERNAME} SPRING_DATASOURCE_PASSWORD: ${SPRING_DATASOURCE_PASSWORD} SPRING_REDIS_HOST: ${SPRING_REDIS_HOST} SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE} MAIL_USERNAME: ${MAIL_USERNAME} MAIL_PASSWORD: ${MAIL_PASSWORD} JWT_SECRET: ${JWT_SECRET} ALLOWED_ORIGIN_URL: ${ALLOWED_ORIGIN_URL} ALLOWED_ORIGIN_HTTPS_URL: ${ALLOWED_ORIGIN_HTTPS_URL} RESET_PASSWORD_URL: ${RESET_PASSWORD_URL} volumes: - image-data:/app/images volumes: mysql-data: image-data:7. application-prod.yml
port: 8080 spring: datasource: url: ${SPRING_DATASOURCE_URL} username: ${SPRING_DATASOURCE_USERNAME} password: ${SPRING_DATASOURCE_PASSWORD} driver-class-name: com.mysql.cj.jdbc.Driver redis: host: ${SPRING_REDIS_HOST} port: 6379 jpa: hibernate: ddl-auto: update show-sql: true properties: hibernate: format_sql: true default_batch_fetch_size: 100 servlet: multipart: max-file-size: 1MB max-request-size: 10MB mail: host: smtp.gmail.com port: 587 username: ${MAIL_USERNAME} password: ${MAIL_PASSWORD} properties: mail: smtp: auth: true starttls: enable: true web: resources: static-locations: file:/app/images/ springdoc: swagger-ui: path: /swagger api-docs: path: /api-docs swagger-ui-url: /swagger-ui.html server-url: https://duck-map.shopjwt: secret: ${JWT_SECRET} props: allowed-origin-url: ${ALLOWED_ORIGIN_URL} allowed-origin-https-url: ${ALLOWED_ORIGIN_HTTPS_URL} reset-password-url: ${RESET_PASSWORD_URL} image-dir: /app/images8. .env
# MySQLMYSQL_ROOT_PASSWORD=루트비밀번호MYSQL_DATABASE=데이터베이스이름MYSQL_USER=이름MYSQL_PASSWORD=비밀번호# Spring Datasource (app 서비스에서 사용)SPRING_DATASOURCE_URL=urlSPRING_DATASOURCE_USERNAME=이름SPRING_DATASOURCE_PASSWORD=비밀번호# RedisSPRING_REDIS_HOST=redis# Spring ProfileSPRING_PROFILES_ACTIVE=prod# MailMAIL_USERNAME=이메일MAIL_PASSWORD=비밀번호# JWTJWT_SECRET=토큰# Frontend URL (for CORS and reset password link)ALLOWED_ORIGIN_URL=urlALLOWED_ORIGIN_HTTPS_URL=urlRESET_PASSWORD_URL=url9. 빌드 및 실행
📦 JAR 파일 빌드
./gradlew bootJarJAR 파일 생성 위치:
build/libs/app-0.0.1-SNAPSHOT.jar🚀 Docker Compose 실행
docker compose up --build다음 편에서는 도메인 연결과 HTTPS 설정, Nginx 리버스 프록시 구성 그리고 약간의 기존의 백엔드 코드를 수정한 것을 다룰 예정입니다! 😎