Self-hosted GitHub Actions runner setup
The CD deploy job runs on a self-hosted GitHub Actions runner installed directly
on the mini PC (production machine). This gives the runner direct access to
kubectl, helm, and the local Kubernetes cluster — no SSH hop from GitHub's
cloud infrastructure is needed.
Prerequisites — tools that must be installed
Install these on the mini PC before registering the runner.
| Tool | Required version | Install command |
|---|---|---|
| Git | any recent | sudo apt install git |
| Node.js | 22.x (matches CI) | see NodeSource |
| npm | bundled with Node | — |
| Docker | 24+ | Docker Engine install guide |
| kubectl | matches cluster | kubectl install |
| helm | 3.x | Helm install |
Verify they are accessible to the user that will run the runner:
git --version
node --version # should print v22.x
npm --version
docker info
kubectl get nodes
helm version
Step 1 — Register the runner
- Go to https://github.com/lihor-hub/news-dashboard/settings/actions/runners
- Click New self-hosted runner
- Select Linux / x64
- Follow the on-screen download and configuration steps:
mkdir -p ~/actions-runner && cd ~/actions-runner
# Download and extract the runner tarball (copy exact URL from the GitHub UI)
curl -o actions-runner-linux-x64.tar.gz -L <URL_FROM_GITHUB_UI>
tar xzf actions-runner-linux-x64.tar.gz
# Register — use the token shown on the GitHub UI; it expires in 1 hour
./config.sh --url https://github.com/lihor-hub/news-dashboard \
--token <REGISTRATION_TOKEN_FROM_GITHUB_UI> \
--name ioachim-minipc \
--labels self-hosted
Accept the defaults for the work folder (_work).
Step 2 — Install as a systemd service
Run the helper script that ships with the runner to create and enable a systemd unit. This ensures the runner restarts automatically on reboot.
cd ~/actions-runner
sudo ./svc.sh install # creates /etc/systemd/system/actions.runner.*.service
sudo ./svc.sh start
sudo systemctl status "actions.runner.*"
Check that the runner appears as Idle in the GitHub UI at
https://github.com/lihor-hub/news-dashboard/settings/actions/runners.
To manage the service later:
sudo systemctl stop "actions.runner.*"
sudo systemctl start "actions.runner.*"
sudo systemctl restart "actions.runner.*"
Step 3 — Environment variables and secrets
The deploy workflow reads one GitHub Actions secret. Add it at https://github.com/lihor-hub/news-dashboard/settings/secrets/actions:
| Secret name | Description |
|---|---|
GHCR_TOKEN | A GitHub Personal Access Token (classic) with read:packages scope only. Used to authenticate Docker and to refresh the Kubernetes imagePullSecret on every deploy. Generate at https://github.com/settings/tokens — set a 1-year expiry and rotate when it expires. |
GITHUB_TOKEN is provided automatically by GitHub for the test and publish jobs;
no manual secret is needed for it.
If the GHCR package is made public (repo Settings → Packages → Change
visibility), GHCR_TOKEN is not required and the docker login / kubectl create secret lines in the workflow can be removed.
No environment variables need to be set at the OS level for the runner user
beyond what the tool installs already place in PATH. If kubectl or helm
are not in the runner user's PATH, add them to ~/.profile for that user.
Step 4 — First-time cluster check
Confirm kubectl is pointing at the correct cluster:
kubectl get nodes
The deploy workflow creates the news-dashboard namespace before refreshing
secrets, and Helm still runs with --create-namespace during install/upgrade.
No manual namespace creation is required before the first workflow deploy.
How the full flow works after setup
git push origin main
└─ test job (ubuntu-latest) pytest + tsc/vite build
└─ publish job (ubuntu-latest) docker build + push to GHCR with :<sha>
└─ deploy job (self-hosted)
├─ docker login ghcr.io
├─ docker pull <image>:<sha>
├─ kubectl apply imagePullSecret
├─ git fetch origin main + checkout FETCH_HEAD (sync chart changes)
├─ helm upgrade --set image.tag=<sha>
├─ kubectl rollout status --timeout=120s
└─ curl http://localhost:30088/api/health smoke test
The deploy job uses concurrency: group=deploy-production, cancel-in-progress: false
so rapid pushes queue rather than race.
Troubleshooting
Runner shows Offline in GitHub UI
sudo systemctl restart "actions.runner.*"
journalctl -u "actions.runner.*" -n 50
kubectl / helm not found during deploy
Add the missing binary's directory to ~/.profile for the runner user and
restart the runner service.
Image pull backoff in pods
kubectl -n news-dashboard get secret ghcr-pull-secret
# If missing, re-run the workflow or create it manually:
kubectl -n news-dashboard create secret docker-registry ghcr-pull-secret \
--docker-server=ghcr.io \
--docker-username=<github-actor> \
--docker-password=<GHCR_TOKEN>
Helm "release not found" error
This is harmless — --install creates the release on first run.
Deployment name
The Helm release name news-dashboard + chart name news-dashboard produces
news-dashboard-news-dashboard for pods and deployments. This is expected
(Helm double-name pattern).
Production HTTPS routing on the runner's host is documented against the single
source-of-truth Caddy config at deploy/Caddyfile — see
HTTPS with Caddy.