Last updated August 18, 2021.

Oddly, I still enjoy running my own Linux server. A few years ago, I became interested in deploying a container-based application using an automated Github-Actions-based CI/CD workflow to a $5/mo Linode VPS server. These workflows run the test suite against PRs and build and deploy docker images via SSH.

I recently split up this workflow into separate files and stopped using the Docker-based docker-compose.test.yml/sut workflow. I’ve replaced it with the native Github Ruby pipeline, which is substantially faster than building a Docker image just to run tests.

Here’s my test workflow pipeline that runs the Ruby-based test suite on every new PR.

# in .github/workflows/test.yml
name: Tests

on: [pull_request]

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: Set up Ruby
        uses: ruby/setup-ruby@v1
        with:
          bundler-cache: true
      - name: Install ruby dependencies
        run: bundle install
      - name: Install javascript dependencies
        run: yarn install
      - name: Run tests
        run: bundle exec rake

Once PRs are merged to main, a follow-up Deploy workflow builds a Docker image and deploys it to my Linode server via SSH:

# in .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches:
      - main

jobs:
  # Push image to GitHub Packages.
  # See also https://docs.docker.com/docker-hub/builds/
  build:
    runs-on: ubuntu-latest
    if: github.event_name == 'push'

    steps:
      - uses: actions/checkout@v2

      - name: Build image
        run: docker build . --file Dockerfile --tag docker.pkg.github.com/my-profile/my-repo/my-image:latest

      - name: Log into registry
        run: echo "$" | docker login docker.pkg.github.com -u $ --password-stdin

      - name: Push image
        run: docker push docker.pkg.github.com/my-profile/my-repo/my-image:latest

  deploy:
    needs: build
    runs-on: ubuntu-latest
    if: github.event_name == 'push'
    steps:
      - uses: actions/checkout@v2
      - name: Deploy
        run: |
          echo "$" > ssh_key
          chmod 700 ssh_key
          ssh -o StrictHostKeyChecking=no -i ssh_key $@$ "sh -s" < deploy.sh
          rm ssh_key

The above Deploy workflow references a custom deploy.sh script. Here’s a generic example of what that script does:

#!/bin/sh

# This script runs on the docker server to deploy the application. It can be kicked off locally via:
#
# ```
# ssh my-server < deploy.sh
# ```

set -e

echo 'Removing dangling images'
yes | docker image prune

echo 'Pulling latest'
docker pull docker.pkg.github.com/my-profile/my-repo/my-image:latest

echo 'Stopping the container'
docker container stop my-app || true

until [ "`docker ps --filter 'name=my-app' --format ''`" == "" ]; do
	sleep 0.1;
done;

echo 'Starting the container'
docker run --rm --name my-app -d -p 3000:3000 docker.pkg.github.com/my-profile/my-repo/my-image:latest startup.sh