Automation

Run GitLab CE in Docker with Docker Compose

Docker gives you the cleanest way to run GitLab CE on a server you control. Everything (PostgreSQL, Redis, Nginx, Puma, Sidekiq, Gitaly) lives inside a single container with its own filesystem, which means no package conflicts with the host OS and trivial upgrades: pull a new image, restart the container. The trade-off is a ~4 GB image download and slightly more memory overhead from the container runtime, but for most teams that’s well worth the isolation.

Original content from computingforgeeks.com - post 119059

This guide deploys GitLab CE 18.10 using Docker Compose on Ubuntu 24.04, with TLS certificates, persistent volumes, backup procedures, and production tuning. Every command was tested on a fresh VM.

Tested March 2026 | Ubuntu 24.04.4 LTS, Docker 28.2.2, Docker Compose 2.37.1, GitLab CE 18.10.1

Prerequisites

  • A server running Ubuntu 24.04 LTS or Debian 13 with root access.
  • At least 8 GB of RAM. Our Docker deployment used 3 GB with Prometheus disabled, but spikes during reconfigure and CI jobs can push past 4 GB easily.
  • 2+ CPU cores (4 recommended).
  • A domain name with a DNS A record pointing to your server (needed for SSL).
  • Ports 80, 443, and optionally 2222 (for Git over SSH) open in the firewall.

Install Docker and Docker Compose

Ubuntu 24.04 ships Docker and the Compose plugin in the default repositories. Install them:

sudo apt update
sudo apt install -y docker.io docker-compose-v2

Enable Docker and verify both components:

sudo systemctl enable --now docker
docker --version
docker compose version

On our test system:

Docker version 28.2.2, build 28.2.2-0ubuntu1~24.04.1
Docker Compose version 2.37.1+ds1-0ubuntu2~24.04.1

If you prefer the latest Docker CE from the official Docker repository instead, follow our guide on installing Docker CE on Linux. The compose syntax is identical either way.

Open the required firewall ports:

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

Obtain a TLS Certificate

GitLab should always run behind HTTPS. Before bringing up the container, obtain a Let’s Encrypt certificate. If your server has a public IP, use the standalone method:

sudo apt install -y certbot
sudo certbot certonly --standalone -d gitlab.example.com --non-interactive --agree-tos -m [email protected]

If the server is behind NAT or on a private network, use DNS validation with the Cloudflare plugin (or whichever DNS provider you use):

sudo apt install -y certbot python3-certbot-dns-cloudflare

Create a credentials file and request the certificate:

sudo mkdir -p /etc/letsencrypt
echo "dns_cloudflare_api_token = YOUR_CLOUDFLARE_TOKEN" | sudo tee /etc/letsencrypt/cloudflare.ini
sudo chmod 600 /etc/letsencrypt/cloudflare.ini
sudo certbot certonly --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  --dns-cloudflare-propagation-seconds 30 \
  -d gitlab.example.com \
  --non-interactive --agree-tos -m [email protected]

A successful run shows:

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/gitlab.example.com/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/gitlab.example.com/privkey.pem

Create the Docker Compose File

Create a directory for the GitLab deployment:

sudo mkdir -p /opt/gitlab
cd /opt/gitlab

Create the docker-compose.yml file. This uses the official gitlab/gitlab-ce image from Docker Hub (not the third-party sameersbn image that older guides reference):

sudo vi /opt/gitlab/docker-compose.yml

Add the following configuration. Replace gitlab.example.com with your actual domain:

services:
  gitlab:
    image: gitlab/gitlab-ce:latest
    container_name: gitlab
    restart: unless-stopped
    hostname: gitlab.example.com
    environment:
      GITLAB_OMNIBUS_CONFIG: |
        external_url "https://gitlab.example.com"

        # SSL - mount host certificates into the container
        letsencrypt["enable"] = false
        nginx["ssl_certificate"] = "/etc/gitlab/ssl/fullchain.pem"
        nginx["ssl_certificate_key"] = "/etc/gitlab/ssl/privkey.pem"
        nginx["redirect_http_to_https"] = true

        # Time zone
        gitlab_rails["time_zone"] = "UTC"

        # Reduce memory for small teams
        puma["worker_processes"] = 2
        sidekiq["concurrency"] = 10

        # Disable Prometheus stack to save ~500 MB of RAM
        prometheus_monitoring["enable"] = false

        # Backup retention (7 days)
        gitlab_rails["backup_keep_time"] = 604800
    ports:
      - "80:80"
      - "443:443"
      - "2222:22"
    volumes:
      - gitlab-config:/etc/gitlab
      - gitlab-logs:/var/log/gitlab
      - gitlab-data:/var/opt/gitlab
      - /etc/letsencrypt/live/gitlab.example.com/fullchain.pem:/etc/gitlab/ssl/fullchain.pem:ro
      - /etc/letsencrypt/live/gitlab.example.com/privkey.pem:/etc/gitlab/ssl/privkey.pem:ro
      - /etc/letsencrypt/archive/gitlab.example.com:/etc/letsencrypt/archive/gitlab.example.com:ro
    shm_size: "256m"
    healthcheck:
      test: ["CMD", "curl", "-fsk", "https://localhost/-/health"]
      interval: 60s
      timeout: 10s
      retries: 5
      start_period: 300s

