vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Run CI jobs in a container chosen by the runner
7da9111ecabb236d48fc48592cd3d1d45f9afe78 · Keenan Nemetz · 15d ago · parent 474a812d
Run CI jobs in a container chosen by the runner Each lease gets a disposable checkout and, when --image is set, its own container from that image. The runner keeps the SSH key and trusted Git cache outside the job, so workflow code cannot reach either; older caches are rebuilt because a previous job may have written to them. The record names the image and runtime, and the queue mirrors them for the run page. Without --image the runner still executes on the host and says so.
docs/ci.md +50
| @@ -109,6 +109,56 @@ vole runner --remote ssh://git@host:2222 --repos demo --repos docs | |||
| 109 | 109 | access to a repository. | |
| 110 | 110 | - `--dir` chooses where checkouts are kept. | |
| 111 | 111 | ||
| 112 | ### Run each job in a container | ||
| 113 | |||
| 114 | A runner can execute every job's steps in a container built from an image you | ||
| 115 | choose. The image belongs to the machine, not to the workflow: the workflow still | ||
| 116 | asks only for labels, and the operator decides what those labels mean. | ||
| 117 | |||
| 118 | ```sh | ||
| 119 | vole runner --remote ssh://git@host:2222 \ | ||
| 120 | --image golang:1.27-bookworm@sha256:... \ | ||
| 121 | --container-runtime podman \ | ||
| 122 | --label linux --label go1.27 | ||
| 123 | ``` | ||
| 124 | |||
| 125 | One container is created per job and removed afterward. The workflow's steps run | ||
| 126 | inside it, but the runner process stays on the host: the SSH key, the trusted Git | ||
| 127 | cache, and the record push are never exposed to the job. Each job gets a fresh | ||
| 128 | clone, so a job cannot leave a Git hook or a modified checkout behind for the next | ||
| 129 | one. | ||
| 130 | |||
| 131 | The image is the dependency manifest. It should contain the toolchains and system | ||
| 132 | packages the builds need, plus `/bin/sh` and a POSIX `sleep` (Vole runs shell steps | ||
| 133 | and keeps the container alive between them). Pin it by digest; a mutable tag under | ||
| 134 | the same label silently changes what a build means. | ||
| 135 | |||
| 136 | Container defaults are intentionally closed: | ||
| 137 | |||
| 138 | - no network (`--container-network none`); add `bridge` only when a build needs it; | ||
| 139 | - read-only root filesystem, with `/tmp` as a writable tmpfs; | ||
| 140 | - no capabilities, `no-new-privileges`, and no host mounts except the job tree; | ||
| 141 | - no Docker socket, SSH agent, or host environment; | ||
| 142 | - the steps run as the runner user's `uid:gid` by default. | ||
| 143 | |||
| 144 | Use `--container-runtime` to choose `docker`, `podman`, or `nerdctl`. The runner | ||
| 145 | user must be able to talk to that runtime: rootless Podman is preferred, because | ||
| 146 | membership in the Docker group is effectively root on the host. Do not mount the | ||
| 147 | host's Docker socket into a job; that would undo the isolation. | ||
| 148 | |||
| 149 | `--container-pull` controls whether an image is pulled (`missing`, `always`, or | ||
| 150 | `never`). `--container-user`, `--container-userns`, `--container-memory`, | ||
| 151 | `--container-cpus`, and `--container-pids` override the user, namespace mode, and | ||
| 152 | resource limits. Rootless Podman uses `keep-id` by default so the numeric user can | ||
| 153 | write the bind-mounted job tree; for rootless Docker, choose a user that matches | ||
| 154 | the runtime's mapping (container root is the runner user) or use a runtime with | ||
| 155 | `keep-id`. | ||
| 156 | |||
| 157 | Without `--image`, jobs run directly on the runner host, as they did before. That | ||
| 158 | is useful for a trusted machine, but it is not a sandbox. The run record names the | ||
| 159 | image and runtime when a container was used, so the run page says what actually | ||
| 160 | executed. | ||
| 161 | |||
| 112 | 162 | All runners share one pool. An idle runner waits for work instead of asking | |
| 113 | 163 | repeatedly, and Vole gives the next job to the repository with the least work in | |
| 114 | 164 | progress. Set `max_jobs_per_repo` when you need a hard limit per repository. | |