Build docker and deploy from GitHub Actions

I will provide brief instructions on how to quickly assemble a project and deploy docker. The days of Docker have not gone away; we still do all small projects on regular Docker, as well as where everything is on-premise and clouds with Kubernetes do not provide it. Maintaining Kubernetes yourself is still a pleasure, especially when customers do not allocate budgets for “golden” (Vanya, hello!) devopsers.

The flow will be very simple: with one job we collect an image (indicating a tag or branch) and put it in a private GitHub image repository, and deploy the other job from there. This is convenient when there are several environments and we build and launch the container from it once with different environment variables. And an important condition: we do not install docker-compose or github runner on the docker server.

Let's begin.

Build the image

Given: a very simple image for a python application. This is not a security and multistage sample, just an example Dockerfile:

FROM python:3.10-slim

WORKDIR /app/

RUN python -m pip install --upgrade pip

COPY requirements.txt .
RUN python -m pip install -r requirements.txt

COPY ./ .

EXPOSE 8501
CMD ["streamlit", "run", "app.py"]

For its deployment it is used docker-compose.yml

version: '3'

services:
  bot:
    image: ${IMAGE_REF}
    container_name: globe
    restart: always
    environment:
      - OPENAI_API_KEY=${OPENAI_API_KEY}
    ports:
      - "8501:8501"

In this case, we have only two environment variables that need to be registered. The first parameter is a link to the image, and the second is the API key. For example, we always use different API keys for all projects and environments (in this case for OpenAI) to track expenses.

Build action file .github/workflows/build.yml:

name: Build and Push Docker Image

on:
    push:
        branches:
            - '*'
env:
    REGISTRY: ghcr.io
    IMAGE_NAME: ${{ github.repository }}

jobs:
    build:
        runs-on: ubuntu-latest

        steps:
            - name: Checkout code
              uses: actions/checkout@v4

            - name: Log in to the Container registry
              uses: docker/login-action@v3.0.0
              with:
                registry: ${{ env.REGISTRY }}
                username: ${{ github.actor }}
                password: ${{ secrets.GITHUB_TOKEN }}
      
            - name: Extract metadata (tags, labels) for Docker
              id: meta
              uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
              with:
                images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
            
            - name: Build and push Docker image
              uses: docker/build-push-action@v5.3.0
              with:
                context: .
                push: true
                tags: ${{ steps.meta.outputs.tags }}
                labels: ${{ steps.meta.outputs.labels }}
                

Let's look at it in a little more detail:

  1. The container is built for each code push in git. In simple projects this is ok, in complex ones you have to add additional conditions for folders, branches, etc.

  2. We take the source code;

  3. Login to the GitHub container repository ghcr.io;

  4. We get the name of the current code branch;

  5. We build and push the docker image.

If you don't change anything, the push will happen automatically. If you are building several containers with different names that differ from the name of the git repository, then you will need to go to Packages and change the access parameters: which git rep has access to which docker repos.

As a result of these actions, you will have a running and, possibly, successfully completed job in Actions.

As a result of these actions, you will have a running and, possibly, successfully completed job in Actions.

And in the repository we see a new docker image, example:

Container deployment

We have already assembled the image, now it’s time to deploy it to the server.

Secrets

Secrets

In the properties of the GitHub repository, you need to add secrets either for the entire repository or for a specific environment (dev, staging, production etc.).

  1. OPENAI_API_KEY – API key of the external service

  2. SSH_HOST – server where we deploy

  3. SSH_PRIVATE_KEY – private ssh key

  4. SSH_USER – ssh user name

Importantly, for security reasons, you should not open ssh to everyone on servers; it is better to limit the GitHub Runner cloud or hosted by IP address. It is also advisable to install a human balancer or nginx on a separate server in front of your docker server.

Deployment script example .github/workflows/deploy.yml:

name: Deploy

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}
  
on:
  workflow_dispatch

jobs:
  deploy:
    runs-on: ubuntu-22.04
    environment: production

    steps:
      - name: Checkout 
        uses: actions/checkout@v4.1.1
        env:
          GIT_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          fetch-depth: 0

      - name: Install ssh keys
        run: |
          install -m 600 -D /dev/null ~/.ssh/id_rsa
          echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
          ssh-keyscan -H ${{ secrets.SSH_HOST }} > ~/.ssh/known_hosts
          docker context create remote --docker host=ssh://${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}

      - name: Extract metadata (tags, labels) for Docker
        id: meta
        uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

      - name: Log in to the Container registry
        uses: docker/login-action@v3.0.0
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Docker compose
        env:
          IMAGE_REF: ${{ steps.meta.outputs.tags }}
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
        run: |
          docker-compose --context remote -f docker-compose.yml up -d

      - name: cleanup
        run: rm -rf ~/.ssh

Some of the steps are repeated from the build, but the most interesting thing here is creating a docker context via an SSH connection. You can use different servers for different environments.

The penultimate step is to execute docker-compose with the new context. The new image will be automatically downloaded and deployed on the desired server.

The last step is to delete the SSH keys.

This job is launched manually (you can do it automatically) from the Actions menu

Conclusion

The given guide will allow a developer to do a deployment on his own without a devops specialist, but obviously this tutorial should be used with caution and adapted to your tasks and security requirements.

PS. Written without ChatGPT

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *