This document provides a guide for setting up an automated pipe that tests an app, builds a docker container around it, pushes that container to a registry and then sshes in and updates the running application via docker-compose.

Configure Server

Install Docker

See this guide for more instructions:

# uninstall conflicting packages
for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done
# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
 
# Add the repository to Apt sources:
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
 
#install latest versions
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Create User to Run Service

Create the user whilst ensuring that we create their home dir (-m) and that they have permission to use docker services (-G docker):

sudo useradd -m -G docker username

As that user… Create docker-compose.yml

mkdir ~/project_name
touch ~/project_name/docker-compose.yml
  • Typically best to version control this file
  • Ensure that any secrets like environment variables are in a separate .env file and that file is .gitignoreed to prevent sensitive info from being stored.
  core:
    env_file: .env.docker
    image: us.gcr.io/some/namespace/image:latest
    ports:
      - 3000:3000
    volumes:
	    ...
    depends_on:
      ...

Create update.sh file

  • Again we can version control this file
#!/bin/bash -e
COMPOSE_FILE=/home/username/project_name/docker-compose.yml
docker compose -f $COMPOSE_FILE pull servicename
docker compose -f $COMPOSE_FILE up -d servicename

Make sure that it is executable:

chmod +x ./updates.sh

Authenticate Docker Repos

If using normal docker registry:

docker login registry.example.com

Otherwise see Log in to Docker Repos for authenticating against google docker repo:

gcloud auth activate-service-account --key-file=/path/to/service_account.json
gcloud auth configure-docker

Create CI file in Github

Let’s create a new file in the project: .github/workflows/test_build_deploy.yaml:

Initial Step and Tests

You probably want to put the deploy step behind some automated tests so that it doesn’t deploy if they fail.

We also set some env vars

name: Test, Build Container & Deploy
 
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
 
env:
  GITHUB_SHA: ${{ github.sha }}
  GITHUB_REF: ${{ github.ref }}
  IMAGE: namespace/imagename
  REGISTRY_HOSTNAME: us.gcr.io
 
jobs:
  test:
    runs-on: ubuntu-latest
 
    steps:
      - uses: actions/checkout@v3
 
      - name: Use Node.js
        uses: actions/setup-node@v3
        with:
          node-version: "20" # You can change this to your preferred Node.js version
 
      - name: Install dependencies
        run: npm ci
 
      - name: Run Vitest
        run: npm run test
 

Option A - Build and Push to GCR

Build a docker container and push to Google Container Registry.

  build-and-push-to-gcr:
    runs-on: ubuntu-latest
    needs: test
    permissions:
      contents: "read"
      id-token: "write"
    steps:
      - uses: actions/checkout@v3
      - uses: RafikFarhad/push-to-gcr-github-action@v5-rc1
        with:
          gcloud_service_key: ${{ secrets.GCLOUD_KEY }} # can be base64 encoded or plain text || not needed if you use google-github-actions/auth
          registry: ${{ env.REGISTRY_HOSTNAME }}
          project_id: cognivita
          image_name: ${{ env.IMAGE }}
          image_tag: latest,${{ github.ref_name }} # set image_tag to the tag name from the git repo
          dockerfile: ./Dockerfile.worker
          context: .
 

Option B - Build and Push to a Vanilla Docker Registry

See:

  docker:
    runs-on: ubuntu-latest
    steps:
      -
        name: Set up QEMU
        uses: docker/setup-qemu-action@v3
      -
        name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      -
        name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      -
        name: Build and push
        uses: docker/build-push-action@v6
        with:
          push: true
          tags: user/app:latest

Deploy Step

Use SSH to log into the prod machine and have it pull the newly created docker image(s):

  deploy-new-containers:
    runs-on: ubuntu-latest
    needs:
      - build-and-push-to-gcr
    steps:
      - name: executing remote ssh commands using ssh key
        uses: appleboy/[email protected]
        with:
          host: ${{ secrets.DEPLOY_SSH_HOST }}
          username: ${{ secrets.DEPLOY_SSH_USERNAME }}
          key: ${{ secrets.DEPLOY_SSH_KEY }}
          script: /home/path/to/update.sh
 

Prepare Git Secrets

SSH

  • Add the IP address of the host machine under DEPLOY_SSH_HOST
  • Add the username of the user who will run the script under DEPLOY_SSH_USERNAME

Generate the SSH key for the user:

ssh-keygen -f example

Copy ssh public key to remote machine

ssh-copy-id -i example  user@machine

Copy the contents of the private key and add to repo under DEPLOY_SSH_KEY

Docker Secrets for Google GCR

If using Google, generate a service account with Artifact Registry Writer permission (see this list for more info). Add a JSON key to the service account and download it. Then base64 encode it:

base64 keyname.json

Copy the output and add it to the repo as a secret called GCLOUD_KEY

Docker secrets for non-GCR Repo

Depending on which registry you are using (See docker/login-action) add:

  • DOCKERHUB_USERNAME - to push to dockerhub registry with
  • DOCKERHUB_TOKEN - application token to push to registry with

Checklist

Server Initial Config

  • Create server VM
  • Install docker
  • Create service user (and grant docker permissions)
  • Create project folder
  • Create docker-compose.yaml
  • Create update.sh
  • Authenticate against docker repo

CI File

  • Add CI file
  • Add testing step
  • Add build step (either using google or docker registry)
  • Add deploy step

Github Secrets

  • Add Env Vars:
    • DEPLOY_SSH_HOST
    • DEPLOY_SSH_USERNAME
    • DEPLOY_SSH_KEY
    • Docker Auth:
      • DOCKERHUB_USERNAME
      • DOCKERHUB_TOKEN
    • or
      • GCLOUD_KEY

Final Checks

  • update.sh executable
  • Path to update.sh on server is correct in ci file
  • container repo name is correct in CI file and docker-compose file.
  • all env vars are set up in git
  • server is authenticated and can pull docker images from repo