Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
name: CD

on:
push:
branches:
- main

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
build-and-push:
name: Build & Push Docker Image
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

outputs:
image-tag: ${{ steps.meta.outputs.tags }}
image-digest: ${{ steps.push.outputs.digest }}

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,prefix=,format=short
type=raw,value=latest

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build and push
id: push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

deploy:
name: Deploy to Server
runs-on: ubuntu-latest
needs: build-and-push
environment: production

steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
envs: ENVFILE,GITHUB_TOKEN,ACTOR
script: |
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest"

echo "$GITHUB_TOKEN" | docker login ghcr.io -u "$ACTOR" --password-stdin

echo "$ENVFILE" > /home/${{ secrets.DEPLOY_USER }}/.aikon.env
chmod 600 /home/${{ secrets.DEPLOY_USER }}/.aikon.env

docker pull $IMAGE

docker stop aikon-app || true
docker rm aikon-app || true

docker run -d \
--name aikon-app \
--restart unless-stopped \
-p 8080:8080 \
--env-file /home/${{ secrets.DEPLOY_USER }}/.aikon.env \
$IMAGE

docker image prune -f
env:
ENVFILE: ${{ secrets.ENVFILE }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ACTOR: ${{ github.actor }}
41 changes: 40 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,36 @@ concurrency:
cancel-in-progress: true

jobs:
lint:
name: Lint (ktlint)
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Ensure Gradlew Executable
run: chmod +x ./gradlew

- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
cache-read-only: false
cache-cleanup: always

- name: Run ktlintCheck
run: ./gradlew ktlintCheck --parallel --build-cache

build-and-test:
name: Build & Test
runs-on: ubuntu-latest
needs: lint

steps:
- name: Checkout
Expand All @@ -35,4 +63,15 @@ jobs:
cache-cleanup: always

- name: Build
run: ./gradlew build --parallel --build-cache
run: ./gradlew build -x test --parallel --build-cache

- name: Test
run: ./gradlew test --parallel --build-cache

- name: Upload Test Report
if: always()
uses: actions/upload-artifact@v4
with:
name: test-report
path: build/reports/tests/test/
retention-days: 7
33 changes: 33 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# ---- Build Stage ----
FROM gradle:9.4.1-jdk21 AS builder

WORKDIR /app

COPY gradlew .

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

gradlew 파일이 복사될 때 실행 권한(+x)이 유실될 수 있습니다. 특히 Windows 환경에서 git 설정(core.filemode)에 따라 권한이 유지되지 않은 채 커밋된 경우, 빌드 단계에서 Permission denied 에러가 발생하며 빌드가 실패할 수 있습니다.\n\n안전한 빌드를 위해 gradlew 복사 후 실행 권한을 명시적으로 부여하는 것을 권장합니다.

COPY gradlew .
RUN chmod +x gradlew

COPY gradle gradle
COPY build.gradle.kts .
COPY settings.gradle.kts .

# 의존성만 먼저 캐싱
RUN ./gradlew dependencies --no-daemon --parallel -q || true

COPY src src

RUN ./gradlew bootJar --no-daemon --parallel -x test

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Spring Boot 2.5 버전 이상부터는 기본적으로 일반 jar(*-plain.jar)와 실행 가능한 bootJar가 함께 생성됩니다. 이로 인해 build/libs/ 디렉토리에 2개의 jar 파일이 존재하게 되며, 이후 런타임 스테이지에서 COPY --from=builder /app/build/libs/*.jar app.jar 수행 시 와일드카드 패턴에 여러 파일이 매칭되어 빌드가 실패하게 됩니다.\n\n이를 방지하기 위해 빌드 단계에서 plain jar를 제외한 실행 가능한 jar 파일을 app.jar라는 고정된 이름으로 복사해두는 과정을 추가하는 것이 좋습니다. 이렇게 하면 버전 변경 시에도 Dockerfile을 수정할 필요가 없어집니다.

RUN ./gradlew bootJar --no-daemon --parallel -x test && \
    cp build/libs/$(ls build/libs | grep -v plain) build/libs/app.jar


# ---- Runtime Stage ----
FROM eclipse-temurin:21-jre-alpine

WORKDIR /app

RUN addgroup -S aikon && adduser -S aikon -G aikon

COPY --from=builder /app/build/libs/*.jar app.jar

RUN chown aikon:aikon app.jar
Comment on lines +25 to +27

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

COPY 명령어의 --chown 옵션을 사용하면 별도의 RUN chown 레이어를 생성하지 않고 파일 복사와 권한 설정을 동시에 처리할 수 있어, 이미지 레이어 수를 줄이고 빌드 효율성을 높일 수 있습니다. 또한 앞서 제안드린 대로 빌드 단계에서 단일화된 app.jar를 복사하도록 변경합니다.

COPY --from=builder --chown=aikon:aikon /app/build/libs/app.jar app.jar


USER aikon

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "app.jar"]
Loading