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
.envfiles and thedocker-compose.ymlto 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.
- Go to your repository on GitHub > Settings > Actions > Runners > New self-hosted runner.
- On your production Mini PC, create a temporary folder (
mkdir actions-runnerandcd actions-runner). - Run the commands GitHub provides on the web to download and install the Runner.
- Very important: To ensure the Runner works in the background and doesn't depend on an open terminal, install it as a
systemdservice: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:
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
- Avoids Double Compilation: We don't run
yarn buildin the cloud if we're going to do adocker buildon the Mini PC later. We save time and prevent RAM crashes on free GitHub runners. - 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.
- System Resilience: Exporting
$PATHand checking for bothdocker-composeanddocker composeinstantly 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.