Hosted runners
Jobs run on Pipemesh's hosted runners unless they name one of
your own. There's nothing to set up: a job with no
runner: runs on the smallest arm64 size.
Pick a size
runner: |
vCPU | Memory | Minutes count |
|---|---|---|---|
linux-arm64-small (default) or linux-amd64-small |
2 | 6.5 GiB | 1× |
linux-arm64-medium or linux-amd64-medium |
4 | 13 GiB | 2× |
linux-arm64-large or linux-amd64-large |
8 | 26 GiB | 4× |
backend_test:
job_type: build
stage: checks
runner: linux-arm64-medium # two test JVMs side by side
script: ./gradlew test
A job past its memory is stopped (out of memory), not its neighbors. CPU isn't capped: a small job gets about 2 vCPU on a full machine, and more on a quiet one. Each job's log ends with what it used:
[runner] linux-arm64-small: peak memory 3.4 GiB of 6.5 GiB, CPU time 131 s
arm64 or amd64
arm64 is the default, and most languages and tools run on it natively.
Use linux-amd64-* for what only runs or builds on x86-64. Both
architectures count the same.
- Your
image:needs a variant for the runner's architecture. Most official images have both, and so does Pipemesh's job image. - Builds target the runner's architecture unless they ask otherwise:
docker buildandpublish:images,npm cinative modules,vercel buildoutput. To ship amd64, use an amd64 runner or build for both. - Caches are shared by both architectures. If a job switches and
caches native builds (
node_modules, a compiledtarget/), put the architecture in the key:key: deps-amd64-${checksum:package-lock.json}.
Minutes and limits
Your organization is charged a hosted job's seconds, from when a runner takes it until it reports, weighted by its size. Docker doubles it. The month's total rounds up to minutes once, so a 20-second job costs 20 seconds. Your own runners aren't metered.
Each organization's limits:
- At once: 20 vCPU, so 10 small jobs or 5 medium.
- A month: 2,000 minutes (calendar month, UTC).
- Sizes: small and medium on either architecture. Large on request.
- A job: its
timeout_seconds:(one hour when absent), 60 minutes at most. At the limit, the job fails.
A job over a limit is held, not failed. Its card reads queued · held with the reason, and it starts once its size fits or the month rolls over.
Queued jobs take turns by organization, then by pipeline, so no burst starves the rest. See usage and allowance in Settings → Organization → Compute.
The job image
With no image: or image_from:, a job runs in Pipemesh's job image:
Ubuntu 24.04, like GitHub's runners, with git, curl, wget, jq,
make, zip, Python 3.12, Node 24 LTS, the AWS CLI and Docker, and
sudo. Scripts from GitHub Actions keep their sudo apt-get install,
and pip install works outside a venv too.
pipemesh/runner-images has
its contents and releases. Jobs pick up a release from their next pod.
For other languages, use your own image:. It's used as-is and pulled
without credentials, so it must be public, and it needs bash, git,
curl and tar. An image_from: image is pulled with Pipemesh's own
credentials.
Docker
dockerd: true gives a job a Docker daemon, for Testcontainers,
docker compose or docker build. Jobs that publish: images get one
too. These jobs run on your organization's own machines and count twice.
- It starts in the background. The first
dockercommand waits for it. A tool that uses the socket directly should run afterdocker info. - Without it, the
dockerCLI still does what needs no daemon (docker buildx imagetools,docker login). A command that needs one says so. - No service containers. No runner starts
services:.
Good to know
- One shell.
before_script:,script:andafter_script:run as one script, which stops at the first failing command. Soafter_script:doesn't run after a failure. - Lost jobs. A job pod that dies is reported at once. If a runner goes silent, its job fails after ten minutes. Your organization is charged up to the runner's last word.
- Isolation. Jobs without Docker run unprivileged on shared machines, can't raise their privileges, and get the runtime's default system-call filter. Jobs with Docker run on their organization's own machines. All reach the internet (Pipemesh at its public address), never Pipemesh's own network.
Building for both architectures
publish: builds natively for its runner's architecture. For one tag
with both platforms, run docker buildx in a job with dockerd: true.
The default job image carries buildx.
Cross-compile when the toolchain can. A stage that runs on the build
platform and compiles for the target needs no emulation: Go with
GOARCH, a JVM jar, a JavaScript bundle. The other platform's stage
only copies the result in.
FROM --platform=$BUILDPLATFORM golang:1.24-alpine AS build
ARG TARGETOS TARGETARCH
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /app .
FROM alpine:3.22
COPY --from=build /app /app
ENTRYPOINT ["/app"]
Emulate the rest. A RUN step in the other platform's stage
(apt-get install, npm ci with native modules) fails with
exec format error until the job registers QEMU. The job's Docker can
run privileged containers, so that's one line. Emulated steps run
several times slower.
The default builder makes one platform per build, so create your own:
image:
job_type: build # checks out the whole repository: the build context
stage: build
dockerd: true
variables:
IMAGE: ghcr.io/acme/app
produces:
app: oci
script: |
docker run --privileged --rm tonistiigi/binfmt --install amd64
docker buildx create --use
docker buildx build --platform linux/arm64,linux/amd64 --push \
--metadata-file meta.json -t "$IMAGE:$CI_COMMIT_SHORT_SHA" .
pipemesh produce oci app --ref "$IMAGE" \
--digest "$(jq -r '."containerimage.digest"' meta.json)"
pipemesh produce checks that the digest resolves in the registry, then
records it as the job's app entry. Other jobs consume it like a
publish: image, and each platform pulls its own image from the tag.
On an amd64 runner, install arm64 instead.