도커 기초 강좌 #5 레지스트리 — Docker Hub, GHCR, push/pull

7 분 소요

#3 까지 만든 이미지는 내 머신 안에서만 살아 있었습니다. 이번 글에서는 그 이미지를 다른 머신 — 동료, CI 서버, 운영 서버 — 가 쓸 수 있게 만드는 방법을 다룹니다. 그게 **레지스트리(registry)**입니다.

도커 기초 강좌 시리즈에서 이번 글의 위치:

레지스트리가 무엇인가 #

GitHub이 코드의 원격 저장소라면, 레지스트리는 이미지의 원격 저장소입니다. 비유 그대로입니다.

코드이미지
만들기git commitdocker build
올리기git pushdocker push
받기git pulldocker pull
호스트GitHub, GitLabDocker Hub, GHCR, ECR

레지스트리의 종류는 많지만, 처음 만나는 건 보통 두 군데입니다.

  • Docker Hub — 도커 공식 레지스트리입니다. python:3.14, nginx:1.27 같은 공식 이미지가 여기 있습니다. 무료 계정으로 퍼블릭 저장소를 만들 수 있습니다.
  • GitHub Container Registry (GHCR) — GitHub이 운영. GitHub 저장소와 권한을 공유하고, GitHub Actions와 잘 어울려서 요즘 자주 쓰입니다.

이 외에 클라우드 사업자들의 레지스트리(AWS ECR, Google Artifact Registry, Azure Container Registry) 가 있는데, 도커 입장에선 다 같은 OCI 규약을 따라서 로그인 명령만 다르고 push/pull 흐름은 동일 합니다.

이미지 이름의 구조 #

도커가 이미지 이름을 어떻게 해석하는지 한 번 풀어볼 가치가 있습니다. 자주 헷갈리는 부분이기 때문입니다.

이미지 이름의 풀 형태
[REGISTRY/]NAMESPACE/REPOSITORY[:TAG][@DIGEST]

예시:

이름풀어 보면
nginxdocker.io/library/nginx:latest
python:3.14-slimdocker.io/library/python:3.14-slim
myuser/myapp:1.0docker.io/myuser/myapp:1.0
ghcr.io/curtis/myapp:1.0GHCR의 curtis/myapp 1.0
ghcr.io/curtis/myapp@sha256:abc...태그 대신 다이제스트로 고정

도커는 레지스트리를 생략하면 **docker.io(= Docker Hub)**로 가정합니다. 네임스페이스를 생략하면 library (공식 이미지의 네임스페이스). 태그를 생략하면 latest. 그래서 nginx 한 글자가 사실은 docker.io/library/nginx:latest의 단축입니다.

latest의 함정 #

latest는 “가장 최근” 이라는 뜻이 아닙니다. 그냥 태그를 안 주면 기본이 되는 이름일 뿐입니다. 누가 1.27을 빌드하면서 latest를 함께 안 붙이면, latest는 옛날 이미지를 가리킬 수 있습니다. 그래서 운영에선 latest를 의존하지 말고 명시적인 버전 태그를 쓰는 게 정석입니다.

태그 전략은 이런 식으로 자주 잡습니다.

  • 시맨틱 버전: myapp:1.2.3
  • 메이저/마이너 별칭: myapp:1.2, myapp:1
  • 환경: myapp:staging, myapp:prod
  • Git 커밋 SHA: myapp:a1b2c3d
  • 항상 동시에: myapp:latest

CI가 한 빌드로 myapp:1.2.3, myapp:1.2, myapp:1, myapp:latest를 동시에 적용해 두면 사용처가 편합니다.

docker tag — 별명 붙이기 #

이미지를 빌드할 때 -t로 한 번 이름을 붙였지만, 같은 이미지에 새 별명을 더 붙일 수 있습니다.

기존 이미지에 새 태그
docker tag hello-docker myuser/hello-docker:1.0
docker tag hello-docker myuser/hello-docker:latest

docker tag는 데이터 복사를 하지 않고 포인터만 추가합니다. 그래서 즉시 끝납니다. docker images로 보면 같은 IMAGE ID에 이름이 여러 개 붙어 있을 것입니다.

