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
// admin login