Home
Alt + H
Logo Logo

DevOps

CI/CD: Shipping containers to GHCR using GitHub Actions

Tags 8 min read Comments
Post - CI/CD: Shipping containers to GHCR using GitHub Actions

Summary

Show content

Introduction

This is part of my DevOps learning journey! If anything seems off, feel free to leave a comment or connect with me on Linkedin (LucJosin) Linkedin (LucJosin) or GitHub (LucJosin) GitHub (LucJosin) .

While studying DevOps and containerized applications, one of the first things that we learn is: How to do a deployment pipeline to get your containers/images built, tagged, and pushed automatically.

In this post, I’ll walk through how I set up a CI/CD workflow using GitHub Actions to publish containers/images to GitHub Container Registry (GHCR).

What is CI/CD?

The CI/CD stands for Continuous Integration and Continuous Deployment (or Delivery).

  • CI: Automates the process of testing and building code every time you push changes.
  • CD: Automates deployment, getting your code into an enviroment (e.g: production/devlop).

By using both, you ensure every change is integrated, tested, built, and shipped quickly.

What is GHCR (GitHub Container Registry)?

The GHCR is GitHub’s native container registry, similar to Docker Hub Docker Hub , but more integrated with GitHub. It allows you to:

  1. Store and manage containers/images
  2. Associate images directly with repositories
  3. Control access via GitHub permissions

It’s free for public repositories and supports private image hosting too. Plus, it works perfect with GitHub Actions.

Visual workflow

Here’s a visual workflow for the CI/CD:

Loading graph...

Application overview

For this post, I built a simple Golang webapp. You can find the code in the GitHub repository:

Setting up the Dockerfile

Before setting up our GitHub Actions workflow, let’s define the Dockerfile, which describes how to build and run our Go application inside a container.

Here’s the complete Dockerfile:

1
# ---- Build Stage ----
2
FROM golang:1.22-alpine AS builder
3
4
# Set working directory
5
WORKDIR /app
6
7
# Install necessary packages
8
RUN apk add --no-cache git
9
10
# Copy go mod file
11
COPY go.mod ./
12
13
# Download dependencies
14
RUN go mod download
15
16
# Copy the rest of the source code
17
COPY . .
18
19
# Build the Go app
20
RUN GOOS=linux go build -o app main.go
21
22
# ---- Run Stage ----
23
FROM alpine:latest
24
25
# Set working directory
26
WORKDIR /app
27
28
# Copy the binary and template files from the builder stage
29
COPY --from=builder /app/app /app/index.tmpl ./
30
31
# Expose port
32
EXPOSE 8080
33
34
# Run the application
35
CMD ["./app"]

In this case, we used a multi-stage Dockerfile:

  1. Build:

    • Sets up the Go environment using a image (with Golang configured).
    • Installs necessary packages (like Git, used for Go fetching dependencies).
    • Copies the source code and modules files.
    • Downloads dependencies with go mod download.
    • Builds the application into a binary using go build.
  2. Run:

    • Uses a lightweight (base) image to keep the final image small.
    • Copies the compiled binary from the build stage.
    • Sets the working directory.
    • Exposes the port the app will use.
    • Defines the command to run the app.

Setting up the GitHub Actions

Now that we have defined the instructions/steps to build the image, let’s configure our build.yml workflow, this file tells GitHub Actions how and when to execute automated actions/steps/tasks.

GitHub let’s you have multiple workflow files, all .yaml configuration have to be in .github/workflows/ directory.

Here’s the complete workflow file:

.github/workflows/build.yml
1
# Workflow name shown in the GitHub Actions
2
name: 'Build and Push to GHCR'
3
4
# Trigger the workflow when pushing a tag like: v1.0.0
5
on:
6
push:
7
tags:
8
- 'v[0-9].[0-9]+.[0-9]'
9
10
# Set environment variables available to all steps
11
env:
12
GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }} # GitHub Container Registry token (set in repository secrets)
13
CONTAINER_NAME: lucjosin/go-webapp-ghcr # Image name to publish (all lowercase)
14
USER_NAME: LucJosin # GitHub username used for GHCR authentication
15
16
jobs:
17
build-and-push:
18
runs-on: ubuntu-latest # Use the latest Ubuntu runner
19
20
steps:
21
# Step 1
22
- name: Set up checkout
23
uses: actions/checkout@v4
24
25
# Step 2
26
- name: Set up latest git tag
27
run: |
28
RAW_TAG="${GITHUB_REF#refs/tags/}"
29
VERSION="${RAW_TAG#*@}"
30
echo "VERSION=${VERSION}" >> $GITHUB_ENV
31
echo "Version: $VERSION"
32
33
# --- Build Stage ---
34
35
# Step 3
36
- name: Docker build
37
run: |
38
docker build -t ghcr.io/${{ env.CONTAINER_NAME }}:latest .
39
docker build -t ghcr.io/${{ env.CONTAINER_NAME }}:${{ env.VERSION }} .
40
41
# --- Push Stage ---
42
43
# Step 4
44
- name: GitHub (GHCR) login
45
run: echo $GHCR_TOKEN | docker login ghcr.io -u $USER_NAME --password-stdin
46
47
# Step 5
48
- name: GitHub (GHCR) push
49
run: |
50
docker push ghcr.io/${{ env.CONTAINER_NAME }}:latest
51
docker push ghcr.io/${{ env.CONTAINER_NAME }}:${{ env.VERSION }}

