Setting Up GitLab Container Registry: A Complete Guide for Self-Hosted Instances

Setting Up GitLab Container Registry: A Complete Guide for Self-Hosted Instances

· 9 min read
gitlab-for-your-team

Introduction

Self-hosted GitLab ships with a built-in Container Registry. Store Docker images locally instead of pulling from Docker Hub. This guide covers setup, usage, and pitfalls — especially behind Cloudflare Tunnel.

Why Use GitLab Container Registry?

  • Avoid Docker Hub rate limits - No pull restrictions
  • Faster CI/CD pipelines - Images cached on your network
  • Consistent versions - All projects share the same base images
  • Single source of truth - Centralized image management
  • Better security - Images stay in your infrastructure

Prerequisites

You need:

  • GitLab EE/CE instance (version 15.0+)
  • Admin access to GitLab
  • Docker installed locally
  • Basic Docker knowledge

Part 1: Enabling Container Registry

Step 1: Verify Registry is Enabled

Recent GitLab versions enable the Container Registry by default. To check:

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

Or at project level:

  1. Go to any project
  2. Navigate to DeployContainer Registry
  3. If the registry interface loads, it’s enabled

Step 2: Server Configuration (Admin)

If disabled, configure it at the server level.

For Omnibus GitLab, 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:

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

One project stores all shared base images. Every other project pulls from it.

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. Images appear in Container Registry once the job completes.

Part 3: Using Images from Your Registry

In GitLab CI/CD Pipelines

Reference images by 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 (pulling)
    • write_registry (pushing)
  3. Login:
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)

GitLab CI provides these variables automatically:

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

Docker Registry pushes can break behind Cloudflare Tunnel.

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 can cause:

  • Upload size limits (100MB on free plan)
  • Timeout restrictions on long-running requests
  • Chunked transfer encoding issues

Solutions

Runners on the internal network bypass the tunnel entirely:

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

# 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

Add registry-specific settings to your cloudflared config:

# 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

Expose the registry through a different path:

  • Direct port forwarding (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. Open 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: latest, v\d+\.\d+\.\d+
    • Remove tags older than 90 days
    • Keep most recent 10 tags

Storage Monitoring

Check usage at 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 scopes
  • Check token expiry
  • Confirm registry URL

During push (after successful login):

  • Check storage quota
  • Verify project permissions (Developer role or higher)
  • Try GitLab CI instead of manual push
  • If using Cloudflare Tunnel, see Part 6

Issue: “manifest unknown”

  • Verify the image tag exists
  • Check image name spelling
  • Confirm you’re logged in

Issue: Slow pulls in CI

  • Put runners on the same network as the registry
  • Check runner cache config
  • Use 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 drop:

  1. Update image tags in the CI pipeline
  2. Test in a feature branch
  3. Merge if tests pass
  4. Pipeline pushes new images automatically
# 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 keeps Docker images inside your infrastructure. Centralize base images to skip Docker Hub rate limits, speed up pipelines, and lock down versions.

Key takeaways:

  1. Use a centralized project for base images
  2. Push via CI pipelines, especially behind proxies
  3. Pin specific version tags for reproducibility
  4. Set up cleanup policies to manage storage
  5. Watch for Cloudflare Tunnel issues if applicable

References