volumes:
  gitlab-config:
  gitlab-logs:
  gitlab-data:

Key points about this configuration:

  • Three named volumes (gitlab-config, gitlab-logs, gitlab-data) persist all GitLab data across container restarts and upgrades.
  • Certificate bind mounts pass the Let’s Encrypt certificates into the container as read-only. The archive directory mount is needed because the live/ symlinks point into archive/.
  • shm_size: 256m prevents Puma from failing with “Cannot allocate memory” errors. The default 64 MB is too small for GitLab.
  • Port 2222 maps to the container’s SSH port (22). This avoids conflicting with the host’s SSH daemon on port 22.
  • start_period: 300s gives GitLab 5 minutes to initialize before the health check starts counting failures. The first boot typically takes 3 to 5 minutes.
  • Disabling Prometheus saves roughly 500 MB of RAM. Re-enable it if you want the built-in monitoring dashboards.

Start GitLab

Pull the image and start the container:

cd /opt/gitlab
sudo docker compose up -d

The first pull downloads about 4 GB (the gitlab/gitlab-ce image is large because it bundles PostgreSQL, Redis, Nginx, and the entire GitLab application). On a 100 Mbps connection, expect 5 to 10 minutes for the download.

Watch the container status until it shows healthy:

sudo docker compose ps

Initially you’ll see health: starting. After 3 to 5 minutes of internal reconfiguration:

NAME     IMAGE                     COMMAND             SERVICE   STATUS                   PORTS
gitlab   gitlab/gitlab-ce:latest   "/assets/wrapper"   gitlab    Up 4 minutes (healthy)   0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp, 0.0.0.0:2222->22/tcp

If you need to troubleshoot the startup, follow the logs in real time:

sudo docker compose logs -f gitlab

Look for gitlab Reconfigured! in the output, which signals that the initial setup completed successfully.

Retrieve the Root Password and Log In

GitLab generates a random root password on first boot. Retrieve it from inside the container:

sudo docker exec gitlab cat /etc/gitlab/initial_root_password

Copy the password. This file is automatically deleted 24 hours after the first reconfigure.

Open https://gitlab.example.com in your browser. You should see the GitLab login page served over a valid TLS certificate:

GitLab CE Docker deployment login page with HTTPS

Log in with username root and the password from the previous step. The dashboard shows the welcome wizard:

GitLab CE Docker dashboard after first login

Change the root password immediately: go to Edit Profile > Password and set something strong.

The Admin Area shows the full instance overview with component versions:

GitLab CE Docker admin area showing instance overview

Disable Public Registration

By default, anyone can create an account. For a private instance, disable this immediately. Go to Admin Area > Settings > General, expand Sign-up restrictions, uncheck Sign-up enabled, and click Save changes.

Alternatively, add this to the GITLAB_OMNIBUS_CONFIG in your compose file and recreate the container:

gitlab_rails["gitlab_signup_enabled"] = false

Git Over SSH on Port 2222

Since the container maps port 2222 on the host to port 22 inside the container, SSH clone URLs need to use port 2222. Tell GitLab about this by adding to GITLAB_OMNIBUS_CONFIG:

gitlab_rails["gitlab_shell_ssh_port"] = 2222

After adding this and running sudo docker compose up -d, the project pages will display the correct SSH clone URL with port 2222.

Users clone with:

git clone ssh://[email protected]:2222/username/project.git

Backup and Restore

Run a backup from inside the container:

sudo docker exec gitlab gitlab-backup create

On our test instance, the backup completed in seconds:

2026-03-25 23:16:18 UTC -- Warning: Your gitlab.rb and gitlab-secrets.json files contain sensitive data
and are not included in this backup. You will need these files to restore a backup.
Please back them up manually.
2026-03-25 23:16:18 UTC -- Backup 1774480575_2026_03_25_18.10.1 is done.

Backups are stored inside the gitlab-data volume at /var/opt/gitlab/backups/. To copy a backup to the host:

sudo docker cp gitlab:/var/opt/gitlab/backups/ /opt/gitlab/backups/

You also need to back up the configuration separately. These files contain encryption keys that are not in the backup tar:

sudo docker exec gitlab cat /etc/gitlab/gitlab-secrets.json > /opt/gitlab/gitlab-secrets.json
sudo docker exec gitlab cat /etc/gitlab/gitlab.rb > /opt/gitlab/gitlab.rb.backup

For automated daily backups, add a cron job on the host:

sudo crontab -e

Add the following line:

0 2 * * * docker exec gitlab gitlab-backup create CRON=1 2>&1 | logger -t gitlab-backup

