Bootcamp/Fundamentals

GHCR

개발자 오리 2026. 8. 17. 23:24

💡 오늘 학습 키워드

  • GHCR (GitHub Container Registry, GitHub 컨테이너 레지스트리)
  • Container Registry (컨테이너 이미지 저장소)
  • Docker Image (Docker 이미지)
  • Tag (이미지 버전 식별자)
  • GITHUB_TOKEN (GitHub Actions 인증 토큰)
  • Package Permission (패키지 접근 권한)
  • Image Digest (이미지 불변 식별자)
  • CI/CD (지속적 통합 및 지속적 배포)

 

 

🎯 학습 내용 정리

Docker를 사용하여 애플리케이션을 컨테이너로 실행하려면 먼저 Docker Image를 만들어야 한다. 하지만 애플리케이션을 개발하고 배포하는 과정에서 매번 서버에서 소스 코드를 내려받아 직접 이미지를 빌드하는 방식은 관리해야 할 작업이 많아진다.

 

특히 MSA 환경에서는 여러 개의 서비스를 각각 빌드하고 배포해야 한다. 서비스마다 Docker Image를 생성하고 서버에 전달하는 과정까지 직접 관리한다면 배포 과정이 복잡해지고, 어떤 버전의 이미지가 실제 운영 서버에서 실행되고 있는지도 추적하기 어려워진다.

이 문제를 해결하는 방법 중 하나가 Container Registry를 사용하는 것이다.

 

Container Registry는 Docker와 OCI(Open Container Initiative) 형식의 컨테이너 이미지를 저장하고 관리하는 저장소이다. GitHub는 GitHub Packages의 Container Registry를 제공하며, GitHub.com에서는 ghcr.io를 통해 접근한다. GHCR은 Docker Image뿐만 아니라 OCI 이미지도 저장할 수 있다.

 

중요한 것은 단순히 GHCR에 이미지를 저장하는 것이 아니다.

소스 코드
  ↓
Docker Image Build
  ↓
GHCR Push
  ↓
배포 서버에서 Image Pull
  ↓
Container 실행

이라는 흐름을 이해하는 것이 중요하다.

 

이 구조를 사용하면 CI 환경에서 이미지를 빌드하고 Registry에 저장한 뒤, 배포 서버는 검증된 이미지를 가져와 실행하는 방식으로 역할을 분리할 수 있다.

 

즉, GHCR을 이해한다는 것은 단순히 ghcr.io라는 주소를 이해하는 것이 아니라 Docker Image를 어떻게 저장하고, 누가 접근할 수 있으며, 어떤 버전을 배포할 것인지까지 관리하는 방법을 이해하는 것에 가깝다.

 

 

1. GHCR은 무엇인가

GHCRGitHub Container Registry의 주소 체계로, GitHub Packages에서 컨테이너 이미지를 저장하고 관리하기 위한 Registry이다.

 

일반적인 Docker Registry와 마찬가지로 Docker Image를 Push하고 Pull할 수 있지만 GitHub Repository와 GitHub Actions를 자연스럽게 연결할 수 있다는 점이 중요한 특징이다.

 

이미지는 일반적으로 다음과 같은 형식으로 관리한다.

ghcr.io/NAMESPACE/IMAGE_NAME:TAG

 

예를 들어 다음과 같이 사용할 수 있다.

ghcr.io/example-org/user-service:latest
ghcr.io/example-org/user-service:1.0.0
ghcr.io/example-org/user-service:a1b2c3d

 

각 요소는 다음과 같은 의미를 가진다.

  • ghcr.io : GitHub Container Registry
  • example-org : GitHub 사용자 또는 Organization
  • user-service : Container Image 이름
  • latest, 1.0.0, a1b2c3d : Image Tag

 

GitHub 공식 문서에서도 GHCR의 이미지 주소는 ghcr.io/NAMESPACE/IMAGE_NAME:TAG 형태로 설명하고 있다.

여기서 중요한 점은 Repository와 Image가 완전히 동일한 개념은 아니라는 것이다.

 

예를 들어 다음과 같은 Repository가 있다고 가정한다.

github.com/example-org/delivery-project

 

