Managed GitLab CI Runners
This guide explains how to configure your GitLab CI/CD pipelines to work on Bunker's managed GitLab-CI runners, which do not allow privileged mode for security reasons.
Objective
Build and deploy container images without Docker-in-Docker (dind), using tools compatible with a non-privileged Kubernetes environment.
Prerequisites
- A GitLab project configured to use Bunker runners
- A container registry (GitLab Container Registry, etc.)
- Registry credentials stored in CI/CD variables
Tools to Use
BuildKit Rootless - Image Building
BuildKit is Docker's next-generation build engine. Rootless mode allows building images without a Docker daemon and without root privileges.
When to use: For any image building (equivalent to docker build)
Official image: moby/buildkit:rootless
Advantages over Kaniko:
- Better performance and memory usage
- More efficient caching
- Native multi-platform build support
- Actively maintained
Crane - Registry Operations
Crane manipulates images in registries without Docker.
When to use:
- Copy an image between registries (equivalent to
docker tag+docker push) - Extract files from an image (
docker cp) - Registry authentication
Installation:
curl -sL "https://github.com/google/go-containerregistry/releases/download/v0.20.2/go-containerregistry_Linux_x86_64.tar.gz" | tar -xzf - -C /usr/local/bin crane
What NOT to Do
| Forbidden | Why | Recommended Alternative |
|---|---|---|
services: [docker:dind] | Requires privileged mode | BuildKit rootless |
docker build | No Docker daemon | BuildKit rootless |
docker push/pull | No Docker daemon | Crane or BuildKit |
docker run | No Docker daemon | Direct image in image: |
docker cp | No Docker daemon | crane export |
image: docker:* | Useless without daemon | Job-specific image |
Variable DOCKER_HOST | No daemon | Remove |
Variable DOCKER_TLS_CERTDIR | No daemon | Remove |
pull_policy: [Always] | Not allowed by runner | Don't specify (uses default) |
Why No Docker-in-Docker (DinD)?
Bunker Kubernetes runners do not allow privileged: true mode and therefore DinD for 3 reasons:
- Security: Privileged mode gives full root access to the host node, forget it :)
- Isolation: Privileged containers can escape their sandbox
- Multi-tenancy: On a shared cluster, this would compromise isolation between clients
What You SHOULD Do
1. Image Building with BuildKit Rootless
build:
stage: build
image:
name: moby/buildkit:rootless
entrypoint: ['sh', '-c']
variables:
BUILDKITD_FLAGS: --oci-worker-no-process-sandbox
script:
- buildctl-daemonless.sh build
--frontend dockerfile.v0
--local context=.
--local dockerfile=.
--output type=image,name=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA,push=true
Key points:
entrypoint: ['sh', '-c']is mandatory to execute shell commandsBUILDKITD_FLAGS: --oci-worker-no-process-sandboxdisables sandboxing (required without privileges)buildctl-daemonless.shlaunches BuildKit without a persistent daemon- Use immutable tags (SHA) for traceability
2. Registry Authentication
GitLab Container Registry:
before_script:
- mkdir -p ~/.docker
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"auth\":\"$(echo -n $CI_REGISTRY_USER:$CI_REGISTRY_PASSWORD | base64)\"}}}" > ~/.docker/config.json
3. Build Cache
BuildKit supports remote caching to speed up builds:
script:
- buildctl-daemonless.sh build
--frontend dockerfile.v0
--local context=.
--local dockerfile=.
--import-cache type=registry,ref=$CI_REGISTRY_IMAGE:cache
--export-cache type=inline
--output type=image,name=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA,push=true
4. Extracting Files from an Image
To run tests with dependencies from the built image while respecting the immutable build principle:
test:
image: node:20 # If you have a NodeJS project
before_script:
- curl -sL "https://github.com/google/go-containerregistry/releases/download/v0.20.2/go-containerregistry_Linux_x86_64.tar.gz" | tar -xzf - -C /usr/local/bin crane
- crane auth login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- crane export $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA - | tar -xf - -C / usr/src/app
script:
- cd /usr/src/app
- npm test
Note: The job's base image (image: node:20) must have the same Node.js version as the Dockerfile to avoid native module errors (NODE_MODULE_VERSION mismatch).
5. Copying Between Registries
To deploy to multiple regions:
deploy:
script:
- curl -sL "https://github.com/google/go-containerregistry/releases/download/v0.20.2/go-containerregistry_Linux_x86_64.tar.gz" | tar -xzf - -C /usr/local/bin crane
- crane auth login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- crane copy $SOURCE_IMAGE $DEST_IMAGE
6. Multi-Platform Build
BuildKit excels at multi-architecture builds:
script:
- buildctl-daemonless.sh build
--frontend dockerfile.v0
--local context=.
--local dockerfile=.
--opt platform=linux/amd64,linux/arm64
--output type=image,name=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA,push=true
Immutable Build Principle
We recommend following the "build once, deploy everywhere" principle:
- Single build: Build the image once in the
buildstage - Immutable tag: Use
${CI_COMMIT_SHA}in the tag - Reuse: Subsequent jobs extract or use this image
- No rebuild: Never rebuild for tests or deployment
build ──► tests (extract app) ──► security scan ──► deploy (copy image)
│ │ │ │
└──────────────┴─────────────────────────┴────────────────────┘
Same image everywhere
Common Errors
"Cannot connect to Docker daemon"
Cause: You're using a docker command without a daemon.
Solution: Replace with BuildKit rootless (build) or Crane (registry operations).
"could not connect to buildkitd.sock"
Cause: BuildKit is trying to use a Unix socket instead of daemonless mode.
Solution: Use buildctl-daemonless.sh (not buildctl) and verify BUILDKITD_FLAGS is defined.
"Unauthorized" on crane export
Cause: Crane is not authenticated to the registry.
Solution: Add crane auth login before crane export.
"NODE_MODULE_VERSION mismatch"
Cause: Job image uses a different Node.js version than the Dockerfile.
Solution: Align image: with the Dockerfile version.
"exec format error"
Cause: Image built for a different architecture.
Solution: Specify --opt platform=linux/amd64 in BuildKit.
"invalid pull policy for container"
Cause: Your GitLab CI configuration (or an included template) specifies a pull_policy (e.g., [Always]) not allowed by the Kubernetes runner.
ERROR: Job failed: invalid pull policy for container "build":
pull_policy ([Always]) defined in GitLab pipeline config is not one of the allowed_pull_policies ([])
Solution: Don't specify pull_policy in your configuration. The runner will use its default policy. If you include an external template that defines pull_policy, replace it with inline configuration:
# ❌ Avoid templates that define pull_policy
include:
- project: 'some-org/some-template'
file: '/template.gitlab-ci.yml'
# ✅ Prefer inline configuration without pull_policy
my-job:
stage: build
image: my-image:latest
# Don't specify pull_policy - runner will use its default policy
script:
- echo "Build"
Complete Example (Node.js app)
Here's a complete pipeline illustrating all best practices:
# .gitlab-ci.yml - Pipeline without privileged mode for Bunker
# Uses BuildKit rootless for image building
variables:
# Immutable tag based on commit SHA
BUILD_IMAGE: '${CI_REGISTRY_IMAGE}:${CI_COMMIT_REF_SLUG}-${CI_COMMIT_SHA}'
# BuildKit rootless configuration
BUILDKITD_FLAGS: --oci-worker-no-process-sandbox
stages:
- build
- test
- security
- deploy
# ============================================
# STAGE: BUILD - Building with BuildKit
# ============================================
build:
stage: build
image:
name: moby/buildkit:rootless
entrypoint: ['sh', '-c']
before_script:
- mkdir -p ~/.docker
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"auth\":\"$(echo -n $CI_REGISTRY_USER:$CI_REGISTRY_PASSWORD | base64)\"}}}" > ~/.docker/config.json
script:
- buildctl-daemonless.sh build
--frontend dockerfile.v0
--local context=.
--local dockerfile=.
--import-cache type=registry,ref=$CI_REGISTRY_IMAGE:cache
--export-cache type=inline
--output type=image,name=$BUILD_IMAGE,push=true
rules:
- when: on_success
# ============================================
# TEMPLATE: Extract app from image
# ============================================
.extract_app: &extract_app
# IMPORTANT: Must match Node.js version from Dockerfile
image: node:20
before_script:
- curl -sL "https://github.com/google/go-containerregistry/releases/download/v0.20.2/go-containerregistry_Linux_x86_64.tar.gz" | tar -xzf - -C /usr/local/bin crane
- crane auth login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- crane export $BUILD_IMAGE - | tar -xf - -C / usr/src/app
# ============================================
# STAGE: TEST - Tests on built image
# ============================================
test:
<<: *extract_app
stage: test
coverage: '/Lines[^:]+\:\s+(\d+\.\d+)\%/'
script:
- cd /usr/src/app
- npm test
artifacts:
reports:
junit: /usr/src/app/coverage/junit.xml
paths:
- /usr/src/app/coverage/
rules:
- when: on_success
lint:
<<: *extract_app
stage: test
script:
- cd /usr/src/app
- npm run lint
rules:
- when: on_success
# ============================================
# STAGE: SECURITY - Vulnerability scans
# ============================================
image-security:
stage: security
image: aquasec/trivy:0.56.2
allow_failure: true
script:
- trivy image
--exit-code 0
--format template
--template "@/contrib/gitlab.tpl"
-o gl-container-scanning-report.json
$BUILD_IMAGE
- trivy image --exit-code 1 --severity CRITICAL $BUILD_IMAGE
artifacts:
reports:
container_scanning: gl-container-scanning-report.json
rules:
- when: on_success
dependency-scan:
stage: security
image: ghcr.io/google/osv-scanner:v1.9.1
allow_failure: true
before_script:
- apk add --no-cache curl
- curl -sL "https://github.com/google/go-containerregistry/releases/download/v0.20.2/go-containerregistry_Linux_x86_64.tar.gz" | tar -xzf - -C /usr/local/bin crane
- crane auth login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- crane export $BUILD_IMAGE - | tar -xf - usr/src/app/package-lock.json --strip-components=3
script:
- /osv-scanner --lockfile package-lock.json
rules:
- when: on_success
# ============================================
# STAGE: DEPLOY - Production deployment
# ============================================
deploy-production:
stage: deploy
image: bitnami/kubectl:latest
before_script:
- curl -sL "https://github.com/google/go-containerregistry/releases/download/v0.20.2/go-containerregistry_Linux_x86_64.tar.gz" | tar -xzf - -C /usr/local/bin crane
- crane auth login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
# Optional: copy to dedicated registry
- crane copy $BUILD_IMAGE $PRODUCTION_REGISTRY/$PROJECT:$CI_COMMIT_SHA
# Deploy to Kubernetes
- envsubst < deploy/deployment.yaml | kubectl apply -f -
- kubectl rollout status deployment/my-app
environment:
name: production
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
Resources
Next Steps
Check out the Concepts to understand Bunker's architecture.