How to Install GitLab CE on Ubuntu 24.04 — Self-Hosted DevOps Platform
GitLab is the most comprehensive open-source DevOps platform available: a complete Git server, code review tool, CI/CD engine, container registry, static site host, and issue tracker in a single package. Running your own GitLab instance on a VPS gives you unlimited private repositories, unlimited CI/CD minutes, and complete control over where your source code lives. This guide walks you through installing GitLab CE (Community Edition) on Ubuntu 24.04 LTS using the official Omnibus package, from bare SSH to a production-ready deployment with HTTPS, email notifications, container registry, Pages, CI/CD runners, backups, and SSO.
Want a simpler Git server? If GitLab feels heavy for your use case, consider our lighter-weight guides for Gitea or Drone CI.
Table of Contents
What is GitLab CE?
GitLab CE is the free, open-source edition of GitLab, distributed under the MIT license. It bundles a Git hosting server (on top of Gitaly), a web UI, merge requests with code review, a built-in CI/CD engine powered by GitLab Runner, a Docker-compatible container registry, a static site host called GitLab Pages, issue tracking with boards and milestones, a wiki, and a package registry for npm, Maven, PyPI, NuGet, and more -- all in one installation.
The Omnibus package bundles every dependency GitLab needs into a single .deb: PostgreSQL (for the main database), Redis (for caching and background jobs), NGINX (as the front-end web server), Puma (the Ruby application server), Sidekiq (background worker), Gitaly (Git RPC service), Workhorse (reverse proxy for large Git and LFS payloads), and Prometheus with Grafana for self-monitoring. You do not need to install these components separately; the Omnibus installer manages the entire stack, handles upgrades, and keeps versions compatible. The upstream documentation at docs.gitlab.com is the authoritative reference for every option covered in this guide.
Teams use self-hosted GitLab for private source control with fine-grained permissions, continuous integration pipelines that build, test, and deploy code on every push, container image hosting for internal Docker workflows, static documentation sites published via GitLab Pages, security scanning (SAST, DAST, dependency, container, and secret scanning are included in CE), and project management through issues, boards, milestones, and time tracking. It is used in production by organizations from single-developer shops to enterprises with tens of thousands of users.
Why Self-Host GitLab Instead of GitLab.com or GitHub?
SaaS Git hosting is convenient, but self-hosting delivers tangible advantages that matter for any team that is serious about its code.
- Data sovereignty -- Your source code, issues, CI artifacts, and container images live on infrastructure you control. For teams in the EU, UK, or regulated industries (finance, healthcare, defense, government), keeping the entire development lifecycle on a server in a known jurisdiction simplifies GDPR, HIPAA, and SOC 2 compliance. You decide who has physical access to the disks, which regions the data replicates to, and how long backups are retained.
- Unlimited private repositories and users -- GitHub Team is $4 per user per month; GitLab.com Premium is $29 per user per month. A 15-person team on GitLab.com Premium is $435 per month -- $5,220 per year. A single self-hosted VPS costs a flat EUR 29.99 per month with unlimited users, unlimited repositories, and no seat metering.
- Unlimited CI/CD minutes -- GitLab.com Free gives 400 CI minutes per month. Heavy pipelines burn through that in a day. Self-hosted GitLab with your own runners has no minute cap -- you pay for the runner VPS, not the minutes.
- No vendor lock-in -- If GitLab changes pricing, deprecates a feature, or goes offline, your instance keeps working. You can migrate between providers, snapshot the whole stack, or move to a bigger server any time.
- Complete customization -- Configure SAML against any identity provider, integrate with any internal tool, patch the web UI, run custom hooks on every push, and set organization-wide policies that SaaS tiers do not expose.
- Offline and air-gapped capability -- GitLab runs fully offline. Government labs, defense contractors, and industrial networks run GitLab in air-gapped environments where SaaS is not an option.
- Predictable, flat-rate cost -- One invoice per month regardless of team size, pipeline volume, storage footprint, or traffic.
Cost Comparison: Self-Hosted GitLab vs. SaaS Alternatives
| Scenario (15 developers) | GitHub Team | GitLab.com Premium | Self-Hosted GitLab CE |
|---|---|---|---|
| Per-user license | $4/user/mo | $29/user/mo | $0 |
| Monthly license total | $60 | $435 | EUR 0 |
| Infrastructure | Included | Included | EUR 29.99/mo (VPS) |
| CI/CD minutes | 3,000/mo shared | 10,000/mo shared | Unlimited |
| Private repos | Unlimited | Unlimited | Unlimited |
| Storage | Metered | Metered | Limited only by disk |
| Data location | US (default) | US (default) | Your choice |
| Annual cost | $720 | $5,220 | EUR ~360 |
Prerequisites
Before you begin, make sure you have:
- An Ubuntu 24.04 LTS VPS with root or sudo access
- At least 4 vCPU and 8 GB of RAM (minimum); 8 vCPU and 16 GB RAM are recommended for small teams
- At least 50 GB of disk space (200 GB+ recommended for repositories, CI artifacts, and registry images)
- A domain name pointed at your server's IP address (for example,
gitlab.yourdomain.com) - Ports 80 and 443 open for HTTP, HTTPS, and Let's Encrypt certificate issuance
- Port 22 open for SSH Git access
- SMTP relay credentials for outbound email (SendGrid, Amazon SES, Mailgun, Postmark, or your own mail server)
Recommended Plan: Business>
GitLab is memory-hungry. The official minimum is 4 GB RAM, but running with only 4 GB means constant swap pressure and slow pipeline execution. For a small to mid-sized team (up to ~50 active users), we recommend the Business plan:>
- 8 vCPU cores
- 16 GB RAM
- 200 GB NVMe SSD
- Unmetered bandwidth
- EUR 29.99/month>
This gives comfortable headroom for GitLab's bundled PostgreSQL, Redis, Sidekiq workers, and Gitaly, plus two to three concurrent CI jobs on a local Docker runner. For teams over 100 active users, step up to a dedicated 32 GB RAM plan.
Connect to your server via SSH to get started:
ssh root@your-server-ipStep 1: Update the System and Install Dependencies
Start by refreshing the package index and upgrading everything. Up-to-date base packages prevent dependency conflicts during the GitLab install.
sudo apt update && sudo apt upgrade -yInstall the packages GitLab Omnibus needs to bootstrap itself and to send mail:
sudo apt install -y curl openssh-server ca-certificates tzdata perl postfixWhen the Postfix configuration prompt appears, choose Internet Site and enter your server's fully qualified domain name (for example, gitlab.yourdomain.com). GitLab uses Postfix only as a fallback; we will configure a proper SMTP relay in Step 5.
Set a correct hostname and timezone:
sudo hostnamectl set-hostname gitlab.yourdomain.com
sudo timedatectl set-timezone UTCIf the kernel was upgraded, reboot:
sudo rebootStep 2: Configure DNS and Open Firewall Ports
Before running the installer, create an A record in your DNS provider pointing gitlab.yourdomain.com to the public IP address of your VPS. Let's Encrypt will attempt to validate this record during the GitLab install; if DNS has not propagated, the certificate issuance will fail and you will have to re-run gitlab-ctl reconfigure afterwards.
Verify the record is live:
dig +short gitlab.yourdomain.comExpected output:
203.0.113.25Open the firewall if UFW is enabled:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw statusIf you plan to use GitLab Pages on a separate wildcard domain (covered in Step 10), also allow port 80 and 443 on the Pages hostname.
Step 3: Add the Official GitLab Package Repository
GitLab provides an installer script at packages.gitlab.com that adds the APT repository, imports the signing key, and configures the gitlab-ce package source.
Download and run the script:
curl -fsSL https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.deb.sh | sudo bashExpected output (abbreviated):
Detected operating system as Ubuntu/noble.
Checking for curl...
Detected curl...
Checking for gpg...
Detected gpg...
Running apt-get update... done.
The repository is setup! You can now install packages.The script adds /etc/apt/sources.list.d/gitlab_gitlab-ce.list and imports the GitLab release key into /etc/apt/keyrings/. From here on, apt treats gitlab-ce like any other package -- future apt upgrade runs will keep GitLab patched.
If you prefer to inspect the script before running it (recommended in production), download it first:
curl -fsSL https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.deb.sh -o /tmp/gitlab-repo.sh
less /tmp/gitlab-repo.sh
sudo bash /tmp/gitlab-repo.shStep 4: Install GitLab CE with EXTERNAL_URL
The EXTERNAL_URL environment variable tells GitLab which domain to use for web access, clone URLs, and Let's Encrypt certificate issuance. Set this to the final HTTPS URL before installing -- changing it later requires a full reconfigure and can break existing clone URLs.
Install GitLab CE with HTTPS enabled from the start:
sudo EXTERNAL_URL="https://gitlab.yourdomain.com" apt install -y gitlab-ceThe install runs for 3-10 minutes depending on CPU speed. During installation, the Omnibus installer:
/opt/gitlab/git, gitlab-www, gitlab-psql, gitlab-redis, gitlab-prometheus, and registry/var/opt/gitlab/postgresql/gitlab.rb at /etc/gitlab/gitlab.rbgitlab-ctl reconfigure to start all servicesEXTERNAL_URL hostname (because it starts with https://)Expected output near the end:
gitlab Reconfigured! _______ __ __ __
/ ____(_) /_/ / ____ _/ /_
/ / __/ / __/ / / __ / __ \
/ /_/ / / /_/ /___/ /_/ / /_/ /
\____/_/\__/_____/\__,_/_.___/
Thank you for installing GitLab!</code></pre></div>
Verify the services are running:
<div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">sudo gitlab-ctl status</code></pre></div>
Expected output:
<div class="code-block" data-lang="text"><div class="code-block__header"><span class="code-block__lang">text</span></div><pre><code class="language-text">run: alertmanager: (pid 1234) 60s; run: log: (pid 1200) 90s
run: gitaly: (pid 1256) 60s; run: log: (pid 1201) 90s
run: gitlab-exporter: (pid 1278) 60s; run: log: (pid 1202) 90s
run: gitlab-kas: (pid 1289) 60s; run: log: (pid 1203) 90s
run: gitlab-workhorse: (pid 1301) 60s; run: log: (pid 1204) 90s
run: logrotate: (pid 1322) 60s; run: log: (pid 1205) 90s
run: nginx: (pid 1344) 60s; run: log: (pid 1206) 90s
run: node-exporter: (pid 1366) 60s; run: log: (pid 1207) 90s
run: postgres-exporter: (pid 1388) 60s; run: log: (pid 1208) 90s
run: postgresql: (pid 1410) 60s; run: log: (pid 1209) 90s
run: prometheus: (pid 1432) 60s; run: log: (pid 1210) 90s
run: puma: (pid 1454) 60s; run: log: (pid 1211) 90s
run: redis: (pid 1476) 60s; run: log: (pid 1212) 90s
run: redis-exporter: (pid 1498) 60s; run: log: (pid 1213) 90s
run: registry: (pid 1520) 60s; run: log: (pid 1214) 90s
run: sidekiq: (pid 1542) 60s; run: log: (pid 1215) 90s</code></pre></div>
Every service should show run: -- any line starting with down: indicates a problem.
Step 5: Configure gitlab.rb
All GitLab configuration lives in a single Ruby file at /etc/gitlab/gitlab.rb. Edit it with your preferred editor:
<div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">sudo nano /etc/gitlab/gitlab.rb</code></pre></div>
The file is already populated with every available option, commented out. Below are the essential sections to configure for a production deployment.
Core URL and Let's Encrypt
Find and update (uncommenting as needed):
<div class="code-block" data-lang="ruby"><div class="code-block__header"><span class="code-block__lang">ruby</span></div><pre><code class="language-ruby">external_url 'https://gitlab.yourdomain.com'
Let's Encrypt automatic renewal
letsencrypt['enable'] = true
letsencrypt['contact_emails'] = ['[email protected]']
letsencrypt['auto_renew'] = true
letsencrypt['auto_renew_hour'] = 3
letsencrypt['auto_renew_minute'] = 30
letsencrypt['auto_renew_day_of_month'] = "*/7"</code></pre></div>GitLab's Let's Encrypt integration renews the certificate automatically every 7 days at 03:30. No separate Certbot install is required -- Omnibus handles the ACME protocol internally.
SMTP for Outbound Mail
GitLab sends email for account confirmation, password resets, notifications, merge request comments, and CI pipeline status. Configure a production SMTP relay (example below uses SendGrid; adjust the host/port for SES, Mailgun, Postmark, etc.):
<div class="code-block" data-lang="ruby"><div class="code-block__header"><span class="code-block__lang">ruby</span></div><pre><code class="language-ruby">gitlab_rails['smtp_enable'] = true
gitlab_rails['smtp_address'] = "smtp.sendgrid.net"
gitlab_rails['smtp_port'] = 587
gitlab_rails['smtp_user_name'] = "apikey"
gitlab_rails['smtp_password'] = "SG.your-sendgrid-api-key-here"
gitlab_rails['smtp_domain'] = "yourdomain.com"
gitlab_rails['smtp_authentication'] = "login"
gitlab_rails['smtp_enable_starttls_auto'] = true
gitlab_rails['smtp_tls'] = false
gitlab_rails['smtp_openssl_verify_mode'] = 'peer'
gitlab_rails['gitlab_email_enabled'] = true
gitlab_rails['gitlab_email_from'] = '[email protected]'
gitlab_rails['gitlab_email_display_name'] = 'GitLab'
gitlab_rails['gitlab_email_reply_to'] = '[email protected]'</code></pre></div>
Container Registry
Enable the bundled Docker-compatible registry on a subdomain:
<div class="code-block" data-lang="ruby"><div class="code-block__header"><span class="code-block__lang">ruby</span></div><pre><code class="language-ruby">registry_external_url 'https://registry.yourdomain.com'</code></pre></div>
If you want GitLab to manage the registry certificate via Let's Encrypt, leave it at that -- Omnibus will request one automatically as long as DNS points registry.yourdomain.com to this server. For a custom certificate, set registry_nginx['ssl_certificate'] and registry_nginx['ssl_certificate_key'] to PEM file paths under /etc/gitlab/ssl/.
GitLab Pages
Pages serves static sites from .pages.yourdomain.com. It requires a wildcard DNS record (*.pages.yourdomain.com -> server IP):
<div class="code-block" data-lang="ruby"><div class="code-block__header"><span class="code-block__lang">ruby</span></div><pre><code class="language-ruby">pages_external_url 'https://pages.yourdomain.com'
gitlab_pages['enable'] = true
pages_nginx['redirect_http_to_https'] = true</code></pre></div>
Performance Tuning for 8 GB RAM
On an 8 GB server, reduce Puma and Sidekiq workers to avoid out-of-memory kills:
<div class="code-block" data-lang="ruby"><div class="code-block__header"><span class="code-block__lang">ruby</span></div><pre><code class="language-ruby">puma['worker_processes'] = 2
sidekiq['max_concurrency'] = 10
postgresql['shared_buffers'] = "512MB"
prometheus_monitoring['enable'] = true</code></pre></div>
On 16 GB (the recommended Business plan), the defaults (puma['worker_processes'] = 0 -- auto) are fine.
Press Ctrl+O, Enter, Ctrl+X to save and exit nano.
Step 6: Reconfigure and First Sign-In
Any change to /etc/gitlab/gitlab.rb requires a reconfigure to take effect:
<div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">sudo gitlab-ctl reconfigure</code></pre></div>
This Chef-based run reads gitlab.rb, regenerates every service config (NGINX virtual hosts, PostgreSQL pg_hba.conf, Redis config, Puma worker settings, Let's Encrypt certificate), restarts affected services, and verifies health. It typically takes 1-3 minutes.
Expected output (abbreviated):
<div class="code-block" data-lang="text"><div class="code-block__header"><span class="code-block__lang">text</span></div><pre><code class="language-text">Starting Chef Infra Client, version 17.10.0
Recipe: gitlab::default
* directory[/etc/gitlab] action create (up to date)
...
Running handlers:
Running handlers complete
Chef Infra Client finished, 256/1408 resources updated in 01 minutes 47 seconds
gitlab Reconfigured!</code></pre></div>
Retrieve the Initial Root Password
On first install, Omnibus generates a random password for the root account and writes it to /etc/gitlab/initial_root_password. This file is automatically deleted after 24 hours, so retrieve the password immediately:
<div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">sudo cat /etc/gitlab/initial_root_password</code></pre></div>
Expected output:
<div class="code-block" data-lang="text"><div class="code-block__header"><span class="code-block__lang">text</span></div><pre><code class="language-text"># WARNING: This value is valid only in the following conditions
1. If provided manually (either via GITLAB_ROOT_PASSWORD environment variable or via gitlab.rb)
2. Password hasn't been changed from the web interface. First login will force a password change.
Password: sR7KpVq9XzN2mB4fJh8cWtYnLu6EgAoD</code></pre></div>
Open https://gitlab.yourdomain.com in a browser. You should see the GitLab sign-in page with a valid Let's Encrypt certificate. Sign in as:
Username: root
Password: (value from initial_root_password)
GitLab will immediately prompt you to set a new password. Choose a strong one and store it in a password manager.Disable Public Sign-Ups (Important)
By default, anyone who finds your GitLab URL can create an account. Disable this immediately:
As root, go to Admin Area (the wrench icon, top nav) -> Settings -> General
Expand Sign-up restrictions
Uncheck Sign-up enabled
Click Save changes From now on, only admins can create new user accounts (or users created through SSO -- see Step 12).
Step 7: Create Your First Project
Click New project on the GitLab dashboard, then Create blank project:
Project name: hello-gitlab
- Visibility level: Private (recommended for all projects until you explicitly want otherwise)
- Leave Initialize repository with a README checked
Click Create project. GitLab creates the Git repository on disk at /var/opt/gitlab/git-data/repositories/ and returns you to the project dashboard.Clone and Push from Your Local Machine
Add your SSH public key to GitLab: top-right avatar -> Edit profile -> SSH keys -> paste the contents of ~/.ssh/id_ed25519.pub -> Add key.
On your local machine:
<div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">git clone [email protected]:root/hello-gitlab.git
cd hello-gitlab
echo "Hello, self-hosted GitLab!" > hello.txt
git add hello.txt
git commit -m "First commit"
git push origin main</code></pre></div>
The push should complete in under a second. Refresh the project page in the browser to see your new file.
Step 8: Register CI/CD Runners
CI/CD pipelines do not run on the GitLab server itself -- they run on GitLab Runner, a separate agent process. You can install runners on the same VPS (fine for small teams) or on dedicated runner machines. We will install one Docker-based runner on the same server.
Install GitLab Runner
<div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash
sudo apt install -y gitlab-runner</code></pre></div>
Install Docker (Required for the Docker Executor)
<div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">sudo apt install -y docker.io
sudo systemctl enable --now docker
sudo usermod -aG docker gitlab-runner</code></pre></div>
Get a Runner Registration Token
In GitLab: Admin Area -> CI/CD -> Runners -> New instance runner. Choose:
Operating systems: Linux
Tags: docker, linux
- Run untagged jobs: checked (so jobs without tags still pick up this runner)
Click Create runner. GitLab displays an authentication token starting with glrt-. Copy it.Register the Runner with docker register
<div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">sudo gitlab-runner register --non-interactive \
--url "https://gitlab.yourdomain.com/" \
--token "glrt-your-registration-token-here" \
--executor "docker" \
--docker-image "alpine:latest" \
--description "dockerized-runner-01" \
--docker-privileged="false"</code></pre></div>
Expected output:
<div class="code-block" data-lang="text"><div class="code-block__header"><span class="code-block__lang">text</span></div><pre><code class="language-text">Runtime platform arch=amd64 os=linux pid=12345 revision=abc12345 version=17.9.0
Registering runner... succeeded runner=glrt-abc
Runner registered successfully. Feel free to start it, but if it's running already the config should be automatically reloaded!</code></pre></div>
Verify the runner appears as online in GitLab's runner list.
Test with a Simple Pipeline
In your hello-gitlab project, create .gitlab-ci.yml:
<div class="code-block" data-lang="yaml"><div class="code-block__header"><span class="code-block__lang">yaml</span></div><pre><code class="language-yaml">stages:
- build
- test
build-job:
stage: build
script:
- echo "Building the project..."
- date
test-job:
stage: test
image: alpine:latest
script:
- echo "Running tests..."
- apk add --no-cache curl
- curl --version</code></pre></div>
Commit and push. Go to CI/CD -> Pipelines in the project sidebar. A new pipeline will appear, pick up the runner, and complete within a minute. Click into each job to see live streaming logs.
Step 9: Enable the GitLab Container Registry
If you followed Step 5 and set registry_external_url, the registry is already running. Verify:
<div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">sudo gitlab-ctl status registry</code></pre></div>
Log in from your local Docker client:
<div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">docker login registry.yourdomain.com
Username: root (or any GitLab user)
Password: (a personal access token with read_registry + write_registry scopes)</code></pre></div>
In GitLab, create a Personal Access Token at User Settings -> Access Tokens, scopes read_registry + write_registry.
Build and Push an Image from CI/CD
Update .gitlab-ci.yml to add a Docker build stage:
<div class="code-block" data-lang="yaml"><div class="code-block__header"><span class="code-block__lang">yaml</span></div><pre><code class="language-yaml">build-image:
stage: build
image: docker:24
services:
- docker:24-dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
script:
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY
- docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" .
- docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"</code></pre></div>
The CI_REGISTRY_USER, CI_REGISTRY_PASSWORD, and CI_REGISTRY_IMAGE variables are injected automatically by GitLab. To run DinD, re-register the runner with --docker-privileged="true" (required for Docker-in-Docker builds).
View pushed images at Packages and registries -> Container Registry in your project.
Step 10: Enable GitLab Pages
GitLab Pages publishes any static site (Hugo, Jekyll, Astro, MkDocs, plain HTML) generated by a CI job. After the configuration in Step 5, add a wildcard DNS record:
<div class="code-block" data-lang="text"><div class="code-block__header"><span class="code-block__lang">text</span></div><pre><code class="language-text">*.pages.yourdomain.com A 203.0.113.25</code></pre></div>
Then add a pages job to .gitlab-ci.yml:
<div class="code-block" data-lang="yaml"><div class="code-block__header"><span class="code-block__lang">yaml</span></div><pre><code class="language-yaml">pages:
stage: deploy
script:
- mkdir -p public
- echo "<h1>Hello from GitLab Pages</h1>" > public/index.html
artifacts:
paths:
- public
rules:
- if: '$CI_COMMIT_BRANCH == "main"'</code></pre></div>
After the pipeline completes, GitLab publishes the public/ artifact to https://.pages.yourdomain.com//. View the URL at Deploy -> Pages in the project sidebar.
Step 11: Configure Backups
GitLab ships a built-in backup tool that snapshots repositories, the database, uploads, LFS objects, artifacts, the registry, and Pages content into a single tarball.
Create a Manual Backup
<div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">sudo gitlab-backup create</code></pre></div>
Expected output (abbreviated):
<div class="code-block" data-lang="text"><div class="code-block__header"><span class="code-block__lang">text</span></div><pre><code class="language-text">Dumping database ... done
Dumping repositories ... done
Dumping uploads ... done
Dumping builds ... done
Dumping artifacts ... done
Dumping pages ... done
Dumping lfs objects ... done
Dumping container registry images ... done
Creating backup archive: 1713268800_2026_04_16_17.9.0_gitlab_backup.tar ... done
Deleting old backups ... done</code></pre></div>
The tarball lands in /var/opt/gitlab/backups/ by default. A typical backup for a small team is 500 MB to 5 GB.
Configure Automatic Daily Backups
In /etc/gitlab/gitlab.rb:
<div class="code-block" data-lang="ruby"><div class="code-block__header"><span class="code-block__lang">ruby</span></div><pre><code class="language-ruby">gitlab_rails['backup_path'] = "/var/opt/gitlab/backups"
gitlab_rails['backup_keep_time'] = 604800 # 7 days in seconds
gitlab_rails['backup_archive_permissions'] = 0644</code></pre></div>
Reconfigure, then add a root cron job:
<div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">sudo gitlab-ctl reconfigure
sudo crontab -e</code></pre></div>
Add:
<div class="code-block" data-lang="text"><div class="code-block__header"><span class="code-block__lang">text</span></div><pre><code class="language-text">0 2 * /opt/gitlab/bin/gitlab-backup create CRON=1</code></pre></div>
This runs every night at 02:00. The CRON=1 flag suppresses normal output, sending only errors to the root mailbox.
Back Up Configuration and Secrets (Critical)
The backup tarball does not include /etc/gitlab/gitlab.rb or /etc/gitlab/gitlab-secrets.json. Without the secrets file, a restored backup cannot decrypt CI/CD variables, 2FA recovery codes, or SSH keys.
Add a second cron entry:
<div class="code-block" data-lang="text"><div class="code-block__header"><span class="code-block__lang">text</span></div><pre><code class="language-text">5 2 * tar -czf /var/opt/gitlab/backups/etc-gitlab-$(date +\%F).tar.gz -C / etc/gitlab</code></pre></div>
Off-Site Backups
Ship backups off the VPS nightly to object storage (S3, Backblaze B2, Wasabi) using rclone:
<div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">sudo apt install -y rclone
sudo rclone config # interactive setup for your remote</code></pre></div>
Then cron:
<div class="code-block" data-lang="text"><div class="code-block__header"><span class="code-block__lang">text</span></div><pre><code class="language-text">30 2 * rclone sync /var/opt/gitlab/backups remote:gitlab-backups --min-age 1h</code></pre></div>
Restore from a Backup
To restore, stop web-facing services, copy the tarball into /var/opt/gitlab/backups, and run:
<div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">sudo gitlab-ctl stop puma
sudo gitlab-ctl stop sidekiq
sudo gitlab-backup restore BACKUP=1713268800_2026_04_16_17.9.0
sudo gitlab-ctl reconfigure
sudo gitlab-ctl restart
sudo gitlab-rake gitlab:check SANITIZE=true</code></pre></div>
Replace the BACKUP= value with the timestamp prefix of your tarball (no _gitlab_backup.tar suffix).
Step 12: Configure SAML or OIDC Single Sign-On
For teams of more than a handful of users, SSO eliminates password sprawl, centralizes offboarding, and enforces corporate MFA. GitLab CE supports both SAML 2.0 and OIDC against any compliant provider (Okta, Azure AD / Entra ID, Google Workspace, Authentik, Keycloak, JumpCloud).
OIDC Example (Authentik / Keycloak / Google Workspace)
In /etc/gitlab/gitlab.rb:
<div class="code-block" data-lang="ruby"><div class="code-block__header"><span class="code-block__lang">ruby</span></div><pre><code class="language-ruby">gitlab_rails['omniauth_enabled'] = true
gitlab_rails['omniauth_allow_single_sign_on'] = ['openid_connect']
gitlab_rails['omniauth_block_auto_created_users'] = false
gitlab_rails['omniauth_auto_link_user'] = ['openid_connect']
gitlab_rails['omniauth_providers'] = [
{
name: "openid_connect",
label: "Company SSO",
args: {
name: "openid_connect",
scope: ["openid", "profile", "email"],
response_type: "code",
issuer: "https://sso.yourdomain.com/application/o/gitlab/",
discovery: true,
client_auth_method: "query",
uid_field: "preferred_username",
send_scope_to_token_endpoint: "true",
client_options: {
identifier: "gitlab-client-id",
secret: "gitlab-client-secret-here",
redirect_uri: "https://gitlab.yourdomain.com/users/auth/openid_connect/callback"
}
}
}
]</code></pre></div>
SAML Example (Okta / Azure AD)
<div class="code-block" data-lang="ruby"><div class="code-block__header"><span class="code-block__lang">ruby</span></div><pre><code class="language-ruby">gitlab_rails['omniauth_providers'] = [
{
name: "saml",
label: "Okta",
args: {
assertion_consumer_service_url: "https://gitlab.yourdomain.com/users/auth/saml/callback",
idp_cert_fingerprint: "AA:BB:CC:...:ZZ",
idp_sso_target_url: "https://your-company.okta.com/app/.../sso/saml",
issuer: "https://gitlab.yourdomain.com",
name_identifier_format: "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent",
attribute_statements: {
email: ["email"],
first_name: ["first_name"],
last_name: ["last_name"],
username: ["username"]
}
}
}
]</code></pre></div>
Apply:
<div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">sudo gitlab-ctl reconfigure</code></pre></div>
The next time you visit the GitLab sign-in page, a new button ("Company SSO" or "Okta") appears alongside the username/password form. First sign-in from each SSO user creates a matching GitLab account automatically.
To force SSO-only access, uncheck Password authentication enabled for web under Admin Area -> Settings -> General -> Sign-in restrictions.
Troubleshooting
<div class="article-table-wrap"><table><thead><tr><th>Problem</th><th>Cause</th><th>Solution</th></tr></thead><tbody><tr><td><code>502 Whoops, GitLab is taking too much time to respond</code></td><td>Puma not running or out of memory</td><td><code>sudo gitlab-ctl status puma</code>. If down, check <code>/var/log/gitlab/puma/puma_stderr.log</code>. Increase RAM or reduce <code>puma['worker_processes']</code>.</td></tr><tr><td>Let's Encrypt fails: <code>Could not complete HTTP-01 challenge</code></td><td>DNS not pointing to server, or port 80 blocked</td><td><code>dig +short gitlab.yourdomain.com</code>. Open port 80. Re-run <code>sudo gitlab-ctl reconfigure</code>.</td></tr><tr><td>Runner shows as offline right after registration</td><td>Runner service not started</td><td><code>sudo systemctl status gitlab-runner</code>. Start: <code>sudo systemctl start gitlab-runner</code>.</td></tr><tr><td><code>Git is taking forever on push</code> with large files</td><td>Workhorse or Gitaly slowdown</td><td>Check <code>/var/log/gitlab/gitlab-workhorse/current</code> and <code>/var/log/gitlab/gitaly/current</code>. Ensure disk has free space.</td></tr><tr><td>CI jobs stuck in <code>pending</code></td><td>No runners matching tags, or all runners busy</td><td>Verify runner tags match <code>.gitlab-ci.yml</code> <code>tags:</code>. Add a second runner or untag jobs.</td></tr><tr><td>Emails not arriving</td><td>SMTP misconfigured or blocked by port 25</td><td>Tail <code>/var/log/gitlab/gitlab-rails/production.log</code>. Test: <code>sudo gitlab-rails console</code> -> <code>Notify.test_email('[email protected]', 'Test', 'Hello').deliver_now</code>.</td></tr><tr><td>Backup restore fails with <code>Permission denied</code></td><td>Ownership mismatch on backup tarball</td><td><code>sudo chown git:git /var/opt/gitlab/backups/*.tar</code>. Retry restore.</td></tr><tr><td><code>gitlab-ctl reconfigure</code> hangs for >15 minutes</td><td>Chef run blocked on a service</td><td>Ctrl+C, then <code>sudo gitlab-ctl tail</code> in another session to find the stuck component.</td></tr></tbody></table></div>
Viewing Logs
GitLab's logs live in /var/log/gitlab/ organized by component. The fastest way to watch everything at once:
<div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">sudo gitlab-ctl tail</code></pre></div>
For a single service:
<div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">sudo gitlab-ctl tail nginx
sudo gitlab-ctl tail gitlab-rails
sudo gitlab-ctl tail sidekiq</code></pre></div>
Run the built-in health check any time:
<div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">sudo gitlab-rake gitlab:check SANITIZE=true</code></pre></div>
It tests database connectivity, Git hooks, repository permissions, LDAP/OIDC config, Redis, and a dozen other subsystems.
FAQ
Is GitLab CE really free, or is it a stripped-down trial?
GitLab CE is fully open-source under the MIT license and free forever with no user cap, project cap, or feature expiry. It includes Git hosting, merge requests, CI/CD, the container registry, Pages, the package registry, issue tracking, boards, milestones, wikis, SAML/OIDC SSO, and basic security scanning. The paid GitLab EE (Premium and Ultimate) tiers add features like multi-level approvals, advanced SAST/DAST, compliance frameworks, portfolio management, and audit logs -- useful for regulated enterprises but not required for the vast majority of development teams. Every guide in this tutorial works identically on GitLab CE.
How much RAM and CPU do I actually need?
GitLab's own sizing documentation recommends 4 GB RAM minimum and 8 GB RAM for up to 500 users, but real-world experience is that 4 GB is painful -- Puma, Sidekiq, PostgreSQL, Redis, and Gitaly together consume 3.5-4 GB just at idle. We recommend 8 vCPU and 16 GB RAM as the practical starting point for any production deployment, which comfortably supports up to ~50 active users and 2-3 concurrent CI jobs on a local runner. For 100+ users, step up to 32 GB RAM and move the Docker runner to a separate VPS so heavy CI workloads do not starve the web front-end. Disk grows fastest from CI artifacts, container images, and repositories -- budget 200 GB NVMe to start and monitor with sudo gitlab-rake gitlab:storage:summary.
Can I migrate from GitHub or GitLab.com to my self-hosted instance?
Yes, and GitLab ships with built-in importers. From the New project screen, choose Import project -> GitHub (paste a personal access token, select repos, GitLab pulls the Git history, issues, pull requests, wiki, releases, and labels). For GitLab.com -> self-hosted, use Direct Transfer (groups and their subgroups/projects) or Project Export (per-project tarball with issues, MRs, milestones, CI configuration). For BitBucket, there is a dedicated importer. After import, update the origin remote on each local clone: git remote set-url origin [email protected]:namespace/project.git.
How do I upgrade GitLab safely?
GitLab follows a strict upgrade path -- you cannot skip major versions. The general process is: check your current version (sudo gitlab-rake gitlab:env:info), review the upgrade path at docs.gitlab.com/update, take a fresh sudo gitlab-backup create plus a copy of /etc/gitlab/, then sudo apt update && sudo apt install gitlab-ce=. The installer runs gitlab-ctl reconfigure automatically and applies database migrations. Minor version bumps (17.9 -> 17.10) are near-zero risk; major version bumps (17.x -> 18.0) require reading the release notes for breaking changes. Never skip from, say, 15.x straight to 17.x -- you must go through 16.x first.
What is the difference between GitLab Omnibus and the Docker or Kubernetes install?
Omnibus (the method in this guide) is a single .deb/.rpm package that installs GitLab and every dependency directly on the host OS. It is the simplest, most battle-tested option and is what GitLab Inc. itself recommends for single-server deployments. Docker runs the same Omnibus image inside a container -- useful if you already standardize on Docker Compose but adds overhead and complicates backups. Helm / Cloud Native Hybrid splits GitLab into microservices on Kubernetes -- appropriate only for deployments with thousands of users or strict Kubernetes-first policies. For any team under ~2,000 users on a single VPS or small fleet, Omnibus is the right choice.
Can I run GitLab behind Cloudflare or another CDN?
Yes, but with caveats. GitLab works fine behind Cloudflare's proxy as long as you (1) set Cloudflare SSL mode to Full (strict) -- Flexible will break Git clones, (2) whitelist the /api/v4/jobs/request endpoint or set a high Cloudflare timeout (some CI job polls run 60+ seconds), (3) disable Cloudflare's Rocket Loader and Auto Minify for JS (they break the GitLab web UI), and (4) grant-list your runner IPs so they are not WAF-challenged. Git operations over SSH ([email protected]:...) bypass Cloudflare entirely and go straight to port 22 on the origin, so no additional config is needed for Git access.
How do I completely uninstall GitLab?
If you need to remove GitLab:
<div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">sudo gitlab-ctl stop
sudo apt purge -y gitlab-ce
sudo rm -rf /etc/gitlab /var/opt/gitlab /var/log/gitlab /opt/gitlab</code></pre></div>
This removes all repositories, databases, uploads, and configuration. Back up first if there is any chance you will want the data later -- there is no undo.
Next Steps
With a production GitLab in place, here are recommended directions to take your setup further:
Scale out CI/CD with dedicated runner VPSs -- Provision a second VPS (or several), install gitlab-runner, and register them with different tags (build, deploy, gpu). Route heavy jobs to dedicated machines so the main GitLab server stays responsive.
Add monitoring and alerting -- GitLab exposes Prometheus metrics at /metrics. Point an external Grafana or Netdata instance at them and alert on Sidekiq queue depth, Puma saturation, and disk usage. Official documentation: docs.gitlab.com.
Move object storage to S3-compatible storage -- For growing registries, artifacts, and LFS objects, switch from local disk to Backblaze B2, Wasabi, or Hetzner Object Storage. Configure via gitlab_rails['object_store'] in gitlab.rb.
- Compare with lighter alternatives -- If you only need Git hosting plus basic CI, Gitea runs in under 150 MB of RAM. Pair it with Gitea Actions for GitHub Actions-compatible CI, or with Drone CI for a pipeline engine that scales horizontally.
Harden and audit -- Enable two-factor authentication for all users, rotate the Rails secret every 12 months, restrict SSH to a VPN or bastion, and review the weekly gitlab-ctl tail output. The full security hardening checklist lives at docs.gitlab.com.
> Need a server sized right for GitLab?
>
> Our Business VPS is tuned for GitLab Omnibus: 8 vCPU, 16 GB RAM, 200 GB NVMe, unmetered bandwidth, in EU or US datacenters.
>
> - Enough headroom for GitLab + 2-3 concurrent CI jobs
> - NVMe disk for fast Git operations and PostgreSQL
> - Full root access -- run gitlab-ctl reconfigure` any time
- Daily off-site snapshots available
>
Deploy a Business VPS -- EUR 29.99/month, ready in under 60 seconds.