GitLab Container Registry

A complete guide to GitLab Container Registry covering overview, authentication, pushing and pulling images, CI/CD integration, and best practices for container image management.

Container Registry GitLab CI/CD Security
What is GitLab Container Registry?

The GitLab Container Registry is a secure, private Docker registry integrated directly into GitLab. It allows you to store, manage, and distribute container images alongside your source code — eliminating the need for a separate registry service like Docker Hub, Amazon ECR, or Google Artifact Registry.

What makes GitLab Container Registry particularly compelling is its integration with GitLab CI/CD. Your pipelines can build images, push them to the registry, and then deploy them — all within GitLab, using the same authentication and permissions. Images are stored per-project, so they're automatically protected by your existing GitLab access controls. You don't need to configure separate credentials for the registry.

Because the registry lives within GitLab, it benefits from the same observability, auditing, and access controls as the rest of your GitLab instance. You can see which images exist for each project, track when they were pushed, and control who can access them. For teams already using GitLab, this integration is a major simplification.

Image Storage

Store Docker and OCI images per project. Supports multi-architecture and manifest lists.

Integrated Auth

Uses GitLab credentials. CI/CD jobs authenticate with $CI_REGISTRY_USER and $CI_REGISTRY_PASSWORD automatically.

CI/CD Integration

Build, push, and pull images in pipelines. No extra credentials needed.

Vulnerability Scanning

Integrated container scanning with GitLab's security scanning features.

Cleanup Policies

Automatic cleanup of old images based on rules (age, tag patterns).

Tag Management

Manage image tags, delete unused tags, and track image history.
Registry URL: For GitLab.com, the registry is at registry.gitlab.com. Image paths follow the pattern registry.gitlab.com/<namespace>/<project>. For self-hosted GitLab, use your GitLab instance's registry URL.
Registry Architecture

GitLab Container Registry is built on top of the open-source Docker Registry (now CNCF Distribution) project, wrapped with GitLab's authentication and authorization layer. This means your image storage is standard OCI-compliant, compatible with any Docker or OCI client, but access to it is controlled by GitLab.

When a client (docker, podman, or GitLab Runner) pushes or pulls an image, it authenticates with GitLab. GitLab verifies the client has access to the project's registry, then issues a short-lived token that the client uses for the actual registry operations. This is why you use docker login registry.gitlab.com with your GitLab credentials — GitLab is the auth server.

The registry itself stores images as content-addressed blobs, exactly like any Docker registry. The image metadata (tags, manifest) is stored in GitLab's database, giving you the visibility and management features you see in the UI. This separation — GitLab for metadata, registry for blobs — is what makes the integration work.

# Registry URL structure # # For GitLab.com: # registry.gitlab.com// # # Examples: # registry.gitlab.com/mygroup/myproject # registry.gitlab.com/mygroup/subgroup/myproject # # For self-hosted GitLab: # registry.example.com// # # Image path: # ///: # # Example: # registry.gitlab.com/mygroup/myproject/app:1.2.3 # Authentication flow: # 1. Client runs docker login registry.gitlab.com # 2. Client provides username and password (GitLab credentials) # 3. GitLab verifies credentials and returns a JWT token # 4. Client uses token for registry operations # 5. Registry validates token with GitLab on each request # Container Registry visibility # Project → Settings → General → Visibility → Container Registry # - Everyone with access: Any project member can pull # - Only project members: Only project members can pull # - Only project members (strict): Only members can push/pull # Enable/disable container registry # Project → Settings → General → Visibility → Container Registry # Check registry status (self-hosted admin) sudo gitlab-ctl status registry sudo gitlab-ctl tail registry
Registry Components:
  • GitLab Rails: Handles authentication and metadata
  • Registry: Stores image blobs (Docker Distribution)
  • PostgreSQL: Stores image metadata
  • Storage: Object storage (S3, GCS) or local filesystem for blobs
Authentication

There are several ways to authenticate with GitLab Container Registry, depending on whether you're working locally, in a CI/CD pipeline, or with a deployment tool. The right method depends on the context — CI jobs get automatic authentication through built-in variables, while local development and deployment tools use personal access tokens or deploy tokens.

The most convenient method is CI/CD automatic authentication. In a pipeline, GitLab sets $CI_REGISTRY_USER, $CI_REGISTRY_PASSWORD, and $CI_REGISTRY automatically. The password is a short-lived job token that only lasts for the duration of the job. This is the most secure and most convenient method — you don't need to configure any credentials.