여기에서 여러 서비스를 관리한다면 GHCR에는 다음과 같이 여러 이미지를 저장할 수 있다.

ghcr.io/example-org/delivery-project/user-service
ghcr.io/example-org/delivery-project/order-service
ghcr.io/example-org/delivery-project/delivery-service

따라서 MSA에서는 하나의 Repository에서 여러 서비스를 관리하면서 서비스별 Container Image를 각각 Registry에 저장하는 구조를 만들 수 있다.

 

 

2. Docker Image를 직접 서버에서 빌드하는 방식의 문제

Container Registry의 필요성을 이해하려면 먼저 Registry 없이 배포하는 과정을 생각해볼 필요가 있다.

예를 들어 EC2에서 직접 애플리케이션을 빌드한다고 가정한다.

GitHub Repository
        ↓
EC2에서 Git Pull
        ↓
Gradle Build
        ↓
Docker Build
        ↓
Container 실행

 

간단한 프로젝트에서는 충분히 사용할 수 있는 구조이다.

하지만 MSA 환경에서는 문제가 커진다.

user-service
order-service
company-service
delivery-service
hub-service
slack-service
...

 

각 서비스마다 소스 코드를 가져와 빌드하고 Docker Image를 생성해야 한다.

또한 운영 서버가 직접 빌드 작업을 수행하기 때문에 애플리케이션 빌드와 서비스 실행이라는 서로 다른 책임이 하나의 서버에 집중된다.

반면 Container Registry를 사용하면 다음과 같이 역할을 분리할 수 있다.

GitHub Actions
      │
      ├─ Gradle Build
      │
      ├─ Docker Image Build
      │
      └─ Docker Image Push
              │
              ▼
            GHCR
              │
              │ Docker Pull
              ▼
             EC2
              │
              ▼
          Container 실행

 

이 구조에서 GitHub Actions는 이미지를 만드는 역할을 담당하고 EC2는 이미지를 실행하는 역할을 담당한다.

따라서 EC2에서 소스 코드를 다시 빌드할 필요가 없다.

 

이러한 역할 분리는 CI/CD 환경에서 상당히 중요하다.

CI에서는 코드가 정상적으로 빌드되고 테스트되는지 검증하고, CD에서는 이미 검증된 산출물을 배포하는 구조를 만들 수 있기 때문이다.

 

 

3. GHCR에 Docker Image를 Push하는 과정

GHCR을 실제로 사용하려면 먼저 Docker Image를 생성해야 한다.

예를 들어 다음과 같은 Dockerfile이 있다고 가정한다.

FROM eclipse-temurin:21-jre

WORKDIR /app

COPY build/libs/app.jar app.jar

ENTRYPOINT ["java", "-jar", "app.jar"]

 

Gradle Build를 수행하면 다음과 같이 JAR 파일이 생성된다.

build/libs/app.jar

 

이 파일을 Docker Image에 포함시켜 이미지를 생성한다.

docker build \
  -t ghcr.io/example-org/user-service:latest \
  .

 

여기서 -t는 생성할 이미지의 이름과 태그를 지정한다.

이미지가 생성되면 Registry에 Push할 수 있다.

docker push ghcr.io/example-org/user-service:latest

 

결국 두 명령어의 역할은 다르다.

docker build
    ↓
로컬 환경에 Docker Image 생성

docker push
    ↓
Registry에 Docker Image 업로드

 

따라서 Docker Image가 생성되었다고 해서 자동으로 GHCR에 저장되는 것은 아니다.

Build와 Push는 별개의 작업이다.

 

이 차이를 이해하지 못하면 "Docker Image가 만들어졌는데 GHCR에는 왜 없는가?"와 같은 문제를 해결하기 어렵다.

 

 

4. GitHub Actions에서는 GITHUB_TOKEN을 사용할 수 있다

CI/CD 환경에서는 개발자가 직접 로컬에서 docker login을 수행하는 것이 아니라 GitHub Actions가 GHCR에 로그인해야 한다.

GitHub Actions에서는 Workflow에 제공되는 GITHUB_TOKEN을 사용할 수 있다.

 

