How to Install GitLab on RHEL 9 — The Right Way
There is a version of this guide that fits in ten lines. Add the repo, run dnf install gitlab-ce, fire a reconfigure, and call it done. You…
How to Install GitLab on RHEL 9 — The Right Way
There is a version of this guide that fits in ten lines. Add the repo, run dnf install gitlab-ce, fire a reconfigure, and call it done. You will have GitLab running on port 80 with no SSL, no backups, a root password that expires in 24 hours, and absolutely no idea what to do when it falls over at 2 AM.
This is not that guide.
Self-hosting GitLab is worth doing well. You get full control over your source code, pipelines, container registry, and team data — all on your own hardware. But the Omnibus package is not just a web app. It is an entire platform: a web server, a database, a cache, a Git RPC service, a background job processor, and a monitoring stack, all running inside a single process supervisor. Treat it like a simple app install and it will punish you. Understand it, and it is genuinely rock-solid.
Here is the full picture — from pre-flight checks to a production-ready, SSL-secured GitLab instance with CI/CD pipelines and automated backups running on RHEL 9.
What You Are Actually Installing
Before a single command, it helps to understand the stack.
GitLab Omnibus bundles: Puma (the Rails web server), Sidekiq (background job processor), Gitaly (handles all Git operations over RPC), GitLab Workhorse (a reverse proxy for large uploads and downloads), a bundled Nginx for SSL termination, PostgreSQL, Redis, and Prometheus with Grafana for internal monitoring. All of this ships as one package and is managed by a single CLI tool — gitlab-ctl.
Traffic flows like this:
Browser / Git client
↓
Nginx (port 443, SSL termination)
↓
GitLab Workhorse
↓
Puma (Rails application)
↓
PostgreSQL / Redis / Gitaly
Every layer needs to be healthy. If Gitaly is down, Git pushes fail silently. If Redis is unavailable, background jobs queue forever. If PostgreSQL has a connection exhaustion problem, the web UI returns 500 errors with no obvious cause. This is why understanding the architecture is step zero.
Pre-Flight: The Checks That Save You Later
Most failed GitLab installs are not installation failures. They are planning failures discovered after installation. These checks take five minutes and prevent hours of debugging.
RAM — The One That Kills Everyone
GitLab is memory-hungry. The minimums are non-negotiable:
- 4 GB — absolute floor for evaluation only
- 8 GB — comfortable for a small team
- 16 GB — recommended for 20–100 users with active CI/CD
free -h
If you are under 8 GB, add swap before proceeding. Less than 4 GB and Puma workers will be OOM-killed mid-request, which produces 502 errors with no log trace pointing to the real cause.
sudo fallocate -l 4G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
CPU
At minimum, 2 cores. For production with CI/CD load, 4 to 8 cores. GitLab will run on 2 cores — it will just be slow during pipeline execution.
nproc
Disk
The /var/opt/gitlab/ directory holds everything: Git repositories, the PostgreSQL database, Redis data, CI artifacts, and LFS objects. Minimum 50 GB, but 200 GB is a more honest starting point for a real team. Use SSD — Git operations and PostgreSQL writes are I/O-intensive, and spinning disks will show.
df -h /
Hostname and DNS
GitLab uses the hostname as the base URL for every link it generates — emails, webhook payloads, clone URLs, the UI. This must match your actual DNS record.
sudo hostnamectl set-hostname gitlab.yourdomain.com
hostname -f
Port Conflicts
GitLab needs ports 80, 443, and 22. If your OS SSH daemon is already on port 22, you have a decision to make before you install. The cleanest solution: move the OS SSH daemon to port 2222 and let GitLab Shell own port 22. Users expect git clone git@gitlab.yourdomain.com:group/repo.git to work without specifying a port.
sudo ss -tulnp | grep -E ':80|:443|:22'
SELinux
GitLab ships its own SELinux policies, but they have friction with default RHEL 9 enforcing mode, particularly around Gitaly’s Unix socket paths and Nginx’s log directory access. Set it to permissive for the initial setup:
sudo setenforce 0
sudo sed -i 's/^SELINUX=enforcing$/SELINUX=permissive/' /etc/selinux/config
Installation
Step 1 — System update and dependencies
sudo dnf update -y
sudo dnf install -y curl wget git openssh-server openssl firewalld \
policycoreutils policycoreutils-python-utils python3
sudo systemctl enable --now sshd
Step 2 — Firewall
sudo systemctl enable --now firewalld
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --permanent --add-port=5050/tcp # Container Registry
sudo firewall-cmd --reload
sudo firewall-cmd --list-all
Step 3 — Add the official GitLab repository
GitLab provides a repository setup script that adds the correct repo for your OS and imports the package signing key:
curl -s https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.rpm.sh | sudo bash
sudo dnf repolist | grep gitlab
Step 4 — Install GitLab CE
Pass the external URL as an environment variable. This is the base URL that gets baked into every GitLab configuration on first install:
sudo EXTERNAL_URL="https://gitlab.yourdomain.com" dnf install -y gitlab-ce
This downloads roughly 1 GB and runs gitlab-ctl reconfigure automatically. It takes 5–15 minutes. A successful install ends with:
gitlab Reconfigured!
Immediately grab the generated root password — it is deleted after 24 hours:
sudo cat /etc/gitlab/initial_root_password
Configuration — The Part Most Guides Skip
Every GitLab setting lives in one file: /etc/gitlab/gitlab.rb. Every change you make there is applied by running sudo gitlab-ctl reconfigure. Think of it as a declarative configuration for the entire stack.
Back it up before touching it:
sudo cp /etc/gitlab/gitlab.rb /etc/gitlab/gitlab.rb.bak
The critical settings to set immediately after install:
External URL Must include https:// if you are using SSL. Mismatching this causes broken clone URLs and webhook payloads that point to the wrong host.
external_url 'https://gitlab.yourdomain.com'
Let’s Encrypt (for production with a real domain) GitLab can obtain and auto-renew SSL certificates automatically if port 80 is open and DNS resolves correctly:
letsencrypt['enable'] = true
letsencrypt['contact_emails'] = ['admin@yourdomain.com']
letsencrypt['auto_renew'] = true
letsencrypt['auto_renew_hour'] = 3
letsencrypt['auto_renew_day_of_month'] = "*/7"
Self-signed certificate (for air-gapped or internal deployments) Generate a certificate, place it in /etc/gitlab/ssl/, and reference it:
sudo mkdir -p /etc/gitlab/ssl
sudo chmod 700 /etc/gitlab/ssl
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /etc/gitlab/ssl/gitlab.yourdomain.com.key \
-out /etc/gitlab/ssl/gitlab.yourdomain.com.crt \
-subj "/CN=gitlab.yourdomain.com"
Then in gitlab.rb:
letsencrypt['enable'] = false
nginx['ssl_certificate'] = "/etc/gitlab/ssl/gitlab.yourdomain.com.crt"
nginx['ssl_certificate_key'] = "/etc/gitlab/ssl/gitlab.yourdomain.com.key"
Puma worker tuning Each Puma worker uses approximately 400 MB of RAM. Tune based on your server:
puma['worker_processes'] = 2 # Adjust to CPU count
puma['min_threads'] = 4
puma['max_threads'] = 4
Disable public registration Unless you want anyone on the internet to create accounts on your instance:
gitlab_rails['gitlab_signup_enabled'] = false
After any change to gitlab.rb:
sudo gitlab-ctl reconfigure
sudo gitlab-ctl status
Every service should show run: in the status output.
SMTP — You Will Need This Eventually
Without SMTP, GitLab cannot send password reset emails, CI/CD failure notifications, or merge request alerts. Configure it before your team starts using the instance, not after the first password reset fails at 11 PM on a Friday.
For Gmail with an App Password:
gitlab_rails['smtp_enable'] = true
gitlab_rails['smtp_address'] = "smtp.gmail.com"
gitlab_rails['smtp_port'] = 587
gitlab_rails['smtp_user_name'] = "your-account@gmail.com"
gitlab_rails['smtp_password'] = "your-app-password"
gitlab_rails['smtp_authentication'] = "login"
gitlab_rails['smtp_enable_starttls_auto'] = true
gitlab_rails['gitlab_email_from'] = 'gitlab@yourdomain.com'
Test it from the Rails console before trusting it:
sudo gitlab-rails console
Notify.test_email('you@example.com', 'GitLab SMTP Test', 'Working').deliver_now
exit
Backups
This section is short because the principle is simple — but it is worth saying clearly.
A GitLab backup contains your repositories, database, uploads, CI artifacts, and LFS objects. It does not contain your gitlab-secrets.json file. That file holds the encryption keys for two-factor authentication tokens and stored CI variables. Without it, a backup archive is unrestorable.
Back up both, store them separately, test restoring from them before you need to.
Run a manual backup first:
sudo mkdir -p /var/opt/gitlab/backups
sudo chown git:git /var/opt/gitlab/backups
sudo gitlab-backup create
Automate daily backups via cron:
sudo crontab -u root -e
# Add: 0 2 * * * /opt/gitlab/bin/gitlab-backup create CRON=1 >> /var/log/gitlab/backup.log 2>&1
Back up the secrets and config separately:
sudo cp /etc/gitlab/gitlab-secrets.json /your/secure/backup/location/
sudo cp /etc/gitlab/gitlab.rb /your/secure/backup/location/
Set backup retention in gitlab.rb to avoid filling your disk:
gitlab_rails['backup_keep_time'] = 604800 # 7 days in seconds
GitLab Runner and CI/CD
The runner is a separate binary that connects to your GitLab instance and executes pipeline jobs. Install it on the same server or a dedicated runner host:
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.rpm.sh" | sudo bash
sudo dnf install -y gitlab-runner
sudo systemctl enable --now gitlab-runner
Get the registration token from Admin Area → CI/CD → Runners, then register:
sudo gitlab-runner register \
--url "https://gitlab.yourdomain.com" \
--token "<YOUR_TOKEN>" \
--description "RHEL9-Runner" \
--executor "shell"
Once registered, the runner shows up with a green dot in the Admin UI and starts picking up pipeline jobs immediately.
A minimal .gitlab-ci.yml to verify everything is wired correctly:
stages:
- test
smoke-test:
stage: test
script:
- echo "Pipeline is working"
- date
- hostname
Push this to main and watch the pipeline run in Build → Pipelines.
The Commands You Will Actually Use
These are worth bookmarking. Most GitLab administration reduces to a small set of gitlab-ctl and gitlab-rake commands.
Service management:
sudo gitlab-ctl status # Health of every service
sudo gitlab-ctl restart puma # Restart the Rails web server
sudo gitlab-ctl restart sidekiq # Restart background job processor
sudo gitlab-ctl hup nginx # Reload Nginx with zero downtime
sudo gitlab-ctl reconfigure # Apply gitlab.rb changes
Logs:
sudo gitlab-ctl tail # All service logs
sudo gitlab-ctl tail nginx # Nginx access + error
sudo gitlab-ctl tail puma # Rails application
sudo gitlab-ctl tail sidekiq # Background jobs
sudo gitlab-ctl tail gitaly # Git operations
Health checks:
sudo gitlab-rake gitlab:check # Full system health check
sudo gitlab-rake gitlab:db:check # Database connectivity
sudo gitlab-rake gitlab:env:info # Version and environment info
sudo gitlab-rake cache:clear # Clear Sidekiq queue and Redis cache
Common Failure Modes
502 immediately after install Puma takes 60–120 seconds to start on first boot. Wait, then retry. If it keeps returning 502, check sudo gitlab-ctl tail puma and look for OOM kill messages in dmesg.
Git SSH fails with connection refused Port 22 conflict with the OS SSH daemon. Move the OS SSH daemon to port 2222 and open that port in the firewall.
Pipeline jobs stuck in Pending forever No runner is registered or the registered runner is offline. Check Admin Area → CI/CD → Runners. The runner must show a green dot.
Let’s Encrypt certificate fails to generate Port 80 is not reachable from the internet, or DNS does not resolve to this server. Check sudo firewall-cmd --list-services and test DNS resolution from an external machine.
Restore fails with encryption errors The gitlab-secrets.json on the restore target does not match the one from the original instance. This is why you back that file up separately.
Production Is a Starting Point, Not an Endpoint
Once GitLab is running, the natural next moves are: enabling LDAP so your team logs in with their corporate credentials, setting up GitLab Geo for disaster recovery if you need a secondary site, configuring the built-in container registry so CI pipelines can push and pull Docker images, and enabling Prometheus alerting so service degradation notifies you before a user does.
The monitoring is already there. Prometheus runs internally at localhost:9090, and Grafana is accessible at https://gitlab.yourdomain.com/-/grafana with dashboards covering Gitaly performance, Sidekiq queue depth, Puma request rates, and PostgreSQL query timing out of the box.
The install is the beginning. Getting familiar with gitlab-ctl, learning what each service does, and understanding the startup/shutdown sequence — that is what makes you the person who can fix it at 2 AM instead of the person who reboots the server and hopes for the best.
GitLab on RHEL is one piece of a broader DevSecOps stack. Pair it with Ansible for infrastructure automation, Jenkins or native GitLab CI for deployment pipelines, Prometheus and Grafana for observability, and Kubernetes for container orchestration — all installable on RHEL using the same disciplined, production-first approach.
메타데이터
- post_id
- 7d8fbbc0f096
- slug
- how-to-install-gitlab-on-rhel-9-the-right-way-7d8fbbc0f096
- url
- https://medium.com/@manjunath.kvmc/how-to-install-gitlab-on-rhel-9-the-right-way-7d8fbbc0f096
- canonical_url
- https://medium.com/@manjunath.kvmc/how-to-install-gitlab-on-rhel-9-the-right-way-7d8fbbc0f096
- author_url
- https://medium.com/@manjunath.kvmc
- status
- ok
- fetched_at
- 2026-07-13 14:08:30