레지스트리에 올리려면 보통 이름에 레지스트리 호스트 / 사용자 이름이 들어간 형태로 태그를 다시 붙입니다.

GHCR 용 태그
docker tag hello-docker ghcr.io/curtis/hello-docker:1.0

이래야 도커가 “어디로 보내야 할지” 압니다. 그냥 hello-docker 같은 짧은 이름으로는 푸시할 수 없습니다.

Docker Hub로 푸시 #

Docker Hub에 계정을 만들고 (hub.docker.com), 터미널에서 로그인합니다.

Docker Hub 로그인
docker login
# Username: myuser
# Password: ******
# Login Succeeded

Personal Access Token 권장: Docker Hub는 비밀번호 대신 PAT(개인 액세스 토큰) 사용을 권장합니다. 계정 설정 → Security → New Access Token으로 만들고, 비밀번호 칸에 PAT를 넣으세요. CI 에서도 PAT가 정석입니다.

로그인 정보는 ~/.docker/config.json에 저장됩니다. macOS/Windows의 Docker Desktop은 OS 키체인에 저장해 더 안전하게 둬요.

이제 푸시:

태그 + 푸시
docker tag hello-docker myuser/hello-docker:1.0
docker push myuser/hello-docker:1.0
# The push refers to repository [docker.io/myuser/hello-docker]
# 5e7d4abc...: Pushed
# 1.0: digest: sha256:abcdef... size: 1234

푸시는 레이어 단위로 이뤄집니다. 이미 레지스트리에 있는 레이어(예: python:3.14-slim의 베이스 레이어들) 는 다시 안 올라가고, 변한 레이어만 올라갑니다. 두 번째 푸시부터는 매우 빠른 이유가 이겁니다.

다른 머신에서 받기:

다른 머신에서 pull
docker pull myuser/hello-docker:1.0
docker run --rm myuser/hello-docker:1.0

GitHub Container Registry (GHCR) 로 푸시 #

GHCR은 GitHub의 권한 모델을 그대로 쓰고, 같은 저장소의 PAT로 인증합니다.

1. PAT 만들기 #

GitHub → Settings → Developer settings → Personal access tokens → Tokens (classic) 에서 새 토큰을 만들고, 권한에 **write:packages**를 체크합니다. (private 이미지 pull까지 한다면 read:packages도.)

.env 같은 곳에 보관
export CR_PAT=ghp_xxxxxxxxxxxxxxxxxx

2. 로그인 #

GHCR 로그인
echo $CR_PAT | docker login ghcr.io -u curtis --password-stdin
# Login Succeeded

--password-stdin으로 토큰을 stdin에서 읽으면 셸 히스토리에 안 남아 안전합니다. CI 에서도 같은 패턴을 씁니다.

3. 태그 + 푸시 #

ghcr 태그 → 푸시
docker tag hello-docker ghcr.io/curtis/hello-docker:1.0
docker push ghcr.io/curtis/hello-docker:1.0

푸시 직후엔 GHCR의 패키지가 기본적으로 private 입니다. 공개로 바꾸려면 GitHub의 패키지 페이지에서 “Change package visibility” → public.

GitHub Actions에서 #

GitHub Actions 안에서는 PAT 대신 자동 발급되는 GITHUB_TOKEN을 그대로 씁니다.

.github/workflows/build.yml (요약)
- uses: docker/login-action@v3
  with:
    registry: ghcr.io
    username: ${{ github.actor }}
    password: ${{ secrets.GITHUB_TOKEN }}

- uses: docker/build-push-action@v5
  with:
    push: true
    tags: ghcr.io/${{ github.repository_owner }}/myapp:${{ github.sha }}

수동으로 PAT 관리할 일이 없어 깔끔합니다. (이 패턴은 도커 실전 시리즈에서 깊이 다룹니다.)

다이제스트 — 가장 정확한 고정 #

태그는 사람이 읽기 좋지만 불변(immutable) 이 아닙니다. 누가 myapp:1.2.3을 같은 태그로 다시 푸시할 수 있습니다. 그래서 정확한 재현이 중요한 환경에선 **다이제스트(SHA-256)**를 씁니다.

