Concourse can be healthy inside the cluster while still failing from the operator’s browser. The common gap is not the web pod. It is the handoff between DNS, ingress controller placement, certificate material, and Concourse’s own external URL.
Use this checklist when moving Concourse from a temporary URL or NodePort to a real hostname such as concourse.example.com.
Confirm The Ingress Target
Start by finding where ingress actually lands. Do not assume every worker should be in DNS until the ingress controller placement confirms it.
kubectl --context cluster-a-prod get daemonset -A | grep ingress
kubectl --context cluster-a-prod get pods -n kube-system \
-l app.kubernetes.io/name=rke2-ingress-nginx -o wide
kubectl --context cluster-a-prod get ingress -n concourse
The important outputs are:
- the ingress class used by the Concourse ingress.
- the node names running ingress controller pods.
- the IP addresses published in ingress status.
- whether control-plane or etcd nodes are unintentionally serving ingress.
On RKE2, the default ingress controller can run as a DaemonSet on every Linux node unless restricted. That may include control-plane or etcd nodes. Decide whether that is acceptable before placing all published addresses into DNS.
Register DNS To Ingress Nodes
For a simple internal round-robin setup, create multiple A records for the same hostname:
concourse.example.com. 300 IN A 192.0.2.10
concourse.example.com. 300 IN A 192.0.2.11
concourse.example.com. 300 IN A 192.0.2.12
If using dynamic DNS updates, build the transaction explicitly so the intended record is replaced as one unit:
nsupdate <<EOF
server 192.0.2.53
zone example.com
update delete concourse.example.com A
update add concourse.example.com 300 A 192.0.2.10
update add concourse.example.com 300 A 192.0.2.11
update add concourse.example.com 300 A 192.0.2.12
send
EOF
Verify against the authoritative resolver, not only the workstation cache:
nslookup concourse.example.com 192.0.2.53
dig @192.0.2.53 concourse.example.com A +short
Create The TLS Secret
Kubernetes ingress expects a TLS secret with the server certificate and private key. If the certificate was issued by an internal CA, put the server certificate first, then intermediates, then the root if your environment requires it.
cat concourse.example.com.crt intermediate-ca.crt root-ca.crt > fullchain.pem
openssl verify \
-CAfile root-ca.crt \
-untrusted intermediate-ca.crt \
concourse.example.com.crt
kubectl --context cluster-a-prod -n concourse create secret tls concourse-web-tls \
--cert=fullchain.pem \
--key=concourse.example.com.key
Do not commit private keys, generated full chains, or downloaded PEM bundles unless the repository is explicitly designed to hold public certificate material. The durable configuration should reference the secret name, not contain the secret contents.
Update Helm Values
There are two separate changes:
- ingress TLS tells Kubernetes which secret to serve for the hostname.
- Concourse
externalUrltells Concourse what URL to advertise and use during login flows.
Example values:
concourse:
web:
externalUrl: "https://concourse.example.com"
web:
ingress:
enabled: true
ingressClassName: nginx
hosts:
- concourse.example.com
tls:
- hosts:
- concourse.example.com
secretName: concourse-web-tls
Apply the values through the normal Helm path:
helm --kube-context cluster-a-prod upgrade --install concourse concourse/concourse \
--namespace concourse \
--values concourse-values.yaml \
--version 20.2.4 \
--wait \
--timeout 10m
Verify The Cutover
Check Kubernetes state first:
kubectl --context cluster-a-prod get ingress -n concourse -o yaml \
| grep -A6 'host:\|tls:\|secretName:'
kubectl --context cluster-a-prod exec -n concourse deployment/concourse-web -- \
env | grep CONCOURSE_EXTERNAL_URL
Then test each ingress node directly with curl --resolve. This separates DNS round-robin from backend health:
for ip in 192.0.2.10 192.0.2.11 192.0.2.12; do
curl -sS -o /dev/null \
--resolve concourse.example.com:443:${ip} \
-w "${ip} http=%{http_code} connect=%{time_connect} tls=%{time_appconnect} total=%{time_total}\n" \
https://concourse.example.com
done
Finally, check the Concourse API through the public hostname:
curl -sS https://concourse.example.com/api/v1/info
Expected signals:
- HTTPS returns
200for the web UI. /api/v1/inforeturns Concourse version metadata.CONCOURSE_EXTERNAL_URLishttps://concourse.example.com.- each DNS target completes TCP connect and TLS handshake.
Failure Patterns
- DNS resolves but browser gets a timeout. DNS may point to nodes that are not running ingress or cannot receive traffic on 443.
- TLS works on one IP but not another. Ingress controller placement or host firewall state differs by node.
- Login redirects to HTTP. Ingress TLS was added, but Concourse
externalUrlstill useshttp://. - Certificate warning remains. The secret may contain only the leaf certificate instead of the full chain, or the wrong secret name is referenced by ingress.
- Helm notes still show HTTP. Do not trust chart notes alone. Verify the rendered ingress and the running
CONCOURSE_EXTERNAL_URLenvironment variable.
The clean cutover is four independent confirmations: DNS points to ingress nodes, ingress references the TLS secret, Concourse advertises the HTTPS URL, and every published IP completes an HTTPS request.