How to Configure Nginx as a Reverse Proxy for Jenkins

How to Configure Nginx as a Reverse Proxy for Jenkins

How to Configure Nginx as a Reverse Proxy for Jenkins

Jenkins ships with its own embedded Winstone/Jetty server, listening by default on port 8080 — perfectly functional, but not something you want exposed directly to the internet in most setups. Putting Nginx in front of Jenkins gets you proper TLS termination, a clean domain instead of a raw IP:port, centralized access logging, and the ability to layer on authentication or IP restrictions at the proxy level before traffic even reaches Jenkins. It also lets Jenkins live alongside other services on the same server without port juggling for your users.

This guide covers the full setup: the proxy configuration itself, the specific headers Jenkins needs to behave correctly behind a proxy (it’s picky about a couple of these), WebSocket support for Jenkins’ real-time build console output, testing, and the security/performance considerations worth knowing before you put this in production.

Why Jenkins Needs Special Proxy Handling

Jenkins isn’t a simple stateless web app. It uses WebSocket-like long-polling and, in more recent versions, actual WebSocket connections for live build console streaming and the “Blue Ocean” UI. It also generates internal links and redirects based on what it thinks its own URL is — if the headers telling it that are wrong, you get broken CSS, redirect loops, or a UI that “mostly works” but silently misbehaves in a few specific spots (classic symptoms: build console output not live-updating, or plugin pages 404ing on assets).

Getting this right mostly comes down to a specific, well-documented set of headers Jenkins expects from anything proxying it — which we’ll set explicitly rather than relying on defaults.

Requirements

Step 1: Confirm Jenkins Is Running and Note Its Port

sudo systemctl status jenkins
sudo ss -tlnp | grep 8080

By default Jenkins listens on 127.0.0.1:8080 or 0.0.0.0:8080 depending on your install method — for security, it’s worth explicitly binding it to localhost only once Nginx is in front of it, editing /etc/default/jenkins (Debian/Ubuntu) or /etc/sysconfig/jenkins (RHEL-based):

JENKINS_ARGS="--httpListenAddress=127.0.0.1 --httpPort=8080"

Restart Jenkins after this change:

sudo systemctl restart jenkins

Step 2: Install Nginx

sudo apt update
sudo apt install nginx -y

Step 3: Configure the Reverse Proxy

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

upstream jenkins {
    server 127.0.0.1:8080 fail_timeout=0;
}

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

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

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

    client_max_body_size 100m;

    access_log /var/log/nginx/jenkins_access.log;
    error_log  /var/log/nginx/jenkins_error.log;

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

        proxy_set_header Host              $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 $scheme;
        proxy_set_header Upgrade           $http_upgrade;
        proxy_set_header Connection        "upgrade";

        # Jenkins-specific header — tells Jenkins to trust our forwarded headers
        proxy_redirect http:// https://;
        proxy_read_timeout 90s;
        proxy_connect_timeout 90s;

        # Prevent Nginx from buffering large build log responses
        proxy_buffering off;
    }
}

A few directives worth explaining in detail:

Step 4: Tell Jenkins Its Own URL

In Jenkins, go to Manage Jenkins → System (or Manage Jenkins → Configure System on older versions) and set the Jenkins URL field to https://jenkins.example.com/. This affects notification links, webhook callback URLs, and anything else Jenkins generates that points back at itself — critical for things like GitHub/GitLab webhook integrations to work correctly through the proxy.

Step 5: Test and Reload Nginx

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

Step 6: Verify Everything Works

Basic access:

curl -I https://jenkins.example.com

Confirm a 200 or expected redirect to the login page, and no mixed-content warnings when loading the actual page in a browser.

Live build console output: Trigger a build and watch the console output page — it should stream in real time rather than requiring manual refreshes. If it doesn’t, the WebSocket/Upgrade headers aren’t taking effect.

Webhook callbacks: If you’re using GitHub/GitLab/Bitbucket webhooks to trigger builds, push a commit and confirm the webhook successfully reaches Jenkins — check Manage Jenkins → System Log or the specific webhook’s delivery history on the Git provider’s side for errors.

Large artifact uploads/downloads: If your pipelines produce large build artifacts, test a full build that uploads one, to confirm client_max_body_size and timeouts are sufficient.

Troubleshooting Common Issues

502 Bad Gateway. Jenkins isn’t running, or is listening on a different port/address than your upstream block expects. Check sudo systemctl status jenkins and sudo ss -tlnp | grep 8080.

Reverse proxy loop / “It appears that your reverse proxy set up is broken” warning in Jenkins UI. This specific Jenkins warning appears when the Host, X-Forwarded-Proto, or X-Forwarded-For headers aren’t being passed correctly. Double-check all four proxy headers are present exactly as shown above — Jenkins actively checks for these and will warn you directly in the UI if something’s missing, which is genuinely one of the more helpful self-diagnostic messages you’ll get from any proxied app.

Live console output doesn’t update / has to be manually refreshed. Missing Upgrade/Connection: upgrade headers, or proxy_buffering isn’t disabled.

CSS/JS assets 404 or load unstyled. Usually a Host header mismatch — Jenkins generates asset URLs based on what it thinks its own hostname is.

Webhooks from GitHub/GitLab fail to trigger builds. Confirm the Jenkins URL setting in Manage Jenkins matches your actual public HTTPS URL, and that your firewall allows inbound traffic from your Git provider’s webhook IP ranges if you’ve restricted access.

Builds time out or fail on large uploads. Raise client_max_body_size, proxy_read_timeout, and check Jenkins’ own executor/pipeline timeout settings too — this is often a two-sided issue.

Security Considerations

location / {
    allow 203.0.113.0/24;
    deny all;
    proxy_pass http://jenkins;
    ...
}
limit_req_zone $binary_remote_addr zone=jenkins_login:10m rate=5r/m;

location /j_spring_security_check {
    limit_req zone=jenkins_login burst=3 nodelay;
    proxy_pass http://jenkins;
}

Performance Tips

upstream jenkins {
    server 127.0.0.1:8080;
    keepalive 32;
}

(Note: when using keepalive in the upstream block, also add proxy_set_header Connection ""; in locations that don’t need the WebSocket upgrade, since the Upgrade/Connection: upgrade pairing conflicts with persistent keepalive connections — for Jenkins, most setups leave the upgrade headers as shown since live console streaming is used constantly.)

location ~ ^/static/ {
    proxy_pass http://jenkins;
    proxy_cache_valid 200 7d;
    add_header Cache-Control "public, max-age=604800";
}

Real-World Use Cases

Best Practices

Wrapping Up

Jenkins behind Nginx is a common, well-supported pattern, and Jenkins itself even helps you debug it by explicitly warning in the UI when proxy headers are misconfigured — a nicer debugging experience than most apps offer. The setup itself is a fairly standard reverse proxy config, with the main gotchas being WebSocket support for live logs, disabling buffering so console output streams properly, and making sure the Jenkins URL setting matches what’s actually being served. Get those three things right, verify with an actual triggered build and a real webhook delivery, and the setup is solid for production CI/CD traffic.

Exit mobile version