Back to Articles

Open Source DevOps at Scale: Running GitLab Yourself

laptop screen showing code in a busy room

Source code is the one asset almost every technology company agrees is critical, and the one most of them keep on infrastructure they do not own, in a jurisdiction they did not choose, under a contract they did not read carefully. That arrangement is usually fine. It is worth knowing what the alternative costs, because GitLab has made self-hosting the whole development lifecycle genuinely practical for over a decade.

It is also a product whose free tier is more generous and whose operational burden is heavier than most people expect. Both of those are worth understanding before you commit, so this guide covers what GitLab self-managed actually is, what the free tier really gives you, and the three operational problems that account for most of the pain.

What GitLab is

Not a Git host with extras bolted on. A Rails application, PostgreSQL, Redis, Gitaly for repository storage, Sidekiq for background jobs, and a container registry, packaged together as one system covering source control, code review, issue tracking, CI/CD, package and container registries, security scanning and deployment.

That breadth is the whole argument. GitHub plus Actions plus Dependabot plus a package registry plus a project tracker is a comparable feature set assembled from parts; GitLab is one application with one permissions model and one database. That is either exactly what you want or more than you need, and which one it is depends on how much of the lifecycle you actually intend to run in one place.

The important architectural fact is the same one that applies to self-hosting your analytics with Matomo, your marketing automation with Mautic or your team chat with Mattermost: it runs where you put it. Your repositories, your CI logs, your artefacts, your database.

The licensing picture, stated plainly

This is genuinely confusing and worth being precise about, because the confusion runs the opposite way to what people expect.

There are two source distributions. Community Edition is MIT licensed. Enterprise Edition has its own more restrictive licence covering the ee/ directory of the repository. So far this sounds like the usual open core arrangement.

The part that surprises people: the Linux package almost everyone installs is the Enterprise Edition build, and with no licence key applied it runs as the Free tier, behaving like Community Edition. You are not running the MIT-licensed distribution unless you have deliberately chosen the CE package. In practice this rarely matters, but if your reason for self-hosting is a strict open source policy rather than data control, it matters a great deal and almost nobody checks.

The commercial tiers are Free, Premium at 29 dollars per user per month billed annually, and Ultimate at custom pricing. Premium adds advanced CI/CD, better project management and priority support. Ultimate adds the security and compliance suite: application security testing, supply chain security, dependency scanning. Both paid tiers now bundle GitLab Credits for the AI features, 12 dollars per user per month on Premium and 24 on Ultimate.

Here is the fact that changes the calculation for most teams, and the exact inverse of the Mattermost situation: the Free tier on self-managed has no user limit. The five-user cap people have heard about applies only to private groups on GitLab.com. Self-hosted Free gives you unlimited users, source control, CI/CD and registries, and you bring your own storage and runners. A hundred-engineer organisation can run entirely on it.

What you give up is security scanning, compliance reporting, advanced approval rules and support. If you are subject to the Cyber Resilience Act and want dependency scanning and SBOM generation built into your pipelines rather than assembled from separate tools, that is an Ultimate conversation. For most other teams, Free is not a trial. It is a viable permanent answer.

Setting it up

A small deployment

GitLab is heavier than it looks. The documented baseline for a single node is 8 vCPU and 16 GB of RAM, and unlike most vendor minimums that number is honest rather than optimistic. It can be squeezed into 8 GB, but you will feel it, and swap should be off because swapping this application is worse than not having the memory.

PostgreSQL is the only supported database. Which version depends on your GitLab version: 17.x wants PostgreSQL 14.14 to 16.x, 18.x wants 16.5 to 17.x, and 19.x wants 17.x. Redis 7.2 is recommended with 7.0 the minimum, and Valkey 7.2 works as a substitute. Standalone instances only, because clustered and serverless Redis variants are not supported.

Install with the Linux package. There are Helm charts, an Operator, Docker images and a from-source path, but the Linux package is the most mature option and it is what GitLab.com itself runs on. It bundles PostgreSQL, Redis and Sidekiq, which means one machine and one config file gets you a working instance.

# /etc/gitlab/gitlab.rb
external_url 'https://git.example.com'

# Let's Encrypt, on by default when external_url is https
letsencrypt['enable'] = true
letsencrypt['contact_emails'] = ['ops@example.com']

# Keep Puma and Sidekiq honest on a small box
puma['worker_processes'] = 2
sidekiq['max_concurrency'] = 9

# Move artefacts and uploads off local disk early
gitlab_rails['object_store']['enabled'] = true
gitlab_rails['object_store']['connection'] = {
  'provider' => 'AWS',
  'region' => 'eu-central-1',
  'aws_access_key_id' => 'REPLACE_ME',
  'aws_secret_access_key' => 'REPLACE_ME'
}

Run gitlab-ctl reconfigure and you have an instance. Set external_url correctly the first time, because it ends up baked into clone URLs, webhooks and registry addresses.

A production deployment

GitLab publishes reference architectures from 1,000 to 50,000 users, and they are worth reading even if you never implement one, because they show which component becomes the bottleneck first.

The advice that saves the most money is GitLab's own, and it argues against complexity: below 3,000 users, they recommend a solid backup strategy over high availability. The documentation is unusually candid about it, noting that a backup approach "does have a slower recovery time" but "means you have a much smaller architecture and less maintenance costs." Above 3,000 users, or where an outage genuinely stops the company, HA becomes the recommendation.

Take that seriously. A single well-backed-up node with a tested restore is more reliable in practice than a half-understood HA cluster, and a great deal cheaper.

Move artefacts, uploads, LFS objects and registry images to object storage from the beginning. Same reasoning as everywhere else: it keeps the node stateless, the backups manageable and the migration possible.