다이제스트로 가져오기
docker pull python@sha256:abc123def456...

푸시할 때 출력에 digest: sha256:...가 나오는데, 그게 그 이미지의 영구 식별자입니다. 같은 다이제스트는 영원히 같은 이미지를 가리킵니다. 운영 환경, 보안 스캔, 컴플라이언스가 중요한 곳에서는 다이제스트로 고정하는 게 정석입니다.

docker inspect로 다이제스트 확인:

이미지의 다이제스트 보기
docker inspect --format '{{index .RepoDigests 0}}' myuser/hello-docker:1.0
# myuser/hello-docker@sha256:abcdef...

docker pull — 받기 #

기본 pull
docker pull nginx:1.27
docker pull ghcr.io/curtis/hello-docker:1.0

자주 쓰는 옵션:

유용한 형태
docker pull --platform linux/amd64 myimage   # 특정 아키텍처만
docker pull -a myuser/myapp                  # 모든 태그

--platform은 Apple Silicon 머신에서 amd64 이미지를 강제로 받을 때 자주 씁니다. 운영 서버가 amd64 인데 로컬 ARM에서 빌드 / 테스트하려는 상황 같은 데에서요. (멀티 아키텍처 빌드는 도커 고급의 주제입니다.)

프라이빗 레지스트리 — 한 단락 #

회사 내부에 자체 레지스트리를 두는 일이 많습니다. 도커는 이미지 자체를 컨테이너화한 registry 이미지를 제공합니다.

로컬 레지스트리 띄우기
docker run -d --restart unless-stopped \
  -p 5000:5000 --name registry \
  -v registry-data:/var/lib/registry \
  registry:2

# 푸시
docker tag myapp localhost:5000/myapp:1.0
docker push localhost:5000/myapp:1.0

연구실 / 사내망에서 자체 호스팅하는 흐름인데, 운영에선 보통 GHCR / ECR / Harbor 같은 매니지드 / 풀 기능 레지스트리를 씁니다. 일단 이런 게 가능하다는 정도로만 기억해 두세요.

자주 만나는 함정 #

  • docker pushdenied: requested access to the resource is denied — 거의 항상 로그인 / 권한 / 태그 이름 셋 중 하나. 태그가 myuser/...가 아니라 someoneelse/...로 박힌 게 흔합니다.
  • 푸시는 됐는데 다른 머신에서 pull이 안 됨 — 패키지가 private 인데 그 머신에서 로그인을 안 했을 가능성. GHCR은 private이 기본이라 자주 만남.
  • macOS에서 빌드한 이미지가 운영 서버(amd64) 에서 안 뜸 — Apple Silicon의 arm64 이미지를 푸시한 것. --platform linux/amd64로 빌드하거나, Buildx로 멀티 아키텍처 빌드.
  • latest가 갱신되지 않은 것처럼 보임 — 그 머신의 도커가 캐시된 latest를 가리킴. docker pull myimage:latest를 한 번 더 치면 됩니다.

정리 #

이번 글에서 잡은 그림:

  • 레지스트리는 이미지의 원격 저장소. GitHub ↔ Docker Hub / GHCR 비유 그대로
  • 이미지 이름 = [REGISTRY/]NAMESPACE/REPO[:TAG][@DIGEST]. 생략 시 docker.io/library/...:latest가 기본
  • **docker tag**는 별명 추가, **docker push**는 레이어 단위 업로드
  • Docker Hub는 Personal Access Token, GHCR은 GitHub PAT(또는 Actions의 GITHUB_TOKEN) 로 로그인
  • 운영에선 latest 의존을 피하고 시맨틱 버전 태그 + Git SHA를 함께 적용한다
  • 정확한 고정이 필요하면 **다이제스트(@sha256:…)**로 참조

다음 글(#6 .dockerignore와 빌드 컨텍스트)에서는 이미지가 비대해지거나 빌드가 느려지는 가장 흔한 원인 — 빌드 컨텍스트의 정체와, 그걸 다루는 .dockerignore의 작성 패턴, 그리고 #2 에서 한 번 짚었던 레이어 캐시를 본격적으로 짭니다.

X