Technical architectural analysis, practical configuration manifests, and deployment instructions for Bitbucket Self-Hosted Runner Setup.
On k8s
Sample
Exec
Create Bitbucket Runner
-
Access https://bitbucket.org/\${organization}/\${repository}/admin/pipelines/runners
- Access the bitbucket Repository main page
-
Access
Repository settingsat the bottom of the left sidebar -
Access
Runnersat the bottom ofPipelines
-
Click
Add runnerbutton -
Select Linux Docker (x86_64) and specify a name and
Next -
Duplicate the Docker execution statement.
docker container run -it -v /tmp:/tmp -v /var/run/docker.sock:/var/run/docker.sock
-v /var/lib/docker/containers:/var/lib/docker/containers:ro
-e ACCOUNT_UUID=$(COPY)
-e REPOSITORY_UUID=$(COPY)
-e RUNNER_UUID=$(COPY)
-e RUNTIME_PREREQUISITES_ENABLED=true
-e OAUTH_CLIENT_ID=$(COPY)
-e OAUTH_CLIENT_SECRET=$(COPY)
-e WORKING_DIRECTORY=/tmp
--name runner-2183e7de-fd6f-5468-b96c-3d75fc6c61b4 docker-public.packages.atlassian.com/sox/atlassian/bitbucket-pipelines-runner
- Label is an element managed by bitbucket, so if there is no separate policy, you can
Finish
K8s runner creation and authentication link
-
Complete the manifest by referring to
- .
- Copy the following from the previously cloned Docker executable statement.
ACCOUNT_UUIDREPOSITORY_UUIDRUNNER_UUIDOAUTH_CLIENT_IDOAUTH_CLIENT_SECRET
- Execute Job and Secret deployment
k apply -f ${runner}.yaml- Check normal operation of pod
k get pods -n $ns
Confirm linking completion
-
Runner when first created
-
runner after pod running
Troubles
Container: docker-in-docker
- network is unreachable
Could not pull image: error pulling image configuration: Get https://docker-blobs.artifacts.atlassian.com/docker-public/docker/registry/v2/blobs/sha256/9e/9ea04d92255bdf0599c84facc7fd7be2e227bfbc07f511df9647a7ccfebdf59c/data?Expires=1745997666&Signature=4f55myfFz1Uzl5UPHc6kdhtTFQwBudvlxFnvi0Q0L-aFCoCOUVdGEulA6yC0aPHmLzPxvYhnqAY6ApBmXmdr6tvD0xXA2rGyxBfHMfxp2WYPHDEFcEa675ke5n3B-JBaGD~SQAHBsScuu~nh0ufPcO5rpJfK-4NHz0cLMUoQRnAb5i91NHeAvz5d23l6dPBvEXSLM5J~UAybGSIvO0YKNNnIOgpPpFlELiJL0xtn2QTlAydjzJncNu8CYEP0sDXJ8A1Ouw427lk~u3Y~SoFG35gbiubMIJxheFFbzVpEgyt9pAKAPIrvFNDFvjqNMb~KeN7qDXgwHKdVlujghJHO7w__&Key-Pair-Id=K3U3YXJT6R73J6:
dial tcp [2600:9000:2891:3400:15:4497:1cc0:93a1]:443: connect:
network is unreachable Refer to the Troubleshooting runners document for help resolving the issue.
- Presumed to be a coredns issue
2. failed to adjust OOM score for shim
time="2025-04-30T06:46:03.754488308Z" level=error msg="Handler for POST /containers/d7d238126da63910fcc0f34b273e2ab5ed66972011e13dee5517bd0d2d55f881/start returned error: io.containerd.runc.v2: failed to adjust OOM score for shim: set shim OOM score: write /proc/416/oom_score_adj: invalid argument\n: exit status 1: unknown"
- Occurs when the runtime versions of CRI’s docker or containerd and DinD image are different.
- CRI Runtime
docker version
Client:
Version: 26.1.3
API version: 1.45
Go version: go1.22.2
Git commit: 26.1.3-0ubuntu1~22.04.1
Built: Mon Oct 14 21:24:40 2024
OS/Arch: linux/amd64
Context: default
Server:
Engine:
Version: 26.1.3
API version: 1.45 (minimum version 1.24)
Go version: go1.22.2
Git commit: 26.1.3-0ubuntu1~22.04.1
Built: Mon Oct 14 21:24:40 2024
OS/Arch: linux/amd64
Experimental: false
containerd:
Version: 1.7.24
GitCommit:
runc:
Version: 1.1.12-0ubuntu2~22.04.1
GitCommit:
docker-init:
Version: 0.19.0
GitCommit:
- Image runtime
- name: docker-in-docker
image: docker:26.1.3-dind
Container: runner
- failed to create task for container: 400
Status 400: {"message":"failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process:
exec: \"/bin/sh\": stat /bin/sh: no such file or directory: unknown"} Refer to the Troubleshooting runners document for help resolving the issue.
- This occurs when the container created by the runner java app is set to entrypoint=โ/bin/shโ, but the corresponding binary is not included in the creation request image (container).
Features
Mount docker daemon.json
#job/${JOB_NAME}-runner
#.spec.template.spec.containers[name=docker-in-docker].volumeMounts
- name: docker-daemon-cm
mountPath: /etc/docker/daemon.json
subPath: daemon.json
#.spec.template.spec.volumes
- name: docker-daemon-cm
configMap:
name: docker-daemon
Added option to enable exposure of Pipeline Log to Container Log
#job/${JOB_NAME}-runner
#.spec.template.spec.containers[name=runner].env
- name: BITBUCKET_PIPELINES_RUNNER_BUILD_LOG_ENABLED
value: 'TRUE'
Use Custom Logback configuration
- logback samples
default
<configuration>
<variable name="BASE_LOG_PATH" value="/tmp" />
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
<evaluator>
<expression>com.atlassian.pipelines.stargate.client.core.exceptions.StargateForbiddenException.class.isInstance(throwable)</expression>
</evaluator>
<onMatch>DENY</onMatch>
</filter>
<encoder>
<!-- print [buildLogName] from SystemLogAppenderFilter if exists, otherwise do not print empty [] -->
<pattern>[%date{ISO8601}]%replace([%X{buildLogName}]){'\[\]',''} %message%n%rootException</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${BASE_LOG_PATH}/runner.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${BASE_LOG_PATH}/runner.%d.%i.log</fileNamePattern>
<maxHistory>7</maxHistory>
<maxFileSize>100MB</maxFileSize>
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
<encoder>
<!-- print [buildLogName] from SystemLogAppenderFilter if exists, otherwise do not print empty [] -->
<pattern>[%date{ISO8601}]%replace([%X{buildLogName}]){'\[\]',''} %message%n%rootException</pattern>
</encoder>
</appender>
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator">
<resetJUL>true</resetJUL>
</contextListener>
<root level="INFO">
<appender-ref ref="STDOUT" />
<appender-ref ref="FILE" />
</root>
<logger name="com.atlassian.pipelines.runner" level="${BITBUCKET_PIPELINES_RUNNER_LOG_LEVEL:-INFO}" />
<logger name="build" level="INFO" />
<logger name="build.service.auth-proxy" level="OFF" />
<logger name="com.netflix" level="OFF" />
<logger name="com.atlassian.pipelines.stargate" level="OFF" />
<logger name="com.github.dockerjava" level="OFF" />
</configuration>
Json style
<configuration>
<variable name="BASE_LOG_PATH" value="/tmp" />
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
<evaluator>
<expression>com.atlassian.pipelines.stargate.client.core.exceptions.StargateForbiddenException.class.isInstance(throwable)</expression>
</evaluator>
<onMatch>DENY</onMatch>
</filter>
<encoder class="net.logstash.logback.encoder.LogstashEncoder" />
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${BASE_LOG_PATH}/runner.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${BASE_LOG_PATH}/runner.%d.%i.log</fileNamePattern>
<maxHistory>7</maxHistory>
<maxFileSize>100MB</maxFileSize>
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
<encoder>
<!-- print [buildLogName] from SystemLogAppenderFilter if exists, otherwise do not print empty [] -->
<pattern>[%date{ISO8601}]%replace([%X{buildLogName}]){'\[\]',''} %message%n%rootException</pattern>
</encoder>
</appender>
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator">
<resetJUL>true</resetJUL>
</contextListener>
<root level="INFO">
<appender-ref ref="STDOUT" />
<appender-ref ref="FILE" />
</root>
<logger name="com.atlassian.pipelines.runner" level="${BITBUCKET_PIPELINES_RUNNER_LOG_LEVEL:-INFO}" />
<logger name="build" level="INFO" />
<logger name="build.service.auth-proxy" level="OFF" />
<logger name="com.netflix" level="OFF" />
<logger name="com.atlassian.pipelines.stargate" level="OFF" />
<logger name="com.github.dockerjava" level="OFF" />
</configuration>
- Create logback configmap
kubectl create configmap -n bitbucket-runner logback-config --from-file=custom-logback.xml
- Add environment variable and volume mount
#job/${JOB_NAME}-runner
#.spec.template.spec.containers[name=runner].env
- name: LOGBACK_CONFIGURATION_FILE
value: /tmp/custom-logback.xml
#.spec.template.spec.containers[name=runner].volumeMounts
- name: logback-config
mountPath: /tmp/custom-logback.xml
subPath: custom-logback.xml
readOnly: true
#.spec.template.spec.volumes
- name: logback-config
configMap:
name: logback-config
Use Insecure Docker Registry
For security reasons, the Docker daemon cannot be configured in Pipelines Cloud Runners. However, Linux self-hosted runners allow custom configuration for the Docker daemon, which can lead to the use of insecure registries.
Dockerfile
The service does not support entrypoint overwrite, so building a custom image is required.
FROM docker:26.1.3-dind
ENTRYPOINT [ "sh", "-c", "dockerd-entrypoint.sh $DOCKER_OPTS" ]
-
pipelines.yml -
self-hosted pipeline using custom defined service
pipelines:
branches:
"feature/pipeline-test":
- step:
name: "on-premise-agent :: Images push & Kubernetes Deploy"
runs-on:
- self.hosted
services:
- custom-dind
image: { client input }
script:
- docker info
- Definitions for using a Docker-in-Docker (DIND) image from a private registry.
# private registry
definitions:
services:
custom-dind:
image:
name: { insecure-dind input }
username: "$private_docker_hub"
password: "$private_docker_hub_pw"
type: docker
variables:
DOCKER_OPTS: "--insecure-registry={ registry input }"
- Definitions for using a Docker-in-Docker (DIND) image from a public registry.
# public registry
definitions:
services:
custom-dind:
image: { insecure-dind input }
type: docker
variables:
DOCKER_OPTS: "--insecure-registry={ registry input }"




Leave a Reply