Self-Hosting GitLab CE: A Complete Guide to Running an Open-Source DevOps Platform

Self-Hosting GitLab CE: A Complete Guide to Running an Open-Source DevOps Platform

GitLab Community Edition is a full open-source DevOps platform with Git hosting, merge requests, CI/CD, a container registry, and project management. This guide walks through self-hosting GitLab CE on Ubuntu 24.04 LTS with Docker Compose, Omnibus volumes, HTTPS, SSH clones, runners, backups, and upgrades.

 Self-hosted GitLab CE DevOps platform

Caption: Self-hosted GitLab CE runs Git hosting, merge requests, CI/CD, and related Omnibus services behind HTTPS on infrastructure you control.

Introduction

GitLab CE (Community Edition) is an open-source DevOps platform that combines source code management, merge requests, issue tracking, CI/CD pipelines, a container registry, and project planning in one application. Unlike a lightweight Git forge that focuses mainly on repositories, GitLab CE aims to cover the path from commit to deploy without stitching together many separate SaaS tools.

Self-hosting GitLab CE makes sense when source code, pipeline definitions, package artifacts, and access policies should stay on your VPS or private network. Agencies, product teams, and serious homelabs often outgrow bare Git remotes or a minimal forge once they need protected branches, runners, registry images, and audit-friendly project history. Running the official Docker image keeps the Omnibus stack (NGINX, Puma, Sidekiq, PostgreSQL, Redis, Gitaly, and more) in a single container with host-mounted volumes for config, logs, and data.

This guide installs GitLab CE on Ubuntu 24.04 LTS using Docker Compose, following the official Docker installation model. The example uses https://gitlab.example.com, stores state under /srv/gitlab, pins a Community Edition image tag, publishes HTTP/HTTPS/SSH, and covers first login, SMTP, runners, backups, and upgrades. Replace domains, passwords, and ports before production use.

Why Choose GitLab CE?

  • All-in-one DevOps surface: Repositories, merge requests, issues, boards, CI/CD, and a container registry live in one product instead of five subscriptions.
  • Official Docker path: The gitlab/gitlab-ce image packages Omnibus GitLab for reproducible Compose deployments.
  • Self-managed control: You decide registration policy, SSO later, backup retention, upgrade windows, and which runners may execute jobs.
  • SSH and HTTPS clones: Teams can use familiar Git workflows with a public hostname and a dedicated SSH mapping when host port 22 is already taken.
  • Built-in CI/CD: .gitlab-ci.yml pipelines coordinate through GitLab; runners execute jobs on separate hosts you control.
  • Operational maturity: Documented backup (gitlab-backup create), configuration via GITLAB_OMNIBUS_CONFIG, and version-pinned upgrades are first-class.
  • Community Edition licensing: CE gives a strong free self-hosted baseline for small and mid-size teams without Enterprise Edition features.

Treat GitLab CE as critical infrastructure. A compromised root account, leaked runner token, or incomplete backup of /etc/gitlab can expose private repositories and make restores impossible. Plan CPU, RAM, and disk before the forge becomes your source of truth.

Prerequisites

Hardware Recommendations:

  • Baseline for a comfortable single-node install: about 8 vCPU and 16 GB RAM per GitLab’s single-node guidance
  • Absolute floor for constrained labs: at least 8 GB RAM (expect slower UI, Sidekiq lag, and longer first boot)
  • 40 GB+ free SSD for package/OS overhead plus repositories, artifacts, registry layers, and logs (start higher if you store many container images)
  • Off-server backup capacity for Omnibus backups plus /etc/gitlab secrets
  • A separate small VM or host for GitLab Runner when you enable CI for more than toy projects

Software and Accounts:

  • Ubuntu 24.04 LTS with sudo access
  • A public hostname such as gitlab.example.com (do not use localhost)
  • DNS A/AAAA records pointing at the server
  • Docker Engine with the Compose v2 plugin
  • OpenSSL, curl, rsync, and a password manager
  • Optional SMTP credentials for notifications and password resets
  • Optional second host for runners

