This article is based on a real-world migration experience from Team Alfonzó, originally published on 126kr.com, describing their move from Jenkins and DockerHub to GitLab CI and the GitLab Container Registry.
Why Migrate from Jenkins to GitLab CI?
Jenkins has long been a staple of continuous integration pipelines, but as teams grow and infrastructure scales, its centralized architecture can become a bottleneck. Team Alfonzó, working alongside their Build Team, decided to explore a more distributed build infrastructure. Their Implementation Wizard project became the test bed for adopting GitLab CI and the GitLab Container Registry — replacing both Jenkins and DockerHub in their workflow.
GitLab CI vs. Jenkins: Pipeline Configuration Compared
If you have written a Jenkinsfile using Groovy-based DSL, you will likely find GitLab CI’s approach cleaner and more immediately readable. Instead of writing pipeline logic in Groovy, GitLab CI uses a .gitlab-ci.yml file checked into the root of your repository. In this YAML file you declare your build stages and the shell scripts that run at each stage.
Example .gitlab-ci.yml Structure
stages:
- build
- test
- deploy
build-job:
stage: build
script:
- docker build -t my-app:$CI_COMMIT_SHA .
test-job:
stage: test
script:
- docker run my-app:$CI_COMMIT_SHA ./run-tests.sh
deploy-job:
stage: deploy
script:
- kubectl apply -f deployment.yaml
only:
- mainCode language: PHP (php)
The YAML syntax enforces a sensible structure while still giving you enough flexibility to configure each stage independently. The tradeoff is that there is no built-in way to test or lint your pipeline scripts locally without committing and pushing changes to the repository. Teams should be aware of this limitation early on to avoid cluttering the commit history with purely experimental CI changes. (Third-party tools such as gitlab-ci-local have since emerged to help with local pipeline testing, though they were not part of this original migration.)
Setting Up a GitLab Runner
A .gitlab-ci.yml file alone does nothing without a GitLab Runner — the agent that picks up jobs and executes them. Team Alfonzó registered a dedicated runner running inside a Docker container. This gave the team full control over the runner environment, though it comes with a resource cost compared to using shared runners provided by GitLab.
If multiple teams share a codebase or similar build requirements, shared runners are worth considering as a more resource-efficient option. Dedicated runners make the most sense when you need a specific environment, custom tooling, or tighter security boundaries.
Bypassing Docker-in-Docker Difficulties
A common challenge when running Docker-based CI pipelines is the need to build Docker images from within a Docker container — a pattern known as Docker-in-Docker (DinD). GitLab officially supports DinD using the docker:dind service image, but in Team Alfonzó’s environment this approach did not work reliably.
The Unix Socket Workaround
To work around the DinD limitation, the team configured the Docker API to share the same Unix socket between the runner container and the stage container (the container that actually executes build steps). This is done by mounting the host Docker socket into the runner container:
# When registering the GitLab Runner, pass the socket mount:
docker run -d \
--name gitlab-runner \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /srv/gitlab-runner/config:/etc/gitlab-runner \
gitlab/gitlab-runner:latestCode language: PHP (php)
By mounting /var/run/docker.sock from the host into the runner container, and then propagating that same socket mount into stage containers, both containers communicate with the same Docker daemon. This sidesteps the need to run a nested Docker daemon entirely.
Security note: Mounting the Docker socket grants the container root-equivalent access to the host. This approach is acceptable in controlled, trusted CI environments but should be carefully evaluated before use in multi-tenant or public-facing setups.
Data Volumes and Kubernetes Deployment
Even with the socket sharing workaround in place, the team found that Docker containers running build stages could not accept data volumes mounted from outside. This created a specific problem during Kubernetes deployments: the deployment manifest file could not be injected via a volume mount into the stage container.
The solution was to use kubectl‘s ability to read configuration from standard input rather than from a file path:
# Pass a Kubernetes manifest via stdin instead of a file
cat deployment.yaml | kubectl apply -f -Code language: PHP (php)
This pattern works reliably when volume mounts are unavailable and is a useful technique in any containerized CI environment where file system sharing between containers is restricted.
Hosting Docker Images with the GitLab Container Registry
GitLab includes an integrated Container Registry that can host Docker images built from your projects. For Team Alfonzó, this proved more convenient than DockerHub for several reasons:
- Granular access control: Registry permissions follow the existing GitLab project permissions model, meaning developers already have appropriately scoped access without a separate admin workflow.
- Reduced friction: No need to manage separate DockerHub credentials or request registry creation from a small group of administrators.
- Tighter integration: Images built and pushed during a CI pipeline are immediately visible and accessible within the same GitLab project.
A typical pipeline stage to build and push an image to the GitLab Registry looks like this:
build-and-push:
stage: build
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHACode language: PHP (php)
GitLab automatically provides the predefined CI variables $CI_REGISTRY, $CI_REGISTRY_USER, $CI_REGISTRY_PASSWORD, and $CI_REGISTRY_IMAGE, so no manual credential management is needed for standard project pipelines.
Known Limitation: One Registry per Project
At the time of this migration, the team noted that GitLab’s registry did not support hosting multiple distinct images under a single project — a limitation that surfaced particularly for projects with external dependencies. This issue was reported to the GitLab team. GitLab’s registry has evolved significantly since 2017, so teams evaluating it today should consult the current GitLab documentation for the latest capabilities.
Conclusion
GitLab CI offers a compelling alternative to Jenkins for teams already using GitLab for source control. The YAML-based pipeline configuration is readable and maintainable, the integrated Container Registry removes the need for a separate image hosting service, and the GitLab Runner architecture supports both shared and dedicated execution environments.
The main challenges encountered — Docker-in-Docker limitations and restricted data volume access in nested containers — each have workable solutions. The Unix socket mount approach and the use of kubectl stdin input are practical patterns that apply well beyond this specific migration scenario.
For teams experiencing high load on a shared Jenkins instance or looking to consolidate their toolchain, GitLab CI is a mature, well-integrated option worth evaluating.