---
title: "How to Automate Deployments (CI/CD) with GitHub Actions Using a Local Mini PC (Self-Hosted Runner)"
description: "Learn how to automate deployments to a local Mini PC using GitHub Actions and a Self-Hosted Runner: zero GitHub minutes used, extreme firewall security without port forwarding, and Docker workflows."
canonical_url: "https://blog.esnube.es/posts/ci-cd-self-hosted-runner-en"
---

> Index of every page on this site: [llms.txt](https://blog.esnube.es/llms.txt)

Automating deployments can be a real headache when you're not using conventional cloud services (like AWS or Vercel) but instead relying on your own hardware—for example, **a Mini PC in your local network** acting as your production server.

In my case, my architecture looked like this:

- **Development Machine:** Where I write the code.
- **Production Server (Mini PC):** Where I run my Dockerized applications. I don't store the source code here; I only keep the `.env` files and the `docker-compose.yml` to spin up the compiled image.

How do we make it so that running `git push` from the development environment tells the Mini PC to fetch the code, build the Docker image, and restart the production service 100% automatically and without path errors?

Here is the ultimate solution.

---

## 💎 The Huge Advantages of This Architecture

Before diving into the code, it's important to highlight why this approach is so powerful compared to traditional alternatives.

### ⏳ 1. Infinite Savings on GitHub Actions Minutes

GitHub Actions offers 2,000 free minutes per month, which can run out quickly if you have heavy build processes like those for Nuxt or Node.js.
**The golden trick:** Minutes executed on *Self-Hosted* machines (your own computer) **DO NOT count towards your GitHub quota**. In this tutorial, we will use the cloud for just a few seconds to run a quick check, and offload 95% of the heavy lifting to our own Mini PC. As a result, you will never exhaust your free quota.

### 🛡️ 2. Extreme Security (Closed Firewall)

With the classic approach, GitHub Actions needed to connect to your home server via SSH, forcing you to open port 22 on your router and exposing yourself to the entire internet.
By using a *Self-Hosted Runner*, **your Mini PC connects to GitHub** securely (outbound HTTPS) to ask if there's new work. You don't have to open a single external port into your home. Your network remains completely sealed off.

### 🔑 3. Secure Secret Management and Disaster Recovery

Thanks to GitHub Secrets, your Docker registry passwords are never left in plain text on your server's disk. They are securely injected into the Mini PC's memory only during execution. Furthermore, by automatically building the image and pushing it to the registry (as we will see below), you guarantee that if the Mini PC burns out, your production state is safe and ready to be downloaded instantly on any other machine.

---

## 1. Install the GitHub Actions Runner on Your Mini PC

The first step is connecting your local server to GitHub. Instead of running actions on GitHub's servers, we are going to use a **Self-Hosted Runner**.

1. Go to your repository on GitHub > **Settings** > **Actions** > **Runners** > **New self-hosted runner**.
2. On your production Mini PC, create a temporary folder (`mkdir actions-runner` and `cd actions-runner`).
3. Run the commands GitHub provides on the web to download and install the Runner.
4. **Very important:** To ensure the Runner works in the background and doesn't depend on an open terminal, install it as a `systemd` service:
```bash
sudo ./svc.sh install
sudo ./svc.sh start
```

From now on, your Mini PC is listening for orders from GitHub.

---

## 2. The Classic Problem with Code, Docker, and Paths

When GitHub Actions tells your Mini PC to execute a task, it **downloads the code into a temporary, hidden folder** (inside `_work`).

This creates a massive problem: if you try to run `docker-compose up` from that temporary folder, it will fail with the error `no configuration file provided: not found`. Why? Because your production `docker-compose.yml` file **is not in your repository**, but in a different folder on your server.

Additionally, if you run shell commands in your GitHub Actions file, you'll often see the Runner throw errors like `docker-compose: No such file or directory` (Exit code 127). This happens because, running as a background service, the Runner operates in an extremely limited environment and cannot find standard system paths.

---

## 3. The Elegant Solution (`ci.yml`)

To solve this, there's no need to overcomplicate the architecture. We simply make the Runner **use the temporarily downloaded code to build the image**, optionally push it to a registry (for secure backup), and then tell it to **navigate to your actual production folder** to restart the service.

At the root of your repository, create the `.github/workflows/ci.yml` file with this content:

```yaml
name: CI/CD to Mini PC

on:
  push:
    branches: [ main, master ]

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout repository
      uses: actions/checkout@v3

    - name: Use Node.js
      uses: actions/setup-node@v3
      with:
        node-version: 20.x
        cache: 'yarn' # Or 'npm'

    - name: Check dependencies
      # This barely consumes seconds from your 2,000 free GitHub minutes
      run: yarn install --frozen-lockfile --ignore-engines

  deploy:
    needs: build-and-test
    runs-on: self-hosted  # Here is where the zero-cost magic happens
    if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master'

    steps:
    - name: Download code temporarily
      uses: actions/checkout@v3

    - name: Build and Deploy
      run: |
        # 1. We build the Docker image using the temporary code downloaded by the Runner
        docker build -t my-registry/my-app:latest .
        
        # Optional (Recommended): Push image for secure backup using Secrets
        # docker login my-registry.com -u ${{ secrets.REGISTRY_USER }} -p ${{ secrets.REGISTRY_PASSWORD }}
        # docker push my-registry/my-app:latest

        # 2. We navigate to your ACTUAL production folder where your docker-compose.yml and .env reside
        cd /absolute/path/to/your/production/server
        
        # 3. We export the PATH to avoid Error 127 in system services
        export PATH=$PATH:/usr/local/bin:/usr/bin:/bin
        
        # 4. We check the docker version available on your system and restart
        if command -v docker-compose &> /dev/null; then
          docker-compose down && docker-compose up -d
        elif command -v docker &> /dev/null && docker compose version &> /dev/null; then
          docker compose down && docker compose up -d
        else
          /usr/local/bin/docker-compose down && /usr/local/bin/docker-compose up -d
        fi
```

---

## 🚀 Summary of the Magical "Fixes" in this Script

1. **Avoids Double Compilation:** We don't run `yarn build` in the cloud if we're going to do a `docker build` on the Mini PC later. We save time and prevent RAM crashes on free GitHub runners.
2. **Ignores Temporary Folders:** Builds the image with the temporary code, but goes to the production folder to spin up the service. Clean separation of concerns.
3. **System Resilience:** Exporting `$PATH` and checking for both `docker-compose` and `docker compose` instantly solves the cursed Error 127 (missing binaries in background processes).

And that's it! With this, you have an infrastructure worthy of an enterprise company running on a humble Mini PC in your living room. Fully automated, hyper-secure, and totally free.

## Sitemap

See the full [sitemap](https://blog.esnube.es/sitemap.md) for all pages.
