Skip to main contentSkip to navigation
[email protected]
Client AreaSupport
Hosting Mammoth
HostingMammothYour Data, Our Responsibility
Home
Solutions
Hosting Services
Store
Pricing
About
Blog
API
Contact

Stay Ahead of the Curve

Get the latest insights on cybersecurity, AI innovations, and enterprise data solutions delivered to your inbox.

Hosting Mammoth
HostingMammothEnterprise Solutions

Enterprise-grade data solutions. Hosting, recovery, cybersecurity, and AI-powered services for businesses worldwide.

[email protected]
Sun - Fri, 9:00am - 5:00pm

Services

  • Cloud Hosting
  • Data Recovery
  • Cybersecurity
  • Legal Support
  • MSP Services
  • Web Development
  • AI Services
  • Free Server Migration

Hosting

  • VPS Hosting (NVMe SSD)
  • VDS Hosting (NVMe)
  • Storage VPS (High SSD)
  • GPU Servers
  • Managed Services
  • Cloud Firewall
  • Load Balancer
  • One-Click Apps
  • n8n Hosting
  • Object Storage
  • FAQ

Company

  • Store
  • Pricing
  • About Us
  • Locations
  • Blog
  • Testimonials
  • Contact
  • Affiliate Program
  • White-Label
  • Terms of Service
  • Privacy Policy
  • Browser Cookies
  • SLA

Support

  • Client Area
  • Submit Ticket
  • Knowledge Base
  • Server Status
  • API Documentation

© 2026 Hosting Mammoth. All rights reserved.

Knowledge Base
Getting StartedAccount ManagementVPS HostingGPU ServersStorage VPSCloud FirewallLoad BalancerServer ManagementBilling & PaymentsSupport & TicketsAffiliate ProgramReseller ProgramMarketplace & Appsn8n HostingManaged ServicesServer MigrationAPI & DevelopersSecurityTroubleshootingGlossaryInstall Guides
  1. Home
  2. /
  3. Support
  4. /
  5. Install Guides
  6. /
  7. How To Install Gitlab Ubuntu
GUIDEInstall Guides

How to Install GitLab CE on Ubuntu 24.04 — Self-Hosted DevOps Platform

