RegularDev

Contents

Introduction

In the previous article, we established a setup for our project on a VPS. However, this setup has a significant drawback: every time you need to update your project, you must:

This process involves switching environments and manually running Docker commands on your VPS, which can be cumbersome and slow, especially when deploying urgent fixes. Let’s simplify our lives by automating these deployment steps.

Image to Registry

The first step we’ll automate is building and storing the Docker image. Instead of handling this on our VPS, we will set it up to happen automatically in a remote registry using GitHub Actions. GitHub Actions is a feature integrated into GitHub that allows you to run pipelines for your projects. To enable this, create the following directory and file in your project:

.github/workflows/docker-publish.yml

The content of this file should look something like this:

#
name: Create, publish and deploy
 
# Configures this workflow to run every time a change is pushed to the branch called `main`.
on:
  push:
    branches: ['main']
 
# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds.
env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.actor }}/astro-blog:latest
 
# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu.
jobs:
  build-and-push-image:
    runs-on: ubuntu-latest
    if: "!contains(github.event.head_commit.message, 'ci skip')"
    # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
    permissions:
      contents: read
      packages: write
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
      - name: Log in to the Container registry
        run: |
          echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
      - name: Build and push Docker image
        run: |
          docker build . --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

The steps in this YAML file are self-explanatory. I borrowed parts of this file from the one used for this website.

Two important points to note:

if: "!contains(github.event.head_commit.message, 'ci skip')"

Deploy

Now that our image is built and stored in the registry, we must still manually pull it on our VPS and instruct Docker to deploy the new container. To automate this, we’ll update our docker-publish.yml file to include a new job named deploy, which only runs after the build-and-push-image job is complete.

#
name: Create, publish and deploy
 
# Configures this workflow to run every time a change is pushed to the branch called `main`.
on:
  push:
    branches: ['main']
 
# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds.
env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.actor }}/astro-blog:latest
 
# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu.
jobs:
  build-and-push-image:
    runs-on: ubuntu-latest
    if: "!contains(github.event.head_commit.message, 'ci skip')"
    # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
    permissions:
      contents: read
      packages: write
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
      - name: Log in to the Container registry
        run: |
          echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
      - name: Build and push Docker image
        run: |
          docker build . --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
  deploy:
    needs: build-and-push-image
    name: deploy image
    runs-on: ubuntu-latest
 
    steps:
      - name: install ssh keys
        # check this thread to understand why its needed:
        # <https://stackoverflow.com/a/70447517>
        run: |
          install -m 600 -D /dev/null ~/.ssh/gthb_ctn
          echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/gthb_ctn
          ssh-keyscan -H ${{ secrets.SSH_HOST }} > ~/.ssh/known_hosts
      - name: connect and pull
        run: ssh -i ~/.ssh/gthb_ctn ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} "cd ${{ secrets.WORK_DIR }} && docker compose pull && docker compose up -d && exit"
      - name: cleanup
        run: rm -rf ~/.ssh

This job will require several secrets to be added to your repository:

SSH Private Key

It’s crucial that the GitHub Actions worker logs into your VPS without a password. Here’s how to set it up:

For more details on SSH key setup, refer to this article.

Docker Compose

Our GitHub Actions workflow is almost ready. The final step is to adjust docker-compose.yml from our previous article to no longer build from the source code on the VPS:

frontend:
-  build: .
+  image: <path.to/image/in:registry>
  container_name: frontend
  env_file:
    - hosts.env

Instead, we specify in the Docker Compose file to pull the image from the registry, where it was pushed by the docker push command during the GitHub Actions workflow.

Conclusion

That’s it! Now, whenever you push changes to the ‘main’ branch without the ‘ci skip’ in the commit message, the GitHub Actions workflow will start. It will build a new image from your source code, push it to the registry, log into your VPS, navigate to the working directory with docker-compose.yml, and redeploy the containers.