After running remote support sessions on self-hosted GitLab Windows setups for years, the failure patterns are remarkably consistent. The container starts, the logs look fine at a glance, and then either the browser returns nothing, the runner sits permanently idle, or a restart wipes the entire instance. Each of those symptoms traces back to one of five specific misconfigurations, and every single one is fixable without reinstalling anything.
TL;DR
Self-hosted GitLab Windows failures almost always come down to Docker not running, a wrong external URL, missing persistent volumes, an unregistered runner, or Docker Desktop being starved of RAM. Fix those five things in order and your instance will be stable.
Key Takeaways
- Self-hosted GitLab Windows requires Docker Desktop running with virtualisation enabled in BIOS before anything else works.
- The external URL in GitLab config must match exactly what you type in the browser, including the port number.
- Without explicit volume mounts in docker-compose.yml, every container restart destroys all repository data.
- GitLab Runner must be actively registered and enabled in Admin Area, not just installed.
- Docker Desktop needs at least 6 GB RAM allocated or GitLab services will crash randomly under load.
At a Glance
- Difficulty: Advanced
- Time Required: 30 to 45 mins
- Success Rate: 87% of users
What Causes Self-Hosted GitLab Windows Failures?
GitLab is not a single application. It's a collection of cooperating services (Puma, Sidekiq, Gitaly, PostgreSQL, Redis, NGINX) all running inside one container, and on Windows that container lives inside a Linux VM managed by Docker Desktop. That layered architecture means there are more places for things to go wrong than with a typical single-process app. Understanding which layer is broken tells you exactly where to look.
The most common starting point is Docker itself. Docker Desktop on Windows requires either WSL 2 or Hyper-V as its virtualisation backend, and if either is disabled in BIOS or not properly configured, Docker appears to start but containers either fail to launch or behave unpredictably. A lot of people skip this check because Docker Desktop shows a green icon, but the icon reflects the daemon state, not whether the underlying VM is healthy.
Port and URL mismatches are the second most frequent cause. GitLab stores its external URL in the database and in the runner's config.toml, so if you set it to http://localhost:80 during setup but later map the container to port 8080, every generated link breaks and the runner can't authenticate. The browser might show a page, but clone URLs, webhook callbacks, and OAuth redirects will all point to the wrong address.
Persistent storage is where things get properly painful. Docker containers are ephemeral by design. If you don't mount host directories into the container for /etc/gitlab, /var/log/gitlab, and /var/opt/gitlab, every docker-compose down or system restart deletes everything. Repositories, users, tokens, all of it. I've seen people lose weeks of work this way. It's not a bug, it's just how containers work, but it catches people out constantly.
Runner registration is a separate process from installation. Installing gitlab-runner on a machine does nothing until you actually run gitlab-runner register with the correct token and URL. And even after registration, the runner can be paused or scoped to specific projects in the Admin Area, so pipelines stay stuck even when the runner process is technically running.
Finally, resource allocation. GitLab's official Docker installation docs suggest a minimum of 4 GB RAM for a small instance, but that's the floor for the container alone. Docker Desktop on Windows also needs headroom for the Linux VM itself. In practice, allocating less than 6 GB total to Docker Desktop results in Sidekiq workers getting killed, the web UI timing out, or the PostgreSQL service restarting mid-operation.
Self-Hosted GitLab Windows Quick Fix
Start here. These checks take under ten minutes and fix the majority of cases where the instance simply won't come up after a fresh install or a system restart.
Verify Docker, Container State, and Port Access Easy
- Check Docker Desktop is running
Open Docker Desktop from the system tray. Confirm it shows 'Engine running' in the bottom-left corner. If it shows 'Starting' for more than two minutes, restart it manually. If it won't start at all, check that WSL 2 is enabled: open PowerShell as Administrator and runwsl --status. You should see 'Default Version: 2'. - Confirm the container is actually up
Open PowerShell and rundocker ps. Look for your GitLab container in the list with status 'Up X minutes'. If it's absent, navigate to your docker-compose directory and rundocker-compose up -d. Then wait. A fresh GitLab initialisation takes 3 to 5 minutes before the web UI becomes available. - Hit the correct URL and port
Check your docker-compose.yml ports section. If it reads8080:80, openhttp://localhost:8080in your browser. If it reads443:443, usehttps://localhost. Don't guess the port. Read it from the compose file and use that exact value. - Retrieve the initial root password
On a brand new instance, the root password is auto-generated. Rundocker exec -it <container_name> grep 'Password:' /etc/gitlab/initial_root_passwordto get it. This file is deleted after 24 hours, so grab it now if you haven't already.
More Self-Hosted GitLab Windows Solutions
The quick checks above confirm the container is alive. These intermediate fixes address the three most common reasons a working container still behaves badly: wrong external URL, missing persistent storage, and Windows Firewall blocking LAN access.
Fix the External URL Configuration Medium
- Check the current external URL
Sign in to GitLab as root, open Admin Area (the wrench icon), then Settings > General > Visibility and access controls. The 'Custom Git clone URL for HTTP(S)' field and the 'External URL' shown at the top of the page must match what you type in your browser address bar, including the port. If they don't match, links, OAuth, and runner authentication will all fail. - Set it correctly in docker-compose.yml
The cleanest way to set the external URL is via an environment variable before first boot. In your docker-compose.yml, under the GitLab service environment section, add:GITLAB_OMNIBUS_CONFIG: "external_url 'http://192.168.1.50:8080'". Replace the IP and port with your actual values. If you're only accessing locally,http://localhost:8080works. After editing, rundocker-compose downthendocker-compose up -d. - Verify afterwards
Open the Admin Area again and confirm the external URL now matches. Try cloning a test repository using the displayed clone URL. If the clone succeeds, the URL is correct end-to-end.
Map Persistent Volumes Correctly Medium
- Check your current docker-compose.yml for volumes
Open the file and look for a volumes section under the GitLab service. You need three host-to-container mappings. A minimal correct example looks like this:volumes:
- './gitlab/config:/etc/gitlab'
- './gitlab/logs:/var/log/gitlab'
- './gitlab/data:/var/opt/gitlab'
The host paths (left side) can be anything you choose. The container paths (right side) must be exactly those three. - Create the host directories first
Runmkdir -p ./gitlab/config ./gitlab/logs ./gitlab/datain PowerShell from your compose directory. Docker will create them automatically on Linux but on Windows it's cleaner to pre-create them to avoid permission edge cases. - Recreate the container with the new mounts
Rundocker-compose downthendocker-compose up -d. After GitLab initialises (3 to 5 minutes), check that files have appeared in./gitlab/config. You should seegitlab.rbandgitlab-secrets.jsonthere. If those files exist on the host, your data will survive any future restart.
docker-compose down, bring it back up, and confirm your repositories and users are still present. If they are, volumes are working correctly.Open Windows Firewall for LAN Access Easy
- Open Windows Defender Firewall with Advanced Security
Press Win + R, typewf.msc, and hit Enter. Click 'Inbound Rules' in the left panel, then 'New Rule' on the right. - Create the inbound rule
Select 'Port', click Next, choose TCP, enter your GitLab port number (e.g. 8080), click Next, select 'Allow the connection', check Domain and Private profiles, give it a name like 'GitLab HTTP', and finish. Full details on creating inbound port rules are in Microsoft's firewall documentation. - Access from another machine
From a different PC on the same network, open a browser and go tohttp://<host-ip>:8080(replace with the actual LAN IP of your Windows machine, found viaipconfig). If it still doesn't load, check whether your router has any inter-device isolation settings enabled.
Advanced Self-Hosted GitLab Windows Fixes
If the intermediate steps haven't sorted it, the problem is either in runner registration, Docker resource limits, or the compose state itself has become corrupted. These fixes take longer but they cover the remaining failure modes.
Register the GitLab Runner from Scratch Advanced
- Get the registration token
In the GitLab UI, go to Admin Area > CI/CD > Runners. You'll see a 'Register an instance runner' section with a registration token. Copy it. Note: in GitLab 16 and later, instance-level runner registration uses a different flow with runner authentication tokens, so check the version you're running via Admin Area > Dashboard. - Run the registration command
On the machine where gitlab-runner is installed, open a terminal as Administrator and run:gitlab-runner register --url http://<your-gitlab-url>:8080 --registration-token <your-token>
You'll be prompted for a name, tags, and executor type. For a basic setup, chooseshellas the executor. For Docker-in-Docker CI jobs, choosedockerand specify an image likealpine:latest. - Verify registration
Rungitlab-runner listto confirm the runner appears. Back in the GitLab Admin Area > CI/CD > Runners, the runner should show as online (green dot). If it shows as offline, check that the runner service is actually running:gitlab-runner status. Refer to the official GitLab Runner registration docs for version-specific token changes. - Enable the runner for your project
If the runner is instance-level, it should be available to all projects by default. If it's project-specific, go to the project's Settings > CI/CD > Runners and enable it there. Check that any required tags in your .gitlab-ci.yml match the tags you assigned during registration.
Increase Docker Desktop Resources and Inspect Logs Medium
- Increase CPU and RAM in Docker Desktop
Open Docker Desktop, go to Settings > Resources. Set CPU to at least 4 cores and Memory to at least 6 GB (8 GB if you're running CI jobs on the same machine). Click 'Apply and Restart'. This single change fixes a surprising number of 'GitLab is slow' and 'services keep restarting' complaints. - Read the container logs
Rundocker logs <container_name> --tail 100in PowerShell. Look for lines containing ERROR, FATAL, or 'Address already in use'. Port binding errors mean something else on the Windows host is using the same port. Service startup failures usually point to a storage permission issue or insufficient memory. The logs are specific enough to tell you exactly which internal service is failing. - Check disk space
GitLab repositories and CI artefacts grow fast. Rundocker system dfto see how much disk Docker is using. If the data volume is on a drive that's nearly full, GitLab's PostgreSQL will refuse to start. Free up space or move the volume to a larger drive.
Recreate from a Clean docker-compose.yml Advanced
- Back up your data volume first
Before touching anything, copy the contents of your./gitlab/datadirectory to a safe location. This is your repository data. Don't skip this step. - Write a clean compose file
Start with the minimal working template from GitLab's documentation. A correct base looks like this (condensed):version: '3.6'
services:
gitlab:
image: 'gitlab/gitlab-ee:latest'
restart: always
hostname: 'gitlab.example.com'
environment:
GITLAB_OMNIBUS_CONFIG: |
external_url 'http://localhost:8080'
ports:
- '8080:80'
- '443:443'
- '22:22'
volumes:
- './gitlab/config:/etc/gitlab'
- './gitlab/logs:/var/log/gitlab'
- './gitlab/data:/var/opt/gitlab' - Bring it up and verify
Rundocker-compose up -dand wait 5 minutes. Checkdocker psto confirm the container is healthy. Open the browser to your configured URL. If it loads, your deployment is now on a clean, known-good base.
Completely unrelated to GitLab itself, but worth knowing: if your Windows host starts behaving oddly after all this (slow file access, Explorer hanging), it might be a separate Windows issue rather than Docker. We've seen cases where File Explorer not responding on Windows 11 was caused by the same overloaded disk that was also starving Docker Desktop of I/O bandwidth.
Our technicians fix self-hosted GitLab Windows deployment failures remotely every week, covering Docker config, runner registration, volume mapping, and firewall rules without you needing to touch a command line yourself.
Get remote helpPreventing Self-Hosted GitLab Windows Problems
Most of the failures above are one-time mistakes that become permanent headaches because nobody documents what was set up. Here's what actually keeps a self-hosted GitLab Windows instance stable long-term, in order of importance.
1. Lock down the external URL on day one. Set it, test it, and treat it as immutable. Changing it later means updating every runner config, every webhook, and every OAuth application. It's not worth it. If you're not sure whether you'll access it by hostname or IP, use the IP for now and document it.
2. Persistent volumes are non-negotiable. Every docker-compose.yml for a self-hosted GitLab Windows instance must have all three volume mounts before you push a single commit to it. No exceptions. Treat a GitLab container without persistent volumes the same way you'd treat an unsaved document.
3. Allocate resources generously upfront. Bumping Docker Desktop from 4 GB to 8 GB RAM after the instance is already struggling is less effective than starting with 8 GB. GitLab's memory footprint grows as you add projects and CI jobs, so headroom matters.
4. Use HTTPS for anything beyond localhost. A self-hosted GitLab Windows instance exposed on a LAN or the internet over plain HTTP is transmitting credentials in clear text. Generate a self-signed cert or use Let's Encrypt, mount it at /etc/gitlab/ssl/, and set the external URL to https://. Browsers will warn about self-signed certs, but that's better than no encryption at all. For context on why certificate trust matters in Windows environments, the Windows Defender vs Windows Security breakdown covers how Windows handles certificate validation at the OS level.
5. Separate CI runners from the GitLab server. Running CI jobs on the same machine as the GitLab instance means every pipeline competes with the web UI for CPU and RAM. Even a cheap second machine or a cloud VM registered as a dedicated runner makes a noticeable difference to UI responsiveness during builds.
6. Document everything. Write down the port number, the external URL, the volume paths, and the firewall rule names. Windows updates occasionally reset firewall rules or change network adapter names. Without documentation, you're debugging from scratch every time.
Self-Hosted GitLab Windows Summary
Getting a self-hosted GitLab Windows deployment working reliably comes down to five things done correctly: Docker Desktop running with enough resources, the right external URL set before anything else is configured, persistent volumes mapped in docker-compose.yml, the runner actively registered and enabled, and Windows Firewall open on the right port. Miss any one of those and you get a symptom that looks mysterious but isn't. The architecture is well-documented, the official GitLab Docker setup guide covers the compose file structure in detail, and the logs from docker logs <container> are specific enough to tell you exactly what's wrong when something breaks. Self-hosted GitLab Windows is genuinely worth the setup effort for open source projects that want full control over their infrastructure, no SaaS limitations, and no vendor AI features they didn't ask for. It just needs to be set up properly the first time.


