Virtualization

Self-host GitLab CE on a VPS in 2026, step by step

You have been burned by GitHub outages, or you need a private repo that does not sit on someone else's servers. Either way, self-hosting GitLab CE on your own VPS is the move. This guide walks you through a full install on Ubuntu 24.04 using Docker Compose v2, gets HTTPS working with a Let's Encrypt certificate, and tells you straight up how much RAM you actually need, because most tutorials lie about that part.

  • Key takeaway 1: GitLab CE is memory hungry. A 2 GB RAM VPS runs an empty instance, barely. Plan on 4 GB minimum for real use, 8 GB if you actually run pipelines.
  • Key takeaway 2: Docker Compose v2 (`docker compose`, not `docker-compose`) is the cleanest way to run and upgrade GitLab. One file, one command, done.
  • Key takeaway 3: Set `external_url` to your real domain before the first boot. Changing it later means reconfiguring and breaking runners.
  • Key takeaway 4: Back up the `config`, `logs`, and `data` volumes. GitLab's own backup task does not cover the whole container the way a volume backup does.

This guide assumes a fresh Ubuntu 24.04 VPS with a non-root sudo user and a domain name pointing at your server's IPv4 address. You also want Docker and Docker Compose installed. If you have not done that yet, install Docker Compose on a Linux VPS first, it takes five minutes.

Why run your own GitLab instead of using GitHub or GitLab.com

The pitch for self-hosting is not about saving a few dollars. It is about control. Your repos live on hardware you rent, under a Linux VPS you fully administer. No one else's terms of service decide what you can push. For a company with proprietary code, that is often a legal requirement, not a preference. For a solo developer, it means unlimited private repos with no per-seat pricing and no usage quotas.

GitLab CE gives you the full DevOps loop in one package: repository hosting, merge requests, issue tracking, a CI/CD runner system, and a container registry. You get all of this from a single Docker image, which is exactly why you should run it that way. A native Omnibus install works, but it spreads files across the system and makes upgrades and rollbacks messier.

自建 GitLab CE 需要 4GB 以上内存,建议用 Docker Compose 部署。

Self-hosting GitLab CE needs at least 4 GB of RAM, and deploying with Docker Compose is the cleanest path.

How much RAM does GitLab CE actually need

Read this section before you spend money on a plan, because it is the difference between a smooth setup and a thrashing, OOM-killing mess. GitLab CE on a fresh boot with zero projects uses roughly 1.5 to 2 GB of RAM. The bundled PostgreSQL, Redis, Gitaly, and the Rails app each hold a chunk of memory. That means a 2 GB RAM VPS is technically enough for a demo, and practically painful for anything else.

For a small team of up to five people with a handful of projects, take a 4 GB RAM VPS. For active CI/CD pipelines with several runners, go 8 GB. The runners execute jobs inside containers, and each concurrent job eats CPU and memory that GitLab itself is not using, so oversubscribing RAM is the fastest way to unlock the OOM killer at 3 AM.

The CPU side is more forgiving. Two vCPUs handle a small team comfortably. Start there, and scale up when pipeline wait times get annoying. Storage matters too: Git repositories are small, but the container registry grows. Give your VPS a roomy NVMe disk from the start, since resizing a registry volume later is tedious.

Step 1, set up the environment on Ubuntu 24.04

Start with a clean system and update everything. Then install Docker and the Compose plugin if they are missing.

sudo apt update && sudo apt upgrade -y
sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu noble stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Using the official Docker repository matters. The Ubuntu package of Docker is older and updates slower, and you want the current runtime for security fixes. The Compose plugin installs as `docker compose`, the v2 syntax, which is what every current tutorial and the GitLab docs assume.

Verify:

docker --version
docker compose version

You should see something like `Docker version 27.x` and `Docker Compose version v2.x`. If either errors, reboot and try again, the docker group sometimes needs a fresh session.

Add your user to the docker group so you do not need `sudo` for every command:

sudo usermod -aG docker $USER
newgrp docker

Step 2, prepare the domain and DNS records

GitLab needs a real hostname, not a bare IP. Using an IP works technically but breaks HTTPS, breaks the container registry, and produces a browser warning that scares off your team. Point an A record for `git.example.com` at your VPS's public IPv4 address.