To restore, place the backup tar in the container’s backup directory, then run:

sudo docker exec gitlab gitlab-ctl stop puma
sudo docker exec gitlab gitlab-ctl stop sidekiq
sudo docker exec gitlab gitlab-backup restore BACKUP=1774480575_2026_03_25_18.10.1
sudo docker exec gitlab gitlab-ctl reconfigure
sudo docker exec gitlab gitlab-ctl restart

Upgrading GitLab

One of the biggest advantages of running GitLab in Docker: upgrades are a three-command operation.

cd /opt/gitlab
sudo docker compose pull
sudo docker compose up -d

Compose pulls the new image, stops the old container, and starts a new one with the same volumes. The data volumes persist across the upgrade, so nothing is lost.

Always take a backup before upgrading. If you need to pin a specific version instead of latest, change the image tag in docker-compose.yml:

image: gitlab/gitlab-ce:18.10.1-ce.0

For major version jumps (e.g., 16.x to 18.x), you must follow GitLab’s upgrade path and step through required intermediate versions.

Resource Usage and Tuning

On our 4-core, 8 GB test VM, the Docker deployment with Prometheus disabled used:

               total        used        free      shared  buff/cache   available
Mem:           7.8Gi       3.0Gi       280Mi       179Mi       4.9Gi       4.7Gi

That’s 3 GB of RAM with a single project. The gitlab/gitlab-ce image itself takes about 4 GB of disk, and the data volume grows with your repositories.

If you’re running on a 4 GB server, keep the Prometheus stack disabled and consider reducing Puma workers further:

puma["worker_processes"] = 0
sidekiq["concurrency"] = 5

Setting Puma workers to 0 runs in single-process mode, which saves ~400 MB but reduces concurrent request handling. Fine for teams under 5 people.

Useful Docker Commands

Common operations you’ll need when managing GitLab in Docker:

sudo docker compose ps

Check container status and health.

sudo docker compose logs -f --tail=100 gitlab

Follow logs (last 100 lines, then stream).

sudo docker exec -it gitlab gitlab-rake gitlab:check SANITIZE=true

Run the built-in health check suite.

sudo docker exec -it gitlab gitlab-ctl reconfigure

Apply configuration changes (after editing GITLAB_OMNIBUS_CONFIG in the compose file and recreating the container, or after modifying /etc/gitlab/gitlab.rb inside the container).

sudo docker exec -it gitlab gitlab-rails console

Open a Rails console for advanced troubleshooting (reset passwords, modify settings, etc.).

sudo docker exec gitlab gitlab-rake gitlab:env:info

Display the full environment (GitLab version, Ruby, PostgreSQL, Redis versions, URL configuration).

Troubleshooting

Container keeps restarting

Check docker compose logs gitlab for the error. During testing, we hit Reading unsupported config value grafana because the grafana configuration key was removed in GitLab 18.x. If you’re copying configuration from older guides, watch for deprecated settings. The fix is to remove the offending line from GITLAB_OMNIBUS_CONFIG and recreate the container.

502 errors after container starts

GitLab takes 3 to 5 minutes to fully start inside Docker. The health check has a 5-minute start_period for this reason. Wait for the health status to change from starting to healthy before testing in the browser. If 502 persists, check if the shm_size is set to at least 256 MB and that the server has enough free RAM.

SSL certificate not found

Let’s Encrypt stores certificates at /etc/letsencrypt/live/domain/, but those are symlinks into /etc/letsencrypt/archive/domain/. You must mount both paths. If you only mount the live/ files, the container sees broken symlinks. Our compose file handles this with the archive directory mount.

Permission denied on volumes

The GitLab container runs internal processes as user git (UID 998). If you’re using bind mounts instead of named volumes, make sure the directories are writable. Named volumes (as in our compose file) handle permissions automatically.

Docker vs Bare-Metal: When to Choose What

Both approaches install the same GitLab Omnibus package. The Docker image is literally the Omnibus installer running inside a container. Choosing between them comes down to your operational preferences:

  • Docker: cleaner upgrades (pull + restart), full isolation from host packages, easier to reproduce on another server. Slightly more RAM and disk overhead. Better when you’re already running other containers.
  • Bare metal: simpler debugging (everything is on the host filesystem), no Docker layer to troubleshoot, direct access to all config files and logs. Better when GitLab is the only thing on the server. See our guide on installing GitLab CE on Ubuntu/Debian for the bare-metal approach.

The Help page in our Docker deployment confirms the same GitLab CE 18.10.1 as the bare-metal install:

GitLab CE Docker help page showing version 18.10.1

Related Articles

Jenkins How To Manage Users and Roles in Jenkins Automation Migrating GitLab from RHEL 6 TO RHEL 7/CentOS 7 Containers Deploy Kubernetes Cluster on Oracle Linux 8 with Kubeadm Containers Run Linux Containers with LXC/LXD on Ubuntu 24.04|22.04|20.04|18.04

Leave a Comment

Press ESC to close