4 Min Read

Implementing CI/CD with GitHub Actions to Deploy Docker‑Compose Services

Step‑by‑step guide to set up a GitHub Actions pipeline that builds Docker images, pushes them to a registry and runs docker‑compose on a server, with tips

GitHub Actions workflow that builds and deploys a Docker‑Compose stack

1. Prerequisites

  • A GitHub repository that contains your docker-compose.prod.yml and a Dockerfile for each service.
  • A Docker registry where the images can be stored (DigitalOcean Container Registry, Docker Hub, etc.).
  • SSH access to the target host (a DigitalOcean droplet, Hetzner server, etc.) with a private key added to the repository secrets.
  • The host must have Docker Compose installed and a directory where the compose file will live.

2. Store secrets safely

Add the following secrets in Settings → Secrets → Actions:

  • SERVER_ENV_PROD, the full contents of the .env file required by your compose file.
  • DIGITALOCEAN_ACCESS_TOKEN, API token for the registry login.
  • GL_SSH_HOST, GL_SSH_USERNAME, GL_SSH_SECRET, GL_SSH_PORT, SSH connection details.

To make the secrets visible inside the job you also need to declare the environment name on the job level, otherwise the secrets stay hidden see this answer.

3. Build and push images

The workflow below follows the pattern described in a high‑voted Stack Overflow answer see source. It checks out the code, creates the .env file, builds the images with Docker Compose, logs in to the registry and pushes the images.

name: Server Build & Push

on:
  push:
    branches: [main]
    paths:
      - 'server/**'
      - 'shared/**'
      - docker-compose.prod.yml
      - Dockerfile

jobs:
  build_and_push:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Checkout the repo
        uses: actions/checkout@v2
      - name: Create env file
        run: |
          touch .env
          echo "${{ secrets.SERVER_ENV_PROD }}" > .env
          cat .env
      - name: Build image
        run: docker compose -f docker-compose.prod.yml build

      - name: Install doctl
        uses: digitalocean/action-doctl@v2
        with:
          token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}

      - name: Log in to DO Container Registry
        run: doctl registry login --expiry-seconds 600

      - name: Push image to DO Container Registry
        run: docker compose -f docker-compose.prod.yml push

      - name: Deploy Stack
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.GL_SSH_HOST }}
          username: ${{ secrets.GL_SSH_USERNAME }}
          key: ${{ secrets.GL_SSH_SECRET }}
          port: ${{ secrets.GL_SSH_PORT }}
          script: |
            cd /srv/www/game
            ./init.sh

The ./init.sh script on the server is a tiny wrapper that runs the actual compose deployment:

docker stack deploy -c <(docker-compose -f docker-compose.yml config) game --with-registry-auth

The --with-registry-auth flag ensures the remote Docker engine can pull private images that were just pushed see source.

4. Deploy with docker‑compose on the target host

  1. Copy the compose file, the workflow pushes the images but does not copy the compose file; you can either keep the file in the repo and pull it on the server via git pull, or scp it in the SSH step.
  2. Run the init script, the SSH action executes ./init.sh, which expands the compose file with the .env values (Docker Compose does not read .env when used with docker stack deploy, so the <(docker-compose … config) trick pre‑processes it).
  3. Verify, after the stack is up, check the service status with docker service ls and docker stack ps game.

5. Common pitfalls and how to avoid them

  • Missing environment on the job, without the environment: key the secrets stay hidden and the build will fail when it tries to read $SERVER_ENV_PROD.
  • Registry login expiry, the doctl registry login --expiry-seconds 600 command gives a short‑lived token; keep the push step within that window.
  • Compose file paths, the paths: filter in the on: block must include every file that, when changed, should trigger a new build; otherwise a change in a service’s Dockerfile will be ignored.
  • SSH host key verification, the Appleboy SSH action disables strict host key checking by default; for production you may want to add a known‑hosts file to avoid man‑in‑the‑middle risks.
  • Bandwidth constraints, use multi‑stage Dockerfiles and keep base images small (Alpine, Distroless) to minimise the data transferred over limited African links.

Quick checklist you can copy

StepDone?
Add all required secrets in GitHub
Declare environment: production on the job
Verify Dockerfiles are multi‑stage and small
Ensure target host has Docker Compose and SSH key
Test the init.sh script manually before CI runs

Deploying Docker‑Compose with GitHub Actions is a lightweight way to get continuous delivery without moving to a full Kubernetes stack. It fits well for NGOs, government agencies or startups that need a predictable, auditable pipeline while keeping operational costs low.

Sources

Ready to turn this into a production‑grade pipeline? Talk to our team about it.


Frequently Asked Questions

Common questions on this topic, answered by the Afriq Silicon team.

What kind of Docker registry can I use with this workflow?
Any registry that supports Docker login works; the example uses DigitalOcean Container Registry but you can swap in Docker Hub, GitHub Packages or a private registry.
How are secret values passed to the workflow?
Secrets must be defined in the repository settings and referenced as ${{ secrets.NAME }}; to make them visible to jobs you also need to set the environment field on the job [see this answer](https://stackoverflow.com/a/77450398).
What if my compose file references environment variables?
Create a .env file inside the runner step and write the secret values into it before building; the workflow in the example does exactly that.
Can I run this on a low‑bandwidth server in Kenya?
Yes, the workflow pushes images first, then the remote host pulls only the needed layers; using a small base image and layered builds reduces transfer size.
What happens if the deployment step fails?
The job will stop and report the error; you can add a retry step or a manual approval before the final SSH command to guard against unintended restarts.

Related

Similar Articles

Stay Informed with Our Latest Articles: Explore the most recent insights, trends, and updates from our industry experts. Dive into a wealth of knowledge to keep you ahead in the ever-evolving tech landscape.

The real reasons software procurement contracts stall in Kenya and how to fix them

September 8th, 2026

project

The real reasons software procurement contracts stall in Kenya and how to fix them

Learn why many software procurement contracts Kenya fail, the clauses that cause risk, and how to embed compliance from day one for public‑sector projects.

By Titus Mwangi 4 Min Read
Software Development in Kenya: 2026 Buyer's Guide

June 18th, 2026

projecttrends

Software Development in Kenya: 2026 Buyer's Guide

A market briefing for procurement teams, program directors, and technology leaders considering Kenya as a software delivery base.

By Harman Kibue, Titus Mwangi 4 Min Read
Enterprise System Integration in Africa: A Practical Guide

June 11th, 2026

trendsproject

Enterprise System Integration in Africa: A Practical Guide

What connecting business systems actually involves in African markets: M-Pesa callbacks, platforms with no API, and designing for the failure path.

By Titus Mwangi 4 Min Read
Custom Software vs Off-the-Shelf in Kenya: 2026 Buyer's Guide

June 2nd, 2026

trendsproject

Custom Software vs Off-the-Shelf in Kenya: 2026 Buyer's Guide

Off-the-shelf looks cheaper on paper. Here's the real Total Cost of Ownership comparison for Kenyan institutions in 2026.

By Titus Mwangi 3 Min Read
noise

Let’s Build Something
Amazing Together

Afriq Silicon

We will help you turn ideas into digital reality whatever industry you want to revolutionize

Solutions

Contact

© 2026 Afriq Silicon, Inc. All rights reserved