I self-hosted GitLab CE on Ubuntu 24.04 — the 502 lasted until Omnibus finished

I self-hosted GitLab CE on Ubuntu 24.04 — the 502 lasted until Omnibus finished

I wanted Git, merge requests, and CI on one box I control. GitLab CE returned 502 for minutes while Omnibus configured; I almost killed the container. After I waited, grabbed initial_root_password, and mapped SSH to 2222, the UI came up.

· Updated · 4 min read #self-hosted #open-source #deployment #docker #vps #git #ci-cd #devops #gitlab

 Self-hosted GitLab CE DevOps platform

Caption: GitLab CE Omnibus on my VPS: Git, MRs, CI, registry — heavy, but one product.

Why I wanted this on my server

Gitea is enough for small repos. I wanted merge trains, CI, and a container registry without five SaaS bills. GitLab CE is the all-in-one: source, MRs, issues, pipelines, registry. Unlike a lightweight forge, it aims to cover commit-to-deploy. It is also a RAM hog. I only did this on a box that could take it. Agencies and serious homelabs outgrow bare remotes once they need protected branches, runners, and registry images.

Treat it as critical. Incomplete /etc/gitlab backups make restores useless. Untrusted CI on the same Docker host as the data volumes is a bad idea. A compromised root account or a leaked runner token exposes private repositories.

What I actually installed

Ubuntu 24.04 LTS, gitlab/gitlab-ce:18.11.11-ce.0, Compose, volumes under /srv/gitlab (GITLAB_HOME), external_url 'https://gitlab.example.com', HTTP/HTTPS published, Git SSH on host 2222. Official docs often show EE tags; CE uses ce instead of ee.

Hardware: GitLab wants about 8 vCPU / 16 GB for a comfortable node. Floor for a constrained lab: at least 8 GB RAM (slower UI, Sidekiq lag, longer first boot). 4 GB is not a CE host. 40 GB+ SSD for packages, repositories, artifacts, registry layers, and logs — more if you store many images. Off-server capacity for Omnibus backups plus /etc/gitlab secrets. Runners on a separate machine when CI is more than a toy.

Security I would not skip: change the initial root password within 24 hours, pin gitlab/gitlab-ce:<version>-ce.0 instead of latest, keep gitlab-secrets.json out of public Git, open only admin SSH, HTTP, HTTPS, and the Git SSH port I chose.

Where it broke

On a fresh Ubuntu 24.04 box this install is famous for 502 / blank page for several minutes after docker compose up -d.

I thought the container was broken and almost compose down. First boot is Omnibus configuring NGINX, Puma, Sidekiq, Postgres, Redis, Gitaly, migrations. The fix is wait:

docker compose logs -f gitlab

Leave it until NGINX is serving. Ctrl+C only stops the log follow.

Second: /etc/gitlab/initial_root_password expires after 24 hours. I grabbed it immediately:

docker exec -it gitlab grep 'Password:' /etc/gitlab/initial_root_password

Miss that window and you need GitLab’s Rails console password reset. Change root before you do anything else.

Third: restore without gitlab-secrets.json. gitlab-backup create is not enough. Copy $GITLAB_HOME/config with the archive or the restore looks encrypted/wrong.

SSH clones hanging: gitlab_rails['gitlab_shell_ssh_port'] = 2222 must match the published host port and UFW.

Other documented issues: HTTPS certificate failures — DNS must already resolve; check Omnibus NGINX logs under $GITLAB_HOME/logs and ports 80/443 from the internet. Container thrashing — memory below GitLab’s practical floor; stop other heavy services. CI jobs stuck pending — no runner, tag mismatch, or the runner host cannot reach the GitLab URL. Disk filling — artifacts, registry layers, and logs under $GITLAB_HOME/data; enable retention and prune old tags. Do not skip required intermediate versions on major upgrades. Pin tags so a reboot cannot float an unexpected release.

If port 443 must be shared, bind GitLab HTTP to localhost and proxy with Caddy: nginx['listen_https'] = false, keep external_url on https://…, publish SSH separately, and keep X-Forwarded-Proto consistent with GitLab’s reverse-proxy guidance.

 GitLab CE Docker Compose topology

Caption: One CE container, host mounts for config/logs/data, HTTP, HTTPS, Git SSH.

The working install

sudo apt update
sudo apt upgrade -y
sudo apt install -y ca-certificates curl gnupg openssl ufw rsync

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# If host SSH stays on 22 and GitLab SSH will use 2222:
sudo ufw allow 2222/tcp
sudo ufw enable
sudo ufw status

This guide keeps host SSH on 22 and maps GitLab Shell to 2222. If you want clone URLs without a port, move host OpenSSH first, as GitLab’s Docker docs describe.

1. Install Docker Engine

sudo apt-get update -qqy
sudo apt-get install ca-certificates curl -qqy
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt-get update -qqy
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin -qqy

sudo usermod -aG docker "$USER"
newgrp docker

docker --version
docker compose version

2. Create GITLAB_HOME volumes

sudo mkdir -p /srv/gitlab
sudo chown "$USER":"$USER" /srv/gitlab

export GITLAB_HOME=/srv/gitlab
echo 'export GITLAB_HOME=/srv/gitlab' >> ~/.bashrc

mkdir -p "$GITLAB_HOME"/{config,logs,data}
Local path Container path Purpose
$GITLAB_HOME/config /etc/gitlab Omnibus config and secrets
$GITLAB_HOME/logs /var/log/gitlab Service logs
$GITLAB_HOME/data /var/opt/gitlab Application data, Git storage, backups

3. Write docker-compose.yml

mkdir -p /opt/gitlab-ce
cd /opt/gitlab-ce