GHCR에 Image를 Push하려면 Workflow에 적절한 Package 권한이 필요하다.

permissions:
  contents: read
  packages: write

 

그리고 Docker Registry에 로그인한다.

- name: Login to GHCR
  uses: docker/login-action@v3
  with:
    registry: ghcr.io
    username: ${{ github.actor }}
    password: ${{ secrets.GITHUB_TOKEN }}

 

이후 Image를 Build하고 Push한다.

- name: Build Docker Image
  run: |
    docker build \
      -t ghcr.io/${{ github.repository_owner }}/user-service:${{ github.sha }} \
      ./user-service

- name: Push Docker Image
  run: |
    docker push \
      ghcr.io/${{ github.repository_owner }}/user-service:${{ github.sha }}

 

여기서 중요한 부분은 다음 두 가지이다.

첫 번째는 packages: write이다.

GHCR에 이미지를 업로드하려면 Workflow의 GITHUB_TOKEN에 Package 쓰기 권한이 필요하다. GitHub의 공식 Docker Image 배포 예제에서도 contents: read와 packages: write 권한을 설정한다.

 

두 번째는 GITHUB_TOKEN이 모든 환경에서 사용할 수 있는 일반적인 Docker Registry 비밀번호라는 의미가 아니라는 점이다.

GitHub Actions에서 실행되는 Workflow가 해당 Repository와 연결된 Package를 관리할 때 사용하는 인증 수단이다. 다른 Private Repository의 Package에 접근하는 경우에는 Package에 Repository 접근 권한을 부여하거나 별도의 인증 수단이 필요할 수 있다.

 

 

5. latest 태그만 사용하는 것이 위험한 이유

Docker Image에는 Tag를 지정할 수 있다.

대표적으로 다음과 같은 방식이 있다.

user-service:latest
user-service:1.0.0
user-service:a1b2c3d

 

처음에는 latest만 사용하는 것이 편해 보인다.

image: ghcr.io/example-org/user-service:latest

 

새로운 이미지를 Push할 때마다 latest가 새로운 이미지를 가리키도록 만들 수 있기 때문이다.

 

하지만 운영 환경에서는 이것이 추적성을 떨어뜨릴 수 있다.

예를 들어 다음과 같은 상황을 생각할 수 있다.

10:00
user-service:latest → Image A

11:00
새로운 배포
user-service:latest → Image B

 

운영 서버에서 latest만 보고 있다면 현재 실행 중인 이미지가 정확히 어떤 Commit에서 만들어졌는지 즉시 파악하기 어렵다.

 

따라서 CI/CD에서는 Commit SHA를 Tag로 사용하는 방법이 유용하다.

ghcr.io/example-org/user-service:a1b2c3d

 

GitHub Actions에서는 다음과 같이 사용할 수 있다.

docker build \
  -t ghcr.io/${{ github.repository_owner }}/user-service:${{ github.sha }} \
  ./user-service

docker push \
  ghcr.io/${{ github.repository_owner }}/user-service:${{ github.sha }}

이렇게 하면 특정 이미지가 어떤 Commit에서 생성되었는지 추적하기 쉬워진다.

 

예를 들어 배포 기록이 다음과 같이 남을 수 있다.

Commit
a1b2c3d
  ↓
Docker Image
ghcr.io/example-org/user-service:a1b2c3d
  ↓
EC2
user-service Container

따라서 문제가 발생했을 때 "어떤 소스 코드가 실제 운영 환경에서 실행되고 있는가?"라는 질문에 답하기 쉬워진다.

latest가 반드시 잘못된 것은 아니다. 개발 환경이나 단순한 배포 환경에서는 사용할 수 있다.

 

다만 운영 환경에서는 Commit SHA, Release Version, Image Digest 등 더 명확한 식별자를 함께 사용하는 것이 배포 추적과 롤백에 유리하다.

 

 

6. Tag와 Digest는 무엇이 다른가

Tag보다 더 엄격한 이미지 식별이 필요한 경우 Image Digest를 사용할 수 있다.

예를 들어 다음과 같은 이미지가 있다고 가정한다.

ghcr.io/example-org/user-service:1.0.0