Security Notes:

  • Retrieve and change the initial root password within 24 hours of first boot
  • Prefer pinning gitlab/gitlab-ce:<version>-ce.0 instead of floating latest in production
  • Keep /srv/gitlab/config (especially gitlab-secrets.json) out of public Git remotes
  • Do not run untrusted CI jobs on the same Docker host that stores your GitLab data volumes
  • Open only SSH (admin), HTTP, HTTPS, and the Git SSH port you choose

Patch the host and set a tight firewall:

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

If you want Git clone URLs without an explicit port, move the host’s OpenSSH daemon to another port (for example 2424) before publishing container port 22, as described in GitLab’s Docker install docs. This guide keeps host SSH on 22 and maps GitLab Shell to host port 2222.

Installation Guide

This deployment follows the official Docker Compose approach: set GITLAB_HOME, write a Compose file that mounts config/logs/data, set external_url, then bring the stack up and watch logs until the first configuration finishes.

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

Pin a Community Edition tag from Docker Hub (replace 18.11.11-ce.0 if a newer patch is appropriate for you). Official docs show EE tags; for CE, replace ee with ce.

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

With external_url 'https://gitlab.example.com', Omnibus NGINX expects to terminate TLS for that hostname. Ensure DNS already points to the server before first boot so certificate provisioning can succeed. If you prefer a reverse proxy in front of GitLab instead, set nginx['listen_https'] = false, publish only loopback HTTP, and terminate TLS on Caddy or Nginx—keep external_url on https://… so generated links stay correct.

 GitLab CE Docker Compose topology

Caption: The official GitLab CE container mounts host directories for config, logs, and data, and exposes HTTP, HTTPS, and Git SSH.

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

First boot can take several minutes while Omnibus configures services and runs migrations. Leave the log stream open until NGINX is serving and health checks settle. Interrupt follow mode with Ctrl+C (the container keeps running).

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 at https://gitlab.example.com as root, then immediately set a new strong password. The initial password file is removed automatically after 24 hours. If you miss that window, reset via the Rails console documented by GitLab—do not leave root on the generated secret.

Configuration

HTTPS and Public URL

external_url must match the URL users type in the browser, including https://. After changing Omnibus settings inside GITLAB_OMNIBUS_CONFIG, recreate the container so the configuration is re-applied:

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

 HTTPS path options for GitLab CE

Caption: Terminate TLS with Omnibus NGINX directly, or place Caddy/Nginx in front while keeping external_url on HTTPS.

SSH Clone Port

Because this Compose file maps container 22 to host 2222, GitLab must advertise that port:

gitlab_rails['gitlab_shell_ssh_port'] = 2222

Clone URLs then look like ssh://git@gitlab.example.com:2222/group/project.git. Verify firewall rules allow 2222/tcp.

Registration and Admin Hardening

In Admin Area → Settings → General:

  • Disable public sign-up unless you intentionally run a public community forge
  • Restrict visibility defaults for new projects
  • Require confirmed email once SMTP works
  • Review sign-in restrictions and session lifetime

Store admin recovery codes and the location of /srv/gitlab/config backups in your password manager notes—not in a public issue tracker.

Optional: Caddy in Front

If port 443 must be shared with other apps, bind GitLab HTTP to localhost and proxy with Caddy. Conceptually you add Omnibus settings such as nginx['listen_https'] = false and nginx['listen_port'] = 80, publish 127.0.0.1:8080:80, and reverse-proxy https://gitlab.example.com to that port while still publishing SSH separately. Keep headers for X-Forwarded-Proto consistent with GitLab’s reverse-proxy guidance.

Usage

