Installing and Configuring GitLab Server
Introduction
Imagine this: your company has 10 developers, the project code is confidential, and you cannot store it on an external cloud service. Or you work at a bank where the regulator requires that all code be stored on servers within the country. Or a simpler scenario — GitHub/GitLab.com has limits on free private repositories, but your own server is free and unlimited.
GitLab (opens in a new tab) solves exactly these problems. It is an open-source DevOps platform that combines source code management, CI/CD, and project management all in one place. Most importantly, you can install it on your own server.
Real-world examples — when do you need self-hosted GitLab:
- Fintech/Banking — regulatory requirement: code and data must be stored within the country
- Government organizations — classified projects, networks with restricted internet access
- Medium/large companies — 50+ developers, GitHub Enterprise is too expensive ($21/user/month), GitLab self-hosted is free
- Startups — limited budget, but a professional DevOps workflow is needed
- Outsourcing companies — separate group/permission configuration is needed for each client
In this guide, we will install a GitLab server from scratch, configure it for production use, connect a Runner, and launch a real CI/CD pipeline.
How Does GitLab Work?
When GitLab is installed, several services start working together on the server. Each one performs its own role:
+-------------------------------------------------------------------+
| GITLAB SERVER |
| |
| +----------+ +-----------+ +------------+ +-----------+ |
| | | | | | | | | |
| | Nginx |--->| Workhorse |--->| Puma |--->| Sidekiq | |
| | (Proxy) | | (Files) | | (Main | | (Back- | |
| | | | | | App) | | ground) | |
| +----------+ +-----------+ +------+-----+ +-----------+ |
| | |
| +----------------------+----+ |
| | | | |
| +------+-----+ +----------+-+ +-------+------+ |
| | | | | | | |
| | PostgreSQL | | Redis | | Gitaly | |
| | (DB) | | (Cache) | | (Git repos) | |
| | | | | | | |
| +------------+ +------------+ +--------------+ |
| |
+-------------------------------------------------------------------+In short:
- Nginx — accepts incoming requests from the browser and routes them to the appropriate component
- Workhorse — handles large files (upload/download) so that Puma is not overloaded
- Puma — GitLab's main application. The web interface, API, login — everything runs here
- Sidekiq — executes background jobs: sending emails, webhooks, creating pipelines
- PostgreSQL — the database where all data is stored
- Redis — cache and session management
- Gitaly — the service that handles Git repository operations
You do not need to install all of these components separately — the GitLab Omnibus package automatically installs and configures everything. There is no need to install Nginx, PostgreSQL, or Redis separately.
GitLab CE or EE — Which One to Choose?
GitLab comes in two editions. Here is how they differ:
| GitLab CE (Community Edition) | GitLab EE (Enterprise Edition) | |
|---|---|---|
| Cost | Free, open-source | Free (core) + paid plans |
| CI/CD | Fully functional | Additional: merge trains, multi-project pipelines |
| Security | Basic features | SAST, DAST, dependency scanning |
| Support | Community only | Official technical support |
| Best for | Small teams, personal projects | Large organizations, enterprises |
Which one should you install? If you think you might need paid features in the future, install GitLab EE. On the free plan, it works almost identically to CE, but you retain the option to enable paid features when needed. In most cases, EE is recommended.
Installation Methods
GitLab can be installed in several ways:
| Method | Description | When to use |
|---|---|---|
| Linux package (Omnibus) | Install directly on a VM or bare-metal server | Most common, recommended for production |
| Docker | Run inside a Docker container | Quick testing, small teams |
| Kubernetes (Helm chart) | Deploy to a K8s cluster | Large organizations, when auto-scaling is needed |
| From source | Compile the code yourself | Special cases only, not recommended |
In this guide, we use the Linux package (Omnibus) method — installing on an Ubuntu VM. This is the most widely used and officially recommended approach by GitLab. All components (Nginx, PostgreSQL, Redis, etc.) come bundled in a single package.
Getting Started
Server Requirements
Minimum Server Requirements
| Host | OS | RAM | CPU | Storage | Static IP |
|---|---|---|---|---|---|
| gitlab | Ubuntu 20.04+ | 8GB | 4 vCPU, 2 core | 80GB | Yes, required |
8GB RAM is sufficient for up to 500 users. If your team is larger, 16GB or more RAM is recommended. Disk space will grow depending on the number of repositories.
DNS Configuration
A domain is required for the GitLab server — we will use this domain to access GitLab from the browser and perform Git operations.
You need to point your domain (or subdomain) to the GitLab server's IP address through your DNS hosting provider. Below is an example using ahost.uz (opens in a new tab):
Navigate to your domain settings and go to the DNS Hosting section:
We have the helm.uz (opens in a new tab) domain. Let's add a gitlab subdomain to it:
- Name ->
gitlab(subdomain name) - Type ->
A - TTL ->
14400 - RDATA -> GitLab server static IP address
Installing GitLab
Once DNS is configured, we can begin the installation.
1-> Update the server and install the required packages.
sudo apt update && sudo apt upgrade -y
sudo apt install -y curl openssh-server ca-certificates tzdata perl2-> Install Postfix for email notifications. GitLab uses a mail server to send notifications to users.
sudo apt install -y postfixDuring installation, a configuration screen will appear:
- On the first screen, select "Internet Site"
- On the second screen, enter your server's domain (e.g.,
gitlab.helm.uz)
3-> Install GitLab. Choose CE or EE by clicking the appropriate tab:
- GitLab CE
- GitLab EE
Add the GitLab CE repository and install it:
curl -s https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.deb.sh | sudo bashSet EXTERNAL_URL to your own domain:
sudo EXTERNAL_URL="https://gitlab.helm.uz" apt install gitlab-ceWhen the installation completes successfully, you should see the following output:
The installation may take 5-10 minutes. Once complete, the console will display a message about the admin password — the root user password is stored in /etc/gitlab/initial_root_password. This file is automatically deleted after 24 hours, so copy the password immediately!
4-> Open your domain in the browser (in our case, gitlab.helm.uz). The login page should appear.
Log in with the root username. Retrieve the password with the following command:
sudo cat /etc/gitlab/initial_root_passwordAfter logging in, the Welcome to GitLab page will appear:
Admin panel: gitlab.helm.uz/admin — here you can view general information about the server.
Configuring GitLab Server
GitLab is installed. Now we need to perform several important configuration steps before using it.
1. Changing the Admin Password
We are currently logged in with the root username and an auto-generated password. Let's change both.
Go to your profile -> Edit profile -> Account section:
Replace root with your own username and click Update username:
Go to the Password section and set a new password:
After saving, you will be redirected to the login page — log in with your new credentials:
2. Disabling Open Registration
By default, anyone can register on GitLab — the login page has a Register now button. This is dangerous because unauthorized people could gain access to your server.
To disable this:
Navigate to the Admin Area:
Find General -> Sign-up restrictions:
Uncheck the Sign-up enabled checkbox and save:
Now only the admin can create new users.
3. Automatic SSL Certificate Renewal
When GitLab is installed, an SSL certificate is automatically obtained via Let's Encrypt. However, the certificate needs to be renewed every 90 days. To automate this:
sudo nano /etc/gitlab/gitlab.rbFind and uncomment (or add) the following lines:
# /etc/gitlab/gitlab.rb
letsencrypt['auto_renew'] = true
letsencrypt['auto_renew_hour'] = "12"
letsencrypt['auto_renew_minute'] = "30"
letsencrypt['auto_renew_day_of_month'] = "*/7"
letsencrypt['auto_renew_log_directory'] = '/var/log/gitlab/lets-encrypt'This configuration checks the certificate every 7 days at 12:30 and renews it if necessary.
Apply the changes:
sudo gitlab-ctl reconfigure4. Production GitLab Configuration (gitlab.rb Tuning)
The default settings are sufficient for small teams, but if you have 20+ users or need to optimize server resources, you should tune the gitlab.rb file.
sudo nano /etc/gitlab/gitlab.rb# /etc/gitlab/gitlab.rb — Production tuning
# Puma worker count — each worker uses ~1GB RAM
# Formula: number of CPU cores + 1, but reduce based on available RAM
# For 8GB RAM, 4 CPUs:
puma['worker_processes'] = 4
# Sidekiq — background job workers
# Default is 1; increase to 2-3 for more users
sidekiq['max_concurrency'] = 20
# PostgreSQL — shared_buffers should be ~25% of total RAM
# For an 8GB server:
postgresql['shared_buffers'] = "2048MB"
# Monitoring — Prometheus and Grafana
# Monitor server health at gitlab.helm.uz/-/grafana
prometheus_monitoring['enable'] = true
grafana['enable'] = true
# Container Registry — for storing Docker images
# Accessible at gitlab.helm.uz:5050
registry_external_url 'https://gitlab.helm.uz:5050'
# Backup settings
gitlab_rails['backup_keep_time'] = 604800 # 7 days (in seconds)RAM calculation formula: GitLab itself uses ~4GB RAM. Each Puma worker adds ~700MB, and each Sidekiq worker adds ~1GB. On an 8GB RAM server with 4 Puma workers and 1 Sidekiq worker, you are close to the limit. If other services are also running on the server, reduce the worker count.
Apply the changes:
sudo gitlab-ctl reconfigure5. Creating Groups and Repositories
GitLab allows you to organize projects into groups. For example, creating a group called DevOps and placing all related repositories inside it is convenient:
Create a new repository inside the group:
Push the project from your local machine:
Installing GitLab Runner
GitLab itself creates CI/CD pipelines, but it needs a separate agent to execute them — that agent is the GitLab Runner. The Runner picks up each job in a pipeline and executes it: building code, running tests, deploying.
Runner Types
+------------------------------------------------------------------+
| GITLAB SERVER |
| |
| +-------------------+ +-------------------+ |
| | Project A | | Project B | |
| +--------+----------+ +--------+----------+ |
| | | |
+------------------------------------------------------------------+
| |
+--------v----------+ +---------v---------+
| Shared Runner | | Specific Runner |
| (Available to | | (Dedicated to a |
| all projects) | | single project) |
+--------+----------+ +---------+---------+
| |
+--------v----------+ +---------v---------+
| Docker | | Shell |
| Executor | | Executor |
+-------------------+ +--------------------+- Shared Runner — serves all projects. Convenient for general build/test tasks
- Specific Runner — assigned to a single project or group only. Used when a specialized environment is required
Executor — determines where the Runner executes jobs:
- Docker executor — creates a new Docker container for each job (good isolation, recommended)
- Shell executor — jobs run directly on the server itself (simple, but risky)
Installing the Runner
1-> Create a runner from the GitLab Admin Area: Admin Area -> CI/CD -> Runners -> New instance runner
Specify tags and settings for the runner:
GitLab will provide a token and the registration command:
2-> Now go to the server and install GitLab Runner:
# Download the binary
sudo curl -L --output /usr/local/bin/gitlab-runner \
https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64
# Grant execution permissions
sudo chmod +x /usr/local/bin/gitlab-runner
# Create a dedicated user for GitLab Runner
sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash
# Install and start as a service
sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
sudo gitlab-runner start3-> We will be using the Docker executor, so Docker must also be installed on the server. Refer to the Installing Docker on Linux Servers (opens in a new tab) guide.
4-> Register the Runner with GitLab using the token provided:
gitlab-runner register \
--url https://gitlab.helm.uz \
--token glrt-xxxxxxxxxxxxxxxxxxxxDuring the registration process, you will be asked several questions:
Runner name: runner1 — this is just for identification, you can use any name
Executor: docker — to run jobs inside Docker containers
Default Docker image: ubuntu:latest — this image is used if no image is specified in .gitlab-ci.yml
5-> Start the Runner:
sudo gitlab-runner runClick View runner in GitLab to verify — it should show a green status:
Runner Configuration
After the Runner is registered, review the settings in the config.toml file:
sudo nano /etc/gitlab-runner/config.toml# /etc/gitlab-runner/config.toml
concurrent = 4 # How many jobs can run simultaneously
check_interval = 0 # Polling interval for new jobs from GitLab (0 = default 3s)
shutdown_timeout = 0 # Wait time before shutdown
[session_server]
session_timeout = 1800 # Timeout for interactive web terminal (seconds)
[[runners]]
name = "runner1"
url = "https://gitlab.helm.uz/"
token = "glrt-xxxxxxxxxxxxxxxxxxxx"
executor = "docker"
[runners.docker]
tls_verify = false
image = "ubuntu:latest" # Default Docker image
privileged = true # Required for Docker-in-Docker
disable_entrypoint_overwrite = false
oom_kill_disable = false
disable_cache = false
volumes = ["/cache"] # Cache storage between jobs
shm_size = 0About privileged = true: This is required for Docker-in-Docker (running Docker inside Docker). For example, when you need to build Docker images inside a CI pipeline. However, this is risky — the container gets full access to the host server. If you do not need Docker builds, set privileged = false.
Grant the Runner permission to use Docker:
sudo usermod -aG docker gitlab-runnerRestart to apply the changes:
sudo gitlab-runner restartYour First CI/CD Pipeline
Everything is ready — GitLab is installed, configured, and the Runner is connected. Now let's launch our first pipeline.
How CI/CD Works
+----------+ +-----------+ +------------+ +----------+
| | | | | | | |
| Dev |---->| GitLab |---->| GitLab |---->| Result |
| (git push)| | (creates | | Runner | | (pass/ |
| | | pipeline)| | (executes | | fail) |
+----------+ +-----------+ | jobs) | +----------+
| +------------+
| |
| .gitlab-ci.yml |
+------------------+- A developer makes code changes and runs git push
- GitLab finds the
.gitlab-ci.ymlfile in the repository and creates a pipeline - The Runner picks up the job and executes it inside a Docker container
- The result (success or failure) is displayed in the GitLab interface
Writing a Pipeline
Create a .gitlab-ci.yml file in the root of the repository. Below are two real-world examples — a simple one and a production-grade multi-stage pipeline:
Simple pipeline — build verification only:
# .gitlab-ci.yml — simple variant
stages:
- build
build:
stage: build
image: node:20
before_script:
- npm install -g pnpm
script:
- pnpm install
- pnpm next build
cache:
key: "$CI_COMMIT_REF_SLUG"
paths:
- node_modules/
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
tags:
- sharedProduction pipeline — build, test, Docker image creation, and deployment:
In real projects, there are usually several stages. For example, for a Node.js project:
# .gitlab-ci.yml — production variant
stages:
- install
- test
- build
- docker
- deploy
variables:
DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
DOCKER_IMAGE_LATEST: $CI_REGISTRY_IMAGE:latest
# Install dependencies — cached for subsequent stages
install:
stage: install
image: node:20
script:
- npm install -g pnpm
- pnpm install --frozen-lockfile
cache:
key: "$CI_COMMIT_REF_SLUG"
paths:
- node_modules/
tags:
- shared
# Run tests
test:
stage: test
image: node:20
script:
- npm install -g pnpm
- pnpm test
cache:
key: "$CI_COMMIT_REF_SLUG"
paths:
- node_modules/
policy: pull # Read-only (faster)
tags:
- shared
# Build the project
build:
stage: build
image: node:20
script:
- npm install -g pnpm
- pnpm build
artifacts:
paths:
- .next/ # Pass build output to the next stage
expire_in: 1 hour
cache:
key: "$CI_COMMIT_REF_SLUG"
paths:
- node_modules/
policy: pull
tags:
- shared
# Build Docker image and push to GitLab Container Registry
docker:
stage: docker
image: docker:24
services:
- docker:24-dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $DOCKER_IMAGE -t $DOCKER_IMAGE_LATEST .
- docker push $DOCKER_IMAGE
- docker push $DOCKER_IMAGE_LATEST
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
tags:
- shared
# Deploy to server (via SSH)
deploy:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache openssh-client
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | ssh-add -
- mkdir -p ~/.ssh
- echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts
script:
- ssh $DEPLOY_USER@$DEPLOY_HOST "
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY &&
docker pull $DOCKER_IMAGE_LATEST &&
docker compose -f /app/docker-compose.yml up -d
"
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: manual # Deploy only runs when manually approved
tags:
- sharedKey concepts in the production pipeline:
artifacts— passes build output to the next stage (e.g., the.next/directory to the docker stage)cache: policy: pull— in the test and build stages, the cache is read-only, which saves time$CI_REGISTRY— GitLab's built-in Container Registry. No separate Docker Hub account is neededwhen: manual— the deploy stage does not run automatically; an admin must manually approve it from the GitLab interface$SSH_PRIVATE_KEY,$DEPLOY_HOST— these variables are stored in Settings -> CI/CD -> Variables (not in the code!)
Add the file to the repository and push to the main branch — the pipeline will start automatically.
Navigate to the Pipelines section:
A successfully completed pipeline:
Click on it to view the details:
Here we wrote a simple single-stage build pipeline. GitLab CI supports tests, deployments, multi-stage pipelines, and much more. For details: CI/CD with GitLab CI (opens in a new tab)
Security and Backup
Installing a GitLab server is not enough — you also need to protect it and take measures to prevent data loss.
Firewall Configuration
Only open the necessary ports on the server, and close everything else:
sudo ufw allow OpenSSH # SSH (port 22) — for terminal access to the server
sudo ufw allow 80/tcp # HTTP — required for Let's Encrypt certificate issuance
sudo ufw allow 443/tcp # HTTPS — GitLab web interface
sudo ufw enable
sudo ufw statusBackup Configuration
In the real world, servers fail, disks break, and people make mistakes. Without backups, all your code, issues, and CI/CD configurations are lost. That is why backup is not something you do "when needed" — it is something you set up from day one.
What backups include:
- All Git repositories
- Database (users, issues, merge requests, CI/CD configurations)
- LFS objects and uploads
What backups do NOT include (must be backed up separately):
/etc/gitlab/gitlab.rb— server configuration/etc/gitlab/gitlab-secrets.json— encryption keys (restoring from backup is impossible without this file)
# Create a manual backup
sudo gitlab-backup create
# Backup files are stored here:
ls /var/opt/gitlab/backups/
# Result: 1710100800_2024_03_11_16.9.1_gitlab_backup.tarAutomated backup including configuration files:
sudo crontab -e# Full backup every day at 2:00 AM
0 2 * * * /opt/gitlab/bin/gitlab-backup create CRON=1
# Separate backup for configuration files (every day at 2:30 AM)
30 2 * * * cp /etc/gitlab/gitlab.rb /var/opt/gitlab/backups/gitlab.rb.$(date +\%F)
30 2 * * * cp /etc/gitlab/gitlab-secrets.json /var/opt/gitlab/backups/gitlab-secrets.json.$(date +\%F)Important: Storing backups on the same server is not enough! If the disk fails, the backup is lost too. In real production, copy backups to a separate location:
# For example, to another server via rsync
rsync -avz /var/opt/gitlab/backups/ backup-user@backup-server:/gitlab-backups/
# Or to S3-compatible storage (MinIO, AWS S3)
# In gitlab.rb:
gitlab_rails['backup_upload_connection'] = {
'provider' => 'AWS',
'region' => 'us-east-1',
'aws_access_key_id' => 'ACCESS_KEY',
'aws_secret_access_key' => 'SECRET_KEY',
'endpoint' => 'https://s3.example.com'
}
gitlab_rails['backup_upload_remote_directory'] = 'gitlab-backups'Restoring from backup:
# Stop GitLab services (keep database and cache running)
sudo gitlab-ctl stop puma
sudo gitlab-ctl stop sidekiq
# Restore from backup (specify the timestamp from the filename)
sudo gitlab-backup restore BACKUP=1710100800_2024_03_11_16.9.1
# Restore configuration files
sudo cp /backup/gitlab.rb /etc/gitlab/gitlab.rb
sudo cp /backup/gitlab-secrets.json /etc/gitlab/gitlab-secrets.json
# Reconfigure and restart
sudo gitlab-ctl reconfigure
sudo gitlab-ctl restartSecurity Checklist
Make sure to implement the following measures on your production server:
1. SSH keys — disable password authentication:
In the gitlab.rb file:
# Disable password-based git clone/push — SSH keys only
gitlab_rails['gitlab_shell_ssh_port'] = 22Users need to add their SSH public keys in User Settings -> SSH Keys. Then:
# Work with SSH keys instead of passwords
git clone git@gitlab.helm.uz:devops/project.git2. 2FA (Two-Factor Authentication) — make it mandatory:
From Admin Area -> Settings -> General -> Sign-in restrictions:
- Enable Two-factor authentication
- Set the Grace period to 3 days — users have 3 days to set up 2FA
3. Regular updates:
GitLab releases security patches every month. The update process in real production:
# Create a backup before updating!
sudo gitlab-backup create
# Update
sudo apt update && sudo apt upgrade gitlab-ee -y
sudo gitlab-ctl reconfigure
# Verify
sudo gitlab-ctl status
sudo gitlab-rake gitlab:check SANITIZE=true4. Monitoring — track server health:
Earlier, we enabled Prometheus and Grafana in gitlab.rb. Now you can access gitlab.helm.uz/-/grafana and monitor server health using built-in dashboards: CPU, RAM, disk, request count, pipeline statistics.
5. Rate limiting — brute-force protection:
# /etc/gitlab/gitlab.rb
# No more than 10 requests per minute to the login page
gitlab_rails['rate_limiting_response_text'] = 'Retry later'Viewing GitLab Logs
When something goes wrong, logs are your best friend. Each GitLab component writes its own logs:
sudo gitlab-ctl tail # All logs (real-time)
sudo gitlab-ctl tail puma # Web application logs
sudo gitlab-ctl tail nginx # Proxy logs
sudo gitlab-ctl tail sidekiq # Background job logs
sudo gitlab-ctl tail gitaly # Git operation logs
sudo gitlab-ctl tail postgresql # Database logsIf the issue isn't clear, run a general diagnostics check:
sudo gitlab-rake gitlab:check SANITIZE=trueThis command checks all GitLab components and reports any issues it finds.
Additional Resources
Additional Resources
- GitLab CI | Releases and Integrations (opens in a new tab)
- CI/CD with GitLab CI (opens in a new tab)
- GitHub Actions CI/CD (opens in a new tab)
- Installing Jenkins on Linux Servers (opens in a new tab)
- From Code to Server: Docker CI/CD with Jenkins (opens in a new tab)
- Kubernetes CI/CD | GitHub Actions + Argo CD | GitOps (opens in a new tab)
References used for this guide
- How To Install and Configure GitLab on Ubuntu (opens in a new tab)
- Install self-managed GitLab (opens in a new tab)
- GitLab Administration Documentation (opens in a new tab)
Date: 2024.06.13 (June 13, 2024)
Last updated: 2026.03.11 (March 11, 2026)
Author: Otabek Ismoilov
| Telegram (opens in a new tab) | GitHub (opens in a new tab) | LinkedIn (opens in a new tab) |
|---|