연습

백 개발 배포

atteri 2025. 8. 20. 00:19

앞서 만들어논 포테이너를 사용할 예정이다.

 

name: Build and Push Docker Image

on:
  push:
    branches:
      - dev

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

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

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

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

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=branch
            type=raw,value=dev


      - name: Build and push Docker image
        uses: docker/build-push-action@v5
        with:
          context: .
          file: ./Dockerfile
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          build-args: |
            DBHOST=${{ secrets.DBHOST }}
            DBPORT=${{ secrets.DBPORT }}
            DB=${{ secrets.DB }}
            USERNAME=${{ secrets.USERNAME }}
            DBPASSWORD=${{ secrets.PASSWORD }}
            JWTSECRET=${{ secrets.SECRET }}
            JWTISSUER=${{ secrets.ISSUER }}

docker-build.yml

 

FROM openjdk:17-jdk-slim
WORKDIR /app

ARG DBHOST
ARG DBPORT
ARG DB
ARG USERNAME
ARG DBPASSWORD
ARG JWTSECRET
ARG JWTISSUER

ENV DBHOST=${DBHOST}
ENV DBPORT=${DBPORT}
ENV DB=${DB}
ENV USERNAME=${USERNAME}
ENV DBPASSWORD=${DBPASSWORD}
ENV JWTSECRET=${JWTSECRET}
ENV JWTISSUER=${JWTISSUER}

COPY gradlew .
COPY gradle gradle
COPY build.gradle .
COPY settings.gradle .
COPY src src

RUN chmod +x ./gradlew
RUN ./gradlew clean bootJar

EXPOSE 8081
ENTRYPOINT ["java", "-jar", "build/libs/board-api.jar"]

Dockerfile

 

 

람다에서 한거랑 비슷하게 dev브런치면 실행하고 환경변수를 쓰는데 PASSWORD, SECRET같은거는 중복이 날수 있다고 경고 뜨더라

아무튼 개발은 태그 dev로 고정해놓고 GitHub Container Registry에 저장된다. 하고보니까 code페이지에 packages에서 확인하더라 아무튼 이미지 생성된건 확인했고 로컬에 풀받아서 실행해보니 실행도 확인됬다.

 포테이너 웹훅 할라그랫는데 무료가 안되나보다 깃 훅으로 되려나

 

 

---

 

 

portainer api가 있어서 그걸로 진행

 

포테이너 로그인하고 우측상단 계정에 my account에서

토큰생성 토큰은 만든직후 아니면 다시 확인 안되는거같다 잘 저장하자

git secrets에

변수 추가하고

portainer_env_no은 

 

portainer url/api/endpoints 치면 나오는 id값인데  

