
Fedora ships fast, and that is exactly why GitLab and Fedora make an awkward couple. GitLab Inc. publishes official Linux packages for Enterprise Linux clones, Debian and Ubuntu. Its own documentation lists Fedora under unsupported distributions: installation is possible, but not supported. So anyone typing “install GitLab on Fedora 44” is already off the paved road.
That does not make it a bad idea. Plenty of teams run Fedora Server on lab boxes, internal build hosts and self-managed dev infrastructure, and they want Git hosting, CI and a container registry on the same machine. The catch is knowing which parts are safe, which parts need care, and which parts should never be trusted to a copy-paste from a forum thread. A GitLab instance holds source code, secrets and deploy keys. Treat it with the same suspicion you would give a database server.
This guide walks through a complete installation of GitLab Community Edition on Fedora 44 using the Linux package (Omnibus). It covers preparation, the repository workaround, HTTPS, firewalld, SELinux, resource tuning, backups and the errors that actually show up in the field. Commands assume a fresh Fedora Server install with root or sudo access. Fedora 44 is a young release, so check each step against your own system before you rely on it. Where something is a judgment call rather than documented behavior, it says so.
Before You Touch the Server
Understand the support situation
Omnibus GitLab bundles its own PostgreSQL, Redis, Nginx, Gitaly, Puma and Sidekiq under /opt/gitlab. That bundling is why it can work on a distribution GitLab never tested. The package carries most of its own dependencies, so the host OS mostly supplies the kernel, systemd, glibc and a few base libraries.
Mostly. Fedora moves forward every six months, and a new release can change glibc, OpenSSL, crypto policies or systemd behavior in ways an EL-built package never saw. A GitLab upgrade or a Fedora upgrade can therefore break things without warning, and nobody at GitLab will take your support ticket. For a personal lab or an internal team server, that risk is acceptable. For a revenue-critical production system, AlmaLinux or Rocky Linux is the safer base, and the commands in this guide transfer to them nearly unchanged.
Decide this up front. Retrofitting a platform choice after two years of merge requests is miserable.
Hardware sizing that matches reality
GitLab’s documented single-node baseline is 8 vCPU and 16 GB of RAM, and 8 GB is the floor for smaller setups. For memory-constrained boxes, GitLab documents a minimum of 2 GB RAM plus 1 GB swap, with 20 GB of storage and fast random I/O, SSD preferred. The package itself needs roughly 2.5 GB of disk, but repositories, artifacts, uploads and logs grow far beyond that.
A practical way to read those numbers:
| Scenario | CPU | RAM | Disk |
|---|---|---|---|
| Personal or tiny team (under 5 users) | 2 vCPU | 4 GB (tuned) | 40 GB SSD |
| Small team (5 to 50 users) | 4 vCPU | 8 GB | 100 GB NVMe |
| Active org (up to roughly 1,000 users) | 8 vCPU | 16 GB | 500 GB+ NVMe |
The 4 GB tier works, but expect to tune it (see the performance section). Below that, the experience is poor. Gitaly, Puma, Sidekiq and PostgreSQL all want memory at once, and the first big clone or CI burst will trigger the OOM killer.
One detail is easy to miss. GitLab’s documentation advises disabling swap where possible on larger installs, because swapping under load hurts performance badly, yet it also recommends a modest swap on small machines. These pieces of advice apply to different tiers. On an 8 GB box, a small swap file as a safety net is reasonable. On 32 GB or more, provision enough RAM that swap never matters.
Domain, DNS and ports
Pick the hostname now, for example git.example.com. Create an A record (and AAAA if you have IPv6) pointing to the server before installation. Let’s Encrypt validation will fail otherwise, and changing external_url later forces a reconfigure and breaks every existing clone URL.
You will need these ports reachable:
- 80/tcp for HTTP and ACME challenges
- 443/tcp for HTTPS
- 22/tcp for SSH administration (and possibly Git over SSH)
A frequent conflict: GitLab’s Git-over-SSH wants port 22, but your admin sshd already owns it. Solve this with a different admin SSH port, or by setting gitlab_rails['gitlab_shell_ssh_port'] later. Decide now.
Preparing Fedora 44
Update the system and set the hostname
Start clean.
sudo dnf upgrade --refresh -y
sudo reboot
The reboot matters if a new kernel was installed. Many people skip it, then wonder why uname -r disagrees with what is installed when they debug something later.
Set a fully qualified hostname:
sudo hostnamectl set-hostname git.example.com
hostnamectl
Confirm the name resolves to the right address:
getent hosts git.example.com
Install base dependencies
sudo dnf install -y curl policycoreutils openssh-server perl chrony
sudo systemctl enable --now sshd chronyd
Time sync deserves a line of justification. Clock drift breaks TLS validation, two-factor authentication codes (TOTP), and CI job signatures. Chrony is the default on Fedora, but verify it is actually synchronized:
chronyc tracking
timedatectl
Many older guides also install Postfix for outgoing mail. Today a relay through a transactional provider (SES, Mailgun, your own SMTP relay) is better. Running a local MTA on a git server tends to end with your notifications landing in spam. Skip Postfix and configure SMTP inside GitLab later.
Open the firewall
Fedora Server uses firewalld by default.
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 --reload
sudo firewall-cmd --list-all
If the server sits on a public network, consider restricting SSH to your office or VPN range with a rich rule rather than leaving it open to the world:
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="203.0.113.0/24" service name="ssh" accept'
Then remove the generic ssh service rule once you are sure the rich rule works from your location. Test from a second session before closing the first. Everyone locks themselves out exactly once.
Keep SELinux enforcing
Check the state:
getenforce
You should see Enforcing. Do not disable it just because a guide from 2018 said so. The Omnibus package ships its own SELinux handling for the standard paths, and the policycoreutils package supplies the tools it calls during reconfigure. If you later relocate data directories, you must label them correctly, which is covered in the troubleshooting section.
Adding the GitLab Repository
Why the official script may fail on Fedora
GitLab’s convenience script (script.rpm.sh) detects your OS and builds a repo definition. On a distribution it does not recognize, it can abort with an unsupported OS message. That is the script being cautious, not the package being broken. Piping a remote script into a root shell is also something many admins avoid on principle.
The alternative is to define the repository by hand and point it at the Enterprise Linux build. A Fedora-oriented walkthrough from Atlantic.Net used exactly this approach, with a gitlab-ce.repo file referencing the el/8 path. EL 8 is long past its usefulness for new installs, so use the el/9 path for current builds. Verify that path exists for the version you intend to install, and if GitLab has published el/10 packages by the time you read this, evaluate that too. Check the package listing at packages.gitlab.com first.
Create the repo file
sudo tee /etc/yum.repos.d/gitlab_gitlab-ce.repo > /dev/null <<'EOF'
[gitlab_gitlab-ce]
name=gitlab_gitlab-ce
baseurl=https://packages.gitlab.com/gitlab/gitlab-ce/el/9/$basearch
repo_gpgcheck=1
gpgcheck=1
enabled=1
gpgkey=https://packages.gitlab.com/gitlab/gitlab-ce/gpgkey
https://packages.gitlab.com/gitlab/gitlab-ce/gpgkey/gitlab-gitlab-ce-3D645A26AB9FBD22.pub.gpg
sslverify=1
sslcacert=/etc/pki/tls/certs/ca-bundle.crt
metadata_expire=300
[gitlab_gitlab-ce-source]
name=gitlab_gitlab-ce-source
baseurl=https://packages.gitlab.com/gitlab/gitlab-ce/el/9/SRPMS
repo_gpgcheck=1
gpgcheck=1
enabled=0
gpgkey=https://packages.gitlab.com/gitlab/gitlab-ce/gpgkey
sslverify=1
sslcacert=/etc/pki/tls/certs/ca-bundle.crt
metadata_expire=300
EOF
Refresh metadata and check that the repository is visible:
sudo dnf clean all
sudo dnf makecache
dnf list --showduplicates gitlab-ce | tail -n 5
If makecache complains about GPG on the repository metadata, see the troubleshooting section. Fedora’s crypto policy and newer rpm signature handling occasionally object to older key material.
Choose a version deliberately
The default dnf install gitlab-ce pulls the newest release. For a first install that is usually fine. For anything with users, pin the version so that a routine dnf upgrade cannot jump you across a required upgrade stop:
sudo dnf install -y 'dnf-command(versionlock)'
After install you will lock it:
sudo dnf versionlock add gitlab-ce
GitLab upgrades follow required stops for database migrations. Skipping those is how you end up restoring from backup at 2 AM. Always read the upgrade path tool on GitLab’s docs before moving more than one minor version.
Installing and Configuring GitLab
Install with the external URL set
Setting EXTERNAL_URL as an environment variable during install writes it into the config automatically and runs the first reconfigure:
sudo EXTERNAL_URL="https://git.example.com" dnf install -y gitlab-ce
Using an https:// URL tells Omnibus to request a Let’s Encrypt certificate at the end of the reconfigure, as long as DNS and port 80 are reachable from the internet. If your server is internal-only, Let’s Encrypt cannot validate it. Use http:// for the initial install and configure your own certificate afterwards.
Expect the install to take several minutes. The package is large and the first reconfigure runs Chef to generate configs, initialize PostgreSQL and run database migrations. Do not interrupt it. If your SSH session is flaky, run it inside tmux.
Retrieve the initial root password
sudo cat /etc/gitlab/initial_root_password
That file is deleted automatically after 24 hours, a design choice that has bitten more than one person who installed on Friday and logged in Monday. Log in as root, change the password immediately, and enable two-factor authentication on the admin account.
If the file is gone, reset the password from the console:
sudo gitlab-rake "gitlab:password:reset[root]"
The settings file that matters
Everything lives in /etc/gitlab/gitlab.rb. Back it up before editing:
sudo cp -a /etc/gitlab/gitlab.rb /etc/gitlab/gitlab.rb.$(date +%F)
sudo vi /etc/gitlab/gitlab.rb
A sensible baseline for a public-facing instance:
external_url 'https://git.example.com'
letsencrypt['enable'] = true
letsencrypt['contact_emails'] = ['sysadmin@example.com']
letsencrypt['auto_renew'] = true
nginx['redirect_http_to_https'] = true
nginx['ssl_protocols'] = "TLSv1.2 TLSv1.3"
gitlab_rails['gitlab_email_from'] = 'gitlab@example.com'
gitlab_rails['gitlab_email_display_name'] = 'Example GitLab'
gitlab_rails['smtp_enable'] = true
gitlab_rails['smtp_address'] = "smtp.example.com"
gitlab_rails['smtp_port'] = 587
gitlab_rails['smtp_user_name'] = "smtp-user"
gitlab_rails['smtp_password'] = "CHANGE_ME"
gitlab_rails['smtp_domain'] = "example.com"
gitlab_rails['smtp_authentication'] = "login"
gitlab_rails['smtp_enable_starttls_auto'] = true
gitlab_rails['backup_keep_time'] = 604800
gitlab_rails['time_zone'] = 'Asia/Jakarta'
Apply it:
sudo gitlab-ctl reconfigure
sudo gitlab-ctl restart
Two habits worth building. First, never edit files under /var/opt/gitlab/ by hand; the next reconfigure overwrites them. Second, treat gitlab.rb as code and keep it in a private configuration repository, with secrets pulled in from a vault or environment rather than committed in plain text.
Verify that everything is healthy
sudo gitlab-ctl status
sudo gitlab-rake gitlab:check SANITIZE=true
sudo gitlab-rake gitlab:env:info
Each service should report run. The gitlab:check task flags missing SSH host keys, bad permissions and uninitialized hooks. Read its output rather than just glancing for green.
Then test from the outside:
curl -I https://git.example.com/users/sign_in
openssl s_client -connect git.example.com:443 -servername git.example.com </dev/null 2>/dev/null | openssl x509 -noout -dates -issuer
Hardening the Instance
Lock down sign-ups and authentication
On a fresh install, open registration is enabled. Anyone who finds the URL can create an account. Go to Admin Area, Settings, General, Sign-up restrictions, and either disable sign-up or require admin approval. Do this before you announce the URL.
Further admin-side settings worth setting on almost every deployment:
- Require two-factor authentication for all users with a short grace period.
- Set a minimum password length of at least 12.
- Restrict allowed SSH key types and minimum key sizes.
- Disable unused features such as Auto DevOps if you do not use it.
- Set default project visibility to private.
SSH hygiene
If you run both admin SSH and GitLab’s Git-over-SSH, keep them clearly separated. Move the system sshd to a high port, or bind it to a management interface only:
sudo vi /etc/ssh/sshd_config.d/10-hardening.conf
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
Validate before reloading, because a typo here costs you the box:
sudo sshd -t && sudo systemctl reload sshd
Fail2ban and rate limiting
GitLab has built-in Rack Attack rate limiting for authentication endpoints, and it deserves to be turned on in Admin Area, Settings, Network. On top of that, Fail2ban against SSH and the Nginx access log catches brute-force noise earlier. Install it from the Fedora repos:
sudo dnf install -y fail2ban
sudo systemctl enable --now fail2ban
If you sit behind a reverse proxy or CDN, set the real client IP header properly (nginx['real_ip_header'] and nginx['real_ip_trusted_addresses']), otherwise every failed login looks like it came from the proxy and you will ban your own load balancer.
Permissions and secrets
Two files deserve special protection: /etc/gitlab/gitlab-secrets.json and /etc/gitlab/gitlab.rb. The secrets file holds the keys used to encrypt database columns, CI variables and two-factor seeds. Lose it, and a database backup restores into an instance that cannot decrypt half its own data.
sudo ls -l /etc/gitlab/
sudo chmod 600 /etc/gitlab/gitlab-secrets.json
Copy it somewhere safe and separate from your data backups. Putting both in the same bucket defeats the purpose.
Performance and Resource Tuning
Know where the memory goes
On a stock install, the heavy hitters are Puma (web), Sidekiq (background jobs), Gitaly (Git operations), PostgreSQL and Prometheus monitoring. GitLab’s documentation notes that each Puma worker is capped at 1.2 GB by default and that a node must never run fewer than two Puma workers in clustered mode. For example, it suggests two workers for 2 cores and 8 GB, and four for 4 cores and 8 GB. Sidekiq starts above 200 MB and can climb past 1 GB on busy servers.
Watch it live:
sudo gitlab-ctl top
free -h
sudo smem -rtk 2>/dev/null | head -n 15
A tuned config for small servers
For a 4 GB machine, GitLab publishes a constrained-environment recipe: single-process Puma, lower Sidekiq concurrency, monitoring off, and a jemalloc setting that releases memory sooner. In gitlab.rb:
puma['worker_processes'] = 0
sidekiq['concurrency'] = 10
prometheus_monitoring['enable'] = false
gitlab_rails['env'] = {
'MALLOC_CONF' => 'dirty_decay_ms:1000,muzzy_decay_ms:1000'
}
Disabling Prometheus means losing built-in metrics. If you still want visibility, run node_exporter and a lightweight external monitor. Reconfigure afterwards:
sudo gitlab-ctl reconfigure
Scaling up on bigger hosts
On an 8 vCPU, 16 GB host, leave Puma in clustered mode and size workers to your cores. A common starting formula is cores multiplied by roughly 1.5 plus one, though memory limits it. Seven workers at 1.2 GB each already eats about 8.4 GB before Sidekiq or PostgreSQL has loaded. So size on memory first, cores second.
puma['worker_processes'] = 4
puma['per_worker_max_memory_mb'] = 1024
sidekiq['concurrency'] = 20
postgresql['shared_buffers'] = "2GB"
PostgreSQL’s shared_buffers is typically set near a quarter of RAM allotted to the database. Treat these numbers as a starting point and measure.
Disk I/O and storage layout
Git workloads punish slow disks with many small random reads, which is why GitLab’s own docs rank SSD above HDD and call out good random I/O. Practical suggestions:
- Put
/var/opt/gitlab/git-dataon NVMe or fast SSD storage. - Use XFS (the Fedora Server default) or ext4 with
noatimeon the data mount. - Keep backups on a separate physical volume or remote storage.
- Monitor inode usage as well as free space, since repositories create enormous numbers of small objects.
df -hT /var/opt/gitlab
df -i /var/opt/gitlab
iostat -xz 5 3
Relocating the data directory is a common need. Here is the clean way:
git_data_dirs({
"default" => { "path" => "/mnt/gitdata" }
})
On SELinux systems, label the new path or Gitaly will fail with permission errors even though Unix permissions look right (see below).
Network considerations
Large clones and CI artifact transfers can saturate a modest uplink. A few options help: enable Git protocol v2 (default in modern Git), place a CDN or caching proxy in front of static assets, and use shallow clones in CI (GIT_DEPTH: 20). If you have runners on the same LAN, they bypass the public path entirely, which cuts bandwidth cost noticeably.
Tune kernel parameters only when you have evidence. A reasonable baseline for busy servers:
sudo tee /etc/sysctl.d/90-gitlab.conf > /dev/null <<'EOF'
vm.swappiness = 10
net.core.somaxconn = 1024
fs.file-max = 500000
EOF
sudo sysctl --system
Backups, Upgrades and Runners
A backup routine that gets tested
GitLab’s built-in backup task produces a tar archive of the database, repositories, uploads and more:
sudo gitlab-backup create STRATEGY=copy
The STRATEGY=copy option copies data first to avoid tar complaining about files changing mid-read on a live system. Remember that this archive does not include gitlab.rb or gitlab-secrets.json, so back those up separately:
sudo tar czf /root/gitlab-config-$(date +%F).tgz -C /etc gitlab
Schedule it with a systemd timer, or cron if you prefer:
sudo crontab -e
15 2 * * * /usr/bin/gitlab-backup create CRON=1 STRATEGY=copy
30 2 * * * tar czf /root/gitlab-config-$(date +\%F).tgz -C /etc gitlab
Ship both off the box (rsync, restic or object storage). The real lesson from production incidents is simple: an untested backup is a hope, not a backup. Restore into a scratch VM every quarter and confirm you can log in and clone a repository.
Upgrading without drama
sudo gitlab-backup create STRATEGY=copy
sudo dnf versionlock delete gitlab-ce
sudo dnf install -y gitlab-ce-<target-version>
sudo dnf versionlock add gitlab-ce
sudo gitlab-ctl status
Check required upgrade stops and background migrations first. GitLab will complain loudly if you skip ahead, but only after the package has already been swapped. Upgrading Fedora itself (for example, 44 to 45 someday) deserves the same caution. Snapshot the VM, back up, and read the GitLab release notes before the OS jump.
Adding a GitLab Runner
CI jobs should not execute on the GitLab server. Use a separate host or VM:
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.rpm.sh" -o runner-repo.sh
less runner-repo.sh
Read it, then run it, or define the repo manually as shown earlier. Register with a token created in the GitLab UI. Prefer the Docker or Podman executor for isolation, and keep runner hosts disposable. A compromised build job on the same machine as your repositories is a nightmare scenario.
Troubleshooting Common Problems
Reconfigure hangs at “ruby_block[wait for redis service socket]”
Usually Redis failed to start. Check:
sudo gitlab-ctl tail redis
sudo gitlab-ctl status
Common causes are a stale socket, a full disk, or vm.overcommit_memory warnings on small hosts. Fix the cause, then:
sudo gitlab-ctl reconfigure
502 Bad Gateway right after boot or restart
Puma takes a minute or two to warm up, especially on a small machine. Wait, then look at the logs:
sudo gitlab-ctl tail puma
sudo gitlab-ctl tail nginx
If Puma is being killed, dmesg -T | grep -i -E 'killed process|out of memory' will confirm an OOM kill. Reduce workers or add RAM. GitLab’s docs warn that around 4 GB is a bare minimum and that you may see 500 errors and odd failures under that line.
Let’s Encrypt fails to issue a certificate
Typical culprits: DNS not yet propagated, port 80 blocked by firewalld or an upstream security group, or a CDN proxying the challenge path. Confirm:
dig +short git.example.com
curl -I http://git.example.com/.well-known/acme-challenge/test
sudo firewall-cmd --list-services
Then force a retry:
sudo gitlab-ctl renew-le-certs
Remember that Let’s Encrypt rate-limits repeated failures. Fix the cause before hammering it.
SELinux denials after moving data
Look for AVC messages:
sudo ausearch -m avc -ts recent
sudo journalctl -t setroubleshoot --since "1 hour ago"
For a relocated Git data directory:
sudo semanage fcontext -a -t var_t "/mnt/gitdata(/.*)?"
sudo restorecon -Rv /mnt/gitdata
sudo gitlab-ctl reconfigure
If semanage is missing, install policycoreutils-python-utils. Resist the urge to set permissive mode as a shortcut. It hides the problem and removes a real protection layer.
GPG or signature errors from dnf
If metadata checks fail with a key or signature error, re-import the key and clean the cache:
sudo rpm --import https://packages.gitlab.com/gitlab/gitlab-ce/gpgkey
sudo dnf clean all && sudo dnf makecache
If the failure persists on Fedora’s stricter crypto policy, check update-crypto-policies --show. Loosening the policy system-wide is a bad trade; prefer investigating the specific key. As a last resort, setting repo_gpgcheck=0 on that single repo (keeping gpgcheck=1 for the packages themselves) is an acceptable compromise that many admins tolerate temporarily.
Emails are not arriving
sudo gitlab-rails console
Notify.test_email('you@example.com', 'Test', 'Hello').deliver_now
Errors there point straight at SMTP authentication, TLS or relay restrictions. Also check SPF, DKIM and DMARC on your sending domain, or Gmail will quietly discard everything.
Git push over SSH fails with “Permission denied (publickey)”
Confirm the key was added to the right user, and that /var/opt/gitlab/.ssh/authorized_keys is being managed by GitLab (not by an admin mistakenly editing it). Test verbosely:
ssh -vT git@git.example.com
If your admin sshd sits on port 22 and GitLab’s shell is also expected there, you have the port conflict mentioned earlier.
Monitoring, Logs and Daily Operations
Useful commands that earn their keep:
sudo gitlab-ctl status
sudo gitlab-ctl tail
sudo gitlab-ctl restart puma
sudo gitlab-rake gitlab:check
sudo gitlab-rake gitlab:doctor:secrets
sudo journalctl -u gitlab-runsvdir -f
Rotate and cap logs. Omnibus ships logrotate-style settings; review logging['svlogd_size'] and logrotate_frequency so that /var/log/gitlab cannot silently consume the root volume. Alert on disk usage above 80 percent, on failing backups, on certificate expiry within 14 days, and on Sidekiq queue latency. A growing Sidekiq queue is often the earliest sign that something upstream is wrong.
For uptime checks, probe the readiness endpoint rather than the home page:
curl -fsS https://git.example.com/-/readiness
The /-/health and /-/liveness endpoints are intended for load balancers and monitoring. Whitelist your monitoring IPs in gitlab_rails['monitoring_whitelist'] so the checks are not blocked.