[Yandex Cloud documentation](../../index.md) > [Tutorials](../index.md) > [Serverless technologies](index.md) > Serverless-based workflows and automation > GitLab Runner execution in Serverless Containers

# Starting GitLab Runner in Yandex Serverless Containers


In this tutorial, you will learn to start [GitLab Runner](https://docs.gitlab.com/runner/) in Yandex Cloud using [Yandex Serverless Containers](../../serverless-containers/index.md).

To start GitLab Runner in Serverless Containers:

1. [Get your cloud ready](#before-begin).
1. [Create a GitLab Runner and get a token](#create-gitlab-runner).
1. [Create a secret](#create-secret).
1. [Create service accounts](#create-sa).
1. [Create an API key for your service account](#create-api-key).
1. [Create a container](#create-container).
1. [Set up a webhook in GitLab](#configure-webhook-gitlab).

If you no longer need the resources you created, [delete them](#clear-out).


## How it works {#how-it-works}

![image](../../_assets/tutorials/serverless-gitlab-runner.svg)

Once a job is created, GitLab sends a webhook request to the new Serverless Containers container. The container runs based on a public image from [Yandex Container Registry](../../container-registry/index.md) and retrieves the secrets from [Yandex Lockbox](../../lockbox/index.md). GitLab Runner deploys inside the container, connects to GitLab and takes the job for execution, runs it in an isolated Docker container, and shuts down when done. This approach eliminates the need to keep [VMs](../../compute/concepts/vm.md) running continuously: you pay only for actual job execution time.

You can check the source code in the SourceCraft [repository](https://sourcecraft.dev/yandex-cloud-examples/serverless-gitlab-runner).

**Key steps**

* The service receives HTTP requests on `PORT` (`8080` by default) and at `WEBHOOK_PATH` (`/` by default).
* It checks the secret in the `X-Gitlab-Token` header using the `GITLAB_SECRET` environment variable, if required.
* The only requests that are processed are those with the `X-Gitlab-Event: Job Hook` header.
* The `build_status` field is extracted from the request body. If it is set to `pending`, the service runs this command:

    ```bash
    gitlab-runner run-single \
      --url CI_SERVER_URL \
      --token RUNNER_TOKEN \
      --wait-timeout WAIT_TIMEOUT \
      --executor docker \
      --docker-privileged \
      --max-builds MAX_BUILDS
    ```

* When running a container, the service also does the following:

    * Mounts `cgroup v2`, if required.
    * Prepares the `/run` and `/var/lib/docker` directories.
    * Starts the built-in `dockerd` daemon and waits till it is ready (`DOCKERD_READY_TIMEOUT`).

**How it works inside the container**

Inside the container, the service starts an HTTP server and a GitLab Runner process, which then takes a new CI/CD job from GitLab and executes it in separate isolated docker containers.

**Environment variables**

The service uses the following environment variables:

Variable              | Default         | Required | Description
----------------------- | -------------------- | ----------- | ---
`RUNNER_TOKEN`          | —                    | Yes          | GitLab Runner token (project/group/instance)
`CI_SERVER_URL`         | `https://gitlab.com` | No         | GitLab CI address
`PORT`                  | `8080`               | No         | HTTP port
`WEBHOOK_PATH`          | `/`                  | No         | Webhook endpoint path
`GITLAB_SECRET`         | —                    | No         | Secret for checking the `X-Gitlab-Token` header
`WAIT_TIMEOUT`          | `10`                 | No         | `gitlab-runner --wait-timeout` value, in seconds
`MAX_BUILDS`            | `1`                  | No         | `gitlab-runner --max-builds` value
`DOCKERD_READY_TIMEOUT` | `5s`                 | No         | Timeout until `dockerd` is ready (`time.Duration`)

**Restrictions**

* This tutorial uses the Docker executor only. You can build a [container](../../serverless-containers/concepts/container.md) yourself with required dependencies and use the Shell executor in it.
* Each webhook request starts a separate ephemeral runner. The state between runners is not retained.
* Jobs should have tags that match the runner’s tags. If a `job` in `.gitlab-ci.yml` has no matching tags, the runner will not take it. Use the `Run untagged jobs` option when creating a runner for it to take all jobs.


## Get your cloud ready {#before-begin}

Sign up for Yandex Cloud and create a [billing account](../../billing/concepts/billing-account.md):
1. Navigate to the [management console](https://console.yandex.cloud) and log in to Yandex Cloud or create a new account.
1. On the **[Yandex Cloud Billing](https://center.yandex.cloud/billing/accounts)** page, make sure you have a billing account linked and it has the `ACTIVE` or `TRIAL_ACTIVE` [status](../../billing/concepts/billing-account-statuses.md). If you do not have a billing account, [create one](../../billing/quickstart/index.md) and [link](../../billing/operations/pin-cloud.md) a cloud to it.

If you have an active billing account, you can create or select a [folder](../../resource-manager/concepts/resources-hierarchy.md#folder) for your infrastructure on the [cloud page](https://console.yandex.cloud/cloud).

[Learn more about clouds and folders here](../../resource-manager/concepts/resources-hierarchy.md).


### Required paid resources {#paid-resources}

The infrastructure support cost includes:

* Fee for the number of container invocations, computing resources allocated to the application, and outgoing traffic (see [Serverless Containers pricing](../../serverless-containers/pricing.md)).
* Fee for storing secrets (see [Yandex Lockbox pricing](../../lockbox/pricing.md)).


## Create a GitLab Runner and get a token {#create-gitlab-runner}

1. Create a project in [GitLab](https://gitlab.com) and open it.
1. In the left-hand panel, select ![image](../../_assets/console-icons/gear.svg) **Settings** → **CI/CD**.
1. Open the **Runners** section and click **Create project runner**.
1. In the **Tags** field, specify tags for the jobs that this runner should process.
1. Click **Create runner**.
1. Under **Step 1**, copy the runner authentication token and save it.


## Create a secret {#create-secret}

Create a [Yandex Lockbox](../../lockbox/index.md) secret with the runner authentication token.

{% list tabs group=instructions %}

- Management console {#console}

  1. In the [management console](https://console.yandex.cloud), select the [folder](../../resource-manager/concepts/resources-hierarchy.md#folder) where you are going to create your infrastructure.
  1. Navigate to **Lockbox**.
  1. Click **Create secret**.
  1. In the **Name** field, specify the [secret](../../lockbox/concepts/secret.md) name: `gitlab-runner-token`.
  1. Under **Secret data**:

      1. Select the `Custom` secret type.
      1. Add the GitLab Runner token value:

          * In the **Key** field, specify `gitlab_runner_token`.
          * In the **Value** field, specify the runner authentication token you [got earlier](#create-gitlab-runner).

  1. Click **Create**.

- Yandex Cloud CLI {#cli}

  If you do not have the Yandex Cloud CLI yet, [install and initialize it](../../cli/quickstart.md#install).

  The folder used by default is the one specified when [creating](../../cli/operations/profile/profile-create.md) the CLI profile. To change the default folder, use the `yc config set folder-id <folder_ID>` command. You can also specify a different folder for any command using `--folder-name` or `--folder-id`. If you access a resource by its name, the search will be limited to the default folder. If you access a resource by its ID, the search will be global, i.e., through all folders based on access permissions.

  To create a [secret](../../lockbox/concepts/secret.md), run this command:

  ```bash
  yc lockbox secret create \
    --name gitlab-runner-token \
    --payload '[{"key": "gitlab_runner_token", "text_value": "<runner_token>"}]'
  ```

  Where:

  * `--name`: Secret name.
  * `--payload`: Contents of the secret provided as a YAML or JSON array.
  * `<runner_token>`: Runner authentication token you [got earlier](#create-gitlab-runner).

  Result:

  ```text
  id: e6qdur7h55vd********
  folder_id: b1g681qpemb4********
  created_at: "2025-09-16T06:04:12.604Z"
  name: gitlab-runner-token
  status: ACTIVE
  current_version:
    id: e6qbaf927irb********
    secret_id: e6qdur7h55vd********
    created_at: "2025-09-16T06:04:12.604Z"
    status: ACTIVE
    payload_entry_keys:
      - gitlab_runner_token
  ```

- API {#api}

  To create a [secret](../../lockbox/concepts/secret.md), use the [create](../../lockbox/api-ref/Secret/create.md) REST API method for the [Secret](../../lockbox/api-ref/Secret/index.md) resource or the [SecretService/Create](../../lockbox/api-ref/grpc/Secret/create.md) gRPC API call.

{% endlist %}


## Create service accounts {#create-sa}

Create two service accounts:

* `gitlab-runner-lockbox-payload-viewer` with the `lockbox.payloadViewer` [role](../../lockbox/security/index.md#lockbox-payloadViewer) to access the Lockbox secret.
* `gitlab-runner-caller` with the `serverless-containers.containerInvoker` [role](../../serverless-containers/security/index.md#serverless-containers-containerinvoker) for the folder. This service account will perform these two functions:

    * GitLab will use it to invoke the container. The API key [will be specified](#configure-webhook-gitlab) in the `Authorization` header.
    * GitLab will use its ID to assign jobs to the runner, enabling asynchronous container invocations. The ID will be provided when [creating](#create-container) a container revision.

    You can split these two functions between two different service accounts as needed.

{% list tabs group=instructions %}

- Management console {#console}

  1. Open the [management console](https://console.yandex.cloud).
  1. Navigate to **Identity and Access Management**.
  1. Click **Create service account**.
  1. Name the service account name: `gitlab-runner-caller`.
  1. Click ![plus](../../_assets/console-icons/plus.svg) **Add role** and select `serverless-containers.containerInvoker`.
  1. Click **Create**.
  1. Similarly, create the `gitlab-runner-lockbox-payload-viewer` service account without assigning a role to it.
  1. Assign a role for the secret to the `gitlab-runner-lockbox-payload-viewer` service account:

      1. Open the [management console](https://console.yandex.cloud).
      1. Navigate to **Lockbox**.
      1. Select the `gitlab-runner-token` secret.
      1. In the left-hand panel, select ![image](../../_assets/console-icons/persons.svg) **Access bindings**.
      1. Click **Assign roles**.
      1. Find and select the `gitlab-runner-lockbox-payload-viewer` service account.
      1. Click ![image](../../_assets/console-icons/plus.svg) **Add role** and select `lockbox.payloadViewer`.
      1. Click **Save**.

- Yandex Cloud CLI {#cli}

  1. Create these service accounts:

      ```bash
      yc iam service-account create --name gitlab-runner-lockbox-payload-viewer
      yc iam service-account create --name gitlab-runner-caller
      ```

      Result:

      ```text
      id: ajenprbpf1s3********
      folder_id: b1g681qpemb4********
      created_at: "2025-09-16T06:06:13.092790480Z"
      name: gitlab-runner-lockbox-payload-viewer

      id: ajetqjm00ji8********
      folder_id: b1g681qpemb4********
      created_at: "2025-09-16T06:06:15.661704808Z"
      name: gitlab-runner-caller
      ```

  1. Assign roles to the service accounts:

      ```bash
      yc lockbox secret add-access-binding \
        --name gitlab-runner-token \
        --role lockbox.payloadViewer \
        --service-account-name gitlab-runner-lockbox-payload-viewer

      yc resource-manager folder add-access-binding \
        --id <folder_ID> \
        --role serverless-containers.containerInvoker \
        --service-account-name gitlab-runner-caller
      ```

      Where:

      * `--name`: Secret name.
      * `--id`: Folder ID.
      * `--role`: Role to assign.
      * `--service-account-name`: Service account name.

      Result:

      ```text
      ...1s...done (4s)
      ...1s...done (2s)
      effective_deltas:
        - action: ADD
          access_binding:
            role_id: serverless-containers.containerInvoker
            subject:
              id: ajetqjm00ji8********
              type: serviceAccount
      ```

- API {#api}

  To create a service account, use the [create](../../iam/api-ref/ServiceAccount/create.md) REST API method for the [ServiceAccount](../../iam/api-ref/ServiceAccount/index.md) resource or the [ServiceAccountService/Create](../../iam/api-ref/grpc/ServiceAccount/create.md) gRPC API call.

  To assign the `lockbox.payloadViewer` role for a secret to the service account, use the [updateAccessBindings](../../lockbox/api-ref/Secret/updateAccessBindings.md) REST API method for the [Secret](../../lockbox/api-ref/Secret/index.md) resource or the [SecretService/UpdateAccessBindings](../../lockbox/api-ref/grpc/Secret/updateAccessBindings.md) gRPC API call.

  To assign the service account the `serverless-containers.containerInvoker` role for the folder, use the [updateAccessBindings](../../resource-manager/api-ref/Folder/updateAccessBindings.md) REST API method for the [Folder](../../resource-manager/api-ref/Folder/index.md) resource or the [FolderService/UpdateAccessBindings](../../resource-manager/api-ref/grpc/Folder/updateAccessBindings.md) gRPC API call.

{% endlist %}


## Create an API key for the service account {#create-api-key}

Create an [API key](../../iam/concepts/authorization/api-key.md) for the service account you are going to use to invoke the container. You will need this API key to set up a webhook in GitLab.

{% list tabs group=instructions %}

- Management console {#console}

  1. Open the [management console](https://console.yandex.cloud).
  1. Navigate to **Identity and Access Management**.
  1. Select the `gitlab-runner-caller` service account you created earlier.
  1. In the top panel, click ![image](../../_assets/console-icons/plus.svg) **Create new key** and select **Create API key**.
  1. In the **Scope** field, select [`yc.serverless.containers.invoke`](../../iam/concepts/authorization/api-key.md#scoped-api-keys).
  1. Click **Create**.

- CLI {#cli}

  1. Create an API key for the `gitlab-runner-caller` service account:

      ```bash
      yc iam api-key create \
        --service-account-name gitlab-runner-caller \
        --scopes yc.serverless.containers.invoke
      ```

      Where:

      * `--service-account-name`: Service account name.
      * `--scopes`: Key [scope](../../iam/concepts/authorization/api-key.md#scoped-api-keys).

      Result:

      ```yaml
      api_key:
        id: aje3oa26mt52********
        service_account_id: ajetqjm00ji8********
        created_at: "2025-09-16T06:08:47.018697312Z"
        scope: yc.serverless.containers.invoke
        scopes:
          - yc.serverless.containers.invoke
      secret: AQVNw8KEa3SiA8vlpXEhfl9k-uYT22Ws********
      ```

- API {#api}

  To create an API key, use the [create](../../iam/api-ref/ApiKey/create.md) REST API method for the [ApiKey](../../iam/api-ref/ApiKey/index.md) resource or the [ApiKeyService/Create](../../iam/api-ref/grpc/ApiKey/create.md) gRPC API call.

{% endlist %}

Save the secret key you got, as it is shown only once and you will not be able to get again.


## Create a container {#create-container}

Create a [container](../../serverless-containers/concepts/container.md) and a [container revision](../../serverless-containers/concepts/container.md#revision) with a Docker image to start GitLab Runner.

{% note info %}

In our example, we mount an ephemeral disk at `/mnt` to expand the available space in the root file system for the period of executing the container. Docker stores data in `/var/lib/docker`: this location becomes available thanks to the root file system expansion.

{% endnote %}

{% note info %}

Specifying a network in the container settings creates a service subnet from the `198.19.0.0/16` range in each availability zone. These subnets are not displayed in the Yandex Cloud interface. Keep this in mind when [configuring](../../managed-gitlab/operations/configure-security-group.md) security group rules. For more information, see [Networking](../../serverless-containers/concepts/networking.md).

{% endnote %}

{% list tabs group=instructions %}

- Management console {#console}

  1. Open the [management console](https://console.yandex.cloud).
  1. Navigate to **Serverless Containers**.
  1. Click **Create container**.
  1. Name the container: `serverless-gitlab-runner`.
  1. Click **Create**.
  1. Navigate to the **Editor** tab.

      1. Under **Resources**, specify the required RAM, e.g., `1024 MB`.
      1. Under **Image settings**:

          1. Click **Enter link**, and specify `cr.yandex/yc/serverless/gitlab-runner` in the **Image URL** field.
          1. In the **Environment variables** field, add the following variables:

              * `CI_SERVER_URL`: `https://gitlab.com`
              * `WEBHOOK_PATH`: `/webhook`

          1. In the **Lockbox secrets** field, specify:

              * **Environment variable**: `RUNNER_TOKEN`.
              * **Secret ID**: `gitlab-runner-token`.
              * **Version ID**: Current version ID.
              * **Secret key**: `gitlab_runner_token`.

        1. Under **Settings**:

            1. In the **Service account** field, specify `gitlab-runner-lockbox-payload-viewer`.
            1. Optionally, in the **Network** field, specify or [create](../../vpc/operations/network-create.md) a network to host your container.
            1. In the **Timeout** field, specify the required value, e.g., `600 seconds`.

        1. Under **Mounted ephemeral disk**:

            1. Click **Add ephemeral disk**.
            1. In the **Mount path** field, specify `/mnt`.
            1. In the **Disk size** field, specify the required value, e.g., `10 GB`.

        1. Under **Asynchronous call**:

            1. Click **Enable**.
            1. In the **Service account** field, select `gitlab-runner-caller`.

  1. Click **Create revision**.

- CLI {#cli}

  1. Create a container:

      ```bash
      yc serverless container create --name serverless-gitlab-runner
      ```

      Result:

      ```text
      id: bba83i1mrb5s********
      folder_id: b1g681qpemb4********
      created_at: "2025-09-16T06:10:03.153Z"
      name: serverless-gitlab-runner
      url: https://bba83i1mrb5s********.containers.yandexcloud.net/
      status: ACTIVE
      ```

  1. Create a container revision:

      ```bash
      yc serverless container revision deploy \
        --container-name serverless-gitlab-runner \
        --runtime=http \
        --cores <number_of_cores> \
        --memory <RAM_amount> \
        --image cr.yandex/yc/serverless/gitlab-runner \
        --environment CI_SERVER_URL=https://gitlab.com \
        --environment WEBHOOK_PATH=/webhook \
        --secret id=<secret_ID>,version-id=<secret_version_ID>,key=gitlab_runner_token,environment-variable=RUNNER_TOKEN \
        --service-account-id <service_account_1_ID> \
        --execution-timeout <timeout> \
        --mount type=ephemeral-disk,mount-point=/mnt,size=<disk_size>\
        --async-service-account-id <service_account_2_ID>
      ```

      Where:

      * `--container-name`: Container name.
      * `--runtime`: Mode.
      * `--cores`: Number of cores available to the container, e.g., `1`.
      * `--memory`: Required memory, e.g., `1GB`.
      * `--image`: `gitlab-runner` Docker image URL.
      * `--environment`: Environment variables:

          * `CI_SERVER_URL`: GitLab CI address.
          * `WEBHOOK_PATH`: Webhook endpoint path.

      * `--secret`: Lockbox secret.

      * `--service-account-id`: `gitlab-runner-lockbox-payload-viewer` service account ID.
      * `--execution-timeout`: Timeout, e.g., `600s`.
      * `--mount`: Ephemeral disk mount settings:

          * `type=ephemeral-disk`: Type of the file system being mounted.
          * `mount-point`: Name of the mount point. The directory with the mounted disk will be accessible at `/mnt`.
          * `size`: Ephemeral disk size in GB, e.g., `10GB`.

      * `--async-service-account-id`: `gitlab-runner-caller` service account ID.

      Result:

      ```text
      id: bba27hejd69a********
      container_id: bba83i1mrb5s********
      created_at: "2025-09-18T09:38:14.528Z"
      image:
        image_url: cr.yandex/yc/serverless/gitlab-runner
        image_digest: sha256:ac62...
        environment:
          CI_SERVER_URL: https://gitlab.com
          WEBHOOK_PATH: C:/Program Files/Git/webhook
      resources:
        memory: "1073741824"
        cores: "1"
        core_fraction: "100"
      execution_timeout: 600s
      concurrency: "1"
      service_account_id: ajenprbpf1s3********
      status: ACTIVE
      secrets:
        - id: e6qdur7h55vd********
          version_id: e6qbaf927irb********
          key: gitlab_runner_token
          environment_variable: RUNNER_TOKEN
      log_options:
        folder_id: b1g681qpemb4********
      mounts:
        - mount_point_path: /mnt
          ephemeral_disk_spec:
            size: "10737418240"
            block_size: "4096"
      runtime:
        http: {}
      async_invocation_config:
        service_account_id: ajetqjm00ji8********
      ```

      {% note tip %}

      In case of the `mounts[0].mount_point_path: Field does not match the pattern /[-_0-9a-zA-Z/]*/` error, escape the mount point name:

      ```bash
      mount-point=//mnt
      ```

      {% endnote %}

- API {#api}

  To create a container, use the [create](../../serverless-containers/containers/api-ref/Container/create.md) REST API method for the [Container](../../serverless-containers/containers/api-ref/Container/index.md) resource or the [ContainerService/Create](../../serverless-containers/containers/api-ref/grpc/Container/create.md) gRPC API call.

  To create a container revision, use the [deployRevision](../../serverless-containers/containers/api-ref/Container/deployRevision.md) REST API method for the [Container](../../serverless-containers/containers/api-ref/Container/index.md) resource or the [ContainerService/DeployRevision](../../serverless-containers/containers/api-ref/grpc/Container/deployRevision.md) gRPC API call.

{% endlist %}


## Set up a webhook in GitLab {#configure-webhook-gitlab}

1. Open your project in [GitLab](https://gitlab.com).
1. In the left-hand panel, select ![image](../../_assets/console-icons/gear.svg) **Settings** → **Webhooks**.
1. Click **Add new webhook**.
1. In the **URL** field, specify the Serverless Containers public endpoint: `https://<container_ID>.containers.yandexcloud.net/webhook`.
1. Under **Custom headers**, click **Add custom header** and add the following ones:

    Header name | Header value | Description
    --- | --- | ---
    `Authorization` | `Api-Key <API_key>` | `gitlab-runner-caller` service account API key. Required if the container is private; otherwise, the request will be denied. Learn more in [Authenticating when invoking a private container via HTTPS](../../serverless-containers/operations/auth.md).
    `X-Ycf-Container-Integration-Type` | `async` | Asynchronous container invocation. The platform will immediately respond to the invocation with `202`.

1. Under **Trigger**, make sure the `Job events` option is enabled.
1. Click **Add webhook**.

After that, you will be able to use serverless GitLab Runners.

{% note tip %}

You can additionally configure GitLab Runner using environment variables and flags.

View the available options by running the `gitlab-runner run-single -h` command. You can also look up the guide section on the [GitLab Runner commands](https://docs.gitlab.com/runner/commands/).

{% endnote %}

## How to delete the resources you created {#clear-out}

To stop paying for the resources you created:

1. [Delete](../../serverless-containers/operations/delete.md) the container.
1. [Delete](../../lockbox/operations/secret-delete.md) the secret.