이거 리스트라더라 원하는 환경 id입력하자

 

      - name: Get existing container ID
        id: get_container
        run: |
          CONTAINER_NAME="${{ github.event.repository.name }}"
          echo "Looking for container: $CONTAINER_NAME"
          
          # API 호출 및 JSON 파싱
          RESPONSE=$(curl -s "${{ secrets.PORTAINERURL }}/api/endpoints/${{ secrets.PORTAINER_ENV_NO }}/docker/containers/json?all=true" \
          -H "X-API-KEY: ${{ secrets.PORTAINERTOKEN }}")
          
          # JSON 유효성 검사
          if echo "$RESPONSE" | jq empty 2>/dev/null; then
          echo "✅ Valid JSON response"
          
          # 컨테이너 ID 추출 (안전한 방법)
          CONTAINER_ID=$(echo "$RESPONSE" | jq -r '
          .[] |
          select(.Names[]? | test("^/'$CONTAINER_NAME'$")) |
          .Id // empty
          ' 2>/dev/null)
          
          # 결과 확인
          if [ -z "$CONTAINER_ID" ] || [ "$CONTAINER_ID" = "null" ]; then
          echo "No existing container found with name: $CONTAINER_NAME"
          CONTAINER_ID=""
          else
          echo "Found container ID: $CONTAINER_ID"
          fi
          else
          echo "❌ Invalid JSON or API error"
          echo "Response: $RESPONSE"
          CONTAINER_ID=""
          fi
          
          echo "container_id=$CONTAINER_ID" >> $GITHUB_OUTPUT
          echo "container_name=$CONTAINER_NAME" >> $GITHUB_OUTPUT

      - name: Delete existing container
        if: steps.get_container.outputs.container_id != '' && steps.get_container.outputs.container_id != 'null'
        run: |
          CONTAINER_ID="${{ steps.get_container.outputs.container_id }}"
          echo "Deleting container ID: $CONTAINER_ID"

          # 안전한 curl 명령어
          RESPONSE=$(curl -s -w "HTTPSTATUS:%{http_code}" \
            -X DELETE "${{ secrets.PORTAINERURL }}/api/endpoints/${{ secrets.PORTAINER_ENV_NO }}/docker/containers/$CONTAINER_ID?force=true" \
            -H "X-API-KEY: ${{ secrets.PORTAINERTOKEN }}")

          HTTP_STATUS=$(echo $RESPONSE | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
          BODY=$(echo $RESPONSE | sed -e 's/HTTPSTATUS:.*//g')

          echo "HTTP Status: $HTTP_STATUS"
          echo "Response: $BODY"

          if [ $HTTP_STATUS -eq 204 ] || [ $HTTP_STATUS -eq 200 ]; then
            echo "✅ Container deleted successfully"
          else
            echo "❌ Failed to delete container (Status: $HTTP_STATUS)"
            # 컨테이너가 이미 없을 수도 있으니 계속 진행
          fi

      - name: Pull latest image (with error handling)
        run: |
          echo "Pulling latest image..."
          REGISTRY_ENCODED=$(echo "${{ env.REGISTRY }}" | sed 's|/|%2F|g')
          IMAGE_ENCODED=$(echo "${{ env.IMAGE_NAME }}" | sed 's|/|%2F|g')
          
          curl -s -X POST "${{ secrets.PORTAINERURL }}/api/endpoints/${{ secrets.PORTAINER_ENV_NO }}/docker/images/create?fromImage=${REGISTRY_ENCODED}%2F${IMAGE_ENCODED}&tag=dev" \
          -H "X-API-KEY: ${{ secrets.PORTAINERTOKEN }}" \
          -H "Referer: ${{ secrets.PORTAINERURL }}"

          
        

      - name: Create new container
        run: |
          echo "Creating new container with image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev"
          curl -s -X POST "${{ secrets.PORTAINERURL }}/api/endpoints/${{ secrets.PORTAINER_ENV_NO }}/docker/containers/create?name=${{ steps.get_container.outputs.container_name }}" \
            -H "X-API-KEY: ${{ secrets.PORTAINERTOKEN }}" \
            -H "Content-Type: application/json" \
            -d "{
              \"Image\": \"${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev\",
              \"HostConfig\": {
                \"NetworkMode\": \"local\",
                \"PortBindings\": {
                  \"8081/tcp\": [{\"HostPort\": \"8081\"}]
                },
                \"RestartPolicy\": {
                  \"Name\": \"unless-stopped\"
                }
              }
            }"

      - name: Start container
        run: |
          echo "Starting container..."
          curl -s -X POST "${{ secrets.PORTAINERURL }}/api/endpoints/${{ secrets.PORTAINER_ENV_NO }}/docker/containers/${{ steps.get_container.outputs.container_name }}/start" \
            -H "X-API-KEY: ${{ secrets.PORTAINERTOKEN }}"
          echo "✅ Deployment completed!"

docker-build.yml

에 내용 추가

 

많은 에러가 있었으나 대부분 오타들로 인한거였고

 

이외에는

 

- name: Pull latest image (with error handling)
  run: |
    echo "Pulling latest image..."
    REGISTRY_ENCODED=$(echo "${{ env.REGISTRY }}" | sed 's|/|%2F|g')
    IMAGE_ENCODED=$(echo "${{ env.IMAGE_NAME }}" | sed 's|/|%2F|g')
    
    curl -s -X POST "${{ secrets.PORTAINERURL }}/api/endpoints/${{ secrets.PORTAINER_ENV_NO }}/docker/images/create?fromImage=${REGISTRY_ENCODED}%2F${IMAGE_ENCODED}&tag=dev" \
    -H "X-API-KEY: ${{ secrets.PORTAINERTOKEN }}" \
    -H "Referer: ${{ secrets.PORTAINERURL }}"

이게 원래 이미지 이름을 json으로 데이터로 보냈는데 계속 실패했는데 파라미터로 담아서 보내니 바로 성공했다

 

 

컨테이너가 잘 생성됬고

바로바로 proxy도 등록하고앞서 만들어논 포테이너를 사용할 예정이다.

 

 

 

