GitHub Actions on your own servers is way more powerful than you think, enabling complex, secure CI/CD pipelines without hitting GitHub’s cloud limits.

Let’s see it in action. Imagine this workflow: a developer pushes code, a GitHub Actions runner on your internal network builds the Docker image, scans it for vulnerabilities, and pushes it to your private registry.

name: Internal CI/CD Pipeline

on:
  push:
    branches:
      - main

jobs:
  build_and_scan:
    runs-on: self-hosted # This is the key!
    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Build Docker image

        run: docker build -t my-app:${{ github.sha }} .


      - name: Scan image for vulnerabilities
        uses: aquasecurity/trivy-action@master
        with:

          image-ref: 'my-app:${{ github.sha }}'

          format: 'table'
          exit-code: '0' # Don't fail build on low/medium vulnerabilities
          ignore-unfixed: true
          vuln-type: 'os,library'
          severity: 'CRITICAL,HIGH'

      - name: Tag and push image
        run: |

          docker tag my-app:${{ github.sha }} your-private-registry.com/my-app:latest

          docker push your-private-registry.com/my-app:latest

The core of this is runs-on: self-hosted. This tells GitHub Actions to look for a runner registered with your Enterprise Server that has the self-hosted label. You’re essentially extending GitHub’s CI/CD infrastructure into your own environment.

To get this working, you first need to set up a GitHub Actions Runner on your own infrastructure. This involves downloading the runner software, registering it with your GitHub Enterprise Server instance, and ensuring it has the necessary permissions and dependencies (like Docker, git, and any build tools) to execute your workflows.

Here’s how you’d typically register a runner. On your server, you’d download the runner package, e.g., actions-runner-linux-x64-2.305.0.tar.gz. Then you’d run:

./config.sh --url https://your-github-enterprise.com --token YOUR_REGISTRATION_TOKEN

The YOUR_REGISTRATION_TOKEN is generated from your GitHub Enterprise Server UI, usually under Settings -> Actions -> Runners -> New runner. You’ll also need to specify the labels during configuration, such as --labels self-hosted,docker,linux. These labels are crucial for matching jobs in your workflow files to available runners.

Once registered, you’ll start the runner as a service:

./run.sh &

Your workflow file (.github/workflows/your-workflow.yml) then specifies which runner to use. In the example above, runs-on: self-hosted directs the job to any runner with the self-hosted label. You can get more granular by using multiple labels: runs-on: [self-hosted, docker, linux]. This ensures that only runners meeting all these criteria will pick up the job.

The real power comes from controlling the execution environment. You can use runners on air-gapped networks, machines with specific hardware, or systems pre-loaded with proprietary build tools, all without exposing sensitive internal infrastructure to the public internet. Your build artifacts and secrets stay within your network boundaries.

The most surprising thing is how seamlessly GitHub Actions manages distributed runners. You don’t need to set up complex orchestrators like Kubernetes or Nomad to manage the runners themselves; GitHub Enterprise Server handles the job queueing and distribution to registered runners. The runner simply polls the server for available jobs matching its labels and executes them.

The next thing you’ll want to tackle is securing these self-hosted runners, especially when dealing with sensitive operations like deploying to production.

Want structured learning?

Take the full Github-actions course →