Build and publish Docker images with GitHub Packages
- 5 minutes read - 934 wordsGitHub has been evolving rapidly in the last few years. With the introduction of GitHub Actions and GitHub Packages, your continuous integration (CI) pipelines can be entirely implemented without leaving GitHub, reducing context switching and increasing productivity for engineers. In this post, we will work through setting up pipelines for a Docker based project including pull request (PR) validation, CI, publishing of CI images and publishing of release images, all in GitHub. Let’s get started!
Application
Firstly, we need something to actually build. For simplicity, let’s do the standard Node.js Express server, but it could be any programming lanuage or framework, because we are building in Docker.
So we have the following files for our application:
Package.json
:
{
"name": "docker_web_app",
"version": "1.0.0",
"description": "Node.js on Docker",
"author": "First Last <first.last@example.com>",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.16.1"
}
}
server.js
:
'use strict';
const express = require('express');
// Constants
const PORT = 8080;
const HOST = '0.0.0.0';
// App
const app = express();
app.get('/', (req, res) => {
res.send('Hello World');
});
app.listen(PORT, HOST);
console.log(`Running on http://${HOST}:${PORT}`);
Dockerfile
:
FROM node:12
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
# A wildcard is used to ensure both package.json AND package-lock.json are copied
# where available (npm@5+)
COPY package*.json ./
RUN npm install
# If you are building your code for production
# RUN npm ci --only=production
# Bundle app source
COPY . .
EXPOSE 8080
CMD [ "node", "server.js" ]
.dockerignore
:
node_modules
npm-debug.log
Now that we have something to build, let’s build it!
GitHub Actions
You could follow the GitHub Docs to learn how to create an Action for your needs. However, I find that it often lags behind the real world with legacy actions and is generally confusing. I find it is faster to let GitHub recommend you an Action, and build from there. For instance, once you have your code commited to a repository, go to the Actions tab and you will see a number of Actions that GitHub recommends that could be useful for you:
In this case, the Publish Docker Container
action is the one that I want and I can simply click Set up this workflow
. From there, I am met with a screen that shows the contents of the Action via a YAML file:
name: Docker
on:
push:
# Publish `main` as Docker `latest` image.
branches:
- main
# Publish `v1.2.3` tags as releases.
tags:
- v*
# Run tests for any PRs.
pull_request:
env:
# TODO: Change variable to your image's name.
IMAGE_NAME: image
jobs:
# Run tests.
# See also https://docs.docker.com/docker-hub/builds/automated-testing/
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run tests
run: |
if [ -f docker-compose.test.yml ]; then
docker-compose --file docker-compose.test.yml build
docker-compose --file docker-compose.test.yml run sut
else
docker build . --file Dockerfile
fi
# Push image to GitHub Packages.
# See also https://docs.docker.com/docker-hub/builds/
push:
# Ensure test job passes before pushing image.
needs: test
runs-on: ubuntu-latest
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v2
- name: Build image
run: docker build . --file Dockerfile --tag $IMAGE_NAME
- name: Log into registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login docker.pkg.github.com -u ${{ github.actor }} --password-stdin
- name: Push image
run: |
IMAGE_ID=docker.pkg.github.com/${{ github.repository }}/$IMAGE_NAME
# Change all uppercase to lowercase
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
# Strip git ref prefix from version
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
# Strip "v" prefix from tag name
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
# Use Docker `latest` tag convention
[ "$VERSION" == "main" ] && VERSION=latest
echo IMAGE_ID=$IMAGE_ID
echo VERSION=$VERSION
docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
docker push $IMAGE_ID:$VERSION
This Action is doing the following things:
- It’s running on pushes to the
main
branch, when any tags matchingv*
are pushed, and on all pull requests - It’s running a test step. Although I don’t have any tests yet, this would be obviously needed for a production application
- It’s running a push step, where the image is either tagged with “latest” if it is coming from the
main
branch, orv*
if from a tag
This is great, it’s exactly what I was intending and I didn’t have to write any YAML! That’s why it’s always good to start with the recommended Actions as they will give you idiomatic, conventional, up-to-date Actions for your project for free.
PR Validation
Once you have reviewed your Action, you can push it to a new branch and create a pull request. You can see on my pull request that the Action was triggered, and only the test check ran:
Great, now what happens when this is merged into main
?
CI
After the PR was merged, the Action was again triggered, running the test and push steps:
A CI image was also created and tagged with latest
in GitHub Packages:
Great, now what happens when a tag is created?
Release
I created a release with tag v1.0.0
against the main
branch. This again triggered the Action and now a release image with tag 1.0.0
was created in GitHub Packages:
Summary
In this post, we took an existing Dockerised application and added PR validation, CI, CI builds and release builds, all within GitHub. We used the recommended Action to do all of this without writing any YAML, whilst having an idiomatic pipeline and staying up-to-date. I would definitely recommend GitHub Actions and GitHub Packages for PR Validation and CI. Depending on your organisation’s needs and IP requirements, you may consider publishing you release images to Docker Hub if they are to be consumed publicly, otherwise GitHub Packages will work nicely for internal or authenticated usage of Docker images.
Resources
- GitHub repo for this post
- GitHub Packages Docs