name: Build and Push Docker Image

 

on:

  push:

    branches:

      - dev

 

env:

  REGISTRY: ghcr.io

  IMAGE_NAME: ${{ github.repository }}

 

jobs:

  build-and-push:

    runs-on: ubuntu-latest

    permissions:

      contents: read

      packages: write

 

    steps:

      - name: Checkout repository

        uses: actions/checkout@v4

 

      - name: Log in to Container Registry

        uses: docker/login-action@v3

        with:

          registry: ${{ env.REGISTRY }}

          username: ${{ github.actor }}

          password: ${{ secrets.GITHUB_TOKEN }}

 

      - name: Extract metadata

        id: meta

        uses: docker/metadata-action@v5

        with:

          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

          tags: |

            type=ref,event=branch

            type=raw,value=dev

 

 

      - name: Build and push Docker image

        uses: docker/build-push-action@v5

        with:

          context: .

          file: ./Dockerfile

          push: true

          tags: ${{ steps.meta.outputs.tags }}

          labels: ${{ steps.meta.outputs.labels }}

          build-args: |

            DBHOST=${{ secrets.DBHOST }}

            DBPORT=${{ secrets.DBPORT }}

            DB=${{ secrets.DB }}

            USERNAME=${{ secrets.USERNAME }}

            DBPASSWORD=${{ secrets.PASSWORD }}

            JWTSECRET=${{ secrets.SECRET }}

            JWTISSUER=${{ secrets.ISSUER }}

docker-build.yml

 

 

 

FROM openjdk:17-jdk-slim

WORKDIR /app

 

ARG DBHOST

ARG DBPORT

ARG DB

ARG USERNAME

ARG DBPASSWORD

ARG JWTSECRET

ARG JWTISSUER

 

ENV DBHOST=${DBHOST}

ENV DBPORT=${DBPORT}

ENV DB=${DB}

ENV USERNAME=${USERNAME}

ENV DBPASSWORD=${DBPASSWORD}

ENV JWTSECRET=${JWTSECRET}

ENV JWTISSUER=${JWTISSUER}

 

COPY gradlew .

COPY gradle gradle

COPY build.gradle .

COPY settings.gradle .

COPY src src

 

RUN chmod +x ./gradlew

RUN ./gradlew clean bootJar

 

EXPOSE 8081

ENTRYPOINT ["java", "-jar", "build/libs/board-api.jar"]

Dockerfile

 

 

 

 

 

람다에서 한거랑 비슷하게 dev브런치면 실행하고 환경변수를 쓰는데 PASSWORD, SECRET같은거는 중복이 날수 있다고 경고 뜨더라

 

아무튼 개발은 태그 dev로 고정해놓고 GitHub Container Registry에 저장된다. 하고보니까 code페이지에 packages에서 확인하더라 아무튼 이미지 생성된건 확인했고 로컬에 풀받아서 실행해보니 실행도 확인됬다.

 

 포테이너 웹훅 할라그랫는데 무료가 안되나보다 깃 훅으로 되려나

 

 

 

 

 

---

 

 

 

 

 

portainer api가 있어서 그걸로 진행

 

 

 

포테이너 로그인하고 우측상단 계정에 my account에서

 

토큰생성 토큰은 만든직후 아니면 다시 확인 안되는거같다 잘 저장하자

 

git secrets에

 

변수 추가하고

 

portainer_env_no은 

 

 

 

portainer url/api/endpoints 치면 나오는 id값인데  

 

