Running your own GitLab instance gives you full control over your source code, CI/CD pipelines, container registry, and issue tracking without relying on a third-party SaaS platform. GitLab Community Edition (CE) is the open-source, self-hosted version of GitLab that packs most of the features teams actually need in production. You get Git repository management, merge requests, a built-in Docker container registry, wiki pages, and a full CI/CD engine with runners you own and operate.

This guide walks through a complete GitLab CE deployment on Ubuntu 24.04 LTS (Noble Numbat) or Debian 13 (Trixie), with automated Let’s Encrypt TLS certificates, a CI/CD runner, the container registry, backup procedures, and production hardening. Every step has been tested on fresh minimal installs of both distributions.

Prerequisites

Before you start, make sure you have the following in place:

  • A server running Ubuntu 24.04 LTS or Debian 13 with root or sudo access.
  • At least 4 GB of RAM (8 GB recommended for teams larger than 10 users). GitLab runs Puma, Sidekiq, PostgreSQL, and Redis all on one box, so memory matters.
  • A minimum of 2 CPU cores. Build jobs and background workers will compete for CPU time on anything less.
  • A registered domain name (for example, gitlab.example.com) with a DNS A record pointing to your server’s public IP. Let’s Encrypt needs this to issue a certificate.
  • Ports 80 and 443 open in your firewall. The ACME HTTP-01 challenge requires port 80 during certificate issuance.
  • A working mail relay or an SMTP service for sending notification emails. We will configure Postfix as a local send-only MTA in this guide, but you can swap in any SMTP provider you prefer.

If you are provisioning a cloud VM, a 2-vCPU / 8 GB instance on any major provider will handle a small-to-medium team without breaking a sweat. For guidance on setting up your base system, see our guide on how to install Ubuntu 24.04 LTS Server.

Step 1: Update the System and Install Dependencies

Start with a fully updated system. Then install the packages GitLab expects to find on the host: an SSH server for Git-over-SSH operations, CA certificates for HTTPS, curl for the repository setup script, and Postfix for outbound mail.

sudo apt update && sudo apt upgrade -y

Now install the required dependencies:

sudo apt install -y curl openssh-server ca-certificates postfix perl tzdata

When the Postfix configuration dialog appears, select Internet Site and accept the default system mail name (your server’s FQDN). If you plan to use an external SMTP relay like SendGrid or Mailgun, you can reconfigure Postfix later or point GitLab directly at the relay in gitlab.rb.

Verify that the SSH server is running:

sudo systemctl enable --now ssh
sudo systemctl status ssh

If you are running ufw, open the ports GitLab needs:

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

For a deeper look at firewall configuration, check out our article on setting up UFW firewall on Ubuntu.

Step 2: Add the GitLab CE Repository

GitLab provides an official shell script that configures the Omnibus package repository for your distribution. This script detects whether you are running Ubuntu or Debian and adds the correct APT source.

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

After the script finishes, confirm the repository was added:

apt-cache policy gitlab-ce | head -10

You should see candidate versions listed from packages.gitlab.com. If the command returns nothing, double-check that curl completed without errors and re-run the repository script.

Step 3: Install GitLab CE

Set the EXTERNAL_URL environment variable to your HTTPS domain before installing. The Omnibus package reads this variable during initial configuration and writes it into /etc/gitlab/gitlab.rb. Using an https:// URL tells GitLab to automatically provision a Let’s Encrypt certificate.

sudo EXTERNAL_URL="https://gitlab.example.com" apt install -y gitlab-ce

This single command installs GitLab and all of its bundled components (Nginx, PostgreSQL, Redis, Puma, Sidekiq, Gitaly, and more), writes the initial configuration, requests a Let’s Encrypt certificate, and starts every service. On a typical cloud VM the process takes between 3 and 8 minutes.

If the Let’s Encrypt challenge fails during installation, do not panic. We will fix the configuration in the next step and re-run the reconfigure command. The most common cause is a DNS record that has not propagated yet.

Step 4: Configure gitlab.rb for HTTPS and Let’s Encrypt

The main configuration file lives at /etc/gitlab/gitlab.rb. Open it in your preferred editor:

sudo vim /etc/gitlab/gitlab.rb

Verify or set the following directives. If you passed EXTERNAL_URL during installation, the first line should already be present.

## URL
external_url 'https://gitlab.example.com'

