GitLab Container Registry & Custom Runner Images: Complete Setup Guide
Ready to supercharge your GitLab CI/CD with custom Docker images? Whether you're building specialized tools, security scanners, or just want consistent environments across your pipelines, GitLab's Container Registry has you covered.
This step-by-step guide will walk you through setting up a secure, production-ready workflow using sanitized examples that you can adapt to your own projects. You'll learn how to build, tag, and push custom runner images, configure authentication, and troubleshoot common gotchas—all while keeping your sensitive details safe.
Let's get your custom images running like a pro!
1. Understanding GitLab Container Registry Structure
First things first—let's demystify how GitLab organizes container images. Registry URLs follow this predictable pattern:
registry.example.com/group123/project123/image-name:tag
Breaking it down:
registry.example.com- Your GitLab instance's registry URLgroup123- Your GitLab group nameproject123- Your specific project nameimage-name- Whatever you want to call your imagetag- Version tags likelatest,stable,v1.2.3
Pro tip: The project name in the URL must match your actual GitLab project name exactly—typos here will give you 404 errors that'll drive you crazy!
2. Creating Deploy Tokens for Registry Authentication
Deploy tokens are your golden ticket to secure GitLab Container Registry access. Think of them as API keys specifically designed for container operations—much safer than using personal access tokens or passwords.
Group-Level Deploy Token (Recommended)
Group-level tokens are the way to go because they work across all projects in your group. Here's how to set one up:
- Navigate to your GitLab group:
https://gitlab.example.com/group123 - Go to Settings → Repository → Deploy Tokens
- Click "Add deploy token"
- Fill in the details:
- Name:
registry-access-token(or whatever makes sense for your team) - Scopes: Check both
read_registryandwrite_registry - Username: Leave auto-generated or create your own
- Expires at: Set an expiration date (recommended for security)
- Name:
- Click "Create deploy token"
- ⚠️ Critical: Copy the username and token immediately—GitLab won't show it again!
Pro tip: Store these credentials in a password manager right away. You'll need them for authentication, and there's no way to retrieve the token value once you navigate away from the page.
3. Build, Tag, and Push Custom Runner Images
Time to get your hands dirty! Building and pushing images to GitLab's registry is straightforward once you know the workflow.
Step 1: Build with the Correct Registry Path
Important: Use the exact project name from your GitLab registry UI. Case matters, typos kill:
# Build with the exact project name from registry UI
docker build -t registry.example.com/group123/project123/security-scanner:latest -f Dockerfile .
Why this matters: GitLab is picky about naming. If your project is called "Security-Scanner" in GitLab but you build with "security-scanner", you'll get authentication errors that are really path errors in disguise.
Step 2: Tag for Multiple Environments
Smart teams use different tags for different environments. Here's a battle-tested approach:
# Tag for different environments
docker tag registry.example.com/group123/project123/security-scanner:latest registry.example.com/group123/project123/security-scanner:stable
docker tag registry.example.com/group123/project123/security-scanner:latest registry.example.com/group123/project123/security-scanner:test
docker tag registry.example.com/group123/project123/security-scanner:latest registry.example.com/group123/project123/security-scanner:unstable
Tagging strategy explained:
latest- Your newest build (use with caution in production)stable- Your production-ready versiontest- For staging environmentsunstable- Development and experimental builds
Step 3: Login to Registry
Before you can push, authenticate with your deploy token:
docker login registry.example.com
# Username: <your deploy token username>
# Password: <your deploy token password>
Pro tip: Docker will cache these credentials, so you only need to login once per machine (unless the token expires).
Step 4: Push All Tags
Now for the moment of truth—ship those images:
# Push each tag individually
docker push registry.example.com/group123/project123/security-scanner:latest
docker push registry.example.com/group123/project123/security-scanner:stable
docker push registry.example.com/group123/project123/security-scanner:test
docker push registry.example.com/group123/project123/security-scanner:unstable
# Or push all tags at once (Docker 20.10+)
docker push --all-tags registry.example.com/group123/project123/security-scanner
You should see upload progress, and once complete, your images will be visible in GitLab's Container Registry UI. ✅
4. Set Up CI/CD Variables for Pipeline Authentication
Your pipelines need credentials too! Here's how to securely store your deploy token for CI/CD use:
-
Navigate to your pipeline project: Settings → CI/CD → Variables
-
Add these variables:
- Variable:
CI_REGISTRY_USER - Value: Your deploy token username
- Flags: Leave unmasked (usernames aren't sensitive)
- Variable:
-
Add the password variable:
- Variable:
CI_REGISTRY_PASSWORD - Value: Your deploy token password
- Flags: Mark as Masked and Protected
- Variable:
Security note: Masked variables hide the value in job logs, and Protected variables only work on protected branches. This keeps your credentials safe while allowing your pipelines to access the registry.
Pro tip: Use these exact variable names (CI_REGISTRY_USER and CI_REGISTRY_PASSWORD) because GitLab's built-in registry features expect them.
5. Configure GitLab Runners for Private Registry Images
Here's where things get interesting. GitLab runners need to be configured to use your custom images, and there are a few ways to do it.
Register Runner with Custom Image
When registering a new runner, specify your custom image directly:
sudo gitlab-runner register \
--url https://gitlab.example.com \
--token <your-runner-token> \
--name security-scanner-runner \
--executor "docker" \
--docker-image "registry.example.com/group123/project123/security-scanner:stable"
Replace these values:
<your-runner-token>- Get this from GitLab → Settings → CI/CD → Runnerssecurity-scanner-runner- Give your runner a descriptive name- The image URL should match your actual registry path
Runner config.toml Configuration
After registration, your runner's config file should look like this:
[[runners]]
name = "security-scanner-runner"
url = "https://gitlab.example.com"
token = "<runner-token>"
executor = "docker"
[runners.docker]
image = "registry.example.com/group123/project123/security-scanner:stable"
privileged = false
pull_policy = "always"
volumes = ["/cache"]
Key settings explained:
pull_policy = "always"- Ensures you get the latest version of your tagged imageprivileged = false- Safer, unless you specifically need privileged accessvolumes = ["/cache"]- Speeds up builds by persisting cache between jobs
🚨 Critical: Runner Host Authentication
Here's the gotcha that trips up most people: The runner host (not just the pipeline) must be authenticated to pull private images.
On your runner server, authenticate as root:
# On the runner server, login as root
sudo docker login registry.example.com
# Use the same deploy token credentials from step 2
Why this matters: When GitLab Runner tries to pull your custom image, it's the Docker daemon on the runner host doing the pulling—not your pipeline job. If the host isn't authenticated, you'll get "pull access denied" errors even though your pipeline can authenticate just fine.
Pro tip: This authentication persists until the token expires or the Docker daemon is restarted. Consider adding this to your runner setup automation.
6. Pipeline Configuration for Custom Images
Now let's put it all together in your .gitlab-ci.yml file. There are two main approaches, each with their own benefits.
Option 1: Use Runner's Default Image (Recommended)
This approach relies on the image configured in your runner's config.toml file:
# Don't specify an image—use the one configured in runner's config.toml
before_script:
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin registry.example.com
stages:
- security-scan
- test
security_scan_job:
stage: security-scan
tags:
- security-scanner-runner # Use the runner with your custom image
script:
- security-tool --version
- security-tool scan /project-directory
- security-tool report --format json --output scan-results.json
artifacts:
reports:
security: scan-results.json
expire_in: 1 week
Why this is recommended:
- Cleaner
.gitlab-ci.ymlfiles - Image version controlled at the runner level
- Easier to update images across multiple pipelines
Option 2: Specify Image in Pipeline
For more explicit control, specify the image directly in your pipeline:
image: registry.example.com/group123/project123/security-scanner:stable
before_script:
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin registry.example.com
stages:
- security-scan
- test
vulnerability_scan:
stage: security-scan
script:
- security-tool --version
- security-tool scan .
- security-tool report --output vulnerabilities.json
artifacts:
reports:
security: vulnerabilities.json
dependency_scan:
stage: security-scan
script:
- dependency-checker scan package.json
- dependency-checker report --format gitlab
When to use this approach:
- Different jobs need different image versions
- You want explicit version control in your pipeline
- Testing new image versions before updating runners
Pro tip: The before_script login ensures your pipeline can pull images and push artifacts if needed. Even though the runner host is authenticated, jobs might need registry access for other operations.
7. Multi-Runner Setup for Different Tools
When you have multiple specialized tools, you'll want dedicated runners for each. Here's how to orchestrate a multi-tool pipeline:
stages:
- security
- quality
- build
# Security scanning with custom security tools
vulnerability_scan:
stage: security
tags:
- security-scanner-runner
script:
- vulnerability-tool scan .
- vulnerability-tool report --format gitlab-security
artifacts:
reports:
security: vulnerability-report.json
dependency_audit:
stage: security
tags:
- dependency-scanner-runner
script:
- dependency-tool audit package.json
- dependency-tool report --output dependencies.json
artifacts:
reports:
dependency_scanning: dependencies.json
# Code quality with specialized linting tools
code_quality_scan:
stage: quality
tags:
- code-quality-runner
script:
- quality-tool analyze src/
- quality-tool report --format codeclimate
artifacts:
reports:
codequality: code-quality.json
# Build with specific build tools
application_build:
stage: build
tags:
- build-tools-runner
script:
- build-tool install
- build-tool run build
- build-tool package --output dist/
artifacts:
paths:
- dist/
Pro tip: Use descriptive runner tags that match your tools. It makes pipelines self-documenting and easier to debug when things go wrong.
Runner organization strategies:
- By tool type:
security-tools,build-tools,test-tools - By environment:
dev-tools,staging-tools,prod-tools - By team:
frontend-tools,backend-tools,devops-tools
8. Troubleshooting Common Issues
Even with perfect setup, things can go sideways. Here are the most common issues and how to fix them:
"Pull Access Denied" or "Repository Does Not Exist"
Symptoms: Pipeline fails with authentication errors, even though you can docker login manually.
Usual suspects:
- Image path typos: Double-check your registry URL, group name, project name, and image name
- Runner host not authenticated: Run
sudo docker login registry.example.comon the runner server - Expired deploy tokens: Check if your token has expired and create a new one
- Wrong token scopes: Ensure your deploy token has
read_registrypermissions
Debug steps:
# On the runner host, test pulling manually
sudo docker pull registry.example.com/group123/project123/your-image:stable
# Check if docker login worked
sudo docker system info | grep -i registry
Pipeline Can Authenticate but Runner Cannot
Symptoms: Pipeline before_script login works fine, but the job fails to start with pull errors.
The fix: This is the classic "two different authentications" issue. Your pipeline authenticates for pipeline operations, but the runner host needs separate authentication to pull the job's container image.
# On the runner host (not in the pipeline)
sudo docker login registry.example.com
404 Errors on Correct-Looking URLs
Symptoms: Everything looks right, but GitLab says the image doesn't exist.
Common causes:
- Case sensitivity:
Security-Scanner≠security-scanner≠Security_Scanner - Project name mismatch: The image name must match your GitLab project name exactly
- Group vs project confusion: Make sure you're using the project URL, not the group URL
Debug tip: Copy the exact path from GitLab's Container Registry UI—don't type it manually.
Runner Registration Succeeds but Jobs Fail
Symptoms: gitlab-runner register completes successfully, but all jobs fail immediately.
Check these:
- Runner host authentication: Most common cause
- Image path accuracy: Verify against GitLab's registry UI
- Token validity: Confirm your deploy token hasn't expired
- Network connectivity: Ensure the runner can reach your GitLab registry
Debug commands:
# Check runner logs
sudo journalctl -u gitlab-runner -f
# Test image pull manually
sudo docker pull registry.example.com/group123/project123/your-image:stable
Pro tip: When troubleshooting, always check the GitLab Runner logs first. They're usually more detailed than the pipeline job logs.
9. Smart Tagging Strategy for Different Environments
A solid tagging strategy will save you headaches down the road. Here's a battle-tested approach that scales with your team:
Build Once, Tag Multiple Times
# Build your image once with a specific version
docker build -t registry.example.com/group123/project123/security-scanner:v1.2.3 .
# Tag for different environments
docker tag registry.example.com/group123/project123/security-scanner:v1.2.3 registry.example.com/group123/project123/security-scanner:latest
docker tag registry.example.com/group123/project123/security-scanner:v1.2.3 registry.example.com/group123/project123/security-scanner:stable
docker tag registry.example.com/group123/project123/security-scanner:v1.2.3 registry.example.com/group123/project123/security-scanner:unstable
# Push all tags at once (Docker 20.10+)
docker push --all-tags registry.example.com/group123/project123/security-scanner
Recommended Tagging Conventions
Version-based tags:
v1.2.3- Specific semantic versionv1.2- Minor version familyv1- Major version family
Environment-based tags:
stable- Production-ready releasesunstable- Development buildstest- Staging environmentlatest- Most recent build (use cautiously in production)
Feature-based tags:
feature-auth-upgrade- Specific feature brancheshotfix-security-patch- Emergency fixesexperimental-new-scanner- Experimental builds
Automated Tagging in CI/CD
Here's how to automate smart tagging in your pipelines:
build_and_tag:
stage: build
script:
# Build with commit SHA for traceability
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
# Tag based on branch
- |
if [ "$CI_COMMIT_BRANCH" == "main" ]; then
docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA $CI_REGISTRY_IMAGE:stable
docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA $CI_REGISTRY_IMAGE:latest
elif [ "$CI_COMMIT_BRANCH" == "develop" ]; then
docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA $CI_REGISTRY_IMAGE:unstable
fi
# Tag if it's a version tag
- |
if [[ "$CI_COMMIT_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
fi
# Push all tags
- docker push --all-tags $CI_REGISTRY_IMAGE
only:
- main
- develop
- tags
Pro tip: Always include the commit SHA in your tags—it makes debugging production issues much easier when you can trace back to the exact code that built an image.
10. Security Best Practices & Production Considerations
Security isn't an afterthought—it's built into every step of this workflow. Here are the key practices to keep your setup secure and production-ready:
Authentication Security
- Use group-level deploy tokens instead of personal access tokens—they're more secure and don't depend on individual user accounts
- Rotate tokens regularly (every 90 days is a good baseline)
- Set token expiration dates to force regular rotation
- Never share tokens in public repos or documentation—always use sanitized examples
- Use masked variables in GitLab CI/CD for all sensitive data
Runner Security
- Avoid privileged runners unless absolutely necessary—they're a security risk
- Use specific runner tags to control which jobs run on which runners
- Keep runners updated to the latest stable versions
- Monitor runner logs for suspicious activity
- Isolate runners in separate networks when possible
Image Security
- Scan images for vulnerabilities before pushing to production
- Use minimal base images (Alpine, distroless) to reduce attack surface
- Keep base images updated with security patches
- Don't store secrets in images—use environment variables or mounted secrets
- Sign images for production use (consider Docker Content Trust)
Network Security
- Use HTTPS for all registry communications (should be default)
- Restrict registry access to specific IP ranges if possible
- Monitor registry access logs for unusual activity
- Use private networks for runner-to-registry communication when feasible
Additional Production Tips
Monitoring and Observability:
- Set up alerts for failed pulls or authentication errors
- Monitor registry storage usage and implement cleanup policies
- Track image pull frequencies to identify unused images
- Log all registry access for security auditing
Performance Optimization:
- Use registry mirrors or caching proxies for frequently-pulled images
- Implement image cleanup policies to manage storage costs
- Consider using multi-stage builds to reduce image sizes
- Use
.dockerignorefiles to exclude unnecessary files
Backup and Recovery:
- Backup critical images to external storage
- Document image rebuild processes for disaster recovery
- Test image restoration procedures regularly
- Keep Dockerfiles and build scripts in version control
Pro tip: Treat your container registry like any other critical infrastructure—monitor it, secure it, and have a recovery plan. Your future self will thank you when things go wrong at 2 AM.
📝 Final Notes & Pro Tips
You've made it! With GitLab Container Registry and custom runner images in your toolkit, you're ready to build some seriously powerful CI/CD pipelines. Here are the key takeaways to remember:
Essential Reminders
- Use deploy tokens, not personal access tokens—they're more secure and team-friendly
- Authenticate the runner host separately from your pipeline jobs—this trips up everyone at least once
- Match image paths exactly to your GitLab project structure—case sensitivity matters
- Use meaningful tags for different environments and versions—your future self will thank you
- Keep security front and center with token rotation, minimal privileges, and regular updates
Quick Reference Commands
Build and push workflow:
# Build, tag, and push in one go
docker build -t registry.example.com/group123/project123/your-image:stable .
docker push registry.example.com/group123/project123/your-image:stable
Runner authentication:
# On runner host
sudo docker login registry.example.com
Debug image issues:
# Test manual pull
sudo docker pull registry.example.com/group123/project123/your-image:stable
# Check runner logs
sudo journalctl -u gitlab-runner -f
What's Next?
Ready to level up your GitLab game? Consider exploring:
- Multi-stage Docker builds for optimized images
- GitLab's dependency proxy for faster, cached builds
- Container scanning with GitLab's built-in security features
- Kubernetes integration for container orchestration
- Registry cleanup policies to manage storage costs
Example Registry URLs (Remember to Replace!)
registry.gitlab.example.com/devops-team/security-tools/vulnerability-scanner:stableregistry.internal.company.com/frontend-team/build-tools/node-builder:v16.14.2
Remember: This guide uses sanitized examples for security. Replace all placeholders (example.com, group123, project123) with your actual values, and never include real company names, usernames, or sensitive details in public documentation.
Ready to supercharge your pipelines? Start building those custom images and watch your CI/CD workflows become more powerful, consistent, and team-friendly.
Happy containerizing!