DEV Community

Cover image for How to Run Jenkins Behind Nginx with SSL on a Subdomain
Dinesh Wijethunga
Dinesh Wijethunga

Posted on Originally published at dineshstack.com

How to Run Jenkins Behind Nginx with SSL on a Subdomain

In this tutorial, I will show you step by step how to run Jenkins behind nginx with SSL on its own subdomain — on a server that already hosts a production app on a wildcard domain, which is exactly where the sharp edges are.

Setup: one VPS, an existing Laravel SaaS serving *.example.com (multi-tenant subdomains), and Jenkins on 127.0.0.1:8080 that we want at https://jenkins.example.com.

Step 1 — The nginx vhost (exact name beats wildcard)

Key nginx fact: when a request arrives, an exact server_name always wins over a wildcard. So even though *.example.com would swallow jenkins.example.com, a dedicated vhost takes precedence:

# /etc/nginx/sites-available/jenkins
server {
    listen 80;
    server_name jenkins.example.com;

    location / {
        proxy_pass         http://127.0.0.1:8080;
        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_http_version 1.1;
        proxy_request_buffering off;   # long-running build log streams
        proxy_read_timeout 90s;
    }
}
sudo ln -sf /etc/nginx/sites-available/jenkins /etc/nginx/sites-enabled/jenkins
sudo nginx -t && sudo systemctl reload nginx

Gotcha from the field: if ln says File exists, there's a stale symlink from an earlier attempt shadowing your new config — rm it and re-link. And if the subdomain still shows your main app, the wildcard vhost is winning because your new vhost isn't actually enabled; verify with nginx -T | grep -A2 "server_name jenkins".

Step 2 — TLS. Read this before running certbot

Here is the mistake, so you don't repeat it. I ran the "friendly" installer mode:

certbot --nginx -d jenkins.example.com   # ← DON'T, on a multi-site server

certbot issued the certificate fine — and then deployed it into the wrong vhost: the production wildcard site. Its ssl_certificate lines suddenly pointed at the Jenkins cert, and every tenant subdomain on *.example.com started throwing certificate-mismatch errors. Production TLS, broken by a CI chore.

The fix was to point those two lines back at the wildcard cert and reload — but the lesson is permanent:

# On any server with more than one vhost: issue only, wire manually
sudo certbot certonly --nginx -d jenkins.example.com

Then add the 443 block yourself, where you decide which file changes:

server {
    listen 443 ssl;
    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;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    location / {
        proxy_pass       http://127.0.0.1:8080;
        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;
    }
}

The Cloudflare shortcut: maybe you need no cert at all

If Cloudflare proxies your zone, check what your origin certificate already covers before issuing anything:

echo | openssl s_client -servername app.example.com -connect YOUR_SERVER_IP:443 2>/dev/null \
  | openssl x509 -noout -ext subjectAltName
# X509v3 Subject Alternative Name:
#     DNS:*.example.com, DNS:example.com

A wildcard SAN like that means any new first-level subdomain can simply reuse the existing cert files in its 443 block — zero certbot, zero new renewals, and Cloudflare's edge certificate covers the browser side. We used exactly this later in the series for the staging vhost.

Step 3 — Lock the box down

# Jenkins should only be reachable through nginx now
sudo ufw deny 8080/tcp

Also set Manage Jenkins → System → Jenkins URL to https://jenkins.example.com/ so redirects and webhook URLs generate correctly.

Next in the series: post #5 connects Jenkins to GitHub — per-repo deploy keys, a minimal fine-grained PAT, the webhook, and the 403 that breaks multibranch scanning when your token is (correctly) minimal.


Originally published at dineshstack.com — read the full version with code samples and updates there.

Top comments (0)