For local development and deployment tools, use a personal access token (PAT) with the read_registry or write_registry scope. For CI/CD systems outside GitLab, use deploy tokens with the read_registry or write_registry scope — deploy tokens are safer because they're scoped to a project and can be revoked without affecting a user.

# Method 1: Automatic CI/CD authentication (recommended) # In a pipeline, these variables are set automatically: # $CI_REGISTRY = registry.gitlab.com # $CI_REGISTRY_USER = gitlab-ci-token # $CI_REGISTRY_PASSWORD = $CI_JOB_TOKEN (short-lived) # $CI_REGISTRY_IMAGE = registry.gitlab.com/ build: stage: build image: docker:24 services: - docker:24-dind script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA . - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA # Method 2: Personal Access Token (local development) # Create a PAT: # User Settings → Access Tokens → New token # Scopes: read_registry, write_registry docker login registry.gitlab.com -u -p docker push registry.gitlab.com/mygroup/myproject/app:latest # Method 3: Deploy Token (CI/CD outside GitLab, deployment) # Create a deploy token: # Project → Settings → Repository → Deploy Tokens # Scopes: read_registry, write_registry docker login registry.gitlab.com -u -p docker pull registry.gitlab.com/mygroup/myproject/app:latest # Method 4: CI/CD job token (from another project) # In a pipeline, use the job token to pull from another project: docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY docker pull $CI_REGISTRY/mygroup/myproject/app:latest # Method 5: Username/password (basic auth) # Not recommended for automation, but works for manual tests docker login registry.gitlab.com -u -p # Authentication best practices: # - Use CI/CD automatic auth in pipelines # - Use deploy tokens for deployment # - Use personal access tokens for local development # - Never use username/password in automation # - Rotate tokens regularly # - Use read_registry scope for pull-only access # Verify authentication docker login registry.gitlab.com # Login Succeeded # Check what you can pull docker pull registry.gitlab.com/mygroup/myproject/app:latest
Authentication Best Practices:
  • Use CI/CD automatic authentication in pipelines
  • Use deploy tokens for production deployments
  • Use personal access tokens for local development
  • Use the read_registry scope for pull-only access
  • Rotate tokens regularly
  • Never commit credentials to the repository
Pushing Images to the Registry

Pushing images to GitLab Container Registry is straightforward: build your image, tag it with the registry's URL and desired tag, then push. The image will be stored under the project's namespace, and it will show up in the project's Container Registry UI page.

Tagging strategy is important. A common pattern is to tag each image with the commit SHA (for traceability) and also with the branch name (for convenience). For releases, tag with the version number. Avoid using :latest in production — it's ambiguous and makes rollbacks harder.

In a CI pipeline, the build job builds and pushes the image. Subsequent jobs (test, deploy) pull it from the registry. This pattern — build once, deploy many times — ensures that what you tested is exactly what you deploy.

# Push an image to the registry # 1. Build the image docker build -t myapp:latest . # 2. Tag with the registry URL docker tag myapp:latest registry.gitlab.com/mygroup/myproject/myapp:latest # 3. Login docker login registry.gitlab.com -u -p # 4. Push docker push registry.gitlab.com/mygroup/myproject/myapp:latest # Push multiple tags docker tag myapp:latest registry.gitlab.com/mygroup/myproject/myapp:v1.2.3 docker tag myapp:latest registry.gitlab.com/mygroup/myproject/myapp:$CI_COMMIT_SHORT_SHA docker push registry.gitlab.com/mygroup/myproject/myapp:v1.2.3 docker push registry.gitlab.com/mygroup/myproject/myapp:$CI_COMMIT_SHORT_SHA # Complete CI/CD pipeline for building and pushing stages: - build - test - deploy variables: IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA build: stage: build image: docker:24 services: - docker:24-dind variables: DOCKER_TLS_CERTDIR: "/certs" before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY script: - docker build -t $IMAGE_TAG . - docker push $IMAGE_TAG # Tag with branch name too - docker tag $IMAGE_TAG $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG test: stage: test image: docker:24 services: - docker:24-dind before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY script: - docker pull $IMAGE_TAG - docker run --rm $IMAGE_TAG npm test deploy: stage: deploy image: bitnami/kubectl:latest before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY script: - kubectl set image deployment/myapp myapp=$IMAGE_TAG only: - main # Tagging strategy: # - $CI_COMMIT_SHORT_SHA: Every commit → immutable # - $CI_COMMIT_REF_SLUG: Branch name → mutable, convenient # - $CI_COMMIT_TAG: Git tag (for releases) → immutable # - latest: Only for convenience, never for production # Push a multi-arch image (buildx) docker buildx build \ --platform linux/amd64,linux/arm64 \ -t registry.gitlab.com/mygroup/myproject/myapp:latest \ --push .
Tagging Anti-Patterns:
  • Using :latest in production — ambiguous and hard to roll back
  • Reusing tags — the tag should point to one immutable image
  • Only using branch names — branches change, commits don't
  • Not tagging with commit SHA — loses traceability
