Introduction
Running CI/CD pipelines is essential for modern software development, but relying on shared GitLab runners can be limiting. You might need to deploy to local Docker services, access internal networks, or handle builds that exceed shared runner quotas.
Self-hosting a GitLab Runner in Docker solves all of this.
In this guide, we’ll walk through setting up a GitLab Runner inside a Docker container, connecting it to your GitLab projects, and configuring it to run production-ready CI/CD pipelines. Whether you’re using GitLab Cloud or self-hosted GitLab, this setup works seamlessly.
By the end, you’ll have a fully functional CI/CD runner that picks up jobs directly from GitLab and executes them on your own infrastructure.
What Is GitLab Runner and Why Self-Host?
GitLab Runner is the application that executes CI/CD pipeline jobs defined in your .gitlab-ci.yml files. By default, GitLab offers shared runners, but self-hosting gives you:
✅ Control over environment — Install exact dependencies your project needs
✅ Local Docker access — Deploy containers, restart services, build images
✅ Internal network access — Reach databases, APIs, and services on your private network
✅ No quota limits — Run unlimited jobs (hardware permitting)
✅ Cost savings — No per-minute billing on shared runners
Prerequisites: What You’ll Need
Before starting, ensure you have:
- Docker Desktop installed and running (macOS/Windows) or Docker Engine (Linux)
- GitLab account — either gitlab.com (cloud) or self-hosted GitLab instance
- A GitLab project you want to connect to the runner
- Mac or Linux machine (this guide uses macOS; Linux paths are nearly identical)
- Basic Docker knowledge — familiarity with Docker Compose is helpful
Step 1: Project Structure Setup
Start by creating a dedicated directory for your GitLab Runner configuration:
gitlab-runner/
├── Dockerfile
├── entrypoint.sh
├── docker-compose.yml
└── config/
└── config.toml # auto-generated after registrationThis structure keeps everything organized and makes it easy to manage your runner configuration.
Step 2: Custom Dockerfile for GitLab Runner
The default GitLab Runner image is minimal. We’ll extend it with Node.js 20 and Docker CLI so your jobs can build images, install dependencies, and control containers.
Create a Dockerfile in your gitlab-runner/ directory:
FROM gitlab/gitlab-runner:latest
USER root
# Install Node.js 20
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get install -y nodejs \
&& npm install -g npm@latest
# Install Docker CLI
RUN apt-get update && \
apt-get install -y ca-certificates curl gnupg && \
install -m 0755 -d /etc/apt/keyrings && \
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg && \
chmod a+r /etc/apt/keyrings/docker.gpg && \
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu jammy stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null && \
apt-get update && \
apt-get install -y docker-ce-cli docker-compose-plugin
# Copy and enable entrypoint
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["gitlab-runner", "run", "--user=root", "--working-directory=/home/gitlab-runner"]Pro tip: If your project doesn’t use Node.js, you can remove that installation section to reduce image size.
Step 3: Create the Entrypoint Script
The entrypoint script runs every time the container starts. It handles two critical tasks:
- Fix Docker socket permissions — Docker Desktop resets socket permissions on restart
- Trust all Git directories — Prevent ownership errors when running as root
Create entrypoint.sh:
#!/bin/bash
chmod 666 /var/run/docker.sock
git config --global --add safe.directory '*'
exec "$@"This small script saves you from common permission and Git authentication errors.
Step 4: Docker Compose Configuration
Now create docker-compose.yml to orchestrate your runner container:
services:
gitlab-runner:
build: . # uses local Dockerfile
container_name: gitlab-runner
restart: always
user: root # run as root to avoid su auth issues
volumes:
- ./config:/home/gitlab-runner/.gitlab-runner # persist config.toml
- /var/run/docker.sock:/var/run/docker.sock # Docker socket access
- /Users/youruser/.ssh:/root/.ssh:ro # SSH key for git pull
- /path/to/your/project:/app/project # mount project folderImportant volume configuration notes:
- Replace
/Users/youruserwith your actual home directory path - Add one volume mount per project the runner needs to access
- On Linux, the Docker socket path is identical:
/var/run/docker.sock - SSH mount allows passwordless Git operations
To build and start the runner:
docker compose up -d --buildStep 5: Register Runner with GitLab
Get Your Registration Token
- Go to your GitLab project → Settings → CI/CD → Runners
- Click New project runner
- Set a descriptive tag (e.g.,
my-server,production,docker-build) - Click Create runner
- Copy the registration token (starts with
glrt-…)
Register Inside the Container
docker exec -it gitlab-runner gitlab-runner registerAnswer the interactive prompts:
| GitLab URL | https://gitlab.com (or your self-hosted URL) |
| Token | Paste your glrt-… token |
| Description | my-server-runner |
| Tags | my-server |
| Executor | shell |
Add GitLab to Known Hosts (First Time Only)
ssh-keyscan gitlab.com >> ~/.ssh/known_hostsImportant: Run this on your host machine, not inside the container—your SSH directory is volume-mounted.
Verify Registration
docker exec -it gitlab-runner gitlab-runner listThe runner should now appear as Online in your GitLab project’s Settings → CI/CD → Runners page.
Step 6: Understanding config.toml
After registration, GitLab Runner auto-generates ./config/config.toml. Here are the key settings:
concurrent = 1 # max parallel jobs (start low on limited hardware)
check_interval = 0
shutdown_timeout = 0
[session_server]
session_timeout = 1800
[[runners]]
name = "my-server-runner"
url = "https://gitlab.com"
id = 12345678
token = "glrt-xxxxxxxxxxxx"
executor = "shell"
request_concurrency = 2 # suppresses long-polling warning
[runners.cache]
MaxUploadedArchiveSize = 0Key parameters explained:
concurrent— Maximum parallel jobs. Keep at1on low-RAM machines; increase only if your hardware supports itrequest_concurrency— Suppresses long-polling warning messages in logs- Each registered project adds a new
[[runners]]block
Creating Your First CI/CD Pipeline
Now let’s create a .gitlab-ci.yml file for a typical Node.js project with Docker deployment:
stages:
- pull
- install
- deploy
variables:
APP_DIR: /app/your-project # must match volume mount in docker-compose.yml
# Only run on development branch
workflow:
rules:
- if: $CI_COMMIT_BRANCH == "development"
pull:
stage: pull
tags:
- my-server # must match runner tag
script:
- cd $APP_DIR
- git pull origin development
only:
- development
install:
stage: install
tags:
- my-server
script:
- cd $APP_DIR
- npm install
only:
- development
deploy:
stage: deploy
tags:
- my-server
script:
- cd $APP_DIR
- docker compose up -d --build your-service-name
only:
- development
environment:
name: developmentBefore pushing to GitLab, verify:
✓ APP_DIR matches your runner’s docker-compose.yml volume mount
✓ Tag (my-server) matches your registered runner tag
✓ Service name matches your project’s docker-compose.yml (not container_name)
Conclusion
You now have a self-hosted GitLab Runner that executes CI/CD pipelines on your own infrastructure. This setup gives you complete control over:
- Environment dependencies and versions
- Docker operations and image builds
- Access to internal networks and services
- Job execution without quota limitations
Whether you’re deploying Odoo modules, Node.js applications, or complex Docker stacks, this runner provides the flexibility and performance your development workflow needs.
Next steps:
- Customize
.gitlab-ci.ymlfor your specific project - Test with a small pipeline first
- Scale up with additional runners if needed
- Monitor performance and adjust
concurrentsettings
Have questions about your setup? The official GitLab Runner documentation has additional executor types and advanced configurations. Other knowledge you might be need, visit Programmermumet.