GitLab Self-Hosted
GitLab is not a git forge with CI bolted on. It is an attempt to replace your entire software delivery toolchain with a single, opinionated platform: source control, CI/CD, container registry, package registry, issue tracking, wikis, environments, deployments, and a growing suite of security scanning tools — all in one process bundle, sharing one configuration file, one PostgreSQL instance, and one authentication system. That integration is both its greatest strength and the reason a base installation consumes more RAM than some teams’ entire Kubernetes nodes. Before you reach for it, you need to know what you are actually running.
The comparison that matters for most self-hosters is against Forgejo or Gitea: a single Go binary that idles at under 100 MB of RAM, speaks GitHub Actions syntax, and requires zero operational background knowledge to upgrade. GitLab CE is not that. It is a Ruby-on-Rails application wrapped in an Omnibus package that bundles PostgreSQL, Redis, Puma, Sidekiq, Gitaly, NGINX, Workhorse, GitLab Shell, and optionally Prometheus, the container registry, GitLab Pages, and more. The threshold question is whether the integrated CI/registry/security toolchain earns that weight for your specific use case. This guide helps you answer that question and, if the answer is yes, actually operate the thing.
What GitLab CE Bundles (and What Stays EE)
GitLab ships in two editions: Community Edition (CE), which is MIT-licensed and free, and Enterprise Edition (EE), which is source-available but license-gated at two tiers — Premium and Ultimate — with pricing per seat per month. The self-hosted EE binary is the same binary as CE; you activate features by entering a license key. Understanding the tier split matters before you architect around a feature.
CE includes: Git repository hosting, merge requests with code review, webhooks, GitLab CI/CD (unlimited pipelines and runners on self-hosted), the built-in container registry, the package registry (PyPI, npm, Maven, Helm, Conan, and others), GitLab Pages, issue boards, milestones, wikis, basic SAST (Static Application Security Testing via analyzers run in CI), secret detection, and dependency scanning. The security scanners run as ordinary CI jobs using GitLab-maintained analyzer images — you get the job templates and the reports, but the vulnerability management dashboard and policy enforcement live behind EE.
Premium (EE, paid) adds: code owners with approval rules, merge trains, multiple approval rules, group-level runners, epics, roadmaps, portfolio-level burndown charts, enhanced audit events, and priority support.
Ultimate (EE, paid) adds: DAST (Dynamic Application Security Testing), container scanning, license compliance, dependency list with full management UI, vulnerability management workflows, security policy enforcement, compliance pipelines, and AI features. DAST and container scanning jobs can technically be run from the CE analyzer images in CI, but the results only integrate meaningfully into the UI at Ultimate tier.
For most self-hosters, CE is sufficient and the SAST/secret-detection/dependency-scanning jobs — which run as standard .gitlab-ci.yml includes — provide genuine value at zero license cost.
Install Paths and Their Trade-offs
There are three supported installation methods, and each has a distinct operational profile.
| Method | Best for | Upgrade mechanism | Operational complexity |
|---|---|---|---|
| Omnibus package (Linux) | Single VM, bare metal, homelab | apt upgrade gitlab-ce with version pinning |
Low — one config file |
| Official Docker image | Existing Docker hosts, quick eval | Pull new image, recreate container | Low-medium — data volumes matter |
| Helm chart / GitLab Operator | Kubernetes, cloud-native HA | helm upgrade with values diff |
High — requires K8s expertise |
Omnibus on a VM
The Omnibus package is the canonical installation method. It bundles every GitLab component, their dependencies, and a supervisord-like process manager (gitlab-ctl) into a single .deb or .rpm. You configure everything in /etc/gitlab/gitlab.rb and run gitlab-ctl reconfigure to apply changes via Chef recipes bundled in the package. This is the path with the most documentation, the most community answers, and the simplest day-two operations.
|
|
After editing gitlab.rb, always run sudo gitlab-ctl reconfigure. For configuration that does not require reconfiguration (like log rotation), sudo gitlab-ctl restart suffices.
Docker
The official gitlab/gitlab-ce image is identical in behavior to the Omnibus package — it runs the same bundled services inside the container. You mount three volumes: config, logs, and data. The most important operational detail is that the gitlab-secrets.json and gitlab.rb files that live in config are not part of the image and must be backed up separately.
|
|
The shm_size: 256m matters — without it, PostgreSQL inside the container will complain about shared memory. Pinning a specific image tag (e.g., gitlab/gitlab-ce:17.9.1-ce.0) rather than latest is essential for upgrade discipline; we cover that in the upgrade section.
For more on composing multi-service homelab stacks, see the Docker Compose for the homelab post.
Helm Chart and GitLab Operator on Kubernetes
The GitLab Helm chart (gitlab/gitlab) is a cloud-native decomposition of GitLab where each component — Webservice (Puma + Workhorse), Sidekiq, Gitaly, Shell, Registry, Pages, and supporting services — runs as a separate Kubernetes Deployment. This enables horizontal scaling and HA configurations that Omnibus cannot match, but it comes at a steep operational cost: the Helm chart has hundreds of configurable values, requires external PostgreSQL and Redis (or you manage those as sub-charts too), requires object storage for most artifact types, and has a significantly more complex upgrade procedure.
The GitLab Operator is the OLM-based alternative targeting OpenShift and OLM-enabled clusters, with a GitLab CRD that wraps the Helm chart. For most homelab Kubernetes deployments, the Helm chart is more flexible.
The honest assessment: unless you are already running production Kubernetes and have strong Helm + K8s operational experience, do not deploy GitLab on Kubernetes. The Omnibus install on a dedicated VM is more reliable, easier to backup, easier to upgrade, and considerably easier to debug when something goes wrong. Kubernetes GitLab is worth it when you need multi-Webservice-pod HA, runner autoscaling via the Kubernetes executor, or a cloud-native hybrid architecture. For a reference, see the Kubernetes for the homelab post on whether your cluster is actually the right tool for this workload.
Component Architecture
Understanding what is running inside GitLab is essential for capacity planning, troubleshooting, and sensible configuration.
Client (browser / git client / API)
|
v
[ NGINX / Let's Encrypt TLS termination ]
|
v
[ GitLab Workhorse ] <-- Go proxy; handles large uploads/downloads,
| git pack operations, CI artifacts directly
|
+---> [ Puma (Ruby on Rails) ] <-- web requests, API
| |
| v
| [ Sidekiq ] <-- async background jobs (emails, webhooks,
| pipeline scheduling, housekeeping)
|
+---> [ Gitaly ] <-- gRPC service owning all git repository I/O
| |
| v
| [ Git repos on disk ]
|
+---> [ GitLab Shell ] <-- SSH git operations
|
+---> [ Container Registry ] <-- Docker distribution
|
+---> [ PostgreSQL ] <-- primary relational store
|
+---> [ Redis ] <-- sessions, Sidekiq queues, cache
|
+---> [ Prometheus + exporters ] <-- metrics (optional)
Runner Architecture (external to GitLab server):
[ GitLab Server ]
|
| HTTPS polling / webhook
v
[ gitlab-runner manager process ]
|
+---> shell executor (runs jobs directly on runner host)
|
+---> docker executor (spins job container per job)
|
+---> docker-autoscaler executor (provisions cloud VMs on demand)
|
+---> kubernetes executor (creates pods in a K8s cluster)
Gitaly deserves special mention. Before GitLab 13, git operations went through an NFS share or were executed directly by Ruby code. Gitaly is now the sole interface to repository data — every git call, whether from Puma, Sidekiq, or GitLab Shell, goes through the Gitaly gRPC service. This is architecturally sound and enables future Gitaly Cluster (HA) configurations, but it means there is an extra process with its own memory footprint for even a single-node installation.
Runner Architecture and Pipeline Execution
GitLab CI/CD is pull-based: the gitlab-runner process polls the GitLab API for pending jobs and claims them. You can register runners at the instance level (shared, available to all projects), the group level (available to all projects in a group), or the project level (exclusive to one project).
Runner Registration: New Authentication Tokens
Since GitLab 16.0, runner registration uses authentication tokens instead of the legacy registration tokens. The old --registration-token workflow was deprecated and disabled by default from GitLab 17.0 onward. New tokens are created in the GitLab UI (Admin > CI/CD > Runners > New instance runner, or at group/project level) and are recognizable by the glrt- prefix. Attempting to register with a legacy token on GitLab 17+ returns a 410 Gone error.
|
|
The token is stored in /etc/gitlab-runner/config.toml after registration. Each runner registered with the same authentication token gets a unique system_id, tracked separately — re-registering with the same token creates a new runner record rather than overwriting the old one.
Executors
Shell: Runs jobs directly on the host where gitlab-runner is installed. No isolation. Fast, low overhead, useful for build jobs that need direct hardware access, but jobs share the host environment and can interfere with each other or the host OS. Avoid for untrusted code.
Docker: Each job spins up a fresh container from the image: specified in .gitlab-ci.yml, runs, and is destroyed. This is the most common executor for homelab and small team setups. Provides isolation without the overhead of full VMs.
Docker Autoscaler: GA since GitLab Runner 17.1, this replaces the deprecated Docker Machine executor. It uses the fleeting plugin system to provision cloud VMs (AWS, GCP, Azure) on demand and tear them down when idle. Each VM runs a Docker executor. Ideal for workloads with spiky CI demand. The Docker Machine executor was deprecated in 17.5 and is scheduled for removal in GitLab 20.0 (May 2027).
Kubernetes: Runs each CI job as a Kubernetes Pod. The runner manager creates a pod per job and deletes it on completion. Scales efficiently within a cluster, enables resource requests/limits per job, and is the natural choice when your runners live inside a Kubernetes cluster. For a discussion of running CI on Kubernetes, see the self-hosted GitHub Actions runners post for the equivalent pattern on the Actions side.
A Minimal Pipeline
|
|
The $CI_REGISTRY, $CI_REGISTRY_USER, and $CI_REGISTRY_PASSWORD variables are automatically populated by GitLab when the built-in container registry is enabled — no external registry credentials to manage. Including the SAST and Secret-Detection templates adds security scan jobs to the pipeline at zero additional configuration cost on CE.
Resource Footprint: The Honest Numbers
GitLab documentation lists 4 GB RAM as the absolute minimum and 8 GB as the recommended floor for a production single-node install serving up to 500 users. In practice, after running gitlab-ctl reconfigure on a fresh install, resident memory consumption before any real workload looks roughly like this:
| Component | Typical RSS (idle) |
|---|---|
| Puma (2 workers + master) | 900 MB – 1.4 GB |
| Sidekiq | 300 – 600 MB |
| Gitaly | 200 – 400 MB |
| PostgreSQL | 200 – 400 MB |
| Redis | 50 – 100 MB |
| NGINX + Workhorse | 50 – 100 MB |
| Container Registry | 50 – 100 MB |
| Prometheus + exporters | 150 – 300 MB |
| Total (idle) | ~2 – 3.4 GB |
That is idle. Under active CI load — multiple concurrent pipeline jobs, artifact uploads, merge request diff rendering, webhook delivery — Puma workers can hit their 1.2 GB per-worker ceiling and trigger restarts, Sidekiq threads multiply, and Gitaly spikes on large repository operations. A 4 GB VM is survivable only with aggressive tuning (disabling Prometheus, reducing Puma workers to 2, reducing Sidekiq concurrency to 10, disabling KAS and Pages). An 8 GB VM is comfortable for a small team. 16 GB is where you stop worrying.
Compare this to Forgejo: a single binary consuming under 100 MB at idle, with SQLite as the default backend. GitLab’s footprint is not a flaw — it is the direct cost of bundling PostgreSQL, a background job system, a gRPC repository service, and a full metrics pipeline into one product. But it is a real cost that must be provisioned for before you commit to the path.
Backup and Restore
GitLab’s backup Rake task (gitlab-backup create) produces a tar archive containing: the database dump, repositories (via Gitaly), uploads, build artifacts, LFS objects, pages, registry data (optional), and CI job traces. What it explicitly does NOT include:
/etc/gitlab/gitlab.rb— your entire configuration/etc/gitlab/gitlab-secrets.json— encryption keys for 2FA OTP secrets, CI variable encryption, and database column encryption
This separation is intentional: bundling the secrets file with the data backup defeats the purpose of encryption at rest. But it means a restore from backup alone will produce a running GitLab instance where 2FA is broken, CI secure variables are unreadable, and integrations using stored credentials fail. You must store gitlab.rb and gitlab-secrets.json separately, in a different location from your backup archive.
|
|
For Docker deployments, the backup runs inside the container:
|
|
Automate both backup creation and the config backup separately. Storing the backup archive and the secrets in the same S3 bucket is acceptable (they are independently secured), but do not rely on the Rake backup alone — without the secrets, the backup is only partially recoverable.
The Upgrade Reality
GitLab’s upgrade path enforces required stop versions: specific versions you must pass through, in order, before advancing to a later release. You cannot jump from 15.4 to 17.0 in one step. Required stop versions exist because background database migrations run asynchronously during normal operation, and skipping a stop version before those migrations complete corrupts the schema or the data.
From GitLab 17.5 onward, required stops occur at x.2, x.5, x.8, and x.11 minor versions within each major series. GitLab maintains an official upgrade path calculator at gitlab-com.gitlab.io/support/toolbox/upgrade-path/ — always use it before planning an upgrade.
The procedure for each stop:
|
|
For the Docker image, the same discipline applies: pull the specific target version tag, recreate the container, and wait for migrations before pulling the next stop version. The practical implication for homelab operators: if you let a GitLab installation drift for six months without updates, returning to current may require touching four or five intermediate versions with a migration wait at each. This is not hypothetical — it is routine. Set a monthly calendar reminder to upgrade.
GitLab CE vs. Forgejo: When Each Wins
This is the decision most self-hosters are actually making.
| Factor | GitLab CE | Forgejo |
|---|---|---|
| RAM at idle | 2–3.5 GB (tuned), 4–8 GB practical | 50–150 MB |
| Disk (install) | ~2.5 GB + data | ~100 MB binary |
| CI/CD | Native, deeply integrated | Forgejo Actions (GitHub Actions compatible) |
| Container Registry | Built-in, zero config | Built-in since Forgejo 1.21 |
| Package Registry | Built-in (many formats) | Limited (npm, PyPI via external) |
| SAST / Secret Detection | Native CI templates (CE) | Via external Action steps |
| Issue boards / Kanban | Yes | Yes (basic) |
| Upgrade complexity | High — required stop versions | Low — binary swap |
| Single binary? | No — 8+ processes | Yes |
| License | MIT (CE) | MIT |
| CI syntax | .gitlab-ci.yml | GitHub Actions YAML |
Choose GitLab CE when:
You need the all-in-one platform and your team is large enough to justify its operational weight. Concretely: if you are running 10+ engineers who all need CI, a container registry, security scanning reports, issue tracking, and environment management without standing up and maintaining separate tools — Gitea Actions + Harbor + Trivy + Linear — GitLab’s integration cost starts to amortize. If you are working in a regulated environment (SOC 2, ISO 27001) where audit logs, compliance pipelines, and a unified vulnerability record matter, GitLab’s EE tier is purpose-built for those workflows. If you already have an 8–16 GB VM to spare and your team already knows GitLab from a corporate context, the familiarity alone justifies it.
Choose Forgejo when:
You are a solo operator, a small team, or running on resource-constrained hardware. Forgejo on a 1 GB VPS with SQLite is a completely legitimate production git host for most homelab and small-project needs. Forgejo Actions covers the majority of CI use cases with familiar GitHub Actions syntax. Adding a separate CI solution — like Woodpecker CI, which runs well alongside Forgejo — keeps each component independently upgradeable and auditable without coupling your git hosting to your pipeline system. The operational simplicity of a Go binary that you upgrade by dropping a new file in is not a consolation prize; it is a genuine operational advantage.
Verdict
GitLab CE is an excellent platform if you can honestly provision it and maintain it. The integration story is real: $CI_REGISTRY_IMAGE in your pipeline just works, SAST templates require one include: line, and merge request pipelines with coverage visualization are genuinely useful for team workflows. You do not need to configure a separate registry, a separate package manager, or a separate secrets scanner — they are there, at no license cost.
But the weight is real too. You are committing to at minimum an 8 GB VM (realistically 12–16 GB for a comfortable team install), an upgrade discipline with required stops, a backup practice that explicitly manages secrets outside the backup archive, and an operational complexity that Forgejo simply does not have. If that weight is not justified by your actual use case — and for most homelab operators running personal projects or a small team, it is not — then Forgejo with an external CI runner is the right answer and you should not feel like you are settling.
The correct mental model: GitLab CE is infrastructure. You do not install it the way you install an app. You install it the way you install a database cluster — with a plan for sizing, backups, secrets management, and upgrades before the first user logs in.
Sources
- GitLab Installation Requirements
- Installation Methods | GitLab Docs
- Install GitLab in a Docker container
- GitLab Helm Chart
- GitLab Operator
- Running GitLab in a memory-constrained environment
- Reference Architectures | GitLab Docs
- GitLab Runner Executors
- Registering Runners | GitLab Docs
- Migrating to the new runner registration workflow
- Docker Autoscaler Executor | GitLab Docs
- Kubernetes Executor | GitLab Docs
- Back up GitLab | GitLab Docs
- Restore GitLab | GitLab Docs
- Plan your upgrade path | GitLab Docs
- GitLab Upgrade Path Tool
- GitLab 18 upgrade notes
- Gitea vs Forgejo vs GitLab CE (2026) | dasroot.net
- Gitea vs GitLab: Which Self-Hosted Git Server in 2026? | blog.elest.io
Comments