이거 리스트라더라 원하는 환경 id입력하자

 

 

 

      - name: Get existing container ID

        id: get_container

        run: |

          CONTAINER_NAME="${{ github.event.repository.name }}"

          echo "Looking for container: $CONTAINER_NAME"

          

          # API 호출 및 JSON 파싱

          RESPONSE=$(curl -s "${{ secrets.PORTAINERURL }}/api/endpoints/${{ secrets.PORTAINER_ENV_NO }}/docker/containers/json?all=true" \

          -H "X-API-KEY: ${{ secrets.PORTAINERTOKEN }}")

          

          # JSON 유효성 검사

          if echo "$RESPONSE" | jq empty 2>/dev/null; then

          echo "✅ Valid JSON response"

          

          # 컨테이너 ID 추출 (안전한 방법)

          CONTAINER_ID=$(echo "$RESPONSE" | jq -r '

          .[] |

          select(.Names[]? | test("^/'$CONTAINER_NAME'$")) |

          .Id // empty

          ' 2>/dev/null)

          

          # 결과 확인

          if [ -z "$CONTAINER_ID" ] || [ "$CONTAINER_ID" = "null" ]; then

          echo "No existing container found with name: $CONTAINER_NAME"

          CONTAINER_ID=""

          else

          echo "Found container ID: $CONTAINER_ID"

          fi

          else

          echo "❌ Invalid JSON or API error"

          echo "Response: $RESPONSE"

          CONTAINER_ID=""

          fi

          

          echo "container_id=$CONTAINER_ID" >> $GITHUB_OUTPUT

          echo "container_name=$CONTAINER_NAME" >> $GITHUB_OUTPUT

 

      - name: Delete existing container

        if: steps.get_container.outputs.container_id != '' && steps.get_container.outputs.container_id != 'null'

        run: |

          CONTAINER_ID="${{ steps.get_container.outputs.container_id }}"

          echo "Deleting container ID: $CONTAINER_ID"

 

          # 안전한 curl 명령어

          RESPONSE=$(curl -s -w "HTTPSTATUS:%{http_code}" \

            -X DELETE "${{ secrets.PORTAINERURL }}/api/endpoints/${{ secrets.PORTAINER_ENV_NO }}/docker/containers/$CONTAINER_ID?force=true" \

            -H "X-API-KEY: ${{ secrets.PORTAINERTOKEN }}")

 

          HTTP_STATUS=$(echo $RESPONSE | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')

          BODY=$(echo $RESPONSE | sed -e 's/HTTPSTATUS:.*//g')

 

          echo "HTTP Status: $HTTP_STATUS"

          echo "Response: $BODY"

 

          if [ $HTTP_STATUS -eq 204 ] || [ $HTTP_STATUS -eq 200 ]; then

            echo "✅ Container deleted successfully"

          else

            echo "❌ Failed to delete container (Status: $HTTP_STATUS)"

            # 컨테이너가 이미 없을 수도 있으니 계속 진행

          fi

 

      - name: Pull latest image (with error handling)

        run: |

          echo "Pulling latest image..."

          REGISTRY_ENCODED=$(echo "${{ env.REGISTRY }}" | sed 's|/|%2F|g')

          IMAGE_ENCODED=$(echo "${{ env.IMAGE_NAME }}" | sed 's|/|%2F|g')

          

          curl -s -X POST "${{ secrets.PORTAINERURL }}/api/endpoints/${{ secrets.PORTAINER_ENV_NO }}/docker/images/create?fromImage=${REGISTRY_ENCODED}%2F${IMAGE_ENCODED}&tag=dev" \

          -H "X-API-KEY: ${{ secrets.PORTAINERTOKEN }}" \

          -H "Referer: ${{ secrets.PORTAINERURL }}"

 

          

        

 

      - name: Create new container

        run: |

          echo "Creating new container with image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev"

          curl -s -X POST "${{ secrets.PORTAINERURL }}/api/endpoints/${{ secrets.PORTAINER_ENV_NO }}/docker/containers/create?name=${{ steps.get_container.outputs.container_name }}" \

            -H "X-API-KEY: ${{ secrets.PORTAINERTOKEN }}" \

            -H "Content-Type: application/json" \

            -d "{

              \"Image\": \"${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev\",

              \"HostConfig\": {

                \"NetworkMode\": \"local\",

                \"PortBindings\": {

                  \"8081/tcp\": [{\"HostPort\": \"8081\"}]

                },

                \"RestartPolicy\": {

                  \"Name\": \"unless-stopped\"

                }

              }

            }"

 

      - name: Start container

        run: |

          echo "Starting container..."

          curl -s -X POST "${{ secrets.PORTAINERURL }}/api/endpoints/${{ secrets.PORTAINER_ENV_NO }}/docker/containers/${{ steps.get_container.outputs.container_name }}/start" \

            -H "X-API-KEY: ${{ secrets.PORTAINERTOKEN }}"

          echo "✅ Deployment completed!"

docker-build.yml

 

에 내용 추가

 

 

 

많은 에러가 있었으나 대부분 오타들로 인한거였고

 

 

 

이외에는

 

 

 