cat > docker-compose.yml <<'EOF'
services:
  gitlab:
    image: gitlab/gitlab-ce:18.11.11-ce.0
    container_name: gitlab
    restart: always
    hostname: 'gitlab.example.com'
    environment:
      GITLAB_OMNIBUS_CONFIG: |
        external_url 'https://gitlab.example.com'
        gitlab_rails['gitlab_shell_ssh_port'] = 2222
        # Optional SMTP — replace before relying on mail:
        # gitlab_rails['smtp_enable'] = true
        # gitlab_rails['smtp_address'] = "smtp.example.com"
        # gitlab_rails['smtp_port'] = 587
        # gitlab_rails['smtp_user_name'] = "gitlab@example.com"
        # gitlab_rails['smtp_password'] = "replace-me"
        # gitlab_rails['smtp_domain'] = "example.com"
        # gitlab_rails['smtp_authentication'] = "login"
        # gitlab_rails['smtp_enable_starttls_auto'] = true
        # gitlab_rails['gitlab_email_from'] = "gitlab@example.com"
    ports:
      - '80:80'
      - '443:443'
      - '2222:22'
    volumes:
      - '$GITLAB_HOME/config:/etc/gitlab'
      - '$GITLAB_HOME/logs:/var/log/gitlab'
      - '$GITLAB_HOME/data:/var/opt/gitlab'
    shm_size: '256m'
EOF

chmod 600 docker-compose.yml

DNS should already point here so Omnibus TLS can succeed. If a reverse proxy owns 443, set nginx['listen_https'] = false, publish loopback HTTP, keep external_url on https://….

4. Start GitLab and wait for first boot

cd /opt/gitlab-ce
export GITLAB_HOME=/srv/gitlab
docker compose up -d
docker compose logs -f gitlab
docker compose ps
curl -I https://gitlab.example.com

5. Retrieve the initial root password

docker exec -it gitlab grep 'Password:' /etc/gitlab/initial_root_password

Sign in as root, set a new password immediately.

Configuration

After changing GITLAB_OMNIBUS_CONFIG, recreate:

cd /opt/gitlab-ce
docker compose up -d

 HTTPS path options for GitLab CE

Caption: Omnibus NGINX terminates TLS, or Caddy in front with external_url still HTTPS.

Clone URLs: ssh://git@gitlab.example.com:2222/group/project.git.

Admin Area → Settings → General: disable public sign-up, restrict visibility, require confirmed email once SMTP works.

Helpful to know

gitlab-secrets.json in /etc/gitlab. People back up the backup tarball and skip config. Restore then looks like a successful command that yields garbage. I would rather copy config twice than debug that.

Also: do not skip required intermediate versions on major upgrades. Pin tags so a reboot cannot float latest.

Usage

  1. root password + 2FA.
  2. Your user, group, private project, SSH key.
  3. Push, open an MR.
  4. HTTPS clone and SSH clone on 2222.

Runner on a separate host:

# On the runner host
docker run -d --name gitlab-runner --restart always \
  -v /srv/gitlab-runner/config:/etc/gitlab-runner \
  -v /var/run/docker.sock:/var/run/docker.sock \
  gitlab/gitlab-runner:alpine

docker exec -it gitlab-runner gitlab-runner register

Token from Admin Area → CI/CD → Runners.

stages: [test]

hello:
  stage: test
  image: alpine:3.20
  script:
    - echo "GitLab CE runner is working"

 GitLab CE and Runner topology

Caption: Pipelines on GitLab; jobs on an isolated runner host.

Jobs stuck pending: no runner, tag mismatch, or the runner cannot reach the GitLab URL.

 GitLab CE backup and upgrade workflow

Caption: gitlab-backup create plus /etc/gitlab, off-server, restore drill.

Backup, expose, next step

# Create a backup inside the container (writes under /var/opt/gitlab/backups)
docker exec -t gitlab gitlab-backup create

# Copy backups and critical config/secrets off-box
sudo mkdir -p /var/backups/gitlab-ce
sudo rsync -a /srv/gitlab/data/backups/ /var/backups/gitlab-ce/data-backups/
sudo rsync -a /srv/gitlab/config/ /var/backups/gitlab-ce/config/
rsync -a /var/backups/gitlab-ce/ backup-user@backup.example.net:/srv/backups/gitlab-ce/
cd /opt/gitlab-ce
docker exec -t gitlab gitlab-backup create
# edit image: gitlab/gitlab-ce:<next>-ce.0
docker compose pull
docker compose up -d
docker compose logs -f gitlab

What I have running now: GitLab CE 18.11.11 behind HTTPS, SSH on 2222, root password rotated, volumes under /srv/gitlab. First boot took minutes; I did not kill it. Next: SMTP, disable sign-up, 2FA on admin, a runner on another VM, a restore rehearsal that clones a private project and re-runs a pipeline. I am not running untrusted CI on this Docker host. I track image tags so the forge stays rebuildable when the next CE release lands.

Did you hit the same wall?

I got stuck on 502 for a long first boot and almost killed Omnibus mid-reconfigure; I also nearly missed initial_root_password (24-hour file). Did you hit the same thing, or a different one — SSH port 2222, restore without secrets, RAM thrash? Tell me in the comments. I read them.

Need this done on your server?

I deploy and harden Laravel/CodeCanyon apps on cPanel or VPS, and offer monthly Server Watch retainers. Hire for deploy · Care plan

References

Share:

Get new posts in your inbox

No spam. One short email per new article — practical PHP, Laravel, devops, and AI-assisted workflows.

Comments

Powered by GitHub Discussions via Giscus. A free GitHub account is required.