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:

  1. Navigate to Admin AreaSettingsGeneral
  2. Expand Visibility and access controls
  3. Look for Container Registry settings

Alternatively, check at the project level:

  1. Go to any project
  2. Navigate to DeployContainer Registry
  3. 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 reconfigure

If 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:check

Part 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 images

Benefits

AspectCentralizedPer-Project
StorageSingle copyDuplicates everywhere
UpdatesUpdate onceUpdate each project
ConsistencyAlways same versionCan drift
CI SpeedFast (cached)Slower

Step 1: Create the Project

  1. Create a new project: your-group/docker-images
  2. Initialize with a README
  3. 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:
    - main

Why 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 main

Monitor 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 build

In 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 --version

In 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 build

Part 4: Authentication Methods

  1. Go to User SettingsAccess Tokens
  2. Create token with scopes:
    • read_registry (for pulling)
    • write_registry (for pushing)
  3. Login with the token:
docker login registry.gitlab.example.com -u your-username -p your-token

Method 2: Deploy Token (For Automation)

  1. Go to Project SettingsRepositoryDeploy tokens
  2. Create token with appropriate scopes
  3. Use in CI or automation scripts

Method 3: CI/CD Variables (Automatic)

In GitLab CI, these variables are automatically available:

VariableDescription
$CI_REGISTRYRegistry URL
$CI_REGISTRY_USERCI user for authentication
$CI_REGISTRY_PASSWORDCI password (token)
$CI_REGISTRY_IMAGEFull image path for the project
before_script:
  - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY

Part 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:latest

Include 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 Debian

Image Variant Selection Guide

VariantSizeUse Case
bookworm-slim~250 MBDefault - Best compatibility
alpine~130 MBMinimal size, some compatibility issues
bookworm (full)~1.1 GBNeed 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

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-slim

Solution 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:tag

Solution 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:404

Solution 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

  1. Navigate to DeployContainer Registry
  2. Browse images and tags
  3. View image details (size, layers, vulnerabilities)

Cleaning Up Old Images

Manual Cleanup

  1. Go to Container Registry in GitLab UI
  2. Select images/tags to delete
  3. Click the trash icon

Automatic Cleanup Policy

  1. SettingsPackages and registriesContainer Registry tag expiration policy
  2. Configure:
    • Keep tags matching pattern: latest, v\d+\.\d+\.\d+
    • Remove tags older than: 90 days
    • Keep most recent: 10 tags

Storage Monitoring

Check storage usage:

  • Project SettingsGeneralStorage
  • 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 registry

Part 9: Automated Image Updates

Schedule Regular Updates

  1. Go to CI/CDSchedules
  2. Create schedule: “Weekly base image updates”
  3. Set cron: 0 2 * * 0 (Sundays at 2 AM)
  4. Target branch: main

Update Workflow

When new versions are released:

  1. Update image tags in CI pipeline
  2. Test in a feature branch
  3. Merge if tests pass
  4. 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-slim

Conclusion

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:

  1. Use a centralized project for base images
  2. Prefer CI pipelines for pushing images (especially behind proxies)
  3. Use specific version tags for reproducibility
  4. Set up cleanup policies to manage storage
  5. Monitor for Cloudflare Tunnel issues if applicable

Happy containerizing!

References