도커 기초 강좌 #5 레지스트리 — Docker Hub, GHCR, push/pull
#3 까지 만든 이미지는 내 머신 안에서만 살아 있었습니다. 이번 글에서는 그 이미지를 다른 머신 — 동료, CI 서버, 운영 서버 — 가 쓸 수 있게 만드는 방법을 다룹니다. 그게 **레지스트리(registry)**입니다.
도커 기초 강좌 시리즈에서 이번 글의 위치:
- #1 컨테이너란
- #2 Dockerfile 첫 작성
- #3 이미지와 컨테이너
- #4 볼륨과 네트워크
- #5 레지스트리 — Docker Hub, GHCR, push/pull ← 이번 글
- #6
.dockerignore와 빌드 컨텍스트
레지스트리가 무엇인가 #
GitHub이 코드의 원격 저장소라면, 레지스트리는 이미지의 원격 저장소입니다. 비유 그대로입니다.
| 코드 | 이미지 | |
|---|---|---|
| 만들기 | git commit | docker build |
| 올리기 | git push | docker push |
| 받기 | git pull | docker pull |
| 호스트 | GitHub, GitLab | Docker 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]예시:
| 이름 | 풀어 보면 |
|---|---|
nginx | docker.io/library/nginx:latest |
python:3.14-slim | docker.io/library/python:3.14-slim |
myuser/myapp:1.0 | docker.io/myuser/myapp:1.0 |
ghcr.io/curtis/myapp:1.0 | GHCR의 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:latestdocker tag는 데이터 복사를 하지 않고 포인터만 추가합니다. 그래서 즉시 끝납니다. docker images로 보면 같은 IMAGE ID에 이름이 여러 개 붙어 있을 것입니다.
레지스트리에 올리려면 보통 이름에 레지스트리 호스트 / 사용자 이름이 들어간 형태로 태그를 다시 붙입니다.
docker tag hello-docker ghcr.io/curtis/hello-docker:1.0이래야 도커가 “어디로 보내야 할지” 압니다. 그냥 hello-docker 같은 짧은 이름으로는 푸시할 수 없습니다.
Docker Hub로 푸시 #
Docker Hub에 계정을 만들고 (hub.docker.com), 터미널에서 로그인합니다.
docker login
# Username: myuser
# Password: ******
# Login SucceededPersonal 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의 베이스 레이어들) 는 다시 안 올라가고, 변한 레이어만 올라갑니다. 두 번째 푸시부터는 매우 빠른 이유가 이겁니다.
다른 머신에서 받기:
docker pull myuser/hello-docker:1.0
docker run --rm myuser/hello-docker:1.0GitHub Container Registry (GHCR) 로 푸시 #
GHCR은 GitHub의 권한 모델을 그대로 쓰고, 같은 저장소의 PAT로 인증합니다.
1. PAT 만들기 #
GitHub → Settings → Developer settings → Personal access tokens → Tokens (classic) 에서 새 토큰을 만들고, 권한에 **write:packages**를 체크합니다. (private 이미지 pull까지 한다면 read:packages도.)
export CR_PAT=ghp_xxxxxxxxxxxxxxxxxx2. 로그인 #
echo $CR_PAT | docker login ghcr.io -u curtis --password-stdin
# Login Succeeded--password-stdin으로 토큰을 stdin에서 읽으면 셸 히스토리에 안 남아 안전합니다. CI 에서도 같은 패턴을 씁니다.
3. 태그 + 푸시 #
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을 그대로 씁니다.
- 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 — 받기
#
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 push가denied: 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 에서 한 번 짚었던 레이어 캐시를 본격적으로 짭니다.