How to Set Up Nginx as a Reverse Proxy for GitLab

How to Set Up Nginx as a Reverse Proxy for GitLab

How to Set Up Nginx as a Reverse Proxy for GitLab

GitLab ships with its own bundled Nginx as part of the Omnibus package, which handles most people’s needs out of the box. But there are plenty of situations where you want an external Nginx instance sitting in front of GitLab instead — maybe you’re running GitLab behind a shared reverse proxy alongside other internal services, you want centralized TLS termination and logging across multiple apps, or you’re running GitLab in Docker/Kubernetes without the bundled Nginx. This guide covers exactly that: configuring a standalone, external Nginx as a reverse proxy in front of GitLab, whether GitLab itself is running via Omnibus with its internal Nginx disabled, or via Docker.

How This Fits Together

GitLab is not a simple single-process web app — it’s a collection of services (Puma/Unicorn for the Rails app, GitLab Workhorse for handling large file operations like Git pushes/pulls and file uploads, and optionally GitLab Pages, Container Registry, and Mattermost, each with their own ports). When you put an external Nginx in front of GitLab, you’re proxying to GitLab Workhorse, which itself sits in front of the Rails application — this is important, because proxying directly to the Rails app instead of Workhorse breaks large file uploads and Git LFS.

The setup looks like this:

Browser/Git client → External Nginx (your reverse proxy) → GitLab Workhorse (port 8181 or similar) → Puma/Rails app

Requirements

Step 1: Disable GitLab’s Bundled Nginx (Omnibus Only)

If you’re running Omnibus GitLab and want your external Nginx to handle everything, edit /etc/gitlab/gitlab.rb:

nginx['enable'] = false
gitlab_rails['gitlab_shell_ssh_port'] = 2222

Then have GitLab listen on an internal port instead, still in gitlab.rb:

gitlab_workhorse['listen_network'] = "tcp"
gitlab_workhorse['listen_addr'] = "127.0.0.1:8181"

Reconfigure GitLab to apply changes:

sudo gitlab-ctl reconfigure

If you’re running GitLab via Docker, you’d instead publish Workhorse’s port to the host or an internal network your Nginx can reach, e.g. -p 127.0.0.1:8181:80 in your docker run or docker-compose.yml.

Step 2: Install Nginx on the Proxy Server

sudo apt update
sudo apt install nginx -y

Step 3: Configure the Reverse Proxy

Create /etc/nginx/sites-available/gitlab.conf:

upstream gitlab_workhorse {
    server 127.0.0.1:8181;
}

server {
    listen 80;
    server_name gitlab.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name gitlab.example.com;

    ssl_certificate     /etc/letsencrypt/live/gitlab.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/gitlab.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    client_max_body_size 500m;

    access_log /var/log/nginx/gitlab_access.log;
    error_log  /var/log/nginx/gitlab_error.log;

    location / {
        proxy_pass http://gitlab_workhorse;
        proxy_http_version 1.1;

        proxy_set_header Host              $http_host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header Upgrade           $http_upgrade;
        proxy_set_header Connection        "upgrade";

        proxy_read_timeout 300s;
        proxy_connect_timeout 300s;
        proxy_redirect off;
    }
}

A few details worth calling out:

Step 4: Tell GitLab Its External URL

Back on the GitLab server, set the external URL to match what users actually connect to, in /etc/gitlab/gitlab.rb:

external_url 'https://gitlab.example.com'

Then reconfigure:

sudo gitlab-ctl reconfigure

This matters because GitLab generates absolute URLs (in emails, webhooks, clone instructions) based on this setting — if it’s wrong, everything technically works through the proxy but the links GitLab generates for users will be broken.

Step 5: Handle Git Over SSH Separately

Reverse proxying HTTP/HTTPS traffic doesn’t touch SSH-based Git operations (git clone git@gitlab.example.com:...), since SSH isn’t HTTP and Nginx doesn’t proxy it in the typical sense. Make sure:

stream {
    upstream gitlab_ssh {
        server gitlab-internal.example.com:22;
    }

    server {
        listen 22;
        proxy_pass gitlab_ssh;
        proxy_timeout 300s;
    }
}

This goes in a separate top-level stream block in nginx.conf, not inside http.

Step 6: Test and Reload

sudo nginx -t
sudo systemctl reload nginx

Enable the site if using the sites-available/sites-enabled pattern:

sudo ln -s /etc/nginx/sites-available/gitlab.conf /etc/nginx/sites-enabled/
sudo systemctl reload nginx

Step 7: Verify Everything Works

Web UI: Visit https://gitlab.example.com and confirm login works and pages load without mixed-content warnings (a sign X-Forwarded-Proto isn’t set correctly).

Git over HTTPS:

git clone https://gitlab.example.com/group/project.git

Large file push (tests client_max_body_size and timeouts):

git clone https://gitlab.example.com/group/project.git
cd project
dd if=/dev/urandom of=largefile.bin bs=1M count=100
git add largefile.bin
git commit -m "test large file"
git push

CI/CD pipelines: Trigger a pipeline and confirm the runner can reach the GitLab instance and artifacts upload successfully — this is often where client_max_body_size and timeout issues first surface in production.

Troubleshooting Common Issues

502 Bad Gateway. GitLab Workhorse isn’t running or isn’t listening where Nginx expects. Check sudo gitlab-ctl status on the GitLab server, and confirm the listen_addr in gitlab.rb matches the upstream block in your Nginx config.

Large pushes fail with “413 Request Entity Too Large.” client_max_body_size isn’t set high enough, or isn’t set on the specific server/location block actually handling the request.

Broken links, mixed content warnings, or infinite redirect loops. X-Forwarded-Proto https; is missing, or GitLab’s external_url doesn’t match what’s actually being served.

Real-time UI features (job logs streaming, notification badges) not updating. WebSocket upgrade headers (Upgrade, Connection: upgrade) are missing from the proxy config.

CI jobs time out uploading artifacts. proxy_read_timeout and proxy_connect_timeout are too short for your artifact sizes — increase them, and check GitLab’s own artifact size limits too.

SSH clone works but HTTPS doesn’t (or vice versa). These are two entirely separate paths — confirm you’re testing and troubleshooting the correct one; an HTTPS fix won’t affect SSH-based Git operations.

Security Considerations

limit_req_zone $binary_remote_addr zone=gitlab_login:10m rate=5r/m;

location /users/sign_in {
    limit_req zone=gitlab_login burst=3 nodelay;
    proxy_pass http://gitlab_workhorse;
}

Performance Tips

upstream gitlab_workhorse {
    server 127.0.0.1:8181;
    keepalive 32;
}

Real-World Use Cases

Best Practices

Wrapping Up

Putting an external Nginx in front of GitLab is a well-trodden pattern, especially for teams consolidating multiple services behind a shared proxy layer or running GitLab in containerized environments without the bundled Omnibus Nginx. The core of it is straightforward — proxy to Workhorse, set the right headers, raise the body size and timeout limits — but the details around X-Forwarded-Proto, WebSocket upgrade headers, and SSH handling are exactly where naive configs quietly break specific GitLab features. Get those right up front, test with an actual large push and a CI pipeline (not just a login page load), and the setup holds up well in production.

Exit mobile version