- name: Pull latest image (with error handling)

  run: |

    echo "Pulling latest image..."

    REGISTRY_ENCODED=$(echo "${{ env.REGISTRY }}" | sed 's|/|%2F|g')

    IMAGE_ENCODED=$(echo "${{ env.IMAGE_NAME }}" | sed 's|/|%2F|g')

    

    curl -s -X POST "${{ secrets.PORTAINERURL }}/api/endpoints/${{ secrets.PORTAINER_ENV_NO }}/docker/images/create?fromImage=${REGISTRY_ENCODED}%2F${IMAGE_ENCODED}&tag=dev" \

    -H "X-API-KEY: ${{ secrets.PORTAINERTOKEN }}" \

    -H "Referer: ${{ secrets.PORTAINERURL }}"

이게 원래 이미지 이름을 json으로 데이터로 보냈는데 계속 실패했는데 파라미터로 담아서 보내니 바로 성공했다

 

 

 

 

 

컨테이너가 잘 생성됬고

 

바로바로 proxy도 등록하고 route53에서 레코드도 등록

 

잘된다.

 

하다보니 web쪽에 Axios.interceptor에서 갱신시 응답이 잘 안오는걸 확인해서 수정했다.

export const init = async () => {
        //console.log("===> Axios.interceptor::init()")
        axios.interceptors.request.use((config) => {

            const accessToken = getAccessToken();

            if (accessToken) {
                config.headers['Authorization'] = `${accessToken}`;
            }
            config.headers['Channel'] = 'WEB';

            config.headers['X-Requested-With'] = 'XMLHttpRequest';
            config.headers['Access-Control-Allow-Credentials'] = true;
            config.headers['Access-Control-Allow-Origin'] = '*';
            config.headers['Access-Control-Allow-Methods'] = 'GET, PUT, POST, DELETE, OPTIONS';
            config.headers['Cache-Control'] = 'no-cache';
            config.headers['Pragma'] = 'no-cache';

            console.log('[ Axios.interceptor ] :: processAwait ' + processAwait);

            return config;
        }, err => {
            console.error(err);
            Promise.reject(err);
        });

        axios.interceptors.response.use(
            response => {
                //console.log('[ Axios.interceptor ] :: response OK');
                return response;
            },async err => {
                const config = err.config;          // 기존에 수행하려고 했던 작업
                const response = err.response;

                console.log(`[ Axios.interceptor ] :: response [${config.method}] ${config.url}`);

                if( response.status === 401)
                {
                    console.log('[ Axios.interceptor ] :: ERR :: [401]');

                    if( response.data.object === -1001) {

                        if (processAwait == null) {
                            processAwait = 'temp'

                            let refreshTokenParams = null;
                            let config = response.config;
                            let headers = config.headers;

                            headers['Authorization'] = `${getRefreshToken()}`;
                            refreshTokenParams = {refreshToken: getRefreshToken()};

                            try {
                                const authBaseURL = import.meta.env.VITE_AUTH_URL;
                                processAwait = axios.create({
                                    baseURL: authBaseURL,
                                    headers
                                }).post("/refresh", refreshTokenParams);

                                const result = await processAwait;
                                const {status, data} = result;

                                if (status == 200) {
                                    console.log('[ Axios.interceptor ] :: ERR :: [/refresh] : GET NEW TOKEN');

                                    // 토큰 세팅
                                    const newAccessToken = data.token;
                                    const newRefreshToken = data.refreshToken;

                                    setAccessToken(newAccessToken);
                                    setRefreshToken(newRefreshToken);

                                    console.log('[ Axios.interceptor ] :: ERR :: [/refresh] : 엑세스 토큰 갱신');

                                    config.headers.Authorization = `Bearer ${getAccessToken()}`;

                                    processAwait = null;

                                    return axios(config);

                                } else {
                                    //토큰재발급 애러 :  토큰 삭제 후 로그인 페이지
                                    processAwait = null;
                                    removeAuthTokens()
                                    location.href = '/login';
                                    return Promise.reject(result);
                                }
                            } catch (e) {
                                //토큰재발급 애러 :  토큰 삭제 후 로그인 페이지
                                removeAuthTokens()
                                location.href = '/login';
                                return Promise.reject(e)
                            }
                        } else {
                            await processAwait

                            let config = response.config;
                            let headers = config.headers;

                            config.headers['Authorization'] = getAccessToken();
                            const retryAxios = axios.create({headers});

                            if (config.method === 'get') {
                                return retryAxios.get(config.url)
                            } else if (config.method === 'post') {
                                return retryAxios.post(config.url, config.data);
                            } else if (config.method === 'patch') {
                                return retryAxios.patch(config.url, config.data);
                            } else if (config.method === 'put') {
                                return retryAxios.put(config.url, config.data);
                            } else if (config.method === 'delete') {
                                return retryAxios.delete(config.url, {data: config.data});
                            }
                        }
                    }
                    else if(response.data.object === -1002)
                    {
                        console.log('[ Axios.interceptor ] :: ERR :: [-1002] : WRONG TOKEN -> go login');
                        removeAuthTokens();
                        location.href = '/';
                    }
                    else
                    {
                        console.log('else')
                        return Promise.resolve(response);
                    }

                }
                else if(response.status === 500)
                {
                    return Promise.resolve(response);
                }

            });
}
await processAwait.then(({status, data})

써가지고 갱신응답이 전달이 안되더라

 

프론트도 배포하자

 

FROM node:22-alpine AS builder

WORKDIR /app

ARG VITE_SERVER_URL
ARG VITE_AUTH_URL
ARG VITE_APP_ENV
ARG VITE_APP_COOKIE_DOMAIN

ENV VITE_SERVER_URL=$VITE_SERVER_URL
ENV VITE_AUTH_URL=$VITE_AUTH_URL
ENV VITE_APP_ENV=$VITE_APP_ENV
ENV VITE_APP_COOKIE_DOMAIN=${VITE_APP_COOKIE_DOMAIN}

# 패키지 파일 복사 및 설치
COPY package.json package-lock.json ./
RUN npm ci

# 소스코드 복사 및 빌드
COPY . .
RUN npm run build

# Production stage
FROM nginx:alpine

# 빌드된 파일을 nginx로 복사
COPY --from=builder /app/dist /usr/share/nginx/html

# Nginx 설정 파일 복사
COPY nginx.conf /etc/nginx/conf.d/default.conf

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

Dockerfile

 

name: Build and Push Docker Image

on:
  push:
    branches:
      - dev

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

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

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

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

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=branch
            type=raw,value=latest

      - name: Build and push Docker image
        uses: docker/build-push-action@v5
        with:
          context: .
          file: ./Dockerfile
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          build-args: |
            VITE_SERVER_URL=${{ secrets.VITE_SERVER_URL }}
            VITE_AUTH_URL=${{ secrets.VITE_AUTH_URL }}
            VITE_APP_ENV=${{ secrets.VITE_APP_ENV }}
            VITE_APP_COOKIE_DOMAIN=board-web.dev.atteri.co.kr

      - name: Get existing container ID
        id: get_container
        run: |
          CONTAINER_NAME="${{ github.event.repository.name }}"
          echo "Looking for container: $CONTAINER_NAME"
          
          # API 호출 및 JSON 파싱
          RESPONSE=$(curl -s "${{ secrets.PORTAINERURL }}/api/endpoints/${{ secrets.PORTAINER_ENV_NO }}/docker/containers/json?all=true" \
          -H "X-API-KEY: ${{ secrets.PORTAINERTOKEN }}")
          
          # JSON 유효성 검사
          if echo "$RESPONSE" | jq empty 2>/dev/null; then
          echo "✅ Valid JSON response"
          
          # 컨테이너 ID 추출 (안전한 방법)
          CONTAINER_ID=$(echo "$RESPONSE" | jq -r '
          .[] |
          select(.Names[]? | test("^/'$CONTAINER_NAME'$")) |
          .Id // empty
          ' 2>/dev/null)
          
          # 결과 확인
          if [ -z "$CONTAINER_ID" ] || [ "$CONTAINER_ID" = "null" ]; then
          echo "No existing container found with name: $CONTAINER_NAME"
          CONTAINER_ID=""
          else
          echo "Found container ID: $CONTAINER_ID"
          fi
          else
          echo "❌ Invalid JSON or API error"
          echo "Response: $RESPONSE"
          CONTAINER_ID=""
          fi
          
          echo "container_id=$CONTAINER_ID" >> $GITHUB_OUTPUT
          echo "container_name=$CONTAINER_NAME" >> $GITHUB_OUTPUT

      - name: Delete existing container
        if: steps.get_container.outputs.container_id != '' && steps.get_container.outputs.container_id != 'null'
        run: |
          CONTAINER_ID="${{ steps.get_container.outputs.container_id }}"
          echo "Deleting container ID: $CONTAINER_ID"

          # 안전한 curl 명령어
          RESPONSE=$(curl -s -w "HTTPSTATUS:%{http_code}" \
            -X DELETE "${{ secrets.PORTAINERURL }}/api/endpoints/${{ secrets.PORTAINER_ENV_NO }}/docker/containers/$CONTAINER_ID?force=true" \
            -H "X-API-KEY: ${{ secrets.PORTAINERTOKEN }}")

          HTTP_STATUS=$(echo $RESPONSE | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
          BODY=$(echo $RESPONSE | sed -e 's/HTTPSTATUS:.*//g')

          echo "HTTP Status: $HTTP_STATUS"
          echo "Response: $BODY"

          if [ $HTTP_STATUS -eq 204 ] || [ $HTTP_STATUS -eq 200 ]; then
            echo "✅ Container deleted successfully"
          else
            echo "❌ Failed to delete container (Status: $HTTP_STATUS)"
            # 컨테이너가 이미 없을 수도 있으니 계속 진행
          fi

      - name: Pull latest image (with error handling)
        run: |
          echo "Pulling latest image..."
          REGISTRY_ENCODED=$(echo "${{ env.REGISTRY }}" | sed 's|/|%2F|g')
          IMAGE_ENCODED=$(echo "${{ env.IMAGE_NAME }}" | sed 's|/|%2F|g')
          
          curl -s -X POST "${{ secrets.PORTAINERURL }}/api/endpoints/${{ secrets.PORTAINER_ENV_NO }}/docker/images/create?fromImage=${REGISTRY_ENCODED}%2F${IMAGE_ENCODED}&tag=latest" \
          -H "X-API-KEY: ${{ secrets.PORTAINERTOKEN }}" \
          -H "Referer: ${{ secrets.PORTAINERURL }}"

      - name: Create new container
        run: |
          echo "Creating new container with image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest"
          curl -s -X POST "${{ secrets.PORTAINERURL }}/api/endpoints/${{ secrets.PORTAINER_ENV_NO }}/docker/containers/create?name=${{ steps.get_container.outputs.container_name }}" \
            -H "X-API-KEY: ${{ secrets.PORTAINERTOKEN }}" \
            -H "Content-Type: application/json" \
            -d "{
              \"Image\": \"${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest\",
              \"HostConfig\": {
                \"NetworkMode\": \"local\",
                \"PortBindings\": {
                  \"80/tcp\": [{\"HostPort\": \"3000\"}]
                },
                \"RestartPolicy\": {
                  \"Name\": \"unless-stopped\"
                }
              },
              \"Env\": [
                \"NODE_ENV=development\",
                \"CHOKIDAR_USEPOLLING=true\"
              ]
            }"

      - name: Start container
        run: |
          echo "Starting container..."
          curl -s -X POST "${{ secrets.PORTAINERURL }}/api/endpoints/${{ secrets.PORTAINER_ENV_NO }}/docker/containers/${{ steps.get_container.outputs.container_name }}/start" \
            -H "X-API-KEY: ${{ secrets.PORTAINERTOKEN }}"
          echo "✅ Deployment completed!"