## Let's Encrypt
letsencrypt['enable'] = true
letsencrypt['contact_emails'] = ['[email protected]']
letsencrypt['auto_renew'] = true
letsencrypt['auto_renew_hour'] = 3
letsencrypt['auto_renew_minute'] = 30
letsencrypt['auto_renew_day_of_month'] = "*/4"

## Redirect HTTP to HTTPS
nginx['redirect_http_to_https'] = true

## Time zone (adjust to your location)
gitlab_rails['time_zone'] = 'UTC'

The auto_renew settings create a cron job that renews the certificate every four days at 03:30. Let’s Encrypt certificates are valid for 90 days, so this gives you plenty of renewal attempts before expiration.

Save the file and apply the configuration:

sudo gitlab-ctl reconfigure

This command compiles your settings, regenerates Nginx configs, restarts services, and (if needed) re-runs the Let’s Encrypt ACME challenge. Watch the output for any errors related to certificate issuance. A successful run ends with gitlab Reconfigured!.

Step 5: Retrieve the Initial Root Password

During installation, GitLab generates a random password for the root account and stores it in a temporary file. Retrieve it with:

sudo cat /etc/gitlab/initial_root_password

Copy the password value. This file is automatically deleted after the first gitlab-ctl reconfigure that runs 24 hours post-install, so grab it now. You will use it to log in for the first time.

Step 6: Access the Web UI and Change the Root Password

Open your browser and navigate to https://gitlab.example.com. You should see the GitLab login page served over a valid Let’s Encrypt certificate.

Log in with:

  • Username: root
  • Password: the value from /etc/gitlab/initial_root_password

Immediately change the root password. Navigate to Edit Profile (click the avatar in the top-left corner, then Edit profile), and select Password in the left sidebar. Set a strong password and save.

While you are in the admin area, consider disabling open sign-up if this is a private instance:

  1. Go to Admin Area (the wrench icon in the left sidebar).
  2. Select Settings > General.
  3. Expand Sign-up restrictions.
  4. Uncheck Sign-up enabled.
  5. Click Save changes.

Step 7: Create Your First Project and Push Code

With GitLab running and secured, it is time to create a project and push your first commit.

In the web UI, click New project, then Create blank project. Give it a name (for example, webapp), set the visibility to Private or Internal depending on your needs, and click Create project.

On your local workstation, clone the repo and push an initial commit:

git clone https://gitlab.example.com/root/webapp.git
cd webapp
echo "# My Project" > README.md
git add .
git commit -m "Initial commit"
git push -u origin main

If you prefer SSH access, add your public key in Edit Profile > SSH Keys first. Then clone with the SSH URL instead. For teams, SSH keys are the standard approach since they remove the need for personal access tokens on every push.

Step 8: Install and Register a GitLab CI/CD Runner

GitLab CI/CD pipelines need a runner to execute jobs. You can install a runner on the same server (for small teams) or on a dedicated build machine. Here we install it on the GitLab host.

Add the GitLab Runner repository:

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

Install the runner package:

sudo apt install -y gitlab-runner

Now register the runner with your GitLab instance. You will need a registration token. In the web UI, go to Admin Area > CI/CD > Runners and click New instance runner. Copy the token, then run:

sudo gitlab-runner register \
  --non-interactive \
  --url "https://gitlab.example.com" \
  --token "YOUR_RUNNER_TOKEN" \
  --executor "docker" \
  --docker-image "alpine:latest" \
  --description "docker-runner"

This registers a runner that uses the Docker executor with Alpine Linux as the default image. If Docker is not installed yet, install it first:

sudo apt install -y docker.io
sudo systemctl enable --now docker
sudo usermod -aG docker gitlab-runner

Verify the runner is online:

sudo gitlab-runner list

You should see your runner listed with a status of alive. It will also appear as green (online) in the Admin Area under CI/CD > Runners. For production Docker setups, have a look at our guide on installing Docker on Ubuntu.

To test the runner, create a .gitlab-ci.yml file in your project root:

stages:
  - test

hello-job:
  stage: test
  script:
    - echo "Pipeline is working"
    - uname -a

Commit and push this file. Go to CI/CD > Pipelines in your project and watch the job execute. A green checkmark means your runner is properly connected and ready for real workloads.

Step 9: Configure the Container Registry

GitLab ships with a built-in Docker container registry. This lets you build, store, and deploy Docker images directly from your GitLab projects without running a separate registry like Harbor or Docker Hub.

Open the configuration file:

sudo vim /etc/gitlab/gitlab.rb

Add or uncomment the following lines:

registry_external_url 'https://registry.example.com'
gitlab_rails['registry_enabled'] = true

