Lab 3: Push to Container Registries
Time: 20 minutes
Objective: Push your container image to Docker Hub and GitHub Container Registry
Overview
Container registries are like GitHub for container images. They store and distribute images so others can pull them.
We'll use two registries:
- Docker Hub - The default public registry, widely used
- GitHub Container Registry (GHCR) - Integrated with GitHub, great for projects
Background: How Container Registries Work
A registry stores image content as immutable layers plus a manifest that lists which layers make up a tagged image. When you push, Docker uploads only layers the registry does not already have, then publishes the tag by writing or updating the manifest reference. That is why pushes can be fast after small changes and why you often see "layer already exists" during uploads.
A tag is a movable pointer, not a cryptographic identity. my-app:v1 can be retagged to different content later unless your team enforces immutability policies, while a digest like @sha256:... uniquely identifies exact bytes. In production systems, tags are convenient for humans and digests are safer for deterministic deployments.
Auth and authorization are separate concerns here. docker login stores credentials locally, but actual push or pull permissions come from registry-side policies tied to your account or token scopes. In AWS terms, this is similar to authenticating your CLI and then being allowed or denied by ECR/IAM policy for a specific repository action.
Docker Hub and GHCR both implement OCI distribution, so the mechanics are the same even though account model and defaults differ. Docker Hub defaults to a global namespace model, while GHCR aligns image access with GitHub users, orgs, and repository permissions. The command sequence in this lab is mostly about naming and credentials, not different image formats.
Keep one practical rule in mind as you work: push once, then verify by pulling into a clean local state so you know the registry copy is truly usable by other systems. For deeper reference, see the Docker registry documentation: https://docs.docker.com/docker-hub/repos/.
Part 1: Docker Hub
Create a Docker Hub Account
- Go to hub.docker.com
- Sign up for a free account
- Remember your username—you'll need it for tagging images
Login to Docker Hub
docker loginEnter your Docker Hub username and password when prompted.
Security Note: This stores credentials in
~/.docker/config.json. In production, use credential helpers or CI/CD secrets.
Tag Your Image for Docker Hub
Docker Hub images follow the format: username/image-name:tag
# Replace YOUR_DOCKERHUB_USERNAME with your actual username
docker tag my-python-app:v1 YOUR_DOCKERHUB_USERNAME/my-python-app:v1For example, if your username is jsmith:
docker tag my-python-app:v1 jsmith/my-python-app:v1Push to Docker Hub
docker push YOUR_DOCKERHUB_USERNAME/my-python-app:v1Watch the layers upload! If a layer already exists in the registry, it skips uploading (deduplication).
Verify on Docker Hub
- Go to hub.docker.com
- Click on your profile → Repositories
- You should see
my-python-app
Pull It Back (Test)
Let's verify by removing the local image and pulling:
# Remove local images
docker rmi YOUR_DOCKERHUB_USERNAME/my-python-app:v1
docker rmi my-python-app:v1
# Pull from Docker Hub
docker pull YOUR_DOCKERHUB_USERNAME/my-python-app:v1
# Run it
docker run -d --name test -p 5000:5000 YOUR_DOCKERHUB_USERNAME/my-python-app:v1
curl localhost:5000
# Clean up
docker rm -f testIt works! Your image is now publicly available.
Part 2: GitHub Container Registry (GHCR)
GHCR is integrated with GitHub and is great for projects because:
- Images can be linked to repositories
- Access control follows GitHub permissions
- No separate account needed
Authenticate with GHCR
You'll need a GitHub Personal Access Token (PAT) with write:packages permission.
Create a PAT:
- Go to GitHub → Settings → Developer settings → Personal access tokens → Tokens (classic)
- Click "Generate new token (classic)"
- Give it a name like "Container Registry"
- Select scopes:
write:packages(this also grantsread:packages)delete:packages(optional, for cleanup)
- Click "Generate token"
- Copy the token immediately - you won't see it again!
Login to GHCR:
# Replace YOUR_GITHUB_USERNAME and YOUR_TOKEN
echo "YOUR_TOKEN" | docker login ghcr.io -u YOUR_GITHUB_USERNAME --password-stdinFor example:
echo "ghp_abc123..." | docker login ghcr.io -u jsmith --password-stdinIn Codespaces: You're already authenticated! Try:
echo $GITHUB_TOKEN | docker login ghcr.io -u $GITHUB_USER --password-stdinTag Your Image for GHCR
GHCR images follow the format: ghcr.io/username/image-name:tag
docker tag my-python-app:v1 ghcr.io/YOUR_GITHUB_USERNAME/my-python-app:v1Push to GHCR
docker push ghcr.io/YOUR_GITHUB_USERNAME/my-python-app:v1Verify on GitHub
- Go to your GitHub profile → Packages
- Or go to
github.com/YOUR_USERNAME?tab=packages - You should see
my-python-app
Make the Package Public (Optional)
By default, GHCR packages are private. To make it public:
- Go to the package on GitHub
- Click "Package settings"
- Scroll to "Danger Zone"
- Click "Change visibility" → Public
Part 3: Assignment - Push Your Student Container (REQUIRED)
You must push your containerized application (with your name from Lab 2) to GHCR in your forked repository. This will be used for the cluster deployment where all student containers will run together.
Step 1: Verify Your Container Has Your Name
First, ensure your container from Lab 2 includes your student information:
# Run your container from Lab 2
docker run -d --name test -p 5000:5000 my-python-app:v2-student
# Verify your name appears
curl localhost:5000/student
# Should show:
# {"name": "Your Name", "github_username": "your-username", "container_tag": "v2-student"}
docker rm -f testIf your name shows "YOUR_NAME_HERE", go back to Lab 2 Part 6 and complete it first!
Step 2: Tag for Your Fork's GHCR
Use this exact naming pattern (required for the cluster deployment):
# Replace YOUR_GITHUB_USERNAME with your actual GitHub username
docker tag my-python-app:v2-student ghcr.io/YOUR_GITHUB_USERNAME/container-course-student:latest
docker tag my-python-app:v2-student ghcr.io/YOUR_GITHUB_USERNAME/container-course-student:v2-studentStep 3: Push to GHCR
# Login if not already
echo "YOUR_PAT" | docker login ghcr.io -u YOUR_GITHUB_USERNAME --password-stdin
# Push both tags
docker push ghcr.io/YOUR_GITHUB_USERNAME/container-course-student:latest
docker push ghcr.io/YOUR_GITHUB_USERNAME/container-course-student:v2-studentStep 4: Make Your Package Public
This is required so the cluster can pull your image!
- Go to your GitHub profile → Packages
- Click on
container-course-student - Click "Package settings"
- Scroll to "Danger Zone"
- Click "Change visibility" → Public
- Confirm by typing the package name
Step 5: Verify Your Image URL
Test that your image is publicly accessible:
# This should work without authentication
docker logout ghcr.io
docker pull ghcr.io/YOUR_GITHUB_USERNAME/container-course-student:latestStep 6: Record Your Image URL
Save this URL - you'll need it for the PR submission:
ghcr.io/YOUR_GITHUB_USERNAME/container-course-student:latest
Part 4: Submit Your Container to the Class Deployment (REQUIRED)
After pushing your container to GHCR, you need to submit it to the master deployment list. All student containers will be deployed to a Kubernetes cluster where you can see everyone's work running together!
Step 1: Fork the Deployment Repository
- Go to:
https://github.com/ziyotek-edu/container-gitops - Click "Fork" to create your own copy
- Clone your fork locally:
git clone https://github.com/YOUR_GITHUB_USERNAME/container-gitops
cd container-gitopsStep 2: Add Your Container to the Deployment List
Edit the students/week-01.yaml file and add your entry:
students:
# ... existing entries ...
- name: "Your Full Name"
github_username: "your-github-username"
container_image: "ghcr.io/your-github-username/container-course-student:latest"
student_endpoint: "/student"
health_endpoint: "/health"
port: 5000Step 3: Create a Pull Request
# Create a new branch
git checkout -b add-YOUR_GITHUB_USERNAME-week01
# Add your changes
git add students/week-01.yaml
# Commit with a descriptive message
git commit -m "Add YOUR_NAME to Week 01 deployment list"
# Push to your fork
git push origin add-YOUR_GITHUB_USERNAME-week01Step 4: Open the Pull Request
- Go to your fork on GitHub
- Click "Pull requests" → "New pull request"
- Ensure it's going from your branch to the main repo's main branch
- Title: "Add [Your Name] - Week 01 Submission"
- Description should include:
- Your container URL
- Confirmation that
/studentendpoint works - Any special notes about your implementation
Step 5: Verify Your Deployment
Once your PR is merged, your container will be automatically deployed! You can check:
- Your App:
https://container-course.lab.shart.cloud/students/YOUR_GITHUB_USERNAME - Class Gallery:
https://container-course.lab.shart.cloud/gallery
The cluster will:
- Pull your container from GHCR
- Deploy it with proper resource limits
- Expose it at a unique URL
- Monitor health via your
/healthendpoint - Display your info from
/studentin the gallery
Part 5: The Full Tagging Strategy
In real projects, you typically push multiple tags:
# Build the image
docker build -t my-python-app:v1.2.3 .
# Tag with multiple variants
docker tag my-python-app:v1.2.3 ghcr.io/user/my-python-app:v1.2.3
docker tag my-python-app:v1.2.3 ghcr.io/user/my-python-app:v1.2
docker tag my-python-app:v1.2.3 ghcr.io/user/my-python-app:v1
docker tag my-python-app:v1.2.3 ghcr.io/user/my-python-app:latest
# Push all tags
docker push ghcr.io/user/my-python-app:v1.2.3
docker push ghcr.io/user/my-python-app:v1.2
docker push ghcr.io/user/my-python-app:v1
docker push ghcr.io/user/my-python-app:latestThis allows users to:
- Pin to exact version:
v1.2.3(safest, won't change) - Float on patch:
v1.2(getsv1.2.4,v1.2.5, etc.) - Float on minor:
v1(getsv1.3.0,v1.4.0, etc.) - Always latest:
latest(dangerous in production!)
Part 5: Pulling Images in CI/CD (Preview)
In Week 8, we'll set up GitHub Actions to build and push automatically. Here's a preview:
# .github/workflows/build.yaml
name: Build and Push
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Push
uses: docker/build-push-action@v5
with:
push: true
tags: ghcr.io/${{ github.repository }}/my-app:${{ github.sha }}No hardcoded credentials—GitHub provides them automatically!
Checkpoint ✅
Before moving on, verify you have completed:
Required for Assignment:
- Your container includes your name (from Lab 2)
- Pushed your student container to GHCR
- Made your GHCR package public
- Submitted PR to deployment repository
- Your container URL is: ghcr.io/YOUR_USERNAME/container-course-student:latest
Skills Learned:
- Login to Docker Hub
- Tag and push an image to Docker Hub
- Login to GHCR with a PAT (or Codespaces token)
- Tag and push an image to GHCR
- Explain the difference between Docker Hub and GHCR
- View your packages on GitHub
Clean Up
You can leave the images in the registries—they're useful for future labs!
Locally, clean up:
docker rm -f $(docker ps -aq) 2>/dev/null || true
docker system prune -fCommon Issues
"denied: permission denied"
- Docker Hub: Check you're logged in (
docker login) - GHCR: Check your PAT has
write:packagesscope - GHCR: Image path must start with
ghcr.io/YOUR_USERNAME/
"unauthorized: authentication required"
Login expired. Run docker login or docker login ghcr.io again.
"name unknown: repository does not exist"
The image path is wrong. Double-check:
- Docker Hub:
username/image:tag - GHCR:
ghcr.io/username/image:tag
Summary
| Registry | Image Format | Login Command |
|---|---|---|
| Docker Hub | username/image:tag |
docker login |
| GHCR | ghcr.io/username/image:tag |
docker login ghcr.io |
Both registries:
- Store layers efficiently (deduplication)
- Support public and private images
- Work with
docker pullanddocker push
GHCR advantages:
- Integrated with GitHub permissions
GITHUB_TOKENworks in Actions (no secrets to manage)- Packages linked to repos
Docker Hub advantages:
- Default registry (no prefix needed for pull)
- Larger ecosystem of public images
- Official images for popular software
Demo
Next Steps
You've completed Week 1! 🎉
For homework:
- Complete the gym exercises:
container-lifecycle,port-mapping-puzzle,exec-detective - Review the Discovery Questions in the main README
- Read the Docker Overview if you haven't
Next week, we'll dive into Dockerfile Mastery: layer caching, multi-stage builds, and security scanning.

