> ## Documentation Index
> Fetch the complete documentation index at: https://namespace.so/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom Base Images for GitHub Actions

export const CenteredImage = ({src, alt, width, caption, className}) => {
  const [basePath, setBasePath] = useState("");
  useEffect(() => {
    const path = window.location.pathname;
    setBasePath(path === "/docs" || path.startsWith("/docs/") ? "/docs" : "");
  }, []);
  return <Frame caption={caption} className={className} style={{
    maxWidth: width,
    marginInline: "auto"
  }}>
			<OptimizedImage src={`${basePath}${src}`} alt={alt} />
		</Frame>;
};

Accelerate your workflows by pre-installing dependencies directly into your runner's base image.
Instead of installing packages at runtime during every job, custom base images let you create optimized runners with your tools already available, eliminating repetitive setup steps and reducing job execution time.

## Choosing an approach

There are three ways to make custom software available to your jobs.
They differ in how much of your existing setup you have to change, and in how much time each job spends getting the software in place:

1. [Custom base image](#custom-base-image) - bake your software into the runner image itself, using [APT packages](#preinstalled-packages) or a [custom Dockerfile](#custom-dockerfile-base). Namespace distributes and optimizes these images ahead of time, so a job does not pull an image at startup. This is the fastest of the three, and what we recommend if you can move your software into the runner image.
2. [Namespace registry](#namespace-registry) - run your job in a container built from an image in your workspace's private registry, [nscr.io](/docs/architecture/storage/container-registry). Your runners are already authenticated to pull from it, so this is the shortest path if you want to reuse container images you already build.
3. [External registry](#external-registry) - run your job in a container using GitHub's own `container:` support, authenticating to your registry with GitHub Actions secrets. This requires the fewest changes, as your images stay where they are today.

Options 2 and 3 pull an image at the start of every job. You can offset much of that cost by enabling [container image caching](/docs/solutions/github-actions/caching#container-images), which caches both the image layers and the unpacking step locally.

## Inspect the standard base images

Before adding your own packages, check what Namespace already ships in its standard base images.
You can browse the contents of each image in the dashboard, for example [ubuntu-26.04](https://cloud.namespace.so/workspace/actions/image/ubuntu-26.04), or from the CLI:

**List the available base images**

```bash theme={null}
nsc github base-image list
```

**Show everything installed in a specific image**

```
nsc github base-image describe ubuntu-26.04
```

The JSON output is handy for comparing the contents of two images, for example `ubuntu-24.04` vs `ubuntu-26.04`:

```bash theme={null}
diff \
  <(nsc github base-image describe ubuntu-24.04 -o json) \
  <(nsc github base-image describe ubuntu-26.04 -o json)
```

See [nsc github base-image describe](/docs/reference/cli/github-base-image-describe) for the full reference.

## Custom base image

You can extend the Namespace base image in two ways.
Selecting APT packages covers most cases and needs no Dockerfile, while a custom Dockerfile lets you install anything else, such as language toolchains or binaries fetched from a release page.
Both are configured per [Runner Profile](https://cloud.namespace.so/workspace/actions/profiles), and both produce an image that Namespace distributes and optimizes ahead of your jobs.

### Preinstalled packages

<Steps titleSize="h3">
  <Step id="enable-custom-base-image" title={<span>Enable custom base image</span>}>
    Go to the desired [Runner Profile](https://cloud.namespace.so/workspace/actions/profiles) and select a customizable base image.
  </Step>

  <Step id="select-apt-packages" title={<span>Select APT packages</span>}>
    Type the Ubuntu packages to install.

    <CenteredImage width={600} alt="Select APT packages" src="/docs/images/github-actions/aptpackages.png" />

    You can find the list of available packages from the [Ubuntu website](https://packages.ubuntu.com/jammy/allpackages?format=txt.gz).
  </Step>

  <Step id="submit-the-profile" title={<span>Submit the profile</span>}>
    After updating your profile definition the custom base image will be built.
    Once the image is ready to use, you will see a confirmation in the profile editor.

    <CenteredImage width={600} alt="APT packages ready" src="/docs/images/github-actions/aptpackagesready.png" />
  </Step>
</Steps>

You can modify the list of pre-installed packages at any time. Any update to the selection will automatically update the base image.

### Custom Dockerfile base

Using a custom Dockerfile, you can add any dependencies on top of the Namespace base image.
Dockerfile syntax is fully supported, allowing customization beyond pre-installing APT packages.

Important to know:

* The final layer **must** be based on NAMESPACE\_BASE\_IMAGE\_REF.
  The base image contains the software required to integrate with GitHub, without it your workflows won't start.
* The user in the final image **must** be runner.
  GitHub's runner software expects to run as this user.
* After an image is built, it needs to be distributed and optimized.
  Workflows using this profile may take longer to start than normal while this is still in progress.

To get started with a Dockerfile-based custom image:

<Steps titleSize="h3">
  <Step id="enable-custom-base-image-2" title={<span>Enable custom base image</span>}>
    Go to the desired [Runner Profile](https://cloud.namespace.so/workspace/actions/profiles) and select a customizable base image.
  </Step>

  <Step id="add-customization-steps" title={<span>Add customization steps</span>}>
    Add your custom Dockerfile steps to the input. Make sure to build on NAMESPACE\_BASE\_IMAGE\_REF in your final layer:

    ```shell theme={null}
    ARG NAMESPACE_BASE_IMAGE_REF=""
    FROM ${NAMESPACE_BASE_IMAGE_REF} AS base
    ```

    <CenteredImage width={600} alt="Custom Dockerfile" src="/docs/images/github-actions/dockerfile-based-custom-image.png" />
  </Step>

  <Step id="submit-the-profile-2" title={<span>Submit the profile</span>}>
    After updating your profile definition the custom base image will be built.
    Once the image is ready to use, you will see a confirmation in the profile editor.

    <CenteredImage width={600} alt="Custom Dockerfile ready" src="/docs/images/github-actions/dockerfile-based-custom-image-ready.png" />
  </Step>
</Steps>

You can modify the Dockerfile at any time. Any update will automatically rebuild the base image.

After a new image is built, it needs to be distributed and optimized before it can be used.
This ensures your custom base images start with the same fast performance as Namespace's standard base images.

## Namespace registry

If you already build container images that contain your tooling, you can run your jobs in those images instead of baking the software into the runner's base image.

Every workspace comes with its own private [container registry](/docs/solutions/docker-builders/registry) at `nscr.io`, and your runners are already authenticated to pull from it.
Your workflow does not need registry credentials or secrets.

<Steps titleSize="h3">
  <Step title="Push your image to nscr.io">
    Configure your local Docker (or your image build pipeline) to push to your workspace registry:

    ```bash theme={null}
    nsc docker login
    ```

    ```bash theme={null}
    docker build . -t nscr.io/<workspace-id>/ci-base:latest --push
    ```

    Your workspace identifier can be found in the [Dashboard](https://cloud.namespace.so/workspace/settings).
  </Step>

  <Step title="Reference the image in your job">
    ```yaml theme={null}
    tests:
      runs-on: namespace-profile-default
      container:
        image: nscr.io/<workspace-id>/ci-base:latest
      steps:
        - uses: actions/checkout@v4
        - run: make test
    ```
  </Step>
</Steps>

Jobs running in a container need extra configuration to reach Namespace features such as Cache Volumes, Git mirrors, and in-runner builders.
See [Running Jobs in Containers](/docs/reference/github-actions/runner-configuration#jobs-in-containers) for working examples.

<Info>
  The image is pulled at the start of every job. Enable [container image caching](/docs/solutions/github-actions/caching#container-images) in your runner profile to make repeated pulls complete in seconds.
</Info>

## External registry

If your images live in a registry outside Namespace, you can keep using them through GitHub's own container job support.
Store the registry credentials as GitHub Actions secrets and pass them to the `container` block:

```yaml theme={null}
tests:
  runs-on: namespace-profile-default
  container:
    image: my-registry.example.com/ci-base:latest
    credentials:
      username: ${{ secrets.REGISTRY_USERNAME }}
      password: ${{ secrets.REGISTRY_PASSWORD }}
  steps:
    - uses: actions/checkout@v4
    - run: make test
```

This option requires no changes to where your images are built or stored.
See GitHub's documentation on [defining credentials for a container registry](https://docs.github.com/en/actions/how-tos/write-workflows/choose-where-workflows-run/run-jobs-in-a-container#defining-credentials-for-a-container-registry) for details.

Pulling from an external registry is usually the slowest of the three options, as the image crosses the public internet whenever it is not cached.
[Container image caching](/docs/solutions/github-actions/caching#container-images) reduces that cost, and as with `nscr.io` images, see [Running Jobs in Containers](/docs/reference/github-actions/runner-configuration#jobs-in-containers) to keep access to Namespace caching from inside the container.
