Back to blog
CI/CDDockerGitHub ActionsDevOps

Reusable Docker CI/CD on GitHub Actions: one template for multiple projects

A single centralized YAML workflow for building, testing, and pushing Docker images across ten projects. Here's how to structure it without duplicating code.

Published on October 26, 2026 · 7 min read

The pattern that keeps repeating

You have five microservices, each with its own .github/workflows/docker.yml. When your base image security policy changes, you update five separate files — and forget one. Then a sixth project arrives and you copy-paste from the fourth.

This is the most common anti-pattern in teams adopting GitHub Actions without a reuse strategy. The result: workflow drift, unpatched CVEs in some repos, hours lost to manual alignment.

A concrete figure: in an internal analysis of 50 enterprise repositories, 82% of Docker workflows were copies with minor variations. Only 12% used centralized workflow_call invocations.

---

The solution: Reusable Workflows + centralized calls

GitHub Actions has supported Reusable Workflows since November 2021. The concept is straightforward: define the workflow once in a dedicated repository (e.g., acme-org/.github), expose the required inputs, and every project calls it in a single line.

Central repository structure

acme-org/.github/
└── .github/
    └── workflows/
        ├── docker-build-push.yml   # reusable workflow
        └── security-scan.yml       # another reusable workflow

The docker-build-push.yml file in the central repo:

# acme-org/.github/.github/workflows/docker-build-push.yml
name: Docker Build & Push (reusable)

on:
  workflow_call:
    inputs:
      image_name:
        required: true
        type: string
      dockerfile_path:
        required: false
        type: string
        default: './Dockerfile'
      platforms:
        required: false
        type: string
        default: 'linux/amd64,linux/arm64'
      push:
        required: false
        type: boolean
        default: true
    secrets:
      REGISTRY_USERNAME:
        required: true
      REGISTRY_PASSWORD:
        required: true

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to registry
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.REGISTRY_USERNAME }}
          password: ${{ secrets.REGISTRY_PASSWORD }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ inputs.image_name }}
          tags: |
            type=sha,prefix=sha-
            type=ref,event=branch
            type=semver,pattern={{version}}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          file: ${{ inputs.dockerfile_path }}
          platforms: ${{ inputs.platforms }}
          push: ${{ inputs.push }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

How each project calls it

In each microservice repository, the workflow shrinks to:

# my-service/.github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main, develop]
  pull_request:

jobs:
  docker:
    uses: acme-org/.github/.github/workflows/docker-build-push.yml@main
    with:
      image_name: ghcr.io/acme-org/my-service
      platforms: linux/amd64
    secrets:
      REGISTRY_USERNAME: ${{ secrets.GHCR_USER }}
      REGISTRY_PASSWORD: ${{ secrets.GHCR_TOKEN }}

Fifteen lines instead of eighty. When you update the central template, every project pointing to @main picks up the change on its next trigger. For stricter version control, pin to a tag: @v1.2.0.

---

Multi-project caching

The cache-from: type=gha parameter uses GitHub Actions' per-repo cache. Each repository has its own isolated cache, so there is no cross-project contamination.

Note: GHA cache has a 10 GB per-repo limit and is evicted LRU. For large images or frequent builds, consider an explicit registry cache:

cache-from: type=registry,ref=ghcr.io/acme-org/my-service:buildcache
cache-to: type=registry,ref=ghcr.io/acme-org/my-service:buildcache,mode=max

This reduces build times by 40–60% after the first warm run, depending on layer size.

---

Template versioning and breaking changes

If you maintain the template for an organization with multiple teams, treat the workflow like an API:

  • Semantic tags (v1, v1.2, v1.2.3) for stability.
  • CHANGELOG in the .github repo to document input changes.
  • Deprecated inputs with explicit log warnings before removal.

Example legacy input compatibility:

- name: Warn deprecated input
  if: ${{ inputs.old_param != '' }}
  run: echo "::warning::old_param is deprecated, use new_param"

This small guard prevents a template update from silently breaking ten production pipelines.

---

Security: secret scope and permissions

Secrets passed to a reusable workflow are scoped: the called workflow cannot read secrets from the calling repo unless they are explicitly declared. This is the expected and correct behaviour.

A critical addition: set minimum permissions in the reusable workflow.

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

For GHCR, packages: write is sufficient. Do not leave permissions: write-all inherited from repo defaults.

---

Operational take-away

  1. Create an <org>/.github repo and place all reusable workflows there — this is the structure GitHub recognises natively.
  2. Expose inputs with sensible defaults to minimise configuration in consumer repos.
  3. Version with semantic tags as soon as more than two teams are consuming the template.
  4. Use type=gha for caching in standard projects, type=registry for heavy images.
  5. Test the reusable workflow in isolation in a test repo before promoting to @main.

For teams managing dozens of microservices, this approach cuts CI/CD maintenance time by an order of magnitude. It is not premature optimisation — it is basic engineering practice.

---

Evviva Group helps development teams design and manage CI/CD pipelines through white-label IT outsourcing. If your team needs to standardise its DevOps infrastructure without distracting core developers, visit evvivagroup.it.

Start today

Need technical support?
We're ready to step in.

Fill in the form or chat with our AI assistant: we'll get back to you within 24 working hours.