Auto deploy Docker-Compose app from Github

August 01, 2026

Build image and push to private Github registry

Create a new workflow to build and push Docker images to GitHub Container Registry (GHCR) using BuildKit in a rootless and daemonless mode. Create a new file in your repository at .github/workflows/build.yml with the following content:

Generate a Personal Access Token (PAT) with write:packages and read:packages permissions and add it to your repository secrets as DEPLOY_TOKEN. This token will be used to authenticate with GHCR.

Add DEPLOY_TOKEN to your repository secrets by navigating to Settings > Secrets and variables > Actions > New repository secret.

Add the following workflow to build and push Docker images to GHCR using BuildKit in a rootless and daemonless mode:

build-image:
  runs-on: self-hosted
  steps:
    - uses: actions/checkout@v7
    - name: Configure registry
      run: |
        mkdir -p /tmp/.docker
        echo "{\"auths\":{\"ghcr.io\":{\"username\":\"${{ github.actor }}\",\"password\":\"${{ secrets.DEPLOY_TOKEN }}\"}}}" > /tmp/.docker/config.json
    - name: Build
      env:
        BUILDKITD_FLAGS: --oci-worker-no-process-sandbox
      run: |
        docker run --rm \
          --security-opt seccomp=unconfined \
          --security-opt apparmor=unconfined \
          -e BUILDKITD_FLAGS \
          -v "$GITHUB_WORKSPACE":/workspace \
          -v /tmp/.docker:/home/user/.docker:ro \
          --entrypoint buildctl-daemonless.sh \
          moby/buildkit:rootless \
          build \
          --frontend dockerfile.v0 \
          --local context=/workspace \
          --local dockerfile=/workspace \
          --output type=image,name=ghcr.io/${{ github.repository }}:latest,push=true

Install a webhook on the server

On the serve, where the app is running install a small webhook server. This server is triggered from Github ci and deploys the app container.

apt-get update
apt-get install webhook

Check status of webhook

sudo systemctl status webhook

It will show inactive because You installed the Ubuntu/Debian webhook package, and its packaged systemd service expects /etc/webhook.conf to exist.

sudo nano /etc/webhook.conf

For your deployment:

[
  {
    "id": "deploy",
    "execute-command": "/opt/deploy.sh",
    "command-working-directory": "/opt/myapp",
    "trigger-rule": {
      "match": {
        "type": "value",
        "value": "YOUR_SECRET_TOKEN",
        "parameter": {
          "source": "url",
          "name": "token"
        }
      }
    }
  }
]

Then

sudo chmod 600 /etc/webhook.conf
sudo systemctl restart webhook
sudo systemctl status webhook

The webhook should be active at this point.

Create /opt/deploy.sh file with the instruction to pull image and deploy container.

#!/bin/bash
set -euo pipefail

cd /opt/myapp

echo "$GHCR_TOKEN" | docker login ghcr.io \
  --username "$GHCR_USERNAME" \
  --password-stdin

docker compose pull
docker compose up -d

docker logout ghcr.io

Make it executable

sudo chmod +x /opt/deploy.sh

Github credentials are supplied by an env file.

sudo mkdir -p /etc/myapp
sudo nano /etc/myapp/ghcr.env

Put the contentes in the /etc/myapp/ghcr.env file

GHCR_USERNAME=username
GHCR_TOKEN=github_pat_xxxxxxxxxxxxxxxxx

Create a directory

sudo mkdir -p /etc/systemd/system/webhook.service.d

Then create a file inside the directory

sudo nano /etc/systemd/system/webhook.service.d/override.conf

Put the content in the file. Make sure port 9000 is not allowed from the public internet in your VM firewall.

Important: Do not run this before enabling the firewall.

[Service]
ExecStart=
ExecStart=/usr/bin/webhook -hooks /etc/webhook.conf -verbose -ip 0.0.0.0 -port 9000
EnvironmentFile=/etc/portfolio/ghcr.env

Restart the webhook

sudo systemctl daemon-reload
sudo systemctl restart webhook

Now modify your routes.yaml file and add the service to connect to the webhook.

http:
  routers:
    portfolio:
      rule: "Host(`yourdomain.com`)"
      entryPoints:
        - websecure
      service: app
      tls:
        certResolver: letsencrypt

    deploy-webhook:
      rule: "Host(`yourdomain.com`) && PathPrefix(`/hooks/`)"
      entryPoints:
        - websecure
      service: webhook-service
      tls:
        certResolver: letsencrypt

  services:
    app:
      loadBalancer:
        servers:
          - url: "http://myapp:8000"

    webhook-service:
      loadBalancer:
        servers:
          - url: "http://host.docker.internal:9000"

Also add extra_hosts config in your Traefik config in the Docker compose file. Without this it cannot reach the webhook running on local machine.

services:
  traefik:
    extra_hosts:
      - "host.docker.internal:host-gateway"

After that update your CI file to trigger the webhook after an image is built.

This WEBHOOK_DEPLOY_TOKEN value is set as secret value from /etc/webhook.conf file.

trigger-webhook:
  runs-on: self-hosted
  container:
    image: ubuntu@sha256:2edbbc5dc405e9612ba3584ce95480277e3eb374407b5505fe26f17df77c7dbc
  needs: build-image
  steps:
    - name: Install curl
      run: |
        apt-get update
        apt-get install -y --no-install-recommends curl ca-certificates
        rm -rf /var/lib/apt/lists/*
    - name: Trigger webhook
      run: |
          curl --fail-with-body --request POST \
            --url "https://sunnychowdhury.dev/hooks/deploy" \
            --header "Content-Type: application/json" \
            --data '{"token":"${{ secrets.WEBHOOK_DEPLOY_TOKEN }}"}'