Tag는 사람이 관리하는 이름이다.

 

반면 Digest는 이미지의 콘텐츠를 기반으로 생성되는 식별자이다.

ghcr.io/example-org/user-service@sha256:abcdef...

Tag는 같은 이름이 새로운 Image를 가리키도록 변경될 수 있다.

 

예를 들어

user-service:latest
  ↓
Image A

였다가 새로운 Push 이후

user-service:latest
  ↓
Image B

가 될 수 있다.

 

반면 Digest를 사용하면 특정 이미지 자체를 정확하게 지정할 수 있다.

user-service@sha256:abcdef...

따라서 재현 가능한 배포가 특히 중요한 환경에서는 Digest가 유용하다.

 

GitHub 역시 특정 이미지를 항상 동일하게 가져오려면 Digest를 사용하여 Pull할 수 있다고 설명한다.

실무에서는 다음과 같은 단계적인 전략을 사용할 수 있다.

개발 환경
→ latest

일반적인 CI/CD
→ Commit SHA

재현 가능한 운영 배포가 중요한 환경
→ Digest

중요한 것은 모든 환경에서 무조건 하나의 방식을 사용하는 것이 아니라 배포 추적성과 운영 복잡성 사이에서 적절한 수준을 선택하는 것이다.

 

 

7. Private GHCR Image를 EC2에서 Pull하려면 인증이 필요하다

GHCR에 이미지를 Push했다고 해서 EC2가 바로 Private Image를 Pull할 수 있는 것은 아니다.

Private Package는 접근 권한이 필요하다.

 

예를 들어 GHCR에 다음 이미지가 있다고 가정한다.

ghcr.io/example-org/user-service:a1b2c3d

 

EC2에서 다음 명령을 실행하면 된다.

docker pull ghcr.io/example-org/user-service:a1b2c3d

 

하지만 해당 Package가 Private이라면 Docker Registry 인증이 필요하다.

echo "$CR_PAT" | docker login ghcr.io \
  -u USERNAME \
  --password-stdin

 

GitHub 공식 문서에서는 Private Container Registry에 접근할 때 적절한 인증 토큰을 사용하도록 안내한다. Package를 내려받는 경우 Personal Access Token (classic)에 read:packages 권한을 사용할 수 있다.

 

여기서 중요한 것은 CI와 CD에서 사용하는 인증 주체가 다를 수 있다는 점이다.

GitHub Actions
    │
    │ GITHUB_TOKEN
    ▼
   GHCR
    ▲
    │
    │ read:packages 인증
    │
   EC2

GitHub Actions에서는 Repository에 연결된 Package를 관리하기 위해 GITHUB_TOKEN을 사용할 수 있지만, EC2는 GitHub Actions Runner가 아니기 때문에 동일한 방식으로 GITHUB_TOKEN을 사용할 수 없다.

 

따라서 EC2가 Private GHCR Image를 Pull해야 한다면 EC2가 사용할 별도의 인증 정보를 안전하게 관리해야 한다.

특히 Token을 Docker Compose 파일이나 Git Repository에 직접 작성하는 방식은 피해야 한다.

# 잘못된 예
environment:
  GHCR_TOKEN: ghp_xxxxxxxxx

Token은 외부에 노출되지 않도록 Secret 또는 서버의 안전한 환경 변수 등을 통해 관리해야 한다.

 

 

8. Package 권한과 Repository 권한은 같은 것이 아니다

GHCR을 사용하면서 자주 혼동하는 부분이 Repository와 Package의 권한이다.

GitHub Container Registry는 Package에 대해 Repository와 연결된 권한을 사용할 수도 있고, 별도의 세부 권한을 설정할 수도 있다.

 

따라서 다음 두 가지를 구분해야 한다.

GitHub Repository
   ≠
GHCR Package

예를 들어 Repository가 Private이라고 해서 모든 Package 접근 방식이 단순하게 동일하다고 가정하면 안 된다.

Container Registry는 Package 자체에 접근 권한을 부여할 수 있으며, Workflow가 Package를 관리하도록 Repository에 Actions 접근 권한을 부여하는 것도 가능하다.

 

