Step 4~5: 이미지 저장소 + Docker Compose

이 문서는 인프라 파이프라인 실습의 세 번째 파일이다. 이전 단계: 02-ci-and-docker.md | 다음 단계: 04-secrets-and-networking.md


용어 안내

이 문서에서 “슬롯”은 학습용 비유다. 실무 용어가 아니다. 각 슬롯 옆에 실무에서 실제로 쓰는 용어를 병기한다.


Step 4. 이미지 저장소 — 슬롯 ③ Artifact Storage (실무: 컨테이너 레지스트리)

왜 필요한가? (WHY)

Step 3에서 Docker 이미지를 빌드했다. 하지만 그 이미지는 내 PC에만 있다.

문제:
- 팀원이 같은 이미지를 쓰려면? → 각자 빌드해야 한다 (시간 낭비, 버전 불일치)
- 서버에 배포하려면? → 서버에서도 빌드해야 한다 (배포가 느려진다)
- 어제 빌드한 이미지를 다시 쓰려면? → 코드가 바뀌었으면 동일한 이미지 재현 불가

해결: 빌드한 이미지를 중앙 저장소에 올려두고, 누구든 꺼내 쓸 수 있게 한다.

용어 설명

Artifact (아티팩트)

  • 영어 뜻: “인공물, 산출물” — 사람이 만들어낸 결과물
  • IT 뜻: 빌드 과정을 거쳐 나온 완성품 (실행 가능한 파일, Docker 이미지 등)
  • 비유: 밀가루(소스 코드) → 반죽·발효·굽기(빌드) → 빵(Artifact)

Registry (레지스트리)

  • 영어 뜻: “등록소, 기록 보관소” — 물건을 등록하고 보관하는 곳
  • IT 뜻: Docker 이미지를 업로드·다운로드할 수 있는 중앙 저장소
  • 비유: 빵을 진열해두는 빵집 진열대. 누구든 와서 원하는 빵을 가져갈 수 있다

4-1. GitHub Container Registry 토큰 발급

GitHub에서 이미지를 저장하려면 인증 토큰이 필요하다.

GitHub.com 접속
→ 오른쪽 상단 프로필 클릭
→ Settings
→ 왼쪽 메뉴 맨 아래 Developer settings
→ Personal access tokens → Tokens (classic)
→ Generate new token (classic)
→ Note: "ghcr-push" (용도 메모)
→ Expiration: 30 days
→ 체크: write:packages, read:packages
→ Generate token → 토큰 복사 (다시 볼 수 없으므로 반드시 메모)

4-2. 로컬에서 이미지 push

# 1. ghcr.io에 로그인 (USERNAME을 본인 GitHub 아이디로 교체)
docker login ghcr.io -u USERNAME --password YOUR_TOKEN

docker login ghcr.io -u sunuk0119 --password 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

# 2. 기존 이미지에 "ghcr.io/내아이디/이미지이름:태그" 형태로 태그 부여
#    태그 = 이미지에 붙이는 이름표. 같은 이미지에 여러 이름을 붙일 수 있다
docker tag my-infra-lab ghcr.io/USERNAME/my-infra-lab:latest


# 3. 태그가 붙은 이미지를 레지스트리에 업로드
docker push ghcr.io/USERNAME/my-infra-lab:latest

확인 방법: GitHub.com → 본인 프로필 → Packages 탭 → my-infra-lab 이미지가 보이면 성공

4-3. GitHub Actions에서 자동 push

CI가 테스트를 통과하면 자동으로 이미지를 레지스트리에 올리는 워크플로우를 만든다.

사전 작업: GitHub repo → Settings → Secrets and variables → Actions → New repository secret

  • Name: GHCR_TOKEN / Value: 위에서 발급한 토큰 붙여넣기

.github/workflows/ci.yml에 아래 스텝을 추가한다:

    # === 이미지 빌드 + 레지스트리 push (테스트 통과 후 실행) ===
    - name: Log in to GitHub Container Registry
      # ghcr.io에 로그인. secrets.GHCR_TOKEN은 위에서 등록한 비밀값
      run: echo "${{ secrets.GHCR_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin

    - name: Build Docker image
      # 현재 디렉토리(.)의 Dockerfile로 이미지 빌드. -t로 이름 지정
      run: docker build -t ghcr.io/${{ github.actor }}/my-infra-lab:latest .

    - name: Push to GHCR
      # 빌드된 이미지를 레지스트리에 업로드
      run: docker push ghcr.io/${{ github.actor }}/my-infra-lab:latest

push 후 GitHub → Actions 탭에서 워크플로우 실행 결과를 확인한다.


Step 5. Docker Compose — 슬롯 ⑤ Orchestration 로컬 버전 (실무: 컨테이너 오케스트레이션)

왜 필요한가? (WHY)

지금까지 Express 앱 하나만 컨테이너로 띄웠다. 하지만 실제 서비스는 이렇다:

실제 서비스 = 앱 + 데이터베이스 + 캐시 + 웹서버 + ...
                ↑       ↑          ↑       ↑
            각각이 별도 컨테이너로 돌아간다

컨테이너를 하나씩 docker run으로 띄우면:

  • 매번 긴 명령어를 입력해야 한다
  • 컨테이너 간 네트워크 연결을 수동으로 설정해야 한다
  • 시작 순서를 사람이 관리해야 한다