First Login Checklist

  1. Open https://gitlab.example.com and sign in as root
  2. Change the root password and enable 2FA for the admin account
  3. Create your user account (or invite teammates after SMTP works)
  4. Create a group and a private project
  5. Add an SSH key under Preferences → SSH Keys
  6. Push a sample repository and open a merge request
  7. Confirm HTTPS clone and SSH clone both work

Register a GitLab Runner

Runners should live on a separate machine whenever possible.

# 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

Use the registration token from Admin Area → CI/CD → Runners (or a project runner token for tighter scope). Prefer the Docker executor with a pinned helper image. Avoid giving untrusted projects a runner that can reach your GitLab data volumes or production credentials.

 GitLab CE and Runner topology

Caption: Coordinate pipelines on GitLab CE; execute jobs on an isolated runner host.

Add a minimal .gitlab-ci.yml to verify the runner:

stages: [test]

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

Testing Checklist

  • HTTPS loads without certificate warnings
  • root password changed; 2FA enabled
  • HTTPS Git clone and SSH clone (port 2222) succeed
  • Merge request UI opens and accepts a review comment
  • Optional SMTP: password reset or invitation mail arrives
  • Runner picks up a pipeline and marks the job green
  • docker compose ps shows a healthy gitlab container after reboot

Screenshots and Visuals

The visuals in this guide are original architecture diagrams rather than scraped product UI. They show the Compose topology, HTTPS options, runner isolation, and the backup path you should rehearse before the forge holds production repositories.

 GitLab CE backup and upgrade workflow

Caption: Useful GitLab backups include gitlab-backup create output plus /etc/gitlab secrets, an off-server copy, a restore drill, and a pinned image upgrade.

Troubleshooting

  • 502 / blank page for several minutes after up -d: First reconfigure is still running. Follow docker compose logs -f gitlab and wait until services finish starting.
  • Cannot find initial root password: The file /etc/gitlab/initial_root_password expires after 24 hours. Use GitLab’s documented Rails console password reset.
  • HTTPS certificate failures: DNS must already resolve to the host; check Omnibus NGINX logs under $GITLAB_HOME/logs and confirm ports 80/443 are reachable from the internet.
  • SSH clone hangs or asks for the wrong port: Align gitlab_rails['gitlab_shell_ssh_port'] with the published host port and firewall rules.
  • Container exits or thrashing under load: Memory is below GitLab’s practical floor. Add RAM or stop other heavy services; 4 GB hosts are not suitable for CE.
  • CI jobs stuck pending: No runner is registered, the runner tag does not match the job, or the runner host cannot reach the GitLab URL.
  • Disk filling quickly: Artifacts, container registry layers, and logs grow under $GITLAB_HOME/data. Enable retention policies and prune old registry tags.
  • Restore fails or data looks encrypted/wrong: You restored a backup without matching /etc/gitlab secrets. Always back up config alongside gitlab-backup archives.

Scaling, Securing, and Next Steps

Create application backups with the Omnibus tool inside the container, then copy both the backup archive and the config directory off the server:

# 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/

Schedule that flow with cron or a systemd timer. Test a restore on a spare VPS before you need it: install the same CE image tag, restore config secrets, restore the backup archive per GitLab’s Docker backup documentation, start the stack, clone a private project, and re-run a pipeline.

For upgrades, read GitLab’s upgrade paths, take a fresh backup, change the image tag in docker-compose.yml, and recreate:

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

Do not skip required intermediate versions on major upgrades. Pin tags deliberately so a host reboot cannot float you onto an unexpected release.

The outcome of this guide is a private, HTTPS-secured GitLab CE instance with Omnibus data on disk, SSH Git access, an admin account you control, a path to attach runners, and backups that include both application data and /etc/gitlab secrets. From here, harden registration, add SMTP, isolate runners, document the restore drill in a runbook, and track image tags so the forge stays rebuildable when the next CE release lands.

Need this done on your server?

I deploy and harden Laravel, CodeCanyon, and open-source 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.