The workflow contains the following steps:

  1. Set up checkout: Setup the configure repository code (so that the workflow can access it).
  2. Set up latest git tag: Extracts the version from the latest Git tag.
  3. Docker build: Builds the image, tag with versions: latest and env.VERSION (defined earlier).
  4. GitHub (GHCR) login: Logs into GHCR.
  5. GitHub (GHCR) push: Pushes the images to GHCR.

The CONTAINER_NAME variable MUST be in lowercase.

After publishing a new release version (like v1.0.0):

  • Your image will appear in the Packages section of your repository.
  • You can pull it on any host by using the full image path.

Preparing the Environment

We need to prepare the repository environment before the workflow run, it involves: Updating workflow permissions and Creating a GHCR token:

Updating workflow permissions

This workflow require additional permissions to run successfully. This ensures that your workflows have the necessary access to read, write, and accessing secrets, follow the steps in the GIF below to update them:

Updating workflow permissions
Updating workflow permissions - View image in a new tab

You can find these settings in your repository’s Settings > Actions > General section

Creating a GHCR token

You can go to github.com/settings/tokens/new?scopes=write:packagesto create a new token, the ?scopes=write:packages param will correctly select the write:packages scope that will be use to download and upload container images.

Note that, according to the docs:

  1. GitHub Packages only supports authentication using a personal access token (classic).
  2. By default, when you select the write:packages scope for your personal access token (classic) in the user interface, the repo scope will also be selected.

See more at docs.github.com

If you don’t know to add a secret in a GitHub Repository, open the box below:

How to add a secret in a GitHub Repository
  1. Go to the repository on GitHub.

  2. Click on the “Settings” tab of the repository.

  3. In the left sidebar, navigate to “Secrets and variables” > “Actions”.

  4. Click the “New repository secret” button.

  5. Fill in the fields:

    • Name: the name of the secret (e.g., GHCR_TOKEN)
    • Secret: the value of the secret (e.g., your personal access token)
  6. Click “Add secret” to save.

Triggering the workflow

Once the GitHub Actions workflow is defined, you can trigger it by pushing a new tag that follows the version pattern defined in the workflow:

Terminal window
1
git tag v1.0.0
2
git push origin v1.0.0

After the tag is pushed, GitHub Actions will automatically run the CI/CD pipeline, building and publishing the app.

Logs from GitHub Actions
Logs from GitHub Actions - View image in a new tab

If everything runs successfully, you’ll see the release listed under both the Releases and Packages sections.

Releases and Packages after push
Releases and Packages after push - View image in a new tab

Using the image

Now that we have pushed the container image to the GHCR, we can use the following methods to pull and run the application locally:

CLI

The first method to pull and run the image is using the terminal with Docker CLI:

Terminal window
1
docker run -d --name golang-app -p 8080:8080 ghcr.io/lucjosin/go-webapp-ghcr

Step by step:

  1. docker run -d: Pull and start and starts a new container in detached mode.
  2. —name golang-app: Define the name golang-app to the container.
  3. -p 8080:8080: Opens port 8080 of the container to port 8080 on your host machine.
  4. ghcr.io/lucjosin/go-webapp-ghcr: This is the image being pulled and run (Remember to use your image).

Using the image using CLI
Using the image using CLI - View image in a new tab

Compose

The second method to pull and run the image is using the compose.yaml file:

1
services:
2
app:
3
image: ghcr.io/lucjosin/go-webapp-ghcr
4
container_name: golang-app
5
ports:
6
- '8080:8080'

Run the following command in the same directory as the file:

Terminal window
1
docker compose up -d
  1. Whether you use the first or second method, the container will be up and running at http://localhost:8080.
  2. Remember to change the ghcr.io/lucjosin/go-webapp-ghcr to use your image path.

Conclusion

Configuring GitHub Actions and deploying to GHCR is a great introduction to CI/CD in the DevOps world. It reduces manual work and enables the creation of more complete automation workflows.

I hope this gives you a clear starting point for automating your own deployments.

Resources and References

Enjoy this post? Feel free to share!

CI/CD: Shipping containers to GHCR using GitHub Actions



Share to LinkedIn
Share to Twitter
Share to Reddit
Copy link
QR Code

Comments

RSS
Tags
Source Code
Logo Logo
Lucas Josino
© 2025 • Built with Astro