Context
The owner registered a self-hosted GitHub Actions runner (label portfolio) on the production VPS,
running as deployer, which is in the docker group. The pipeline had been written for disposable
GitHub-hosted machines: it started the full stack under the production Compose project name, published
test ports, installed packages with apt, and reached the host over SSH to deploy.
Options
- Keep GitHub-hosted runners. Strongest isolation, but uses hosted minutes and ignores the owner's choice.
- Move the jobs unchanged. Would have replaced the live containers on every test run and opened test ports to the internet.
- Adapt the pipeline to share the host safely.
Decision
CI, evidence sync, CodeQL and the deploy run on the self-hosted runner, adapted to the shared host:
- Language jobs run inside pinned containers as the runner's own user; Playwright, Lighthouse and
pdftotextrun in pinned images. Nothing needsaptorsudoon the host. - The CI stack uses its own Compose project (
aliakbari-ci) and binds to127.0.0.1; the deploy-script test uses another (aliakbari-deploytest).deploy.shtakes the project name and bind address from the environment, with production defaults unchanged. - CI images are named
ci.local/akynte/*; production pruning only touchesghcr.io/akynte/*release tags, and CI cleanup only removes its ownci.localtags. - Images are scanned from
docker savetarballs, so the scanner never gets the Docker socket. - The deploy job calls
/srv/aliakbari/deploy.shdirectly. The SSH deploy key and its secrets were removed. - The uptime probe and the post-deploy verification stay on GitHub-hosted machines: a host cannot report its own outage or see itself from the internet.
- Pull requests run only when they come from this repository.
Consequences
- Trust boundary: any workflow on the runner acts with
deployer's rights, and Docker access is equivalent to root. Code in pull requests from this repository (including Dependabot updates) is built and tested on the production host. Most of it runs inside containers without the Docker socket, but repository scripts that orchestrate the stack run on the host. Only people with write access can trigger this. - Resources: builds and browser tests share 4 CPUs and 8 GB RAM with the live site; production containers keep their CPU and memory limits, but response times can rise while CI runs.
- Availability: if the host is down, CI and deploys stop too (the uptime probe still reports it).
- Disk: the build cache is capped with
docker buildx prune --max-used-space 12gbafter each stack job.