Cheap and minimal container orchestration with Docker Context using GitHub Actions

Grey car being manufactured

Workflow settings and setup steps

GitHub Actions is the CI/CD platform of choice for this example . For the sake of example, the two triggers for this workflow are workflow_call and workflow_dispatch.

name: Deploy

on:
  workflow_call:
   
jobs:
  deploy:
    name: Deploy
    runs-on: ubuntu-latest
    steps:

Checking out the code is an easy first step because without it, we won't be able to build our image in the runner. And ecause I'm using DigitalOcean's container registry, we'll install DigitalOcean’s CLI, doctl, and authenticate with a personal access token. Best practice for using tokens and secrets in a workflow is to store the secrets in the repository so that we can conveniently reference secrets in the workflow. Once that is done, we can authenticate Docker with the container registry.

- name: Checkout code
  uses: actions/checkout@v3
 
- name: Install doctl
  uses: digitalocean/action-doctl@v2
  with:
    token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}

- name: Authenticate Docker with registry
  run: doctl registry login

Build, cache, and push the image

To build and push the image, we'll set up and use docker/build-push-action to build and push Docker image with Buildx with full support of the features provided by Moby BuildKit builder toolkit. What's great about this action is that it can be configured to cache the build to and from our repository so that we don't have to build our image from scratch every time this workflow runs. The only tradeoff with caching is that the runner needs to download and upload the cache to the repository, but the time saved from using the cache during the build is worth it more times than not.

- name: Set up Docker Buildx
  uses: docker/setup-buildx-action@v2

- name: Build and push
  uses: docker/build-push-action@v4
  with:
	context: .
	push: true
	tags: registry.digitalocean.com/<registry-name>/<image-name>:latest
	cache-from: type=gha
	cache-to: type=gha,mode=max

Pull and run the image remotely

The image has been built and pushed to our container registry, and now it’s time to update the image running on the remote server. There’s an infinite number of ways this can be done, but we’ll be using Docker Context to help make this as simple as possible. Definitely take the time to read the small amount of documentation about Docker Context, but the TL;DR is that the docker context command allows the local Docker CLI to interface with Docker running on a remote machine.

Docker Context is helpful because even though your local Docker CLI is controlling Docker on a remote machine, local docker related commands will still use the local file system. So if we tell Docker to use a remote context and run a docker compose command with a local compose file, Docker will use the local compose file on the remote machine. This behavior will allow us to update and run the image running on our remote server from within the workflow.

In order to create and use the context in the runner, we’ll use a shortcut from a community action and use SSH to control Docker running on the production server. To get the public key needed for the ssh_cert input, run ssh-keyscan -H <ip-address> and store the output as a repository secret. For the ssh_key input, copy whatever private key you use to connect as the local user and store is as a repository secret.

- name: Create and use production server Docker Context
  uses: arwynfr/actions-docker-context@v2
  with:
    docker_host: 'ssh://<user>@<ip-address:port>'
    context_name: 'production'
	ssh_cert: ${{ secrets.SSH_CERT_SECRET }}
	ssh_key: ${{ secrets.SSH_KEY_SECRET }}
	use_context: true

Now that we have access to Docker and docker-compose on the server, we can run the same docker-compose commands we would use if we wanted to run the application locally in our dev environment. And after a quick image prune step to remove any unused layers from the previous image, we have successfully updated the application 😎

- name: Pull
  run: docker-compose pull

- name: Up
  run: docker-compose up -d

- name: Remove previous image
  run: docker image prune -af

Bonus link to Synology owners - Docker Contexts & Synology.