깃액션

 

server {
    listen 80;
    server_name localhost;
    root /usr/share/nginx/html;
    index index.html;

    # SPA 라우팅을 위한 설정
    location / {
        try_files $uri $uri/ /index.html;
    }

    # 정적 파일 캐싱
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

nginx.config

 

 

nginx proxy에서 80설정해놔서 외부접근시에는 3000으로 하고

당연히 프록시에도 3000으로 해야되는줄 알았는데 안되서 찾아보니

같은 네트워크 네부라서 80으로 바로 연결되서 3000이 아닌 80으로 해야되더라

 

 

  • React 앱은 nginx로 빌드되어 컨테이너 내부에서 80번 포트로 실행
  • 호스트 포트 매핑으로 외부에서는 3000번으로 접근
  • 프록시는 내부 네트워크에서 80번으로 직접 접근

일단 이렇게 내부 공유용 개발배포를 진행했다.

운영의 경우 Amazon Elastic Beanstalk로 해보려고한다. 

 

'연습' 카테고리의 다른 글

Elastic Beanstalk & CloudFront  (5) 2025.08.22
백+프론트 연결  (0) 2025.08.19
api서버 & 람다 추가  (1) 2025.08.19
프론트  (0) 2025.08.16
람다 3  (3) 2025.08.15