Tutorial

Migrate GitLab.com to a Self-Managed Instance on a VPS

Deployment11 min read14 steps

Your agency keeps several clients' code on one GitLab.com account, and retention and billing are not set by you. Taking control back means running GitLab on your own VPS, but a project export does not carry everything: CI/CD variables, registry images, webhooks and tokens stay behind. This guide plans the migration, walks through direct transfer, settles how members are matched and rebuilds what is missing, with a switchover order you can reverse. The old project stays read-only until you validate.

Contents· Why leave GitLab.com for a self-managed instance1/14
  1. 01Why leave GitLab.com for a self-managed instance
  2. 02What you gain by taking hosting back
  3. 03What the migration does not carry, to inventory first
  4. 04Project export or direct transfer: what each method covers
  5. 05Prerequisites: versions, permissions and instance resources
  6. 06Plan and run the migration, from inventory to switchover
  7. 07Members and assignees: the matching rule
  8. 08CI/CD variables and container registry: rebuild by hand
  9. 09Move registry images to the new URL
  10. 10Keep the way back open
  11. 11Troubleshooting: authentication and import errors
  12. 12Checks before you cut off the old access
  13. 13The official documentation
  14. 14Next: consolidate around your instance

Why leave GitLab.com for a self-managed instance

When the pricing of a SaaS tool shifts, like the recent Atlassian Cloud increase covered in our article on the double price shock, teams re-examine their whole development toolchain. For an agency the question is concrete: the code of ten clients lives on a single account, at a third party, with retention and access rules you do not set yourself.

A self-managed instance moves those decisions to you: where repositories are stored, how long artifacts are kept, who can sign in, and what it costs each month. The price of that freedom is a migration job that GitLab does not fully do for you.

This guide does not repeat the choice of forge, covered in the forge comparison, nor the installation, described in the GitLab CE installation guide. It starts from a GitLab instance that is already online and only covers the move from GitLab.com to it.

What you gain by taking hosting back

  • Access under your control — accounts, groups and roles live on your instance, not on a SaaS account shared between clients.
  • Retention you decide — how long artifacts and logs are kept and how large the registry grows are all set in the administration area.
  • A bill that follows your VPS — the cost depends on the machine you size, not on a seat count.
  • A domain name of your own — the repository answers on git.your-domain.com, with your certificate and your firewall.
  • A step-by-step migration — the old project stays in place, read-only, until the new one is validated.

What the migration does not carry, to inventory first

The official documentation for project export lists what is not exported: child pipeline history, pipeline triggers, CI/CD job traces and artifacts, container and package registry images, CI/CD variables, webhooks, encrypted tokens, the number of required approvals, deploy keys allowed to push to protected branches, secure files and pipeline schedule variables. Deploy keys are not imported either: you must enable them again in the imported project.

Direct transfer has its own exclusion list, which overlaps with the first: CI/CD variables, webhooks, container registry images, the package registry, job logs and job artifacts. At group level, CI/CD variables, deploy tokens and push rules do not follow either.

Write this inventory in a table before you start: one row per project, one column per missing item, one column for the person who rebuilds it. This document, more than the export itself, protects you the day the old access closes.

Project export or direct transfer: what each method covers

Scroll the table

ItemProject export (file)Direct transfer
GranularityOne project at a time, through a downloaded fileWhole groups, with or without their projects, from the interface
CI/CD variablesNot exportedNot migrated
Container registry imagesNot exportedNot migrated
WebhooksNot exportedNot migrated
Job logs and artifactsNot exportedNot migrated
MembersMatched if the source public email equals the destination primary emailMatched if the source public email equals a confirmed destination email
What the instance needsAn administrator performs the importFeature enabled by an administrator, token with the `api` scope on the source

Prerequisites: versions, permissions and instance resources

First rule, the version. A file import accepts exports from a version up to two minor versions behind the destination, and the documentation asks that the destination be at least at the source version. For direct transfer, the documentation recommends GitLab 16.8 or later on both sides, and asks that a source be no more than two minor versions behind the destination. GitLab.com moves continuously: aim for the latest stable release on your instance, update it before migrating, not after, and reread the accepted gap in the documentation on the day.

On permissions, you need the Owner role on the source group, an administrator on the target instance, and a personal access token with the api scope created on GitLab.com. The two instances must reach each other over HTTPS, with free space in /tmp.

On resources, our editorial recommendation: 4 vCPU and 8 GB of RAM for an instance that also hosts the registry, an NVMe disk sized to the combined size of repositories and images, and runners on a separate VPS as soon as builds get heavy. The installation itself is described in the GitLab CE on VPS guide.

Plan and run the migration, from inventory to switchover

  1. Inventory groups, projects and dependencies

    List every group, every project, its variables, webhooks, runners and integrations. Note who owns each one on the client side.

  2. Update the target instance

    Check the version in the administration area and aim for the latest stable release, at least equal to GitLab.com, before any import.

  3. Create users on the target

    Members are not created during migration. Create accounts beforehand, with the primary email each person uses on GitLab.com.

  4. Plan one group per client

    Create on the target one group or subgroup per client, with minimal permissions, before importing.

  5. Have public emails published on the source

    Each member sets a public email in their GitLab.com profile, at least until the import is done.

  6. Enable direct transfer on the target instance

    An administrator enables group migration by direct transfer in the application settings. It is off by default on a self-managed instance.

  7. Start the group import

    On the target, choose Create new then New group then Import group, enter https://gitlab.com and your token, then Connect instance. Select the groups with Import with projects.

  8. Export standalone projects by hand

    For a project outside a group, open Settings then General then Advanced then Export project, download the file and import it on the target.

  9. Follow the import and read the errors

    The Status column shows progress. On a failed or partial import, Show errors details the cause. Allow several hours for a large number of projects.

  10. Hook up the registry and CI

    Rebuild variables and images as described below, then run a full pipeline on the target before touching the old project.

