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)