EC2 + Docker Compose 기반 GitHub Actions CI/CD 자동배포 플로우
이번 프로젝트에서의 배포 자동화는 develop 브랜치에 PR 시 GitHub Actions가 Docker 이미지를 새로 빌드하고, Docker Hub에 push한 뒤, EC2 서버에서 Docker Compose를 통해 최신 이미지로 재배포하는 구조로 구성해 보았다.
GitHub Actions를 선택한 이유
우리는 프로젝트에서 이미 GitHub를 통해 코드를 관리하고 있었음.
PR 생성 → CI 테스트
develop merge → Docker 빌드/푸시 → EC2 배포
이 플로우를 코드 관리하는 환경인 Github에서 그대로 사용할 수 있기에 편할 거 같았다.
Jenkins 나 다른 자동화 CI/CD 환경을 비교해 생각해봤을 때 우리의 프로젝트 규모에 맞으려면 서버를 띄우지 않는게 맞다고 판단했다.
GitHub Actions는 GitHub-hosted runner를 쓰면 GitHub가 실행 환경을 제공해줘서, 별도 CI 서버를 직접 운영하지 않아도 됨.
Docker Hub 계정, EC2 SSH 키, 서버 IP 같은 민감한 값은 GitHub Secrets에 넣고 workflow에서 참조하면 됨.
그래서 코드에 비밀번호나 pem 키를 직접 올리지 않고도 배포 자동화를 구성할 수 있었음.
CI/CD 자동화 환경세팅 중 발생한 이슈들
처음 docker compose pull을 했을 때 not found 에러가 발생했다.
docker-compose.yml에 image: jinsungzz/hoppin만 적혀 있어서 기본 태그가 latest로 해석됐다.
그리고 Docker Hub에도 아직 이미지가 없었다.
image: jinsungzz/hoppin:staging먼저 프로젝트 루트에 Dockerfile을 만들었다.
FROM eclipse-temurin:17-jdk
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
처음 빌드할 때 아래 에러가 발생했다.
COPY build/libs/*.jar app.jar
lstat /build/libs: no such file or directory
./gradlew clean bootJar
docker build -t jinsungzz/hoppin:staging .
jar 파일 생성 후 빌드하도록 수정했다.
로컬에서 이미지를 push한 뒤 EC2에서 이미지를 pull 했더니 아래 에러가 발생했다.
no matching manifest for linux/amd64
나는 맥북 M2 아키텍쳐 모델을 사용 중이다.
docker buildx build --platform linux/amd64 -t jinsungzz/hoppin:staging --push .
이 코드를 통한 빌드로 로컬에서 push 할 때 이미지를 linux/amd64 아키텍쳐로 수정 후 빌드하게헀다.
나는 분명 Secrets 키 값을 입력했는데 Actions 로그인했는데 아래 에러가 발생했다.
Must provide --username with --password-stdin
Run echo "" | docker login -u "" --password-stdin
시크릿 값이 비어있다는 에러이다.
그래서 다시 시크릿 값을 잘못 넣었나? 생각해 계속 수정해서 넣어도 똑같은 에러를 받았다.
원초적으로 값이 비어있다는 것에 관점을 두고 생각해봤다.

Name과 Secret에 1:1로 환경변수를 적용해 주었다.

위의 STAGING_SSH_KEY 값에는 .pem의 키 값을 넣어줘야되는데
-----BEGIN OPENSSH PRIVATE KEY-----
중간 키 내용 전체
-----END OPENSSH PRIVATE KEY-----
이런 형태로 되어 있다.
이때 BEGIN / END 내용까지 모두 시크릿 값에 넣어줘야 한다!!!!!
CI scripts
pr 시 브랜치가 main, develop인지 확인하기 (pr 브랜치 기준으로 실행)
on:
pull_request:
branches: [ develop, main ]
Github가 제공하는 ubuntu 서버에서 CI가 실행됨
runs-on: ubuntu-latest
PR에 올라온 코드를 GitHub Actions 실행 환경으로 가져옴
- name: Checkout
uses: actions/checkout@v4
파일이 실제로 있는지 디버깅
- name: Show files
run: find . -maxdepth 3 -type f | sort
Spring Boot 빌드에 필요한 Java 17을 설치함
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
리눅스 환경에서는 gradlew에 실행 권한이 없으면 실행이 안 돼서 ./gradlew 실행 전에 권한을 부여했음
- name: Grant execute permission
run: chmod +x ./gradlew
- name: Run tests
run: ./gradlew clean test
7번에 테스트를 진행했기 때문에 -x test로 테스트를 제외하고 빌드만 확인함
- name: Build check
run: ./gradlew build -x test
CI 코드
name: PR to Develop CI
on:
pull_request:
branches: [ develop, main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Show files
run: find . -maxdepth 3 -type f | sort
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- name: Grant execute permission
run: chmod +x ./gradlew
- name: Run tests
run: ./gradlew clean test
- name: Build check
run: ./gradlew build -x test
CD scripts
develop 브랜치에 push가 발생하면 실행됨
즉 PR이 merge돼 develop이 업데이트되면 자동 배포가 시작됨
on:
push:
branches:
- develop
GitHub가 제공하는 Ubuntu runner에서 실행됨
GitHub Actions 서버에서 빌드하고 이미지를 push함
runs-on: ubuntu-latest
현재 develop 브랜치의 코드를 Actions runner로 가져와 스프링 부트를 실행하기 위해 JDK 17을 세팅함
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v4
Linux 환경에서 ./gradlew를 실행할 수 있도록 권한을 부여하고 Spring Boot 실행 jar 파일을 만들고 검증함
검증 파일은 개발한 파일 넣으면 됨
- name: Grant execute permission
run: chmod +x ./gradlew
- name: Build jar
run: ./gradlew clean bootJar -x test
- name: Verify jar contents
run: |
jar tf build/libs/*.jar | grep BOOT-INF/classes/application
jar tf build/libs/*.jar | grep -E "AiController|AuthController|MeController"
도커 허브 로그인 후 환경을 맞추기위해 어느 환경에서도 빌드해도 상관없는 Buildx 로 세팅
- name: Docker login
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
검증 시 개발한 파일을 넣으면 되는데 일단 임시로 넣어둠
docker buildx build \
--no-cache \
--platform linux/amd64 \
-t jinsungzz/hoppin:staging \
--push .
- name: Verify docker image contents
run: |
docker run --rm --entrypoint sh jinsungzz/hoppin:staging -c 'jar tf /app.jar | grep BOOT-INF/classes/application'
docker run --rm --entrypoint sh jinsungzz/hoppin:staging -c 'jar tf /app.jar | grep -E "AiController|AuthController|MeController"'
- name: Deploy to staging EC2
uses: appleboy/ssh-action@v1.0.3
GitHub Actions가 EC2에 SSH로 접속해서 배포 명령을 실행함
접속 정보는 Secrets에서 가져옴
script: |
echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
cd /home/ubuntu/app
EC2에서도 Docker Hub에서 private/public 이미지를 pull할 수 있도록 로그인하고, 배포 디렉토리로 이동
docker compose down
docker rm -f hoppin 2>/dev/null || true
docker image rm jinsungzz/hoppin:staging 2>/dev/null || true
docker system prune -af
이걸 넣은 이유는 배포했는데 이전 이미지/컨테이너가 계속 남아서 안 바뀌는 문제를 막기 위해 이전 컨테이너와 이미지를 정리하는 것임
docker network inspect hoppin-net >/dev/null 2>&1 || docker network create hoppin-net
docker pull jinsungzz/hoppin:staging
docker compose up -d --force-recreate
네트워크 확인/생성 + 최신 이미지 pull + Docker Compose 재기동
CD 코드
name: Deploy Staging
on:
push:
branches:
- develop
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Grant execute permission
run: chmod +x ./gradlew
- name: Build jar
run: ./gradlew clean bootJar -x test
- name: Verify jar contents
run: |
jar tf build/libs/*.jar | grep BOOT-INF/classes/application
jar tf build/libs/*.jar | grep -E "AiController|AuthController|MeController"
- name: Docker login
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and Push Docker image
run: |
docker buildx build \
--no-cache \
--platform linux/amd64 \
-t jinsungzz/hoppin:staging \
--push .
- name: Verify docker image contents
run: |
docker run --rm --entrypoint sh jinsungzz/hoppin:staging -c 'jar tf /app.jar | grep BOOT-INF/classes/application'
docker run --rm --entrypoint sh jinsungzz/hoppin:staging -c 'jar tf /app.jar | grep -E "AiController|AuthController|MeController"'
- name: Deploy to staging EC2
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.STAGING_HOST }}
username: ${{ secrets.STAGING_USER }}
key: ${{ secrets.STAGING_SSH_KEY }}
script: |
echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
cd /home/ubuntu/app
docker compose down
docker rm -f hoppin 2>/dev/null || true
docker image rm jinsungzz/hoppin:staging 2>/dev/null || true
docker system prune -af
docker network inspect hoppin-net >/dev/null 2>&1 || docker network create hoppin-net
docker pull jinsungzz/hoppin:staging
docker compose up -d --force-recreate
위 workflow는 자동 배포 파이프라인이다
develop 업데이트
→ jar 빌드
→ Docker 이미지 빌드
→ Docker Hub push
→ EC2 pull
→ Docker Compose 재실행