dig +short git.example.com

Verify: the output is your server's IP. If it returns nothing or a different address, wait for DNS propagation or fix the record before continuing. Also make sure port 80 and 443 are reachable from the internet. If you run a firewall, open them now, or the Let's Encrypt challenge in a later step will fail:

sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw status

Do not open port 22 to the world if you already hardened SSH, keep that rule in place. If you have not locked down SSH yet, that is a separate task, and you should do it before exposing GitLab.

Step 3, write the docker-compose.yml for GitLab CE

Create a directory for GitLab and drop in a compose file. This is the whole deployment, and it is the file you will edit for every future upgrade.

mkdir -p /opt/gitlab
cd /opt/gitlab
nano docker-compose.yml

Paste this configuration:

services:
  gitlab:
    image: gitlab/gitlab-ce:latest
    container_name: gitlab
    restart: always
    hostname: git.example.com
    environment:
      GITLAB_OMNIBUS_CONFIG: |
        external_url 'https://git.example.com'
        letsencrypt['enable'] = true
        letsencrypt['contact_emails'] = ['[email protected]']
        gitlab_rails['gitlab_email_enabled'] = true
        puma['worker_processes'] = 2
    ports:
      - "80:80"
      - "443:443"
      - "22:22"
    volumes:
      - ./config:/etc/gitlab
      - ./logs:/var/log/gitlab
      - ./data:/var/opt/gitlab
    shm_size: "256m"

Three things in this file matter more than the rest:

  • `external_url` must be your real domain with `https://`. Change it after first boot and GitLab reconfigures everything, invalidates runners, and may confuse the Let's Encrypt flow.
  • The volume mounts keep all state in `/opt/gitlab`. Backing up this folder backs up your entire instance, config, repos, database, everything.
  • `shm_size: "256m"` prevents PostgreSQL crashes under memory pressure. The default 64 MB shared memory inside the container is a known GitLab pain point; setting 256 MB fixes random database restarts.

Mapping port 22 on the host to GitLab's SSH assumes nothing else uses SSH on the host. If you changed the host SSH port as recommended, adjust the left side, for example `2222:22`, and tell users to use `ssh -p 2222`. Keep it consistent, because GitLab displays the SSH clone URL based on this mapping.

Step 4, start GitLab and wait for the first boot

Pull the image and start the stack:

docker compose up -d

First boot is slow. The container runs an initial reconfigure that initializes the database, compiles assets, and sets up the internal services. On a 4 GB VPS this takes three to five minutes. Do not interrupt it, do not restart the container, just wait and monitor the logs:

docker compose logs -f gitlab

Verify readiness by watching for the line where GitLab says it is ready, or poll the health endpoint:

docker exec gitlab grep 'GitLab is fully configured' /var/log/gitlab/reconfigure || true
curl -I https://git.example.com/users/sign_in

Once the login page answers with HTTP 200, the instance is up. The root password is generated and stored in the container on first boot. Grab it before you log in:

docker exec gitlab cat /etc/gitlab/initial_root_password

Verify: the file prints a random 24-character password. Log in as `root` with it and change the password immediately, this file auto-deletes after 24 hours anyway.

Step 5, configure HTTPS with Let's Encrypt

If you set `letsencrypt['enable'] = true` in the compose file, the reconfigure step already issued a certificate. Check that it worked before assuming anything:

docker exec gitlab ls /etc/gitlab/ssl/

Verify: you see a file named `git.example.com.pem` and its key. If the directory is empty, the automated challenge failed, usually because port 80 was closed or DNS was not pointing at this server yet. Re-run the issuance manually:

docker exec gitlab gitlab-ctl reconfigure

Watch the output for the Let's Encrypt section. If it still fails, check the renewal log for the specific error:

docker exec gitlab tail -50 /var/log/gitlab/lets-encrypt/lets-encrypt.log

Renewal is automatic. The omnibus config renews certificates 30 days before expiry and GitLab restarts its nginx process to pick up the new certificate. You do not need a cron job when the container keeps running.

Step 6, create a project and test the whole loop

Do a real end-to-end test now, not just a login. Create a new project in the web UI, then clone, push, and run a pipeline if you have a runner configured.

