개요 
express.js로 토이 프로젝트를 진행하던 중 cicd를 구현하려는데 마땅한 글을 찾지 못해 직접 구현한 후 정리하게 되었다.
준비물 
1. ssh 접속이 가능하고 docker, docker-compose가 정상 설치된 서버
2. docker hub 계정
3. 프로젝트 repositoryGithub Repository Secrets 등록 
repository -> Settings -> Secrets and Variables -> Actions
1. SERVER_IP - 서버 ip
2. SERVER_USER - SSH 사용자명
3. SSH_PRIVATE_KEY - 서버 SSH 접속 시 사용하는 private key
4. DOCKER_USERNAME - docker hub 유저명
5. DOCKER_PASSWORD - docker hub 비밀번호
6. PROJECT_NAME - 프로젝트 명(소문자와 '-'으로만 구성 Ex. korea-quote)프로젝트 디렉토리 구조 
project
├── .github/workflows
├── docker-compose.yml
├── Dockerfile
├── package.json
└── server.js서버 authorized_keys 설정 
cd ~.ssh
vi authorized_keys ## 서버 SSH 접속 시 사용하는 public key 복붙 후 저장
chmod 600 authorized_keysGithub Action workflow 등록 
name: CI/CD Pipeline
# 파이프라인이 실행될 트리거를 설정
on:
  push:
    branches:
      - master # master 브랜치에 push 이벤트 발생 시 실행
  pull_request:
    branches:
      - master # master 브랜치에 대한 pull request 이벤트 발생 시 실행
jobs:
  build:
    runs-on: ubuntu-latest # 빌드 작업을 실행할 환경 설정 (최신 우분투)
    steps:
    - name: Checkout repository # GitHub repository 체크아웃 
      uses: actions/checkout@v4   
    - name: Set up Docker Buildx # Docker Buildx 설정
      uses: docker/setup-buildx-action@v3
    - name: Login to DockerHub # DockerHub 로그인
      uses: docker/login-action@v3
      with:
        username: ${{ secrets.DOCKER_USERNAME }} # GitHub Secrets DockerHub 사용자명 사용
        password: ${{ secrets.DOCKER_PASSWORD }} # GitHub Secrets DockerHub 비밀번호 사용
    - name: Build Docker image # Docker 이미지 빌드
      run: |
        docker build . -t ${{ secrets.DOCKER_USERNAME }}/${{ secrets.PROJECT_NAME }}:${{ github.sha }} # Docker 이미지 빌드 및 태그
        docker tag ${{ secrets.DOCKER_USERNAME }}/${{ secrets.PROJECT_NAME }}:${{ github.sha }} ${{ secrets.DOCKER_USERNAME }}/${{ secrets.PROJECT_NAME }}:latest # latest 태그 추가
    - name: Push Docker image to Docker Hub # Docker hub에 이미지 push
      run: |
        docker push ${{ secrets.DOCKER_USERNAME }}/${{ secrets.PROJECT_NAME }}:${{ github.sha }} # 빌드된 이미지 push
        docker push ${{ secrets.DOCKER_USERNAME }}/${{ secrets.PROJECT_NAME }}:latest # latest 태그 이미지 push
  deploy:
    runs-on: ubuntu-latest # 배포 작업을 실행할 환경 설정 (최신 우분투)
    needs: build # build 작업이 성공적으로 완료된 후에만 실행
    steps:
    - name: Checkout repository # GitHub repository 체크아웃
      uses: actions/checkout@v4
    - name: Deploy to server # 서버에 SSH로 접속하여 배포 작업 수행
      uses: appleboy/ssh-action@v1.0.3
      with:
        host: ${{ secrets.SERVER_IP }} # GitHub Secrets 서버 IP 사용
        username: ${{ secrets.SERVER_USER }} # GitHub Secrets 서버 사용자명 사용
        key: ${{ secrets.SSH_PRIVATE_KEY }} # GitHub Secrets SSH 개인 키 사용
        port: 22 # SSH 접속 포트 (기본: 22)
        script: |
          sudo docker pull ${{ secrets.DOCKER_USERNAME }}/${{ secrets.PROJECT_NAME }}:${{ github.sha }} # 새로운 Docker 이미지 pull
          sudo docker stop ${{ secrets.PROJECT_NAME }} || true # 기존 Docker 컨테이너 중지 (오류 무시)
          sudo docker rm ${{ secrets.PROJECT_NAME }} || true # 기존 Docker 컨테이너 삭제 (오류 무시)
          sudo docker run -d -p 3000:3000 --name ${{ secrets.PROJECT_NAME }} ${{ secrets.DOCKER_USERNAME }}/${{ secrets.PROJECT_NAME }}:${{ github.sha }} # 새로운 Docker 컨테이너 실행docker-compose.yml 
version: '3.8'
services:
  web:
    build: .
    ports:
      - "3000:3000"
    volumes:
      - .:/usr/src/app
      - /usr/src/app/node_modules
    environment:
      - NODE_ENV=developmentDockerfile 
# 베이스 이미지 설정
FROM node:20
# 작업 디렉토리 설정
WORKDIR /app
# 패키지 설치를 위해 package.json 및 package-lock.json 복사
COPY package*.json ./
# 패키지 설치
RUN npm install
# 어플리케이션 코드 복사
COPY . .
# 어플리케이션이 실행될 포트 설정
EXPOSE 3000
# 서버 실행
CMD ["node", "server.js"]server.js (예시) 
// server.js
const express = require('express');
const app = express();
const PORT = 3000;
app.get('/', (req, res) => {
  res.send('Hello World!');
});
app.listen(PORT, () => {
  console.log(`Server is running on http://localhost:${PORT}`);
});
마무리 
정상적으로 진행 되었다면 http://ip:3000으로  접속 시 Hello World!라는 문구를 볼 수 있을 것입니다 :)