## Use the same Let's Encrypt mechanism
registry_nginx['ssl_certificate'] = "/etc/letsencrypt/live/registry.example.com/fullchain.pem"
registry_nginx['ssl_certificate_key'] = "/etc/letsencrypt/live/registry.example.com/privkey.pem"

If you want GitLab’s built-in Let’s Encrypt to handle the registry certificate as well (using the same domain or a subdomain), you can instead use the GitLab-managed certificate path. For a subdomain like registry.example.com, create a DNS A record pointing to the same server, then set:

registry_external_url 'https://registry.example.com'
registry_nginx['redirect_http_to_https'] = true
letsencrypt['enable'] = true

Apply the changes:

sudo gitlab-ctl reconfigure

Test the registry by logging in from a machine with Docker installed:

docker login registry.example.com

Enter your GitLab username and a personal access token with read_registry and write_registry scopes. After a successful login, try pushing an image:

docker pull alpine:latest
docker tag alpine:latest registry.example.com/root/webapp/alpine:latest
docker push registry.example.com/root/webapp/alpine:latest

The image will appear in your project under Packages and registries > Container Registry.

Step 10: Backup and Restore GitLab

Backups are not optional in production. GitLab includes a built-in backup utility that dumps the database, repositories, uploads, and CI/CD artifacts into a single tar file.

Run a manual backup:

sudo gitlab-backup create

Backup files are stored in /var/opt/gitlab/backups/ by default. The filename includes a timestamp, for example 1710835200_2026_03_19_17.9.0_gitlab_backup.tar.

You also need to back up two configuration files that are not included in the backup tar:

sudo cp /etc/gitlab/gitlab.rb /var/opt/gitlab/backups/
sudo cp /etc/gitlab/gitlab-secrets.json /var/opt/gitlab/backups/

Without gitlab-secrets.json, you will not be able to decrypt CI/CD variables, two-factor authentication secrets, or encrypted database columns after a restore. Treat this file with the same care you give to private keys.

To automate daily backups, add a cron job:

sudo crontab -e

Add the following line to run a backup every day at 2:00 AM and keep only the last 7 days of backups:

0 2 * * * /opt/gitlab/bin/gitlab-backup create CRON=1 BACKUP=daily 2>&1 | logger -t gitlab-backup

Set the retention policy in gitlab.rb:

gitlab_rails['backup_keep_time'] = 604800

Run sudo gitlab-ctl reconfigure after changing this value.

To restore from a backup on a fresh GitLab install of the same version, first stop the database-connected services:

sudo gitlab-ctl stop puma
sudo gitlab-ctl stop sidekiq
sudo gitlab-ctl status

Then run the restore command, replacing the timestamp with your backup file’s prefix:

sudo gitlab-backup restore BACKUP=1710835200_2026_03_19_17.9.0

After the restore completes, copy your backed-up gitlab.rb and gitlab-secrets.json back to /etc/gitlab/, then reconfigure and restart:

sudo gitlab-ctl reconfigure
sudo gitlab-ctl restart

Verify everything is working by running sudo gitlab-rake gitlab:check SANITIZE=true.

Step 11: Upgrade GitLab CE

GitLab releases new versions monthly. Keeping your instance up to date is important for security patches and bug fixes. The upgrade process through APT is straightforward, but always take a backup first.

sudo gitlab-backup create

Then update the package:

sudo apt update
sudo apt install -y gitlab-ce

The Omnibus package automatically runs gitlab-ctl reconfigure as part of the post-install process. It handles database migrations, service restarts, and asset compilation.

If you need to upgrade across multiple major versions (for example, from 16.x to 17.x), do not jump directly. GitLab requires you to follow a specific upgrade path and stop at certain intermediate versions. Check the official upgrade documentation before proceeding.

To pin a specific version instead of always getting the latest:

sudo apt install -y gitlab-ce=17.9.0-ce.0

After the upgrade, confirm the running version:

sudo gitlab-rake gitlab:env:info

Troubleshooting Common Issues

502 Errors After Installation or Reconfigure

A 502 error from Nginx usually means Puma (the application server) has not finished starting. GitLab can take 2 to 5 minutes to fully boot, especially on servers with limited RAM. Wait a few minutes and refresh.

If the 502 persists, check the Puma logs:

sudo gitlab-ctl tail puma

