Months ago I posted about using Shell Script for CI/CD in a personal project in which the script includes cloning the repo locally, building the Docker image, and running it as a container right in the VPS. I know, I know. It’s not ideal, and it’s leaking the source code to the server, so let’s redo it.
I want to focus on using GitHub Actions on a private repository to build a Docker image and store it on GitHub Container Registry (ghcr.io). It’s perfect for personal projects since we can use up to 500MB total for all images on the GitHub Free plan - it’s counted as GitHub Package storage usage, by the way.
What I am looking for in this round is:
- Build the Docker image on GitHub Actions.
- Push the image into GitHub Container Registry.
- Pull the image into the server.
- Run a script to start the image as container on the server.
Let’s take a look at how the action looks like.
name: CI/CD Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
# this sets permission to read repo content and push image to ghcr.io
permissions:
contents: read
packages: write
steps:
# 1. Checkout the repository
- name: Checkout repository
uses: actions/checkout@v4
# 2. Set up Go
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: '1.22'
# 3. Install dependencies
- name: Get dependencies
run: |
go get -v -t -d ./...
# 4. Run Golang tests
- name: Run tests
run: go test -v ./...
# 5. Login to ghcr.io
- name: Login to the container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# 6. Extract metadata (tags, labels) for Docker
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# 7. Build and push image to ghcr.io
- name: Build and push docker image
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# you can pass build-args and secrets here
# see https://github.com/marketplace/actions/build-and-push-docker-images
deploy:
# this prevent deployment when image building failed
needs: build
runs-on: ubuntu-latest
steps:
# 8. SSH to the server and run the script
- name: executing remote ssh commands using password
uses: appleboy/ssh-action@v1.2.0
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
password: ${{ secrets.PASSWORD }}
port: ${{ secrets.PORT }}
script: |
# log in to container registry
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ${{ env.REGISTRY }} -u ${{ github.actor }} --password-stdin
# pull the image
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
# export necessary environment variables for the script here
# export VARIABLE_NAME=value
# download and run deploy script using curl with auth header
curl -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Accept: application/vnd.github.v3.raw" \
-o /vps/path/to/save/script_name.sh \
-L "https://api.github.com/repos/${{ github.repository }}/contents/repo/path/to/script_name.sh?ref=${{ github.sha }}"
chmod +x /vps/path/to/save/script_name.sh
/vps/path/to/save/script_name.sh
# remove script after run completed
rm -f /vps/path/to/save/script_name.sh
With the above changes, no more cloning is needed to build images on the server.
All the build processes happen in the pipeline, and when it’s successfully built, the SSH script will log in to ghcr.io and pull the image. I chose to do Docker login before the script to prevent passing the GITHUB_TOKEN
secret to the script.
P.S.
GITHUB_TOKEN
is a provided secret value. No need to add them to the repository secret.The only thing I did was adding secrets for the ssh step.
Only the script is downloaded into the server; the rest of the source code is not. This way, the script can be safely stored in the same repository. Moreover, it’s deleted from the server after the run is completed. Neat!
Just like the previous post, you can do anything inside the script_name.sh
. The only difference would be that this time it would be simpler without the Docker build step:
#!/bin/sh
# Name for the Docker image
IMAGE_NAME="${IMAGE_NAME:-username/imagename:latest}"
CONTAINER_NAME="${CONTAINER_NAME:-containername}"
# Check if a container with CONTAINER_NAME exists and stop/remove it
EXISTING_CONTAINER=$(docker ps -aq -f name="$CONTAINER_NAME")
if [ ! -z "$EXISTING_CONTAINER" ]; then
echo "Stopping and removing existing container with name $CONTAINER_NAME..."
docker stop $CONTAINER_NAME
docker rm $CONTAINER_NAME
fi
# Run Docker container
echo "Running Docker container..."
docker run -d --restart always --name $CONTAINER_NAME $IMAGE_NAME
echo "Docker container started."
Finally, you might be wondering whether GitHub has a dashboard for your images like what DockerHub does. Worry not! You can go to https://github.com/username?tab=packages (change it to your username) to see a list of images you have stored in ghcr.io.
Photo by Richy Great on Unsplash