또한 Public Container Package는 다른 GitHub Packages Registry와 달리 인증 없이 익명 Pull이 가능하다. 반면 Private Package는 인증이 필요하다.

 

따라서 배포 문제가 발생했을 때 단순히

"GHCR 로그인이 안 된다."

라고 판단하기보다 다음을 순서대로 확인하는 것이 좋다.

  1. Package가 존재하는가?
  2. Package가 Private인가 Public인가?
  3. Pull을 수행하는 주체가 누구인가?
  4. 해당 주체에 Read 권한이 있는가?
  5. Docker Registry에 정상적으로 Login 되었는가?
  6. 요청한 Image Tag가 실제로 존재하는가?

이 과정을 통해 인증 문제와 이미지 존재 문제를 구분할 수 있다.

 

 

9. GHCR을 사용하는 CI/CD의 전체 흐름

앞의 내용을 하나로 연결하면 다음과 같은 구조가 만들어진다.

Developer
    │
    │ Git Push
    ▼
GitHub Repository
    │
    ▼
GitHub Actions
    │
    ├─ Source Checkout
    │
    ├─ Build
    │
    ├─ Test
    │
    ├─ Docker Image Build
    │
    ├─ GHCR Login
    │
    └─ Docker Image Push
             │
             ▼
            GHCR
             │
             │ Pull
             ▼
             EC2
             │
             ├─ Docker Login
             ├─ Docker Pull
             └─ Container Run

이 구조에서 중요한 것은 소스 코드와 실행 산출물을 분리한다는 점이다.

 

GitHub Repository에는 소스 코드가 저장된다.

Java
Gradle
Dockerfile
application code

 

GHCR에는 빌드된 실행 산출물인 Container Image가 저장된다.

user-service image
order-service image
delivery-service image

EC2는 소스 코드를 빌드하지 않고 GHCR에서 필요한 Image를 가져와 실행한다.

 

이 구조를 사용하면 배포 서버가 개발 환경과 동일한 방식으로 애플리케이션을 다시 빌드할 필요가 없다.

CI에서 생성한 Image 자체를 배포 대상으로 사용할 수 있기 때문에 "테스트했던 코드와 실제 배포된 코드가 같은가?"라는 문제를 보다 명확하게 관리할 수 있다.

 

 

10. MSA에서는 GHCR의 가치가 더 커진다

단일 애플리케이션에서는 하나의 Image만 관리하면 되지만 MSA에서는 서비스 수만큼 Image가 필요하다.

예를 들어 다음과 같은 서비스가 있다고 가정한다.

user-service
company-service
delivery-service
hub-service
order-service
slack-service

 

각 서비스는 독립적인 Docker Image가 된다.

ghcr.io/example-org/user-service:a1b2c3d
ghcr.io/example-org/company-service:b2c3d4e
ghcr.io/example-org/delivery-service:c3d4e5f
ghcr.io/example-org/hub-service:d4e5f6a
ghcr.io/example-org/order-service:e5f6a7b
ghcr.io/example-org/slack-service:f6a7b8c

이 구조에서는 서비스별로 독립적인 배포도 가능하다.

 

예를 들어 order-service만 변경되었다면 전체 서비스를 다시 빌드하고 배포하는 대신 다음 Image만 새롭게 생성할 수 있다.

order-service
     │
     ▼
Docker Build
     │
     ▼
GHCR Push
     │
     ▼
EC2 Pull
     │
     ▼
order-service Container 교체

이것이 MSA에서 Container Registry를 사용하는 중요한 이유 중 하나이다.

서비스별로 독립적인 빌드 산출물을 관리할 수 있기 때문이다.

 

다만 서비스가 많아질수록 Image와 Tag도 많아진다.

따라서 다음과 같은 운영 정책이 필요해진다.

  • Image 이름 규칙
  • Tag 전략
  • Private/Public 정책
  • Package 접근 권한
  • 오래된 Image 정리 정책
  • 배포 버전 추적 방법
  • Rollback 방법

GHCR을 도입하는 것 자체보다 Image를 어떻게 관리할 것인가를 함께 설계하는 것이 중요하다.

 

 

