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
- A working Jenkins installation (via .deb/.rpm package, WAR file, or Docker) listening on an internal port (default 8080).
- A separate Nginx installation, either on the same host or a dedicated proxy server.
- A domain name pointed at your Nginx server.
- SSL certificate (Let’s Encrypt/Certbot recommended).
- Jenkins itself should NOT also be configured with its own reverse-proxy-related settings conflicting with Nginx (we’ll cover the one Jenkins-side setting that matters).
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:
proxy_set_header Host $host;— Jenkins uses this to construct absolute URLs; getting it wrong causes broken links throughout the UI.Upgrade/Connection: "upgrade"— required for Jenkins’ WebSocket-based live log streaming (used heavily in the classic UI’s build console and even more in Blue Ocean).proxy_buffering off;— build console output streams incrementally; buffering it in Nginx delays what users see and can make live logs appear to hang.proxy_redirect http:// https://;— rewrites anyLocation:header Jenkins sends ashttp://back tohttps://, catching cases where Jenkins doesn’t fully respect the forwarded proto header for redirects.
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
- Bind Jenkins to
127.0.0.1once Nginx is confirmed working, so it’s not directly reachable if someone hits the server’s IP on port 8080 directly. - Never expose the Jenkins CLI port or JNLP agent port publicly without understanding the implications — these are separate from the HTTP UI and have their own security considerations (Jenkins’ JNLP port has had real-world CVEs; keep it firewalled to only your build agents).
- Add basic auth or IP allow-listing at the Nginx layer as a second factor in front of Jenkins’ own authentication for sensitive internal instances:
location / {
allow 203.0.113.0/24;
deny all;
proxy_pass http://jenkins;
...
}
- Enable Jenkins’ built-in CSRF protection (enabled by default in modern versions) — a proxy alone doesn’t protect against this class of issue.
- Rate limit the login endpoint to slow down credential-stuffing attempts:
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;
}
- Keep Jenkins itself patched aggressively — it’s a historically frequent target given how much access a compromised Jenkins instance often has (deploy keys, cloud credentials, source code access).
Performance Tips
- Disable
proxy_bufferingspecifically for build console/log endpoints, but consider leaving it on for general static asset serving if you notice unnecessary latency elsewhere — you can scope this per-location if needed. - Enable
keepalivebetween Nginx and the Jenkins upstream to avoid repeated TCP handshake overhead on Jenkins’ notoriously chatty UI (many small AJAX polling requests in the classic UI):
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.)
- Serve static Jenkins assets (
/static/, plugin resources) with reasonable cache headers to reduce repeated requests during heavy UI usage:
location ~ ^/static/ {
proxy_pass http://jenkins;
proxy_cache_valid 200 7d;
add_header Cache-Control "public, max-age=604800";
}
Real-World Use Cases
- A DevOps team runs Jenkins behind Nginx on the same host as several other internal tools, using distinct subdomains and a single shared TLS certificate management workflow across all of them.
- A company restricts Jenkins access to their office and VPN IP ranges at the Nginx layer, adding a security layer in front of Jenkins’ own authentication for their CI infrastructure.
- An organization running Jenkins agents across multiple cloud regions keeps the Jenkins controller behind Nginx specifically to centralize webhook ingestion from GitHub across several repositories into one clean HTTPS endpoint.
Best Practices
- Always set the four core proxy headers (
Host,X-Real-IP,X-Forwarded-For,X-Forwarded-Proto) — Jenkins actively validates these and warns you in-app if they’re wrong, which is worth paying attention to rather than ignoring. - Disable
proxy_bufferingfor a responsive live-console experience. - Set the Jenkins URL setting to match your actual public HTTPS address exactly.
- Bind Jenkins to localhost once the proxy is confirmed working.
- Test webhook delivery and live build console output specifically after any proxy config change — these are the two features most likely to silently break.
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.