Pulling Images from the Registry

Pulling images from GitLab Container Registry is equally straightforward: login (if not already authenticated), then pull by the full image path. Images are pulled using the same authentication as pushing, so a token with read_registry scope is sufficient.

In CI pipelines, pulling is often implicit — when you deploy an image, the deployment tool (kubectl, helm) pulls it from the registry. In that case, the deployment needs registry credentials, which are often provided as Kubernetes image pull secrets or cloud IAM roles.

For Kubernetes deployments, use imagePullSecrets to grant the cluster access to pull from GitLab Container Registry. Create the secret from a deploy token, then reference it in your pod spec. This is more secure than embedding credentials in the image.

# Pull an image from GitLab Container Registry # 1. Login docker login registry.gitlab.com -u -p # 2. Pull docker pull registry.gitlab.com/mygroup/myproject/myapp:latest # 3. Run docker run registry.gitlab.com/mygroup/myproject/myapp:latest # Pull with a deploy token (recommended for deployment) docker login registry.gitlab.com -u -p docker pull registry.gitlab.com/mygroup/myproject/myapp:v1.2.3 # Kubernetes image pull secret kubectl create secret docker-registry gitlab-registry \ --docker-server=registry.gitlab.com \ --docker-username= \ --docker-password= \ --docker-email=unused \ --namespace=default # Reference in deployment apiVersion: apps/v1 kind: Deployment metadata: name: myapp spec: template: spec: imagePullSecrets: - name: gitlab-registry containers: - name: myapp image: registry.gitlab.com/mygroup/myproject/myapp:v1.2.3 # Using a service account with the secret apiVersion: v1 kind: ServiceAccount metadata: name: myapp-sa imagePullSecrets: - name: gitlab-registry # Check what images are available # GitLab UI: Project → Packages & Registries → Container Registry # List tags via API curl --header "PRIVATE-TOKEN: " \ "https://gitlab.com/api/v4/projects//registry/repositories" # List tags for a repository curl --header "PRIVATE-TOKEN: " \ "https://gitlab.com/api/v4/projects//registry/repositories//tags" # Pulling tips: # - Use specific tags in production (never latest) # - Cache images on nodes (kubelet does this) # - Use imagePullPolicy: IfNotPresent for efficiency # - Use imagePullPolicy: Always for critical updates
Image Pull Policy:
  • IfNotPresent: Pull only if not cached (default for tagged images)
  • Always: Pull every time (needed for :latest)
  • Never: Never pull (use cached image)
  • For immutable tags (commit SHA), use IfNotPresent for efficiency
CI/CD Integration

The GitLab Container Registry is designed to work seamlessly with GitLab CI/CD. Every pipeline has automatic access to the registry, and built-in variables eliminate the need to configure credentials. This makes build-and-push workflows extremely simple.

The typical pipeline builds an image in the build stage, pushes it to the registry, runs tests against the pushed image in the test stage, and deploys it in the deploy stage. Because the image is stored in the registry between stages, each stage uses exactly the same image — eliminating the "works in build but not in deploy" problem.

For more sophisticated pipelines, you can also use the registry to store cache layers, to distribute builds across multiple jobs, and to share images between projects (using job tokens or deploy tokens).

