Skip to main content
Novita Actions Runner Controller runs every GitHub Actions job in its own isolated Novita Sandbox. A long-running controller receives assigned jobs from GitHub, creates one fresh runner sandbox per job, and kills the sandbox when the job finishes. Each job starts from a clean environment, and there are no runner machines to maintain. The controller uses GitHub’s official actions/scaleset client to receive jobs and desired runner capacity. The same controller image can run in two places. In Novita Hosted mode, the recommended default, the controller runs in a long-running Control Sandbox on Novita, so it stays online without depending on your machine. In Local Docker mode, the controller runs on a machine you manage and still creates ephemeral runner sandboxes on Novita.
The controller uses GitHub’s Scale Set API, which GitHub currently labels Public Preview. The client API is stable, but its Go interfaces may evolve.

How it works

Hosted mode uses two public Novita templates, so there is nothing to build before deploying: Each job’s just-in-time (JIT) runner configuration reaches the runner sandbox over the adapter’s stdin and the sandbox filesystem API. The runner entrypoint reads and unlinks the file, then exports it only to the runner child process. The configuration never appears in sandbox creation environment values, metadata, process arguments, or controller logs.

Prerequisites

Prepare the following on the machine where you run the deployment script:
  • Bash and Python 3.
  • Node.js 20+ and npm, if the novita CLI is not already installed.
  • The latest novita CLI (version 2.1.0 or newer). See CLI Authentication.
  • A Novita API key. It selects the Novita team that will own the Control Sandbox and brokered Secrets; a separate browser login is not required.
  • A GitHub personal access token (PAT) that can manage self-hosted runners:
    • For a classic PAT, use repo for a repository target or admin:org for an organization target.
    • For a fine-grained PAT, grant repository Administration: read and write, or organization Administration: read plus Self-hosted runners: read and write.
  • The exact repository or organization URL to register, such as https://github.com/novitalabs/github-actions-runner-controller.
Install and verify the CLI if needed:
CLI

1

Export credentials

Export the three required values in the current shell. Do not commit them to the repository.
CLI
GITHUB_CONFIG_URL is mandatory. The script rejects the example OWNER/REPO and OWNER/REPOSITORY placeholders so it cannot silently deploy against an unintended target.Verify that the CLI sees the intended team before deployment:
CLI
2

Run the deployment script

Clone the repository and run the Hosted deployment from the repository root:
CLI
The script runs on your machine and creates the long-running Control Sandbox in Novita. On success it prints the target identity, the Control Sandbox ID, and the health state:
Keep this output. The resume script below accepts the sandbox ID it contains.
3

Point workflows at the runner

The default Scale Set label is novita-scale-set. In the target repository, point each job that should run on Novita at this label:
.github/workflows/ci.yml
For complete workflow files, see the GitHub CI examples in the Novita Sandbox cookbook.If you use a coding agent, you can also ask it to write the workflow for you:
Prompt
The controller creates or reuses the named Scale Set. It intentionally does not delete it on shutdown, so controller restarts are safe.

Multiple repositories or organizations

One controller serves exactly one GITHUB_CONFIG_URL target. How many repositories that covers depends on whether the target is a personal repository or an organization. Personal repositories. A controller registered against a repository URL only serves that repository. To use Novita runners in several personal repositories, run the deployment once per repository. The script derives a stable target ID from the normalized GITHUB_CONFIG_URL, so each repository gets its own controller identity, brokered Secrets, and Control Sandbox, and a second deployment cannot reuse or overwrite the first one.
CLI
Organizations. A controller registered against an organization URL serves every repository in that organization. Deploy once with the organization URL and the PAT scopes for an organization target described in Prerequisites:
CLI
For public repositories in the organization to use the runners, open the organization’s Settings → Actions → Runner groups → Default on GitHub and enable Allow public repositories.

Resume the Control Sandbox

The resume script accepts either the exact sandbox ID or the same GitHub target URL. Target-based lookup checks both running and paused sandboxes and fails on duplicates instead of choosing one arbitrarily.
CLI
Always pass --timeout when resuming manually. Without it, Novita resets a resumed sandbox to its short default lifetime (about five minutes). The lifecycle stays pause + auto-resume, but the controller would pause again almost immediately.
Control Sandboxes created before target metadata was introduced must still be resumed by their sandbox ID.

Alternative: Local Docker

Use Local mode when the controller should run on a machine you manage. Install Docker and prepare the same GitHub PAT and Novita API key described above. Clone the repository and copy the example environment file. The .env file is ignored by Git:
CLI
Edit .env and fill in the target URL and credentials. The remaining values match the defaults and can stay as they are:
.env
Start the controller and check its health endpoint:
CLI
Local mode pulls the immutable novitalabs/github-actions-controller:v0.1.1 image. Credentials are provided only through GITHUB_TOKEN and NOVITA_API_KEY in .env. Workflows use the same runs-on: novita-scale-set configuration shown above. For every variable the controller accepts, see Configuration.

Custom runner template

To preinstall tools, build your own runner template on top of the public runner image and point the controller at it. See Build Template for the full novita template create reference.
1

Write the template Dockerfile

Start from templates/runner/novita.Dockerfile in the controller repository. Keep the three conventions the controller relies on: the user account, the /home/user/actions-runner directory, and the entrypoint.sh start command. The controller writes each job’s JIT configuration into that directory and the entrypoint consumes it.
templates/runner/novita.Dockerfile
Add further apt-get install packages or tool downloads as needed. Install user-level tools such as rustup or nvm while USER user is active so they land in /home/user.
2

Build the template

Choose a name that does not collide with the public github-actions-runner template. Resources default to 2 vCPU and 4 GB; raise them for heavy compiles.
CLI
Rebuild with the same name after each Dockerfile change, or use a new name if the platform rejects a duplicate.
3

Set NOVITA_RUNNER_TEMPLATE

Hosted mode reads the template name at deployment time. Kill the existing Control Sandbox for the target, then redeploy with the override:
CLI
Local Docker mode reads it from .env, then docker compose up -d:
.env
Workflows do not change. Re-run a job and it lands on the new template.
One controller uses exactly one runner template. To offer several toolchains, deploy one controller per template with a distinct SCALE_SET_NAME for each, and pick the toolchain in a workflow with runs-on.

Configuration

Hosted deployment also accepts CONTROLLER_TEMPLATE, GITHUB_SECRET_NAME, NOVITA_SECRET_NAME, and CONTROL_TIMEOUT. Explicit identity or Secret name overrides are intended for advanced operation; the per-target defaults are safer for normal multi-project deployments. CONTROL_TIMEOUT controls the lifetime used during deployment and by scripts/resume-hosted.sh. For lifecycle ownership and failure boundaries, see the project’s design notes.
Last modified on November 2, 2026