Setting Up GitLab Container Registry: A Complete Guide for Self-Hosted Instances
Learn how to configure GitLab Container Registry on self-hosted instances, including best practices for Docker image management, CI/CD integration, and troubleshooting common issues like Cloudflare Tunnel configurations.
Introduction
If you’re running a self-hosted GitLab instance and want to store Docker images locally instead of relying on Docker Hub, GitLab’s built-in Container Registry is an excellent solution. This guide walks you through setting up and using GitLab Container Registry, with special attention to common pitfalls—especially when running behind a reverse proxy like Cloudflare Tunnel.
Why Use GitLab Container Registry?
- Avoid Docker Hub rate limits - No more pull restrictions
- Faster CI/CD pipelines - Images cached locally on your network
- Consistent versions - All projects use the same base image versions
- Single source of truth - Centralized image management
- Better security - Keep images within your infrastructure
Prerequisites
Before starting, ensure you have:
- GitLab EE/CE instance (version 15.0+)
- Admin access to GitLab (for initial setup)
- Docker installed on your local machine
- Basic understanding of Docker concepts
Part 1: Enabling Container Registry
Step 1: Verify Registry is Enabled
GitLab Container Registry is typically enabled by default in recent versions. To verify:
- Navigate to Admin Area → Settings → General
- Expand Visibility and access controls
- Look for Container Registry settings
Alternatively, check at the project level:
- Go to any project
- Navigate to Deploy → Container Registry
- If you see the registry interface, it’s enabled
Step 2: Server Configuration (Admin)
If Container Registry isn’t enabled, you’ll need to configure it at the server level.
For Omnibus GitLab installations, edit /etc/gitlab/gitlab.rb:
# Enable Container Registry
registry_external_url 'https://registry.gitlab.example.com'
# Or use the same domain with a different port
# registry_external_url 'https://gitlab.example.com:5050'
gitlab_rails['registry_enabled'] = true
gitlab_rails['registry_host'] = "registry.gitlab.example.com"
gitlab_rails['registry_port'] = "443"Then reconfigure GitLab:
sudo gitlab-ctl reconfigureIf you’re using Cloudflare Tunnel or another reverse proxy, ensure the registry URL is properly configured in your tunnel settings. The registry uses different endpoints than the main GitLab application.
Step 3: Verify Registry Status
Check if the registry service is running:
# Check registry status
sudo gitlab-ctl status registry
# Check registry configuration
sudo gitlab-rake gitlab:container_registry:checkPart 2: Creating a Centralized Docker Images Repository
Why Centralize Base Images?
Instead of each project pulling from Docker Hub, create a dedicated project to store shared base images:
your-group/
├── docker-images/ ← Centralized image storage
│ └── Container Registry
│ ├── node:24.12.0-bookworm-slim
│ ├── python:3.12-slim
│ └── golang:1.21-alpine
│
├── project-a/ ← Uses centralized images
├── project-b/ ← Uses centralized images
└── project-c/ ← Uses centralized imagesBenefits
| Aspect | Centralized | Per-Project |
|---|---|---|
| Storage | Single copy | Duplicates everywhere |
| Updates | Update once | Update each project |
| Consistency | Always same version | Can drift |
| CI Speed | Fast (cached) | Slower |
Step 1: Create the Project
- Create a new project:
your-group/docker-images - Initialize with a README
- Verify Container Registry is enabled for this project
Step 2: Set Up CI Pipeline for Image Uploads
Create .gitlab-ci.yml in your docker-images project:
stages:
- build
variables:
DOCKER_TLS_CERTDIR: '/certs'
# Node.js 24 (Recommended for most projects)
push-node-24:
stage: build
image: docker:24-cli
services:
- docker:24-dind
timeout: 30m
before_script:
- echo "Logging into GitLab Container Registry..."
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- echo "Pulling node:24.12.0-bookworm-slim from Docker Hub..."
- docker pull node:24.12.0-bookworm-slim
- echo "Tagging for GitLab registry..."
- docker tag node:24.12.0-bookworm-slim $CI_REGISTRY_IMAGE/node:24.12.0-bookworm-slim
- echo "Pushing to GitLab Container Registry..."
- docker push $CI_REGISTRY_IMAGE/node:24.12.0-bookworm-slim
- echo "Image available at: $CI_REGISTRY_IMAGE/node:24.12.0-bookworm-slim"
only:
- main
retry:
max: 2
when:
- runner_system_failure
- stuck_or_timeout_failure
# Add more images as needed
push-python-312:
stage: build
image: docker:24-cli
services:
- docker:24-dind
timeout: 30m
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker pull python:3.12-slim
- docker tag python:3.12-slim $CI_REGISTRY_IMAGE/python:3.12-slim
- docker push $CI_REGISTRY_IMAGE/python:3.12-slim
only:
- mainWhy use CI instead of manual push? GitLab CI runners typically have direct network access to the registry, bypassing any proxy limitations. This is especially important when using Cloudflare Tunnel or similar services.
Step 3: Trigger the Pipeline
git add .gitlab-ci.yml
git commit -m "ci: add docker image push pipeline"
git push origin mainMonitor the pipeline in GitLab UI. Once complete, your images will be available in the Container Registry.
Part 3: Using Images from Your Registry
In GitLab CI/CD Pipelines
Reference images using the full registry URL:
# Use your centralized Node.js image
image: registry.gitlab.example.com/your-group/docker-images/node:24.12.0-bookworm-slim
stages:
- build
- test
- deploy
build:
stage: build
script:
- npm ci
- npm run buildIn Local Development
# Login to your GitLab registry
docker login registry.gitlab.example.com
# Pull an image
docker pull registry.gitlab.example.com/your-group/docker-images/node:24.12.0-bookworm-slim
# Use in docker run
docker run -it --rm \
registry.gitlab.example.com/your-group/docker-images/node:24.12.0-bookworm-slim \
node --versionIn Dockerfiles
FROM registry.gitlab.example.com/your-group/docker-images/node:24.12.0-bookworm-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run buildPart 4: Authentication Methods
Method 1: Personal Access Token (Recommended for Users)
- Go to User Settings → Access Tokens
- Create token with scopes:
read_registry(for pulling)write_registry(for pushing)
- Login with the token:
docker login registry.gitlab.example.com -u your-username -p your-tokenMethod 2: Deploy Token (For Automation)
- Go to Project Settings → Repository → Deploy tokens
- Create token with appropriate scopes
- Use in CI or automation scripts
Method 3: CI/CD Variables (Automatic)
In GitLab CI, these variables are automatically available:
| Variable | Description |
|---|---|
$CI_REGISTRY | Registry URL |
$CI_REGISTRY_USER | CI user for authentication |
$CI_REGISTRY_PASSWORD | CI password (token) |
$CI_REGISTRY_IMAGE | Full image path for the project |
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRYPart 5: Image Naming Best Practices
Use Specific Version Tags
# Good - Specific and reproducible
node:24.12.0-bookworm-slim
# Avoid - Can change unexpectedly
node:24
node:latestInclude OS Variant
# Clear about the base OS
node:24.12.0-bookworm-slim # Debian Bookworm (stable)
node:24.12.0-alpine3.21 # Alpine Linux
python:3.12-slim # Minimal DebianImage Variant Selection Guide
| Variant | Size | Use Case |
|---|---|---|
bookworm-slim | ~250 MB | Default - Best compatibility |
alpine | ~130 MB | Minimal size, some compatibility issues |
bookworm (full) | ~1.1 GB | Need build tools |
For most Node.js projects, bookworm-slim is the best choice. It’s significantly smaller than the full image while maintaining excellent compatibility with npm packages.
Part 6: Special Considerations for Cloudflare Tunnel
If your GitLab instance is behind Cloudflare Tunnel, you may encounter issues with Docker Registry pushes.
The Problem
docker push registry.gitlab.example.com/group/project/image:tag
# Push starts, then fails with "unauthorized: authentication required"Symptoms:
- Login succeeds
- Push starts (layers begin uploading)
- Fails mid-transfer with “unauthorized” error
- Registry namespace created but empty
Why This Happens
Cloudflare Tunnel may have:
- Upload size limits (100MB on free plan)
- Timeout restrictions for long-running requests
- Chunked transfer encoding issues
Solutions
Solution 1: Use GitLab CI (Recommended)
If your GitLab Runner is on the internal network, it bypasses the tunnel:
push-image:
stage: build
image: docker:24-cli
services:
- docker:24-dind
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker pull node:24.12.0-bookworm-slim
- docker tag node:24.12.0-bookworm-slim $CI_REGISTRY_IMAGE/node:24.12.0-bookworm-slim
- docker push $CI_REGISTRY_IMAGE/node:24.12.0-bookworm-slimSolution 2: Push from Internal Network
If you have access to the internal network:
# Use internal GitLab URL directly
docker login http://192.168.x.x:5050
docker tag image:tag 192.168.x.x:5050/group/project/image:tag
docker push 192.168.x.x:5050/group/project/image:tagSolution 3: Configure Cloudflare Tunnel
In your cloudflared config, add specific settings for the registry:
# config.yml
ingress:
- hostname: registry.gitlab.example.com
service: http://localhost:5050
originRequest:
noTLSVerify: true
connectTimeout: 30s
tcpKeepAlive: 30s
noHappyEyeballs: true
- service: http_status:404Solution 4: Separate Registry Exposure
Consider exposing the registry through a different method:
- Direct port forwarding for registry (port 5050)
- VPN for internal access
- Separate subdomain with different tunnel config
Part 7: Registry Management
Viewing Images
- Navigate to Deploy → Container Registry
- Browse images and tags
- View image details (size, layers, vulnerabilities)
Cleaning Up Old Images
Manual Cleanup
- Go to Container Registry in GitLab UI
- Select images/tags to delete
- Click the trash icon
Automatic Cleanup Policy
- Settings → Packages and registries → Container Registry tag expiration policy
- Configure:
- Keep tags matching pattern:
latest,v\d+\.\d+\.\d+ - Remove tags older than: 90 days
- Keep most recent: 10 tags
- Keep tags matching pattern:
Storage Monitoring
Check storage usage:
- Project Settings → General → Storage
- Or via API:
curl --header "PRIVATE-TOKEN: <token>" \
"https://gitlab.example.com/api/v4/projects/<project-id>" | jq '.statistics'Part 8: Troubleshooting
Issue: “unauthorized: authentication required”
During login:
- Verify token has correct scopes
- Check token hasn’t expired
- Confirm registry URL is correct
During push (after successful login):
- Check storage quota
- Verify project permissions (need Developer role+)
- Try GitLab CI instead of manual push
- If using Cloudflare Tunnel, see Part 6
Issue: “manifest unknown”
- Verify image tag exists
- Check spelling of image name
- Ensure you’re logged in
Issue: Slow pulls in CI
- Ensure runners are on the same network as registry
- Check runner cache configuration
- Consider using Dependency Proxy for external images
Debugging Commands
# Check Docker credentials
cat ~/.docker/config.json | jq
# Test registry authentication
curl -u "username:token" https://registry.gitlab.example.com/v2/
# Check GitLab registry logs (server access required)
sudo gitlab-ctl tail registryPart 9: Automated Image Updates
Schedule Regular Updates
- Go to CI/CD → Schedules
- Create schedule: “Weekly base image updates”
- Set cron:
0 2 * * 0(Sundays at 2 AM) - Target branch:
main
Update Workflow
When new versions are released:
- Update image tags in CI pipeline
- Test in a feature branch
- Merge if tests pass
- Pipeline automatically pushes new images
# Example: Adding a new Node.js version
push-node-24-13:
extends: .push-image-template
variables:
SOURCE_IMAGE: node:24.13.0-bookworm-slim
TARGET_TAG: node:24.13.0-bookworm-slimConclusion
GitLab Container Registry provides a powerful way to manage Docker images within your infrastructure. By centralizing base images, you can:
- Eliminate Docker Hub rate limits
- Speed up CI/CD pipelines
- Maintain consistent versions across projects
- Keep full control over your container images
The key takeaways:
- Use a centralized project for base images
- Prefer CI pipelines for pushing images (especially behind proxies)
- Use specific version tags for reproducibility
- Set up cleanup policies to manage storage
- Monitor for Cloudflare Tunnel issues if applicable
Happy containerizing!
References
Share this post
Found this helpful? Share it with your network!
