GitHub Actions runners
Run GitHub Actions jobs on Composal. Choose VM or sandbox runners, use the included tools, or preload your own Docker image.
Contents
Use the included tools
Composal runners include Node 26, AWS CLI v2, GitHub CLI (gh) and jq.
Use them directly in your workflow steps.
actions/setup-node or your project's tool manager can select another Node
version.
Set up runner access
Install Vex 1.17.90 or newer to manage runners. Sign in and check the pools and images available to your organization:
curl -fsSL https://dl.composal.com/install-vex -o /tmp/install-vex.sh
sh /tmp/install-vex.sh
vex login
vex --org acme runner github list
vex --org acme runner images list
vex agent-help
Your organization needs GitHub Actions runner access enabled and a GitHub App connection with access to the repository. Organization owners and administrators can configure images and pools. A pool serves one exact repository/label pair.
Choose a VM or sandbox
| Workflow label | Runtime | Choose for |
|---|---|---|
composal-vm-x64-4x | VM | Docker builds, container actions and service containers |
composal-sandbox-x64-4x | Sandbox | Shell and JavaScript actions, lint and tests without Docker |
composal-sandbox-x64 | Sandbox, 1 CPU / 2 GiB | Lightweight scripts and tests without Docker |
composal-sandbox-x64-0.5x | Sandbox, 0.5 CPU / 1 GiB | Short scripts, lint and small checks without Docker |
composal-x64-4x | Organization-configured backend | Builds and tests; 4 vCPUs / 16 GiB on Boxmad-enabled pools |
composal-gen2-x64-4x | Nomad / gVisor sandbox | Docker jobs, container actions and service containers on a Boxmad-enabled pool |
composal-gen2 | Nomad / gVisor sandbox, 1 vCPU / 2 GiB | Lightweight checks on a Boxmad-enabled pool |
Each label family offers 2x, 4x, 8x and 16x sizes, with 2/4/8/16 physical CPU cores and 4/16/32/64 GiB RAM, respectively, for Modal pools. Modal sandboxes do not run a Docker daemon; use a Modal VM for container actions, service containers and Docker builds. Boxmad labels use 2/4/8/16 requested vCPUs and a private Docker daemon inside each gVisor sandbox. They require a Boxmad-enabled pool and an approved Docker-capable runner image.
Standard labels can use Boxmad without a generation suffix. An organization
admin configures the pool backend; the label keeps its CPU and memory size.
Use composal for 1 vCPU / 2 GiB, composal-x64-4x for 4 vCPUs / 16 GiB,
or composal-x64-2x-8gb for 2 vCPUs / 8 GiB. Existing pools keep their
configured backend until an admin changes it. Migrating an existing pool to Boxmad requires all
queued and running jobs to finish and their runners to be cleaned up.
Historical attempts retain their original backend and billing records. A used
Boxmad pool keeps its backend so pending or delayed Spot recovery remains free.
Retained dependency-cache volumes keep their storage measurements after migration;
changing the pool backend does not delete cached data.
Gen2 labels select Boxmad runners. Use composal-gen2 for 1 vCPU / 2 GiB,
composal-gen2-x64-2x for 2 vCPUs / 4 GiB, and the 4x, 8x or 16x suffix
for 4/8/16 vCPUs and 16/32/64 GiB. composal-gen2-x64-2x-8gb keeps two
vCPUs with 8 GiB. Existing composal-boxmad-* labels remain compatible.
Composal's archive-cache proxy applies to Node actions on the outer runner.
Actions inside a container: job use GitHub's standard cache storage. This is
the same cache boundary as Modal VM runners.
Choose one of the smaller sandbox labels for jobs that need fewer resources.
The bare composal-sandbox-x64 label selects 1 CPU and 2 GiB RAM.
The composal-sandbox-x64-0.5x label selects 0.5 CPU and 1 GiB RAM.
Use a static outbound IP
Open your repository’s Actions page or Organization settings → VCS → Nodes and find Static outbound IP. Organization owners and admins can select Request static IP. The request remains pending until a Composal admin approves and provisions the organization’s networking.
Once active, copy the displayed IPv4 CIDR into your services’ IP allowlists. The setting covers new Gen2 jobs across the organization’s repositories, including their service containers. The displayed assignment comes from the current routing policy; an activation request alone does not enable it. Classic runners are outside this setting.
Connect runners through AWS PrivateLink
Admin-managed rollout: Approved Gen2 organizations can connect native Docker
runners on AWS in Frankfurt (eu-central-1) to specific private services. Contact
Composal to request activation. The AWS PrivateLink panel on your repository’s
Actions page and organization runner settings shows the endpoint ID, region,
hostname, ports and setup status. Use Copy ID to send the endpoint ID to your
service provider for allowlisting.
Awaiting Customer Allowlisting and Awaiting Connectivity Validation mean runner access is not enabled. Enabled for AWS Runners means an admin has activated and verified tenant-scoped DNS, routing and AWS-only placement. New jobs wait for a prepared AWS worker rather than falling back to a worker without private connectivity. Existing jobs keep their original network until they finish. Internet traffic keeps the organization’s assigned public egress IP.
Private connectivity is currently qualified for native Docker runners, including job service containers. VM runners are not supported. Requesting a static outbound IP does not create a PrivateLink connection.
PrivateLink lets a runner connect to a specific service through private endpoint addresses. Your service stays in your AWS account. This connection does not grant the runner access to every resource in your VPC, and it does not replace your service's authentication or TLS configuration.
Send your service details
Ask the AWS administrator who owns your service to provide:
| Detail | Example or purpose |
|---|---|
| Composal organization and repositories | Identify the CI workloads that need access |
| Endpoint service name | com.amazonaws.vpce.eu-central-1.vpce-svc-… |
| AWS region and supported Availability Zone IDs | Confirm compatible worker placement |
| Service ports and protocol | For example, TCP 443 |
| Hostnames used by your tests | For example, api.internal.example.com |
| Private DNS configuration | Whether the endpoint service has a verified private DNS name |
| Health check and authentication requirements | Define a safe test that proves the connection |
Send service identifiers and networking requirements, not AWS credentials or application secrets. Composal will provide the consumer AWS principal ARN and the endpoint connection ID for your administrator to review.
If you have an existing PrivateLink connection with another CI provider, share its service name and DNS requirements. That provider's consumer endpoint is not reused by Composal.
Approve the connection in AWS
Your AWS administrator allows the exact consumer principal supplied by Composal on the endpoint service. When connection acceptance is required, they also approve Composal's specific endpoint request. Review the service's load balancer, target health and security groups for the intended ports. AWS documents these steps in endpoint service permissions and acceptance.
Composal configures the consumer endpoint and runner-side DNS. A provider-managed private DNS name requires domain verification; see AWS PrivateLink DNS. Your workflow should use the hostname covered by your service's TLS certificate, rather than an endpoint IP address.
Verify before changing existing access
Before customer enablement, Composal must validate organization-scoped access and required AWS placement. Jobs requiring the connection must wait for eligible AWS capacity rather than run on a machine without that connection. Classic runners and Hetzner workers are outside this rollout.
Run a canary with Composal using the agreed runner label. Check private DNS and an authenticated request to your service. If your workflow uses Docker service containers, check access from those containers too. Composal must also verify that an unrelated workload cannot reach the endpoint and that revoking the connection removes access. Keep your existing network access until your organization's canary succeeds.
If a request fails, check endpoint acceptance, private DNS resolution, target health, the permitted port and your application's credentials. PrivateLink traffic does not use a static public egress IP; public IP allowlists alone do not establish this connection. Contact Composal with the GitHub run URL and the failing hostname and port.
Recover from a Spot interruption
For confirmed AWS Spot interruptions, Composal automatically reruns the affected Gen2 job after GitHub finishes the interrupted workflow attempt. It retries each job at most twice. The job starts again from its first step, using the same commit; GitHub also reruns dependent jobs. Automatic recovery attempts have zero billable runner minutes, including dependent jobs, while their runtime remains visible. These free attempts can run even when your included CI minutes are exhausted.
The connected GitHub App needs Actions: Read and write permission. Approve GitHub’s permission-update request if the installation currently has read-only access. Normal test failures and customer cancellations do not trigger recovery. Jobs with ambiguous duplicate display names require a manual rerun.
Understand runner usage
Classic runner sizes use physical CPU cores. Usage billing uses vCPUs: each physical core counts as two vCPUs, so a 4x runner maps to the 8-vCPU CI-minute class. Completed jobs round up to whole minutes using GitHub's job start and completion times.
Classic and Gen2 Linux x64 compute rates match the published Blacksmith rate of $0.002 per vCPU-minute. Gen2 bills its requested vCPU count: $0.004/minute for 2 vCPUs, $0.008 for 4, $0.016 for 8 and $0.032 for 16. The 1-vCPU runner is $0.002/minute. Each executed job has a one-minute minimum and rounds up to the next whole minute: a 60-second job bills one minute; a 61-second job bills two. Provisioning and unused reservations do not count. Gen2 uses a separate rate class and does not use the standard-runner allowance. Classic retains the physical-core conversion and its existing allowance terms; for example, Classic 4x bills the 8-vCPU rate of $0.016/minute. Bursting into spare CPU capacity does not increase the saved allocation or its charge.
Paid runner add-ons are separate from compute:
| Service | Rate |
|---|---|
| Verified static egress IP | $100 per IP per calendar month |
| Retained Docker layer cache | $0.50 per decimal GB per calendar month |
| Persistent dependency disks | $0.50 per decimal GB per calendar month |
Add-ons are prorated from trusted usage observations. Cache storage continues while retained, including when its builder sleeps; unused sparse disk capacity is not billed. Jobs and presets sharing one dependency disk do not multiply its storage charge. The regular Actions cache, checkout cache and ordinary runner scratch disks have no additional runner add-on charge. PrivateLink is not a static-IP purchase. Static IPs still require an administrator to enable them. Existing negotiated prices and allowances take precedence. These rates do not add a new free-minute allowance.
The 1-CPU sandbox uses the 2-vCPU class; the 0.5-CPU sandbox uses the 1-vCPU class.
Composal saves the CPU and memory allocation when it launches a runner. Changing a pool later does not change that job's recorded resources. CI minutes cover the runner allocation; RAM is recorded with the job and has no separate compute charge.
Configure a runner pool
Configure a pool to use the standard tools:
vex --org acme runner github configure acme/monorepo \
--label composal-sandbox-x64-0.5x --default-image --max-concurrency 8
Then select that exact label in GitHub Actions:
jobs:
check:
runs-on: composal-sandbox-x64-0.5x
steps:
- uses: actions/checkout@v7
- run: node --version && aws --version && gh --version && jq --version
Reuse Git objects between jobs
Use Composal Checkout on a Composal runner to reuse Git objects across jobs:
permissions:
contents: read
jobs:
test:
runs-on: composal-sandbox-x64-4x
steps:
- uses: composalai/checkout@1
- run: npm ci && npm test
The working tree and .git metadata stay on local runner disk. Composal mounts
a repository-scoped Git object cache read-only. Warm checkouts of an exact
commit read those objects without a network fetch. New commits add missing
objects to the cache through a separate preparation worker.
Standard checkout inputs work, including ref, path, fetch-depth, tags and
sparse checkout. The action waits up to 10 seconds for preparation before
fetching from GitHub. If preparation is still pending, it keeps any completed
cached history attached and fetches missing data from GitHub. Preparation
continues for later jobs.
Set cache-wait-seconds between 0 and 300 to change the wait. Use 0 to start
the GitHub fetch immediately, reusing mounted objects when available.
The log message partial Git object cache means completed history remains
attached for the GitHub fetch. In that case, cache-hit: false and
checkout-source: github describe the missing objects and their transport.
The message cache unavailable means no usable object cache was attached.
All jobs in the same GitHub installation and repository can read cached source. Workflow code cannot write to this cache. Only objects confirmed as readable from the GitHub repository enter it; unpublished platform refs and credentials are excluded.
SSH, Git LFS, submodules, other repositories and runners without the mount use
the standard GitHub transport. If a later container action needs Git access but
cannot see the host mount, use dissociate: true to copy the needed objects to
the local checkout. See the action's input guide
for examples and outputs.
Mount dependency caches
Mount persistent storage at a package cache directory with a stable key:
- uses: composalai/disk-volume@v1.2.0
with:
key: npm-store-v1
path: ~/.npm
- run: npm ci
Package managers use the mounted directory in place. The post step flushes writes; there is no archive restore, copy-back or automatic cache cleanup. The checkout and workspace stay on local runner disk; this action mounts only the dependency cache path you specify.
Keys are scoped to the GitHub installation and repository. Jobs with the same key share a writable cache across branches and runner sizes, including pull requests and failed jobs. Use download stores with package integrity checks, such as npm, pnpm and Cargo caches. Keep credentials and build outputs out of these directories. Choose separate keys for tools that require exclusive access.
The action accepts an empty absolute, workspace-relative or ~/ path, with up
to five mounts per job. cache-hit: true means the directory exists; the package
manager still installs missing dependencies. The first run after this storage
update starts cold.
Cache Turborepo builds
Serve task artifacts over localhost from the mounted persistent cache:
- uses: composalai/turborepo@v0
- run: npm ci
- run: npx turbo run build
The action exports the Turbo remote cache settings and flushes artifact writes
when the job ends. Configure task outputs in turbo.json so Turbo can restore
build files. Artifacts are separated by repository, Git ref and optional key
(default turborepo-v0). Jobs on the same ref share artifacts; other refs start cold.
There is no automatic eviction. For untrusted workflows, use Turbo artifact
signatures: the mounted volume is writable by workflow code. See the
Turborepo cache guide for a complete workflow, task output
configuration, signed artifacts, and troubleshooting.
Preload your own tools
For additional tools or pinned dependencies, write a linux/amd64 Ubuntu 24.04 Dockerfile with Node 22 or newer on PATH. Custom images keep their own Node version. Keep credentials out of image layers. For example:
FROM node:24-bookworm-slim AS node
FROM ubuntu:24.04
COPY --from=node /usr/local/ /usr/local/
RUN apt-get update && apt-get install -y ca-certificates curl git gh jq
Build and push it to your registry. Register the immutable digest printed by the push, replacing the placeholder with its real 64-character SHA-256:
docker buildx build --platform linux/amd64 \
--tag ghcr.io/acme/actions-tools:v1 --push .
vex --org acme runner images create tools-v1 \
--image ghcr.io/acme/actions-tools@sha256:<64-hex-digest> --wait
vex --org acme runner github configure acme/monorepo \
--label composal-sandbox-x64-4x --image tools-v1
Composal prepares the image once, adding the Actions runner and its supporting
tools. Preparation moves through pending and building, then ends at ready
or failed. New runners reuse the prepared image. Composal replaces CMD and
ENTRYPOINT, so put permanent setup in Dockerfile RUN instructions.
Pull from a private registry
Private registries use --registry-secret RUNNER_REGISTRY. This names an active
organization secret exposed to CI whose JSON value contains exactly
REGISTRY_USERNAME and REGISTRY_PASSWORD, both nonempty strings. Store the
value using secret management with stdin or a private file. Composal uses these credentials
to pull the image; it does not add them to workflow environments. Registration does not grant
registry access.
Update tools or restore the defaults
Inspect preparation and retry a failed build:
vex --org acme runner images get tools-v1 --json
vex --org acme runner images prepare tools-v1
vex --org acme runner images wait tools-v1 --json
Ready images are immutable. Publish a new digest and image name to update tools.
Images are scoped to their organization. Switching a pool affects new runners;
running jobs finish on their original image. A pool waits for its selected image
to become ready. Verify a real GitHub job receives a composal-gh-* runner,
prints the expected tool versions, and reaches a terminal result.
Use --default-image to restore the standard toolchain. Omit both image flags
to retain the current selection. Configuring a label does not rewrite workflows
or disable other pools; each pool has its own concurrency limit.