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-ceimage 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.ymlpipelines coordinate through GitLab; runners execute jobs on separate hosts you control. - Operational maturity: Documented backup (
gitlab-backup create), configuration viaGITLAB_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/gitlabsecrets - 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 uselocalhost) - DNS
A/AAAArecords 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
rootpassword within 24 hours of first boot - Prefer pinning
gitlab/gitlab-ce:<version>-ce.0instead of floatinglatestin production - Keep
/srv/gitlab/config(especiallygitlab-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.
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
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
- Open
https://gitlab.example.comand sign in asroot - Change the root password and enable 2FA for the admin account
- Create your user account (or invite teammates after SMTP works)
- Create a group and a private project
- Add an SSH key under Preferences → SSH Keys
- Push a sample repository and open a merge request
- 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.
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
rootpassword 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 psshows a healthygitlabcontainer 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.
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. Followdocker compose logs -f gitlaband wait until services finish starting. - Cannot find initial root password: The file
/etc/gitlab/initial_root_passwordexpires 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/logsand confirm ports80/443are 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/gitlabsecrets. Always back up config alongsidegitlab-backuparchives.
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