프로젝트 루트 아래에 yml 파일 생성

.github/workflows/deploy.yml
name: Deploy to Server

on:
  workflow_dispatch:  # 수동 빌드

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest

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

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

      - name: Grant execute permission for gradlew
        run: chmod +x ./gradlew

      - name: Build with Gradle
        run: ./gradlew clean build -x test --parallel

      - name: Copy JAR to server
        uses: appleboy/scp-action@master
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SERVER_SSH_KEY }}
          source: "build/libs/excuse_dict-0.0.1-SNAPSHOT.jar"
          target: "/home/ubuntu/app/"
          strip_components: 2

      - name: Deploy on server
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SERVER_SSH_KEY }}
          script: |
            cd /home/ubuntu/app
            
            # Dockerfile 생성
            cat > Dockerfile << 'EOF'
            FROM openjdk:17
            WORKDIR /app
            COPY excuse_dict-0.0.1-SNAPSHOT.jar app.jar
            EXPOSE 8081
            ENV JAVA_OPTS="-Xms256m -Xmx512m -XX:MaxMetaspaceSize=128m -XX:+UseG1GC"
            ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar --spring.profiles.active=prod"]
            EOF
            
            # 컨테이너 재시작
            docker stop excuse-dict || true
            docker rm excuse-dict || true
            docker build -t excuse-dict:latest .
            
            docker run -d --name excuse-dict \\
              --network host \\
              --restart unless-stopped \\
              -e MYSQL_PASSWORD="${MYSQL_PASSWORD}" \\
              -e GMAIL_USERNAME="${GMAIL_USERNAME}" \\
              -e GMAIL_PASSWORD="${GMAIL_PASSWORD}" \\
              -e JWT_KEY="${JWT_KEY}" \\
              -e RECAPTCHA_SECRET_KEY="${RECAPTCHA_SECRET_KEY}" \\
              -e ADMIN_EMAILS="${ADMIN_EMAILS}" \\
              -e GEMINI_API_KEY="${GEMINI_API_KEY}" \\
              excuse-dict:latest
            
            sleep 10
            docker ps | grep excuse-dict

올리고 푸시하면 해당 레포지토리의 Actions 탭이 업데이트됨