Docker Compose는 이것을 한 파일로 정의하고 한 명령어로 해결한다.

용어 설명

Orchestration (오케스트레이션)

  • 영어 뜻: “오케스트라 지휘” — 여러 악기를 조화롭게 연주시키는 것
  • IT 뜻: 여러 컨테이너의 시작·중지·연결·확장을 자동으로 관리하는 것
  • 비유: 지휘자 없이 30명이 제각각 연주하면 소음. 지휘자(Compose)가 “바이올린 먼저, 플루트 다음” 하고 조율해야 음악이 된다
  • 실무: 로컬 개발에서는 Docker Compose, 프로덕션에서는 Kubernetes를 쓴다

5-1. app.js에 Redis 연동 추가

Redis(캐시 서버)를 연결해서 컨테이너 간 통신을 체험한다.

# ioredis = Node.js에서 Redis에 접속하기 위한 라이브러리
npm install ioredis

app.js에 아래 코드를 추가한다:

// Redis 클라이언트 라이브러리 불러오기
const Redis = require('ioredis');

// Redis 서버에 접속. host는 docker-compose에서 정의한 서비스 이름
// Docker Compose는 서비스 이름을 자동으로 DNS로 등록해준다
const redis = new Redis({
  host: process.env.REDIS_HOST || 'redis',  // 환경변수 또는 기본값 'redis'
  port: 6379                                 // Redis 기본 포트
});

// GET /count → Redis에 저장된 숫자를 1 증가시키고 반환
app.get('/count', async (req, res) => {
  // INCR: Redis 명령어. 키의 값을 1 올리고 결과를 돌려준다
  const count = await redis.incr('visit_count');
  // 결과를 JSON으로 응답
  res.json({ count: Number(count) });
});

5-2. docker-compose.yml 작성

프로젝트 루트에 docker-compose.yml을 만든다:

# Docker Compose 파일 — 여러 컨테이너를 한 번에 정의
# 이 파일 하나로 "어떤 컨테이너가 필요한지"를 모두 선언한다

services:
  # === 서비스 1: 우리가 만든 Express 앱 ===
  app:
    build: .                    # 현재 디렉토리의 Dockerfile로 이미지 빌드
    ports:
      - "3000:3000"             # 호스트 3000번 포트 → 컨테이너 3000번 포트 연결
    depends_on:
      - redis                   # redis 서비스가 먼저 시작된 후에 app이 시작됨
    environment:
      - REDIS_HOST=redis        # app 컨테이너 안에서 redis라는 이름으로 접속 가능
      - NODE_ENV=production     # Node.js 환경을 production으로 설정
    env_file:
      - .env                    # 추가 환경변수를 .env 파일에서 읽어온다

  # === 서비스 2: Redis (인메모리 캐시/데이터 저장소) ===
  redis:
    image: redis:7-alpine       # Docker Hub에서 공식 Redis 이미지를 가져옴 (alpine = 경량)
    volumes:
      - redis-data:/data        # 컨테이너가 삭제되어도 데이터가 유지되도록 볼륨 마운트

# === 볼륨 정의 ===
# Docker가 관리하는 저장 공간. 컨테이너를 삭제해도 데이터가 남는다
volumes:
  redis-data:                   # 위의 redis 서비스에서 참조하는 볼륨 이름

.env 파일도 만든다 (아직 비밀값이 없어도 구조를 미리 잡아둔다):

# .env — 환경변수 파일 (Step 6에서 자세히 다룬다)
PORT=3000

5-3. 실행하고 확인

# 1. 모든 서비스를 백그라운드(-d)로 시작
#    build + 네트워크 생성 + 컨테이너 시작을 한 번에 수행
docker compose up -d

# 2. 헬스체크 — 앱이 정상적으로 뜨는지 확인
curl http://localhost:3000/health

# 3. 카운트 엔드포인트 — 여러 번 호출해보자
curl http://localhost:3000/count   # → {"count":1}
curl http://localhost:3000/count   # → {"count":2}
curl http://localhost:3000/count   # → {"count":3}
# 숫자가 증가한다 = 앱 컨테이너가 Redis 컨테이너와 통신하고 있다는 증거

# 4. 앱 컨테이너의 로그 확인
docker compose logs app

# 5. 모든 서비스 중지 + 컨테이너 삭제
docker compose down

체크리스트

이 단계를 마쳤으면 아래를 모두 확인한다:

  • ghcr.io에 로그인할 수 있다
  • 로컬에서 docker push로 이미지를 레지스트리에 올렸다
  • GitHub Packages에서 내 이미지를 확인했다
  • GitHub Actions에서 자동으로 이미지가 push되는 워크플로우를 추가했다
  • docker-compose.yml에 app과 redis 두 서비스를 정의했다
  • docker compose up -d로 두 컨테이너가 함께 뜬다
  • /count를 여러 번 호출하면 숫자가 증가한다 (Redis 통신 확인)
  • docker compose down으로 정리할 수 있다

다음 단계

04-secrets-and-networking.md — 환경변수 관리와 Nginx 리버스 프록시를 설정한다.

Comments

  • // 댓글을 불러오는 중...
main ⚠ 0 ✕ 0 Ln 1, Col 1 Spaces: 2 UTF-8 LF Markdown