The three things that go wrong

Everything above is in the documentation. These are the ones that produce incidents.

Your backup does not contain the thing that decrypts it

This is the single most important paragraph in this article.

gitlab-backup create captures a great deal: the database, repositories, LFS objects, CI artefacts and job logs, registry images, wikis, uploads, Pages content, Terraform state, snippets. What it does not capture is the configuration directory, and specifically /etc/gitlab/gitlab-secrets.json.

That file holds the database encryption key. The documentation is blunt about the consequence: "If you lose it, then the GitLab application will not be able to decrypt any encrypted values in the database." That means CI/CD variables, tokens, two-factor secrets and integration credentials. You will have a backup that restores into an instance that cannot read its own secrets.

Also excluded: /etc/gitlab/gitlab.rb, TLS keys and certificates, SSH host keys, and object storage contents when object storage is configured. That last one catches people who did the right thing architecturally and then assumed the backup covered it.

So back up /etc/gitlab separately, store it somewhere other than alongside the archive, since it is the key to the archive, and then restore the whole thing into a scratch machine and confirm you can log in and read a CI variable. An untested restore is not a backup, and in GitLab's case an untested restore is usually a broken one.

You cannot upgrade in one jump

GitLab has required upgrade stops, and this is not advisory. You cannot skip them. Since 17.5 the stops are predictable, landing at x.2, x.5, x.8 and x.11, so upgrading 18.0 to 19.2 means passing through 18.2, 18.5, 18.8, 18.11 and 19.0 on the way.

Each stop involves background migrations that must fully complete before you move to the next one. Starting the next upgrade while migrations are still running is how instances end up in states that need support to unpick. On a large instance those migrations can take hours.

Two practical consequences. Upgrade regularly, because a year of deferred upgrades is a weekend of sequential ones. And always take the latest patch release of a target minor version rather than the first, which the documentation states explicitly. GitLab maintains an upgrade path tool that computes the sequence for you, and it is worth using rather than reasoning about it yourself.

Runners are where the real cost is

The GitLab server does not run your CI. GitLab Runner is a separate component that you install, configure and pay for, on infrastructure you provide. Self-managed Free includes no compute minutes at all, because there is no included compute to give: you bring your own machines.

This is usually a good deal, since a dedicated runner is cheaper per minute than hosted CI at any serious volume, and you can attach whatever hardware a build needs. But it is real infrastructure work. You will make decisions about executors, whether shell, Docker or Kubernetes; about autoscaling, so that runners are not idling at three in the morning; and about caching, which is the difference between a four-minute pipeline and a fourteen-minute one.

Budget for the runner fleet as its own line item. Teams that model the cost of self-hosting by looking only at the GitLab node underestimate it by a wide margin, then discover the shortfall as a queue of pending jobs.

Moving from GitHub

The GitHub importer is good, considerably better than most vendor migration tools, and it brings across repository data, branches, LFS objects, issues and pull requests with their comments, reviews and discussion replies, wiki pages, releases and attachments, labels, milestones, branch protection rules and collaborators with role mapping.

The documented gaps are worth planning around. Organisations and groups do not come across, so the group structure is yours to design rather than inherit, which is usually an improvement. GitHub Actions workflows do not convert into GitLab CI. Pull request comments predating 2017 import as separate threads because of GitHub API constraints, and repositories with more than roughly 30,000 comments need the alternative comments import method enabled.

Because GitHub uses # for both issues and pull requests and GitLab distinguishes them, some cross references will not resolve. Nothing is lost, but links in old discussions may point at the wrong thing.

Plan the CI rewrite as the actual project, because it is. Everything else is an import job you can run and check.

The European angle

For companies operating in Europe there is a compliance dimension beyond cost. Source code, build pipelines and artefacts are among the most sensitive things a technology company holds, and where they live is increasingly a question you get asked rather than one you choose to answer.

Self-hosting puts them inside a boundary you control, which simplifies GDPR transfer analysis and NIS2 supply chain questions in one move, and it is the same sovereignty argument the Cloud and AI Development Act is built around.

The sharper connection is the Cyber Resilience Act. If you ship software into the EU market you will need dependency inventories, vulnerability handling and a coordinated disclosure process. Those obligations are satisfied in your build pipeline rather than in a document, and having the pipeline, the registry and the scanning in one system you operate makes producing the evidence considerably less painful than assembling it from four vendors.

When not to bother

If you are under twenty engineers with no regulatory pressure and no strong feelings about where the code lives, use the SaaS. GitLab.com and GitHub are both excellent, and the operational work will cost more than the subscription.

If your organisation is deep inside GitHub's ecosystem, count what you would lose honestly. Actions, the marketplace and the sheer familiarity of the platform to every candidate you hire are real assets, and the answer is not automatically that GitLab wins.

And if nobody will own the instance, do not start. GitLab rewards an owner who patches it, watches the runner fleet and tests the restore. Without that person it degrades into an unpatched box holding your most valuable asset, which is worse than the SaaS you were trying to leave.

Getting help

We deploy and operate self-hosted development infrastructure for companies working in Europe, including the parts nobody enjoys: upgrade sequences across required stops, runner fleets that autoscale properly, object storage migrations, and backup schemes that have actually been restored.

If you want a GitLab instance sized honestly, a GitHub migration planned by someone who has done the CI rewrite before, or a review of whether your current backup would survive contact with a real failure, write to office@c9group.dev. More about our infrastructure work on the AWS cost optimisation page.

If you are assembling a full self-hosted stack, the same reasoning applies to analytics, marketing automation and team messaging.


Published: August 8, 2026 Categories: DevOps, Open Source, Privacy