Publishing and consuming packages from popular registries like npm, Maven, and PyPI directly through GitLab isn’t just a convenience; it’s a fundamental shift in how you manage dependencies and distribute your own code. The most surprising thing? GitLab acts as a single source of truth for all your code, including the libraries your applications rely on and the libraries you create for others. This eliminates the need for separate, external registry management for these common package types.

Let’s see it in action. Imagine you’ve got a Python project that needs a custom internal library.

First, you’d set up a GitLab project to hold your Python library. Let’s call it my-internal-python-lib. Inside this project, you’d have your library code, a pyproject.toml file defining your package, and crucially, a .gitlab-ci.yml file.

# pyproject.toml for my-internal-python-lib
[project]
name = "my-internal-python-lib"
version = "0.1.0"
description = "My awesome internal Python library"
authors = [{ name="Your Name", email="you@example.com" }]
license = { file="LICENSE" }
readme = "README.md"
requires-python = ">=3.8"
dependencies = [
    "requests>=2.28.1",
]

Your .gitlab-ci.yml would look something like this to build and publish:

# .gitlab-ci.yml for my-internal-python-lib
stages:
  - build
  - publish

build-package:
  stage: build
  image: python:3.10-slim
  script:
    - pip install build twine
    - python -m build
  artifacts:
    paths:
      - dist/

publish-to-gitlab:
  stage: publish
  image: python:3.10-slim
  needs:
    - build-package
  script:
    - export TWINE_USERNAME="__token__"
    - export TWINE_PASSWORD="${CI_GITLAB_TOKEN}" # GitLab provides a token scoped to the project
    - twine upload --repository-url ${CI_SERVER_URL}/api/v4/projects/${CI_PROJECT_ID}/packages/pypi/ \
      -u "${TWINE_USERNAME}" -p "${TWINE_PASSWORD}" \
      dist/*
  rules:
    - if: '$CI_COMMIT_TAG' # Only publish on tags

When you create a Git tag (e.g., v0.1.0) for your my-internal-python-lib project, this pipeline runs. It builds the package into the dist/ directory and then uses twine to upload it to GitLab’s built-in PyPI-compatible package registry. The CI_GITLAB_TOKEN is a special GitLab CI/CD variable that’s automatically available and has permissions to interact with the project’s package registry. The --repository-url is key: it points directly to your GitLab instance, project, and the PyPI endpoint for packages.

Now, in another project, say my-main-python-app, you want to consume this library. You’ll need a pip.conf or pip.ini file, or set an environment variable.

# pip.conf or pip.ini in my-main-python-app project
[global]
extra-index-url = ${CI_SERVER_URL}/api/v4/projects/${CI_PROJECT_ID}/packages/pypi/

Or, in your .gitlab-ci.yml for my-main-python-app:

# .gitlab-ci.yml for my-main-python-app
variables:
  PIP_EXTRA_INDEX_URL: "${CI_SERVER_URL}/api/v4/projects/${CI_PROJECT_ID}/packages/pypi/"

install-dependencies:
  stage: .pre # Run early
  image: python:3.10-slim
  script:
    - pip install --upgrade pip
    - pip install -r requirements.txt # This will now look at your GitLab registry

When pip install runs in the pipeline, it consults the PIP_EXTRA_INDEX_URL (or the pip.conf). It will first check the public PyPI, and if it doesn’t find my-internal-python-lib, it will then query your GitLab instance. GitLab, recognizing the request for a PyPI package from your project’s registry, will serve up the version you published. The CI_PROJECT_ID in the URL ensures it’s looking at the correct, isolated registry for that specific project. If you have multiple projects publishing packages, each has its own private registry endpoint.

This pattern extends to npm and Maven. For npm, you’d configure your .npmrc file to point to CI_SERVER_URL/api/v4/projects/${CI_PROJECT_ID}/packages/npm/. For Maven, you’d add a repository configuration to your settings.xml or pom.xml pointing to CI_SERVER_URL/api/v4/projects/${CI_PROJECT_ID}/packages/maven/. GitLab handles the authentication automatically using the CI job token for pipelines.

The mental model here is that GitLab isn’t just hosting your source code anymore; it’s hosting your artifacts. Each GitLab project can be configured to act as a registry for npm, Maven, PyPI, or NuGet packages. When a pipeline runs in a project that needs to consume a package, it’s configured to look at its own project’s registry endpoint first, or a group-level registry if you set that up. This allows for fine-grained control and isolation of dependencies. You can have public packages available to anyone, or private packages accessible only within your GitLab instance, or even private packages accessible only to specific projects using group-level configurations.

One subtle but powerful aspect is how GitLab manages permissions. When you publish a package, it’s tied to the project’s CI/CD job token. When another project consumes it, the consuming pipeline’s job token is used for authentication. This means you can grant read access to the package registry at the project or group level to specific users or roles, ensuring that only authorized pipelines and developers can access your internal libraries. This is far more granular than managing access to a shared external registry.

The next step in managing your software supply chain within GitLab is exploring group-level package registries, which allow you to share packages across multiple projects without repeating configurations.

Want structured learning?

Take the full Gitlab course →