# Complete CI/CD pipeline using GitLab Container Registry stages: - build - test - security - deploy variables: DOCKER_DRIVER: overlay2 DOCKER_TLS_CERTDIR: "/certs" IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA # Build and push image build: stage: build image: docker:24 services: - docker:24-dind before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY script: - docker build -t $IMAGE_TAG . - docker push $IMAGE_TAG # Additional tags for convenience - docker tag $IMAGE_TAG $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG # Test the pushed image test: stage: test image: docker:24 services: - docker:24-dind before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY script: - docker pull $IMAGE_TAG - docker run --rm $IMAGE_TAG npm test # Container scanning (GitLab Premium) container_scanning: stage: security image: registry.gitlab.com/security-products/container-scanning:latest variables: DOCKER_IMAGE: $IMAGE_TAG script: - /analyzer run artifacts: reports: container_scanning: gl-container-scanning-report.json only: - main # Deploy with the pushed image deploy_staging: stage: deploy image: bitnami/kubectl:latest before_script: - echo "Deploying to staging" script: - kubectl set image deployment/myapp myapp=$IMAGE_TAG -n staging environment: name: staging only: - main deploy_production: stage: deploy image: bitnami/kubectl:latest script: - kubectl set image deployment/myapp myapp=$IMAGE_TAG -n production environment: name: production when: manual only: - tags # Using registry as build cache build_cached: stage: build image: docker:24 services: - docker:24-dind before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY script: - | docker build \ --cache-from $CI_REGISTRY_IMAGE:cache \ --tag $IMAGE_TAG \ --tag $CI_REGISTRY_IMAGE:cache \ . - docker push $IMAGE_TAG - docker push $CI_REGISTRY_IMAGE:cache # Multi-arch build build_multiarch: stage: build image: docker:24 services: - docker:24-dind before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - docker run --privileged --rm tonistiigi/binfmt --install all - docker buildx create --use script: - | docker buildx build \ --platform linux/amd64,linux/arm64 \ --tag $IMAGE_TAG \ --push \ .
CI/CD Integration Best Practices:
  • Use built-in $CI_REGISTRY_* variables for auth
  • Build once, deploy many times (use the same image everywhere)
  • Tag with commit SHA for traceability
  • Use cache-from to speed up builds
  • Enable container scanning for vulnerabilities
  • Restrict deploy jobs to protected branches
Cleanup Policies

Registry storage grows quickly if you don't clean up old images. Every pipeline that pushes an image adds storage, and without cleanup, costs spiral. GitLab provides automatic cleanup policies to remove old, unused images based on rules you define.

Cleanup policies can be based on age (delete images older than N days), name pattern (delete images matching a regex), tag pattern (delete tags matching a pattern), and keep count (keep only the N most recent images per tag). You can combine multiple rules for precise control.

The most common pattern is: keep the last 10 images per branch, delete anything older than 30 days, and always keep images tagged with release versions. This balances storage cost with the need for rollback capability.

# Configure cleanup policies # Project → Settings → Packages and registries → Cleanup policies # Cleanup policy options: # - Age (delete images older than N days) # - Name pattern # - Tag pattern # - Keep count # Example: Delete images older than 30 days # Delete images older than 30 days # unless tagged with "keep" # Example: Keep last 10 images per branch # Keep the 10 most recent images # Example: Delete dev images after 7 days # Tag pattern: dev-* # Age: 7 days # Example: Keep only release images # Tag pattern: ^v\d+\.\d+\.\d+$ # Keep forever # Cleanup policy in .gitlab-ci.yml (no, cleanup is UI-configured) # API to configure cleanup curl --request PATCH \ --header "PRIVATE-TOKEN: " \ --header "Content-Type: application/json" \ --data '{ "container_expiration_policy_attributes": { "cadence": "1d", "enabled": true, "keep_n": 10, "older_than": "30d", "name_regex_delete": ".*", "name_regex_keep": "release-.*" } }' \ "https://gitlab.com/api/v4/projects/" # Cleanup policy attributes: # - cadence: How often to run (1d, 7d, 14d, 1month) # - enabled: true/false # - keep_n: Number of images to keep per tag # - older_than: Age threshold (7d, 30d, 90d) # - name_regex_delete: Which tags to consider for deletion # - name_regex_keep: Which tags to always keep # Manual cleanup # Delete a specific tag curl --request DELETE \ --header "PRIVATE-TOKEN: " \ "https://gitlab.com/api/v4/projects//registry/repositories//tags/" # Cleanup strategy: # 1. Keep release tags forever # 2. Keep last 10 images per branch # 3. Delete feature branch images after 7 days # 4. Delete dev/test images after 30 days # Monitor storage usage # Project → Packages and Registries → Container Registry # Shows storage usage per project # Registry cleanup for self-hosted sudo gitlab-ctl registry-garbage-collect
Cleanup Policies:
  • Cleanup policies run daily automatically
  • Deleted images can't be recovered
  • Test cleanup rules with a dry run first
  • Keep release images forever (use regex)
  • Monitor storage usage
  • Use cleanup to reduce costs