28 min read

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?
  • Why Self-Host GitLab Instead of GitLab.com or GitHub?
  • Prerequisites
  • Step 1: Update the System and Install Dependencies
  • Step 2: Configure DNS and Open Firewall Ports
  • Step 3: Add the Official GitLab Package Repository
  • Step 4: Install GitLab CE with EXTERNAL_URL
  • Step 5: Configure gitlab.rb
  • Step 6: Reconfigure and First Sign-In
  • Step 7: Create Your First Project
  • Step 8: Register CI/CD Runners
  • Step 9: Enable the GitLab Container Registry
  • Step 10: Enable GitLab Pages
  • Step 11: Configure Backups
  • Step 12: Configure SAML or OIDC Single Sign-On
  • Troubleshooting
  • FAQ
  • Next Steps
  • 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 TeamGitLab.com PremiumSelf-Hosted GitLab CE
    Per-user license$4/user/mo$29/user/mo$0
    Monthly license total$60$435EUR 0
    InfrastructureIncludedIncludedEUR 29.99/mo (VPS)
    CI/CD minutes3,000/mo shared10,000/mo sharedUnlimited
    Private reposUnlimitedUnlimitedUnlimited
    StorageMeteredMeteredLimited only by disk
    Data locationUS (default)US (default)Your choice
    Annual cost$720$5,220EUR ~360
    For any team of four or more, self-hosted GitLab pays for itself in the first month.

    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:

    bash
    ssh root@your-server-ip

    Step 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.

    bash
    sudo apt update && sudo apt upgrade -y

    Install the packages GitLab Omnibus needs to bootstrap itself and to send mail:

    bash
    sudo apt install -y curl openssh-server ca-certificates tzdata perl postfix

    When 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:

    bash
    sudo hostnamectl set-hostname gitlab.yourdomain.com
    sudo timedatectl set-timezone UTC

    If the kernel was upgraded, reboot:

    bash
    sudo reboot

    Step 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:

    bash
    dig +short gitlab.yourdomain.com

    Expected output:

    text
    203.0.113.25

    Open the firewall if UFW is enabled:

    bash
    sudo ufw allow OpenSSH
    sudo ufw allow 80/tcp
    sudo ufw allow 443/tcp
    sudo ufw enable
    sudo ufw status

    If 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:

    bash
    curl -fsSL https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.deb.sh | sudo bash

    Expected output (abbreviated):

    text
    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:

    bash
    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.sh

    Step 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:

    bash
    sudo EXTERNAL_URL="https://gitlab.yourdomain.com" apt install -y gitlab-ce

    The install runs for 3-10 minutes depending on CPU speed. During installation, the Omnibus installer:

  • Unpacks the GitLab binaries into /opt/gitlab/
  • Creates system users git, gitlab-www, gitlab-psql, gitlab-redis, gitlab-prometheus, and registry
  • Initializes the bundled PostgreSQL cluster in /var/opt/gitlab/postgresql/
  • Generates a default gitlab.rb at /etc/gitlab/gitlab.rb
  • Runs the initial gitlab-ctl reconfigure to start all services
  • Requests a Let's Encrypt certificate for the EXTERNAL_URL hostname (because it starts with https://)
  • Expected output near the end:

    text
    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'] = &quot;*/7&quot;</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'] = &quot;smtp.sendgrid.net&quot; gitlab_rails['smtp_port'] = 587 gitlab_rails['smtp_user_name'] = &quot;apikey&quot; gitlab_rails['smtp_password'] = &quot;SG.your-sendgrid-api-key-here&quot; gitlab_rails['smtp_domain'] = &quot;yourdomain.com&quot; gitlab_rails['smtp_authentication'] = &quot;login&quot; 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'] = &quot;512MB&quot; 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 &quot;Hello, self-hosted GitLab!&quot; &gt; hello.txt git add hello.txt git commit -m &quot;First commit&quot; 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 &quot;https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh&quot; | 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 &quot;https://gitlab.yourdomain.com/&quot; \ --token &quot;glrt-your-registration-token-here&quot; \ --executor &quot;docker&quot; \ --docker-image &quot;alpine:latest&quot; \ --description &quot;dockerized-runner-01&quot; \ --docker-privileged=&quot;false&quot;</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 &quot;Building the project...&quot; - date

    test-job: stage: test image: alpine:latest script: - echo &quot;Running tests...&quot; - 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: &quot;/certs&quot; script: - echo &quot;$CI_REGISTRY_PASSWORD&quot; | docker login -u &quot;$CI_REGISTRY_USER&quot; --password-stdin $CI_REGISTRY - docker build -t &quot;$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA&quot; . - docker push &quot;$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA&quot;</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 &quot;&lt;h1&gt;Hello from GitLab Pages&lt;/h1&gt;&quot; &gt; public/index.html artifacts: paths: - public rules: - if: '$CI_COMMIT_BRANCH == &quot;main&quot;'</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'] = &quot;/var/opt/gitlab/backups&quot; 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: &quot;openid_connect&quot;, label: &quot;Company SSO&quot;, args: { name: &quot;openid_connect&quot;, scope: [&quot;openid&quot;, &quot;profile&quot;, &quot;email&quot;], response_type: &quot;code&quot;, issuer: &quot;https://sso.yourdomain.com/application/o/gitlab/&quot;, discovery: true, client_auth_method: &quot;query&quot;, uid_field: &quot;preferred_username&quot;, send_scope_to_token_endpoint: &quot;true&quot;, client_options: { identifier: &quot;gitlab-client-id&quot;, secret: &quot;gitlab-client-secret-here&quot;, redirect_uri: &quot;https://gitlab.yourdomain.com/users/auth/openid_connect/callback&quot; } } } ]</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: &quot;saml&quot;, label: &quot;Okta&quot;, args: { assertion_consumer_service_url: &quot;https://gitlab.yourdomain.com/users/auth/saml/callback&quot;, idp_cert_fingerprint: &quot;AA:BB:CC:...:ZZ&quot;, idp_sso_target_url: &quot;https://your-company.okta.com/app/.../sso/saml&quot;, issuer: &quot;https://gitlab.yourdomain.com&quot;, name_identifier_format: &quot;urn:oasis:names:tc:SAML:2.0:nameid-format:persistent&quot;, attribute_statements: { email: [&quot;email&quot;], first_name: [&quot;first_name&quot;], last_name: [&quot;last_name&quot;], username: [&quot;username&quot;] } } } ]</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.

    Was this article helpful?

    ← Back to Install GuidesBrowse all categories →

    Still have questions?

    Contact Support →Submit a Ticket