Deploy GitLab on Rocky Linux VPS for distributed teams

Distributed development teams face a real problem when the code repository sits offshore: every fetch and push from Hanoi or Ho Chi Minh City crosses international transit, and the team feels it as lag, timeouts, and flaky CI runs. The fix is to put GitLab where the team is. This guide walks through deploying GitLab CE on a Rocky Linux 9 VPS with a Linux VPS located in Vietnam, then hardens it so it stays up for years. You will end with a working instance, a registered runner, and a backup routine you can trust. No Docker, no Composer, no extra layers: plain packages managed by the OS.
Prerequisites
- A Rocky Linux 9 VPS with at least 4 GB RAM (2 GB is technically possible but painful under CI load; 4 GB is the real floor for a team of 5-10).
- A domain name pointing at the VPS IPv4 (A record). GitLab without HTTPS is a security incident waiting to happen.
- Root or sudo access to the server.
- A registered domain you control for email notifications, or skip outgoing mail entirely.
Why host GitLab inside Vietnam for a distributed team
A self-hosted GitLab in the same country as your developers cuts latency on every Git operation. A clone that takes 40 seconds from a Singapore or US mirror drops to a few seconds when the server sits in a Vietnam VPS with a dedicated IPv4. The difference matters most for large repositories and for CI pipelines that pull the whole history repeatedly.
Data residency is the second reason. Deploying on a VPS in a Tier 3 datacenter in Vietnam keeps source code under local jurisdiction, which simplifies compliance for teams working with Vietnamese partners under Decree 53 type data-localization requirements. You also stop paying per-seat SaaS fees. GitLab CE is free, and the only cost is the VPS itself. For a 10-person team that is usually a fraction of what GitLab.com charges for premium seats, and you keep full control of backups, uptime, and access.
Step 1 - Install GitLab CE on Rocky Linux 9
Rocky Linux 9 ships with SELinux enabled and enforcing. GitLab supports SELinux on RHEL-family distributions, but you must install the policy package so the service can bind sockets and write to its directories. Add the official repository and install:
sudo dnf install -y gitlab-ce
Before that works you need the repository file. Create it with the official GitLab package URL for el9, then re-run the install command. The package pulls in nginx, redis, and postgresql as dependencies. GitLab bundles its own nginx and postgres; this is intentional and simpler to maintain than splitting services.
Once installed, configure the external URL. Edit /etc/gitlab/gitlab.rb and set:
external_url 'https://git.example.com'
Then reconfigure. This step generates SSL certificates if you later run the Let's Encrypt integration, sets up the internal nginx, and starts all services. It takes a few minutes and produces a lot of output; wait for it to finish without interrupting.
sudo gitlab-ctl reconfigure
Verify:
sudo gitlab-ctl status
Expected output shows run: nginx, run: postgresql, run: redis, run: puma, run: sidekiq, and run: gitaly. If any service shows down, check the logs with sudo gitlab-ctl tail <service>.
| Service | Role | Default port |
|---|---|---|
| nginx | Web server and reverse proxy | 80, 443 |
| puma | Ruby application server | 8080 (internal) |
| sidekiq | Background job processor | - |
| postgresql | Main database | 5432 (internal) |
| redis | Cache and queue | 6379 (internal) |
| gitaly | Git repository storage | 8150 (internal) |
Step 2 - Set up HTTPS with Let's Encrypt
GitLab has built-in Let's Encrypt support. Edit /etc/gitlab/gitlab.rb again and add these lines below the external URL:
letsencrypt['enable'] = true
letsencrypt['contact_emails'] = ['[email protected]']
letsencrypt['auto_renew'] = true
Run sudo gitlab-ctl reconfigure again. The reconfigure process requests a certificate for your domain and configures nginx to use it. Auto-renewal happens via a cron job that GitLab installs; you do not need certbot. Before you do this, make sure port 80 and 443 are reachable from the internet, otherwise the ACME challenge fails.
Verify:
curl -I https://git.example.com
Expected output: HTTP/2 302 (a redirect to the sign-in page) and no certificate errors. If curl returns a TLS error, the certificate generation failed; check sudo gitlab-ctl tail nginx for the reason. The initial root password is stored in /etc/gitlab/initial_root_password and expires 24 hours after reconfigure, so change it right after the first login.
Step 3 - Open the firewall the right way
Rocky Linux uses firewalld by default. GitLab needs ports 80 and 443 open for web traffic, and port 22 for SSH if you push over SSH. Do not disable firewalld. Add the services:
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
Verify:
sudo firewall-cmd --list-all
Expected output shows services: dhcpv6-client http https ssh. All other ports stay closed. If you have a separate runner on another machine, open the GitLab instance IP in your cloud firewall rather than exposing more ports on the GitLab box itself.
Step 4 - Tune GitLab for 4 GB RAM
GitLab is hungry. Out of the box, a 4 GB VPS will swap heavily once two CI jobs run at the same time. Puma defaults to consuming most of the memory. Adjust these settings in /etc/gitlab/gitlab.rb:
puma['worker_processes'] = 2
puma['min_threads'] = 1
puma['max_threads'] = 4
sidekiq['max_concurrency'] = 5
postgresql['shared_buffers'] = "256MB"
prometheus_monitoring['enable'] = false
Disabling the bundled Prometheus and Grafana frees around 500 MB. On a 4 GB box you do not need them; use an external monitoring option instead. Reducing Puma workers to 2 keeps the UI responsive while leaving headroom for Gitaly and postgres.
Verify:
sudo gitlab-ctl reconfigure
sudo gitlab-ctl status
free -h
Expected output: all services running, and free -h shows a few hundred MB free rather than zero with heavy swap usage. If you plan to run CI jobs directly on this host, 4 GB is the absolute minimum. For anything more serious, an 8 GB or 16 GB plan gives the runner and the web service room to breathe at the same time. With a 2 GB VPS, GitLab itself fits but CI does not; register the runner on a separate machine.
Step 5 - Register a GitLab Runner for CI/CD
A GitLab without a runner is just a place to store code. Add the runner on a separate VPS or the same host if you have RAM. Install the runner binary on the target machine:
sudo dnf install -y gitlab-runner
Register it against your instance. You need the registration token from your GitLab project or group: Settings, CI/CD, Runners. Run:
sudo gitlab-runner register \
--url https://git.example.com \
--token GL_TOKEN_HERE \
--executor shell \
--description "rocky-runner-01"
The executor matters. shell is the simplest and works well on Rocky if you only run one job at a time. For multiple concurrent jobs, use docker executor and install Docker Engine, which needs more RAM. Do not let the runner concurrency exceed the CPU count on a shared host.
Verify: In the GitLab web UI, go to Settings, CI/CD, Runners. The runner shows up with a green circle. Push a test commit with a .gitlab-ci.yml containing echo "hello" as the first job and confirm it runs.
Step 6 - Set up daily backups to remote storage
GitLab stores everything in postgres, the repository files under /var/opt/gitlab/git-data, and uploads. Back up all three. GitLab provides a backup command that packages database and repositories into one tar file:
sudo gitlab-backup create
The file lands in /var/opt/gitlab/backups. This is on the same disk, so it is not a backup yet. Copy it to another host or object storage. Cron the whole thing at 2 AM when nobody pushes:
sudo crontab -e
0 2 * * * gitlab-rake gitlab:backup:create CRON=1
The old-style gitlab-rake command still works in current versions; gitlab-backup create is the wrapper around it. For restoring on a fresh box, install GitLab, copy the tar file back, run sudo gitlab-backup restore BACKUP=timestamp and then reconfigure to fix the secrets.
定时备份并异地存储,是 GitLab 自托管安全性的底线。
Automated backups stored off-site are the baseline for a safe self-hosted GitLab.
Step 7 - Harden SSH and keep SELinux enforcing
Most GitLab compromises start with a weak SSH password on the host. Disable password authentication and use keys only. Edit /etc/ssh/sshd_config and set:
PasswordAuthentication no
PermitRootLogin prohibit-password
Restart SSH, but keep your current session open until the new one connects:
sudo systemctl restart sshd
Leave SELinux enforcing. GitLab publishes an SELinux policy package for RHEL derivatives; after installation check that it is active:
getenforce
Expected output: Enforcing. If you get Permissive, something earlier went wrong. The policy is installed as gitlab-selinux from the same repo and applies automatically. Keep enforcing; it will stop a stray nginx exploit from reading your postgres data files. Restrict access to the web UI further by allowing only your office IP range in firewalld if the team is small and does not roam.
For a team that needs access while moving, consider a WireGuard VPN back to the office network before hitting GitLab, though that setup is outside the scope of this guide. At minimum, enforce two-factor authentication for every GitLab account under Settings, General, Sign-in restrictions.
Troubleshooting
- 502 error after reconfigure. Puma did not start cleanly. Run
sudo gitlab-ctl tail puma. A common cause is low memory; checkfree -hand reduce Puma workers further, then reconfigure. - Certificate renewal fails. Port 80 must be open for the ACME HTTP challenge. Run
sudo firewall-cmd --list-alland confirmhttpis listed. Also check that your DNS A record points to the VPS public IPv4. - Push times out over SSH. The git user needs an SSH key added in the GitLab UI under Preferences, SSH Keys. Test with
ssh -T [email protected], which should return a welcome message.
FAQ
How much RAM do I need for GitLab on Rocky Linux?
For a team of up to 10 with light CI usage, 4 GB is the practical minimum. GitLab itself uses about 2.5 GB at idle on Rocky Linux after tuning. With 2 GB the web UI becomes sluggish and CI jobs will kill the host. For concurrent pipelines on the same machine, choose an 8 GB or higher plan.
Should I install GitLab with Docker or native packages?
Native packages are easier to maintain in production. The omnibus package bundles nginx, postgres, and redis with tested versions and one reconfigure command. Docker adds a layer of abstraction and storage driver issues for no benefit unless you already standardize everything on containers.
Can I run GitLab CE and CI jobs on the same VPS?
Only with enough RAM. GitLab needs about 2.5 GB at idle; a shell-executor runner needs another 512 MB to 1 GB per job. On 4 GB you can run one job at a time. Anything parallel requires a separate runner host.
Is GitLab CE really free for commercial teams?
Yes. GitLab CE is MIT-licensed and free to self-host for any number of users. The paid tiers add features like merge request approvals and security scanning. Most distributed teams get by with CE plus a few open-source additions.
What is the best way to back up GitLab repositories?
GitLab's gitlab-backup create command produces a single tar file with the database and repositories. Store it off-host daily, and also back up /etc/gitlab/gitlab-secrets.json, which is required for a full restore. Automate both with cron.
Related articles
- Deploy GitLab self-hosted on AlmaLinux VPS in Vietnam
- Self-hosted GitLab CE minimum viable server spec
- Configure a GitLab runner for CI/CD on a Linux VPS
Rocky Linux 上自建 GitLab 的关键配置
在越南机房的自管 VPS 上部署 GitLab CE,团队代码的克隆和推送会明显更快。生产环境建议直接用系统包安装,不用 Docker,记得开启 SELinux 并只开放 80、443、22 端口。至少 4 GB 内存才能跑 Web 服务和轻量 CI,Puma 线程数要调低,关闭内置 Prometheus 可省下约 500 MB。每日用 gitlab-backup 定时备份到异地,同时备份 gitlab-secrets.json 才能在故障后完整恢复。


