GitHub Actions로 간단한 CI/CD 구현

발행: (2025년 12월 2일 오후 04:01 GMT+9)
6 min read
원문: Dev.to

Source: Dev.to

Ian Taylor의 컨테이너 선박

Ian Taylor의 컨테이너 선박
Ian Taylor

작은 회사에서 IT가 종종 비용 센터로 인식되는 상황에서 소프트웨어 엔지니어로서 저는 배포 프로세스를 단순하게 유지하려고 합니다: 작은 Docker 이미지, 보안 단계, 그리고 가능하면 비용 제로.

최근 YouTube 영상에서 비공개 레포지토리도 복제될 수 있다는 사실을 보여주었고, 이는 .env, yml, 혹은 Docker Compose 파일에 있는 비밀 정보가 조심하지 않으면 노출될 수 있음을 의미합니다. 그래서 제 (단순한) CI/CD 설정을 다시 검토하게 되었습니다. 아래는 .NET 앱을 온‑프레미스 서버에 배포하기 위해 제가 사용하는 가벼운 워크플로우입니다. 참고용이며, 경로, 환경 변수, 서비스 이름 등을 여러분의 환경에 맞게 조정해야 할 수도 있습니다.

면책 조항

이 CI/CD 구성은 제 배포 환경을 기준으로 한 참고용입니다. 서버 설정은 다를 수 있으므로 디렉터리 경로, 환경 변수, 서비스 이름 등을 여러분의 환경에 맞게 조정해야 할 수도 있습니다.

로컬 개발 (매우 간단)

appsettings.json(또는 다른 설정 파일)에 필요한 구성을 추가하고 평소와 같이 애플리케이션을 로컬에서 실행합니다.

배포를 위한 Docker 준비

Docker 설정

로컬 및 서버 배포 모두를 위한 멀티스테이지 Dockerfile을 만듭니다. 아래 예시는 Alpine 기반의 자체 포함 .NET Web API 이미지를 빌드하고, DOTNET_SYSTEM_GLOBALIZATION_INVARIANTfalse로 설정(시간대 설정에 필요)하며, 비루트 사용자도 생성합니다.

FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS publish
WORKDIR /src

COPY your_project.csproj ./
RUN dotnet restore "./your_project.csproj" --runtime linux-musl-x64

COPY . .
RUN dotnet publish "your_project.csproj" \
    -c Release \
    -o /app/publish \
    --no-restore \
    --runtime linux-musl-x64 \
    --self-contained true \
    /p:PublishSingleFile=true

FROM mcr.microsoft.com/dotnet/runtime-deps:9.0-alpine AS final
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
RUN apk add --no-cache icu-libs
ENV LD_LIBRARY_PATH=/usr/lib
RUN apk upgrade musl

RUN adduser --disabled-password \
    --home /app \
    --gecos '' dotnetuser && chown -R dotnetuser /app

USER dotnetuser
WORKDIR /app
COPY --from=publish /app/publish .

ENTRYPOINT ["./your_project"]

로컬 개발을 위한 Docker Compose 파일을 만들고, 모든 환경 변수를 environment 섹션에 넣습니다.

version: '3.8'

services:
  project:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: project_name
    ports:
      - hardware_port1:container_port1
      - hardware_port2:container_port2
    environment:
      - ASPNETCORE_ENVIRONMENT=Production
      - ASPNETCORE_URLS=http://+:container_port1;http://+:container_port2
      - MYSQL_CONNECTION=server=localhost;port=3306;userid=root;password=your_password;database=your_database
    restart: always
    networks:
      - project_name
    extra_hosts:
      - "host.docker.internal:host-gateway"

networks:
  project_name:
    name: project_name_network
    driver: bridge

컴포즈를 실행합니다:

docker compose -f docker-compose.Development.yml up -d --build

동작한다면, 개발용 Docker Compose 파일과 appsettings.json.gitignore에 추가해 푸시되지 않도록 하고, appsettings.json.dockerignore에도 추가합니다(모든 환경 변수가 이미 compose 파일에 있기 때문).

GitHub Actions 로 서버에 배포

GitHub Action 설정

  1. 레포지토리에서 Settings → Security → Secrets and Variables → Actions → New repository secret 로 이동합니다.
  2. 로컬 Docker Compose 파일에 있는 각 환경 변수를 비밀로 추가하고, 이름은 그대로 유지합니다(예: ASPNETCORE_ENVIRONMENT).
  3. SSH 인증 정보를 비밀로 추가합니다: SSH_HOST, SSH_USERNAME, SSH_PRIVATE_KEYS.

서버 전용 Docker Compose 파일을 만들고 비밀을 참조하도록 합니다:

version: '3.8'

services:
  project:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: project_name
    ports:
      - hardware_port1:container_port1
      - hardware_port2:container_port2
    environment:
      - ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT}
      - ASPNETCORE_URLS=${ASPNETCORE_URLS}
      - MYSQL_CONNECTION=${MYSQL_CONNECTION}
    networks:
      - project_name
    extra_hosts:
      - "host.docker.internal:host-gateway"

networks:
  project_name:
    name: project_name_network
    driver: bridge

워크플로우 파일(예: .github/workflows/deploy.yml)을 추가해 이미지를 빌드하고, 이미지와 compose 파일을 서버에 전송한 뒤 배포를 수행합니다.

name: Build and Deploy

on:
  push:
    branches: [ master ]

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

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

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

      - name: Build Docker image
        run: |
          docker build -t your_project:latest .
          docker save your_project:latest -o your_project.tar

      - name: Create .env file from secrets
        run: |
          cat > .env << EOF
          ASPNETCORE_ENVIRONMENT=${{ secrets.ASPNETCORE_ENVIRONMENT }}
          ASPNETCORE_URLS=${{ secrets.ASPNETCORE_URLS }}
          MYSQL_CONNECTION=${{ secrets.MYSQL_CONNECTION }}
          EOF

      - name: Copy files to server
        uses: appleboy/scp-action@master
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USERNAME }}
          key: ${{ secrets.SSH_PRIVATE_KEYS }}
          source: "your_project.tar,docker-compose.yml,.env"
          target: "/tmp/deploy_your_project"

      - name: Deploy with Docker Compose
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USERNAME }}
          key: ${{ secrets.SSH_PRIVATE_KEYS }}
          script: |
            # Create app directory if it doesn't exist
            mkdir -p ~/apps/your_project

            # Move uploaded files into project directory
            mv /tmp/deploy_your_project/docker-compose.yml ~/apps/your_project/
            mv /tmp/deploy_your_project/.env ~/apps/your_project/

            # Load Docker image
            docker load -i /tmp/deploy_your_project/your_project.tar

            # (optional) Move tar file into project directory
            mv /tmp/deploy_your_project/your_project.tar ~/apps/your_project/

            # Navigate to the app directory and start the services
            cd ~/apps/your_project
            docker compose up -d --remove-orphans
Back to Blog

관련 글

더 보기 »

AWS와 Docker에서 Jenkins

Jenkins on AWS + Docker용 표지 이미지 https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-upload...