11. 실제 배포에서는 Image Tag보다 배포 전략이 중요하다

예를 들어 다음과 같은 Compose 설정을 사용할 수 있다.

services:
  user-service:
    image: ghcr.io/example-org/user-service:a1b2c3d
    restart: unless-stopped

  order-service:
    image: ghcr.io/example-org/order-service:e5f6a7b
    restart: unless-stopped

 

이 구조에서는 Compose 파일이 실행해야 할 정확한 Image를 선언한다.

새로운 버전을 배포한다면 다음과 같이 변경할 수 있다.

services:
  user-service:
    image: ghcr.io/example-org/user-service:b2c3d4e

 

그리고 EC2에서 새로운 Image를 Pull한 후 Container를 재생성한다.

docker compose pull
docker compose up -d

 

이때 중요한 것은 Docker Compose가 build:를 사용하는 방식과 image:를 사용하는 방식의 차이이다.

# 서버에서 Build
services:
  user-service:
    build:
      context: ./user-service

이 방식에서는 서버가 직접 Image를 빌드한다.

 

반면

# Registry에서 Image Pull
services:
  user-service:
    image: ghcr.io/example-org/user-service:a1b2c3d

이 방식에서는 이미 만들어진 Image를 가져온다.

 

CI/CD에서 GHCR을 사용하는 핵심 목적은 보통 두 번째 구조와 연결된다.

CI
→ Build
→ Test
→ Docker Image 생성
→ GHCR Push

CD
→ GHCR Pull
→ Container 실행

따라서 Build 책임과 Deploy 책임을 명확하게 분리할 수 있다.

 

 

12. GHCR을 사용할 때 고려해야 할 운영 포인트

GHCR을 실제 운영 환경에 적용하면 단순한 Image 저장소 이상의 문제가 발생한다.

 

첫 번째는 인증 정보 관리이다.

Private Image를 Pull하기 위한 Token을 Repository에 Commit하면 안 된다.

 

두 번째는 Tag 관리이다.

latest 하나만 사용하는 것보다 Commit SHA나 Release Version을 활용하면 배포 버전을 추적하기 쉬워진다.

 

세 번째는 Image 정리이다.

CI/CD가 실행될 때마다 새로운 Image Tag가 생성된다면 GHCR에 Image가 계속 쌓일 수 있다.

따라서 실제 운영 환경에서는 오래된 Image와 불필요한 Tag를 어떻게 관리할 것인지 정책을 정해야 한다.

 

네 번째는 Rollback이다.

배포에 문제가 발생했을 때 이전 Image로 돌아갈 수 있어야 한다.

예를 들어 다음과 같은 Image가 존재한다고 가정한다.

user-service:a1b2c3d
user-service:b2c3d4e
user-service:c3d4e5f

 

현재 배포 버전이 c3d4e5f이고 장애가 발생했다면 이전 버전인 b2c3d4e를 다시 배포할 수 있다.

이처럼 Image를 단순히 "서버에서 실행할 파일"로 보는 것보다 배포 가능한 불변 산출물(artifact)로 바라보는 것이 중요하다.

 

결국 GHCR의 핵심 가치는 Docker Image를 저장하는 것에만 있지 않다.

Source Code
    ↓
검증
    ↓
Immutable Artifact
    ↓
Registry
    ↓
Deployment

라는 배포 흐름을 만들고, 어떤 버전의 애플리케이션을 어디에 배포했는지 추적할 수 있게 만드는 데 의미가 있다.

 

 

📚 한줄 정리

GHCR은 단순한 Docker Image 저장소가 아니라 CI에서 검증된 Container Image를 표준화된 산출물로 저장하고, CD에서 동일한 이미지를 배포하도록 만들어 빌드와 배포를 분리하고 버전 추적과 롤백을 가능하게 하는 Container Registry이다.

'Bootcamp > Fundamentals' 카테고리의 다른 글

Secure Coding  (0) 2026.08.19
모니터링 시스템  (0) 2026.08.18
좋은 API 설계란? - 2편  (1) 2026.08.04
좋은 API 설계란? - 1편  (1) 2026.08.03
Spring AI Fundamentals  (0) 2026.07.31