GitHub Actions로 간단한 CI/CD 구현
Source: Dev.to

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_INVARIANT를 false로 설정(시간대 설정에 필요)하며, 비루트 사용자도 생성합니다.
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 설정
- 레포지토리에서 Settings → Security → Secrets and Variables → Actions → New repository secret 로 이동합니다.
- 로컬 Docker Compose 파일에 있는 각 환경 변수를 비밀로 추가하고, 이름은 그대로 유지합니다(예:
ASPNETCORE_ENVIRONMENT). - 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