Skip to content
Documentation
Gitlab Server Setup

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)
CostFree, open-sourceFree (core) + paid plans
CI/CDFully functionalAdditional: merge trains, multi-project pipelines
SecurityBasic featuresSAST, DAST, dependency scanning
SupportCommunity onlyOfficial technical support
Best forSmall teams, personal projectsLarge 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:

MethodDescriptionWhen to use
Linux package (Omnibus)Install directly on a VM or bare-metal serverMost common, recommended for production
DockerRun inside a Docker containerQuick testing, small teams
Kubernetes (Helm chart)Deploy to a K8s clusterLarge organizations, when auto-scaling is needed
From sourceCompile the code yourselfSpecial 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

HostOSRAMCPUStorageStatic IP
gitlabUbuntu 20.04+8GB4 vCPU, 2 core80GBYes, 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 perl

2-> Install Postfix for email notifications. GitLab uses a mail server to send notifications to users.

sudo apt install -y postfix

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

Add the GitLab CE repository and install it:

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

Set EXTERNAL_URL to your own domain:

sudo EXTERNAL_URL="https://gitlab.helm.uz" apt install gitlab-ce

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

After 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.rb

Find 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 reconfigure

4. 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 reconfigure

5. 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 start

3-> 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-xxxxxxxxxxxxxxxxxxxx

During 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 run

Click 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 = 0

About 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-runner

Restart to apply the changes:

sudo gitlab-runner restart

Your 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 |
                      +------------------+
  1. A developer makes code changes and runs git push
  2. GitLab finds the .gitlab-ci.yml file in the repository and creates a pipeline
  3. The Runner picks up the job and executes it inside a Docker container
  4. 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:
    - shared

Production 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:
    - shared

Key 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 needed
  • when: 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 status

Backup 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.tar

Automated 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 restart

Security 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'] = 22

Users 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.git

2. 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=true

4. 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 logs

If the issue isn't clear, run a general diagnostics check:

sudo gitlab-rake gitlab:check SANITIZE=true

This command checks all GitLab components and reports any issues it finds.

Additional Resources