git clone http://git.example.com/root/test.git
cd test
echo "# test" > README.md
git add README.md
git commit -m "first commit"
git push origin main

Verify: the push succeeds and the project page shows the README. Then check the SSH path works too, so your team is not stuck using HTTPS with a password every time:

git remote set-url origin [email protected]:root/test.git
git push origin main

If SSH push fails, test the raw SSH connection:

ssh -T [email protected]

A successful response says Welcome to GitLab, @root!. If it times out, check that port 22 is forwarded correctly in the compose file and that the host firewall allows it.

Step 7, back up GitLab properly

The container keeps everything in the three volumes under `/opt/gitlab`. Backing up the entire directory with a tool like `restic` or `rsync` to a second location is the simplest reliable strategy. GitLab's built-in backup task exports a tar of the database and repositories, which is useful, but it does not include the config volume, and restoring a broken instance from scratch needs that config.

A simple offsite copy for a small instance:

sudo rsync -a --delete /opt/gitlab/ backupuser@backup-host:/backups/gitlab/

For a better schedule, use a cron job or a systemd timer running this nightly. Restoring means installing Docker and Compose on a fresh VPS, copying the directory back, and running `docker compose up -d`. Test this once before you need it. A backup you have never restored is a guess, not a backup.

Troubleshooting common GitLab on Docker failures

Your first boot will hit at least one of these. Here is the fix for each.

Case 1, 502 Bad Gateway after boot. GitLab is still reconfiguring or the puma workers are starting. Wait a few minutes, then check:

docker compose logs gitlab | tail -100

If you see puma or database errors, the reconfigure did not complete. Restart the container and let it finish. Do not edit config while it is mid-boot.

Case 2, the OOM killer kills the container. You ran this on a 2 GB VPS. Check the kernel log:

dmesg | grep -i oom | tail -20

If you see GitLab processes there, resize to a 4 GB RAM VPS. There is no configuration trick that makes GitLab run well on 2 GB, and adding swap just makes it slow instead of dead.

Case 3, Let's Encrypt renewal fails after months of working. Usually the domain's A record changed, or the firewall silently blocks port 80. Run the renew manually to see the exact error:

docker exec gitlab /opt/gitlab/embedded/bin/certbot renew --force-renewal

Read the error, fix the DNS or firewall, then run a reconfigure.

FAQ

What is the minimum RAM for GitLab CE in 2026?

2 GB runs an empty demo instance but triggers OOM kills under real use. Use a 4 GB RAM VPS for a small team and 8 GB if you run CI/CD pipelines. Memory is the single biggest factor in GitLab stability, more than CPU count.

Should I run GitLab with the Omnibus package or Docker Compose?

Docker Compose, in 2026. It keeps all state in three volumes inside one directory, makes upgrades a single `docker compose pull && docker compose up -d`, and rollbacks are trivial. The Omnibus package works but scatters files across the OS and complicates rebuilds.

Can I self-host GitLab on a 2 GB RAM VPS?

You can boot it, but do not run it. The bundled PostgreSQL and Redis alone consume most of 2 GB. Add a merge request or a pipeline and the kernel starts killing processes. Buy a bigger VPS or use a lighter solution for tiny workloads.

How do I change the GitLab domain after the first boot?

Edit `external_url` in the compose file, then run `docker compose exec gitlab gitlab-ctl reconfigure`. Be aware that runners and any hardcoded clone URLs keep the old domain until you update them, so set the domain correctly from the start.

Does GitLab CE include a CI/CD runner?

No. GitLab CE includes the CI/CD coordination system, but the runner itself is a separate component you install on another machine or as a container. Register it against your instance's URL and registration token to start running pipelines.

Related articles

VPS 自建 GitLab CE 要点

在 Ubuntu 24.04 上用 Docker Compose 部署 GitLab CE,配置 external_url 指向真实域名并启用 Let's Encrypt 即可获得 HTTPS。内存至少 4GB,否则数据库进程会被内核杀死。所有数据保存在 config、logs、data 三个卷中,定期备份整个目录即可。首次启动后立即从 initial_root_password 文件获取 root 密码并修改。

Note: This guide is for general reference. Every system and infrastructure has its own specifics, so test each step in a safe environment and consult a qualified engineer before applying it in production.