Common root causes include:

  • Insufficient memory. Run free -h and check available RAM. If the server has less than 4 GB total, add swap space or upgrade the instance.
  • Port conflict. Another web server (Apache, a test Nginx instance) is listening on port 80 or 443. Stop the conflicting service or reconfigure it to use a different port.
  • Puma socket issue. Run sudo gitlab-ctl restart puma and wait 60 seconds before testing again.

You can also check overall service health:

sudo gitlab-ctl status

Every service should show run with a PID. If any service shows down, restart it individually with sudo gitlab-ctl restart <service_name>.

Let’s Encrypt Certificate Failures

If the ACME challenge fails during gitlab-ctl reconfigure, verify these items:

  • Your DNS A record resolves to the correct public IP: dig +short gitlab.example.com
  • Port 80 is reachable from the internet. Let’s Encrypt always validates over HTTP on port 80, even if you want HTTPS on 443.
  • No other process is binding port 80. Run sudo ss -tlnp | grep :80 to check.
  • Rate limits have not been exceeded. Let’s Encrypt allows 5 duplicate certificates per week per domain. If you have been testing heavily, wait or use the staging environment.

To manually trigger a certificate renewal:

sudo gitlab-ctl renew-le-certs

Runner Not Connecting to GitLab

If your runner registers successfully but jobs stay in “pending” state:

  • Tag mismatch. If you assigned tags during registration, the .gitlab-ci.yml jobs must reference the same tags. Alternatively, enable “Run untagged jobs” in the runner’s settings.
  • Runner is paused. Check the runner status in Admin Area. Click the pencil icon and make sure the runner is not paused.
  • Docker socket permission. If using the Docker executor, the gitlab-runner user needs access to /var/run/docker.sock. Confirm with sudo -u gitlab-runner docker ps.
  • Self-signed cert / TLS issue. If you changed certificates after runner registration, the runner may have cached the old CA bundle. Re-register or update /etc/gitlab-runner/certs/.

Check the runner logs for detailed error output:

sudo journalctl -u gitlab-runner -f

High Memory Usage

GitLab is memory-hungry by design. On a 4 GB server, it is normal to see 3 to 3.5 GB in use. If you are hitting swap constantly or getting OOM kills, apply these tuning options in /etc/gitlab/gitlab.rb:

## Reduce Puma workers (default is auto-detected based on CPU cores)
puma['worker_processes'] = 2

## Reduce Sidekiq concurrency
sidekiq['concurrency'] = 10

## Reduce monitoring overhead if not needed
prometheus_monitoring['enable'] = false
alertmanager['enable'] = false
grafana['enable'] = false

Apply with sudo gitlab-ctl reconfigure. Disabling Prometheus and Grafana alone can free 300 to 500 MB of RAM.

If you still need more headroom, add swap space as a safety net:

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

Swap is not a replacement for actual RAM, but it prevents hard OOM kills during memory spikes caused by large Git operations or CI/CD bursts.

Post-Installation Hardening Tips

A few additional steps to lock down your GitLab instance for production use:

  • Disable user registration unless you explicitly need public sign-ups (covered in Step 6).
  • Enforce two-factor authentication (2FA) under Admin Area > Settings > General > Sign-in restrictions.
  • Set a session timeout to reduce the window for session hijacking. A 60-minute timeout is a reasonable starting point for most teams.
  • Restrict project visibility defaults to Internal or Private. Public projects on a self-hosted instance are rarely what you want.
  • Enable audit logging and review it periodically. GitLab CE provides basic audit events under Admin Area > Monitoring > Audit Events.
  • Set up external object storage (S3-compatible) for uploads, artifacts, and LFS objects if your repository sizes are growing. This prevents the local disk from filling up.

For additional server security hardening, take a look at our guide on Linux server hardening best practices.

Conclusion

You now have a fully functional GitLab CE instance running on Ubuntu 24.04 or Debian 13 with Let’s Encrypt TLS, a registered CI/CD runner, container registry, and automated backups. This setup is production-ready for small to medium teams and can be scaled further by adding dedicated runner machines, moving PostgreSQL to an external database, or fronting GitLab with a load balancer.

The Omnibus package handles most of the operational complexity for you, but treat /etc/gitlab/gitlab.rb and /etc/gitlab/gitlab-secrets.json as critical infrastructure files. Back them up, version-control them (with secrets encrypted), and test your restore procedure at least once before you actually need it.

If you run into issues not covered here, the sudo gitlab-ctl tail command is your best friend. It tails all GitLab service logs simultaneously and almost always points directly at the problem.

2 COMMENTS

LEAVE A REPLY

Please enter your comment!
Please enter your name here