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]

  # Run tests.
  # See also
    runs-on: ubuntu-latest

      - uses: actions/checkout@v2
      - name: Set up Ruby
        uses: ruby/setup-ruby@v1
          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

      - main

  # Push image to GitHub Packages.
  # See also
    runs-on: ubuntu-latest
    if: github.event_name == 'push'

      - uses: actions/checkout@v2

      - name: Build image
        run: docker build . --file Dockerfile --tag

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

      - name: Push image
        run: docker push

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

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


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

set -e

echo 'Removing dangling images'
yes | docker image prune

echo 'Pulling latest'
docker pull

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

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

echo 'Starting the container'
docker run --rm --name my-app -d -p 3000:3000