Switch to GitHub Actions and Packages
To no one's surprise, posting regularly to a blog when you have a job and when you don't are two different things. Nevertheless, I'm trying to reincorporate writing into my daily routine. If you're reading this, then I have succeeded! In the previous entry, I briefly described that I settled on a Go backend and a React web app for this site, explaining how code generation based on the OpenAPI specification simplifies the synchronization of any changes between the two.
Once the code is written and tested, it's time to deploy it to my VPS. I used a couple of tools for that, with the actual commands stored in Makefiles. For the Go service, it looked like this:
- Build a new Docker image.
- Save it as a tar.gz archive.
- Copy it to a remote server using scp.
- Load this image to Docker.
- Update the running container.
While I believe there's nothing wrong with this approach given the nature of the project, I wanted to switch to GitHub Actions and Packages. That would eradicate the need to define deployment properties on every machine where I code, let alone maintaining them up to date if. Moreover, even though the exact tools differ from company to company, this approach would be much closer to what a real business would do with their product. In other words, besides the personal interest, it is relevant to my job!
The transition was rather simple. I was already building Docker images, so all I had to do was to move this part to GitHub and push images to their registry. Here, I want to document what I did and think about what can be done next.
GitHub Packages
Any GitHub user can create and publish packages, including Docker images. Even free accounts can use up to 500MB of storage and 1GB of data transfer per month; you can see the details here. That is more than enough for my needs, but even if I manage to exceed these limits, GitHub offers quite reasonable pricing for extra allocation: $0.25 for an additional 1GB of space and $0.50 for an extra gigabyte of traffic.
GitHub Actions
Similar to Jenkins jobs, GitHub Actions can be used to manage CI/CD workflows. One common practice is the execution of various quality gates for pull requests, such as automated tests and static code analyzers. A project can be configured to block the ability to merge if a check fails, and do pretty much anything else you'd expect from a system like this.
I employed actions to build Docker images and push them to the registry. Jobs are stored in the repository itself in the .github/workflows
directory, where each workflow is described in a dedicated YAML file. For instance, here is an action that manages the image creation for my backend:
name: Service docker image
on:
push:
branches: ["main"]
paths:
- "service/**"
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build the Docker image
run: |
docker build ./service --file service/Dockerfile --tag ghcr.io/parfentjev/simple-blog-service:latest
docker push ghcr.io/parfentjev/simple-blog-service:latest
This part configures when the job should start, executing only when there are changes in the service/
directory on the main branch, ensuring updates for the web app don't trigger new builds for the service. The workflow_dispatch
property allows manual workflow execution on GitHub - just in case.
on:
push:
branches: ["main"]
paths:
- "service/**"
workflow_dispatch:
The next two steps checkout the code and log in to the container registry using automatically available variables, I didn't configure them myself:
- uses: actions/checkout@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
Finally, the image is built and pushed:
- name: Build the Docker image
run: |
docker build ./service --file service/Dockerfile --tag ghcr.io/parfentjev/simple-blog-service:latest
docker push ghcr.io/parfentjev/simple-blog-service:latest
Perhaps, specifying the actual Dockerfile might be unnecessary here since the build context is already set to the service/
directory. I'll change that in my code and see if it still works!
For the React app, I have another file that tracks changes in the webapp/
directory. Both jobs are detailed here. With this setup, each time I push changes to the main branch, GitHub automatically updates the respective Docker images. That eliminates the need for manual work from my side. The only issue I encountered was related to permissions: GitHub Workflows couldn't push to ghcr.io. I don't recall how I fixed that, but a quick search on the internet will surely help! :D
For more information on GitHub Actions, see the official documentation.
Update containers
Once the build phase is completed, it's time to deploy! I don't use cloud providers to manage my containers, and Kubernetes is also out of the question - all that would be overkill. Therefore, I could simply create a bash script to execute a couple of commands:
#!/bin/bash
SERVICE_DIR="/path/to/service/"
WEBAPP_DIR="/path/to/webapp/"
cd $SERVICE_DIR
docker pull ghcr.io/parfentjev/simple-blog-service:latest
docker compuse up -d
cd $WEBAPP_DIR
docker pull ghcr.io/parfentjev/simple-blog-webapp:latest
docker compuse up -d
Watchtower offers another way to do this. Normally, it's always up and updating running containers on schedule. However, it can also be configured to run only once:
version: "3"
services:
watchtower:
image: containrrr/watchtower
environment:
- REPO_USER=YOUR_GITHUB_USERNAME
- REPO_PASS=YOUR_GITHUB_TOKEN
- WATCHTOWER_RUN_ONCE=true
volumes:
- /var/run/docker.sock:/var/run/docker.sock
Then, updating both containers would only require running the following command:
docker compuse up -d
In the directory with Watchtower's compose file. I'm yet to decide whether I'll use this method, maybe not. I'm mostly documenting the environment variables here as they can be useful for other projects :)