Registry Security

Container images often contain sensitive information (API keys accidentally committed, internal code, vulnerable dependencies). Registry security covers both protecting access to images and ensuring images themselves are safe.

The key access controls are: visibility settings (who can access the registry), deploy tokens (scoped credentials for external access), and cleanup policies (reduce exposure window). For image safety, use container scanning (scan for vulnerabilities), image signing (verify integrity), and least privilege (don't store secrets in images).

GitLab's container scanning integrates with the registry to scan images after they're pushed. When a new image is pushed, the scanner pulls it, scans it, and reports vulnerabilities in the pipeline. This catches issues before images reach production.

# Registry visibility # Project → Settings → General → Visibility → Container Registry # Options: # - Everyone with access: Public and internal projects # - Only project members: Private projects # - Only project members (strict): Strictest # Deploy token for registry # Project → Settings → Repository → Deploy Tokens # - Name: registry-read # - Username: (auto-generated) # - Expires at: 1 year from now # - Scopes: read_registry # Using deploy token docker login registry.gitlab.com -u -p # Container scanning (GitLab Premium) # Include the scanner template include: - template: Security/Container-Scanning.gitlab-ci.yml container_scanning: variables: DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA # Image signing with Cosign # Install cosign - curl -L https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64 -o /usr/local/bin/cosign - chmod +x /usr/local/bin/cosign # Sign image (after pushing) sign: stage: security image: bitnami/cosign:latest script: - cosign sign --key $COSIGN_PRIVATE_KEY $IMAGE_TAG only: - main # Verify signature (before deploy) verify: stage: deploy image: bitnami/cosign:latest script: - cosign verify --key $COSIGN_PUBLIC_KEY $IMAGE_TAG # Never store secrets in images # BAD: ENV API_KEY=supersecret in Dockerfile # GOOD: Pass secrets at runtime via env vars # Scan images for secrets (gitleaks) secret_scan: stage: security image: zricethezav/gitleaks:latest script: - gitleaks detect --source /builds # Registry security best practices: # 1. Set appropriate visibility (don't expose to everyone) # 2. Use deploy tokens with minimal scope # 3. Enable container scanning # 4. Sign images with Cosign # 5. Never store secrets in images # 6. Use image pull policies wisely # 7. Rotate tokens regularly # 8. Audit access to the registry
Registry Security Checklist:
  • Restrict registry visibility appropriately
  • Use deploy tokens for external access
  • Enable container scanning
  • Sign images with Cosign
  • Never store secrets in images
  • Rotate tokens regularly
  • Monitor for unauthorized pushes
  • Use cleanup policies to limit exposure
Frequently Asked Questions
What's the registry URL for GitLab.com?
registry.gitlab.com. Image paths follow the pattern registry.gitlab.com/<namespace>/<project>. For self-hosted GitLab, use your instance's registry URL.
How do I authenticate to GitLab Container Registry?
In CI/CD, use automatic auth (built-in variables). Locally, use a personal access token. For deployment, use deploy tokens. Never use username/password in automation.
Is GitLab Container Registry Docker Hub compatible?
Yes. It's a standard Docker registry, so any Docker or OCI client works. You can use docker pull, docker push, docker login as you would with Docker Hub.
Can I pull images from another project's registry?
Yes, using a deploy token or a CI/CD job token. In a pipeline, use $CI_JOB_TOKEN to pull from other projects the job has access to.
How do I clean up old images?
Use cleanup policies in Project → Settings → Packages and Registries → Cleanup policies. Configure age, tag patterns, and keep counts. Policies run daily automatically.
Are container images scanned for vulnerabilities?
Yes, if you enable container scanning (GitLab Premium or above). It scans images after they're pushed and reports vulnerabilities in the pipeline.
Is GitLab Container Registry free?
GitLab.com offers a free tier with limited storage (10GB). Paid tiers offer more storage (unlimited on Ultimate). Self-hosted storage depends on your infrastructure.
How do I use images from the registry in Kubernetes?
Create an image pull secret from a deploy token (kubectl create secret docker-registry), then reference it in the pod spec or service account.
Previous: Runner Security Next: Registry Cleanup Policies

GitLab Container Registry simplifies container image management by integrating registry storage directly into your GitLab workflow — no more juggling separate registry accounts and credentials.