Members and assignees: the matching rule

On a self-managed instance, the published rule is this: users must already exist on the destination, and the public email of the source account must match the primary email of the destination account. If the conditions are met, the person is added as a direct member. Otherwise, the association changes to the user performing the import: your comment authors and assignees all end up under your name.

Before importing, check the Public email field on every GitLab.com profile. For a project export, you can also add public emails by editing the project_members.ndjson file in the archive. The owner of the top-level group must run the export so that direct and inherited members are included, and an administrator must perform the import.

With direct transfer, imported memberships are first associated with placeholder users, pending reassignment. An Import user memberships checkbox also lets you skip importing memberships. In that case, the source public email against a confirmed destination email decides the match, not the primary email as with a file import.

For an agency, structure the target before importing: one top-level group per client, or one subgroup per client under an agency group. Give each client the minimal role on its own group only (Reporter or Developer rather than Owner), keep the Owner role for the internal team, and you can later move a client out by transferring its group or exporting its projects, without touching the others.

CI/CD variables and container registry: rebuild by hand

Variables do not travel, because GitLab treats them as sensitive. List them on the source with the API: curl --header "PRIVATE-TOKEN: <token>" "https://gitlab.com/api/v4/projects/<id>/variables". The list gives the key, the masked and protected attributes and, usually, the value; but not every value is necessarily readable, depending on the version and how the variable was created.

Then recreate them on the target with curl --request POST --header "PRIVATE-TOKEN: <token>" --form "key=NAME" --form "value=value" "https://git.your-domain.com/api/v4/projects/<id>/variables". The key accepts 255 characters among A-Z, a-z, 0-9 and _. When the value is missing or stays hidden, recreate the secret at its provider and store it in the agency vault.

Group variables are recreated from the group's CI/CD settings. Runners are registered again on the new instance: their tokens do not follow.

Move registry images to the new URL

  1. Enable the registry on the target instance

    In /etc/gitlab/gitlab.rb, set registry_external_url over HTTPS, for example https://registry.your-domain.com, then run sudo gitlab-ctl reconfigure. The registry requires a TLS certificate.

  2. Create a token for each side

    A token with read_registry is enough to pull images, and write_registry as well to push them.

  3. Pull each image from the old URL

    Sign in with docker login registry.gitlab.com, then docker pull registry.gitlab.com/group/project/image:tag.

  4. Retag, then push to the target

    Run docker tag registry.gitlab.com/group/project/image:tag registry.your-domain.com/group/project/image:tag, then docker login registry.your-domain.com and docker push registry.your-domain.com/group/project/image:tag. Registry repository paths are written in lowercase.

Keep the way back open

Delete nothing on the source. Archive the old project or remove write access, keep it readable, then run a pipeline and a full git clone from the target. As long as the client has not signed off on validation, the switchover stays reversible: you only need to reopen the old project.

Troubleshooting: authentication and import errors

On docker login or docker push, the message access forbidden means, according to the documentation, that the Authorization header was stripped before reaching GitLab, often by a proxy in front of the instance. Check the reverse proxy configuration.

An UNAUTHORIZED (401) error points to wrong or expired credentials, missing scopes, or a password used with two-factor authentication enabled: use a token instead. A DENIED (403) means the credentials are valid but have no right on the registry.

On a large image, unauthorized: authentication required signals a token that expired before the push finished. An administrator can lengthen its lifetime.

On the import side, a milestone title that already exists gets a unique suffix such as 18.0 (imported-…): rename the milestone before migrating to avoid it.

Checks before you cut off the old access

  • Identical repositories — compare branches and tags, then do a full clone from the target.
  • Full green pipeline — a pipeline on the main branch with variables recreated and runners registered again.
  • Images present — every tag used by a deployment exists in the new registry.
  • Members and roles reviewed — no author or assignee has moved to the account that started the import.
  • Webhooks and deploy keys recreated — chat, deployment and monitoring integrations included.
  • Written sign-off from the client — the old project only closes after that agreement.

The official documentation

The rules in this guide come from the GitLab documentation: project export and import, group migration by direct transfer, migrated items, project variables API, registry authentication and registry on a self-managed instance. Reread them on the date of your migration: the lists of migrated items evolve.

Next: consolidate around your instance

Once the projects are validated, the instance becomes the central point of the agency's portfolio. For a heavier registry, Harbor on a VPS can take over. If your migration comes from another ticketing tool, the Jira to OpenProject or Plane guide follows the same inventory logic. And if you are still unsure about the forge, moving from GitLab to Gitea remains a lighter option.

Take back control of your clients' code

An agency that hosts its repositories on its own instance controls access, retention and billing. On ServOrbit the instance runs on a root VPS with a dedicated IPv4 address, and the repository's domain name is managed in the same place as your hosting and your reseller account.

Need help?

Browse our help center and FAQ, or reach our team — callback, WhatsApp or email. Support in French, English and Arabic.

Message us on WhatsAppopens in a new tab