# Candidate HTTPS on the canonical hostname Port 443 is reserved for the existing production stack. A retained, isolated candidate uses `https://modelforge.example.com:18444`. The two ports never share a proxy route. Before configuring port 443, verify that the existing production Console uses the HTTPS API origin and matching CSP through a separately authorized production release. The candidate's `tls` service belongs to its own Compose project and only joins that project's `ingress` network. It sends `/api/*` and `/v1/*` to that project's API and everything else to that project's Web service. Use a valid Nginx Proxy Manager certificate for the canonical hostname. Its key must not be mounted directly from NPM or copied into the source tree or image build context. For each retained candidate, stage only `fullchain.pem` and `privkey.pem` under a root-owned `tls` directory inside that candidate's private root but outside its `source` directory. After choosing an exact candidate root and certificate directory: ```sh candidate_root=/path/to/private/modelforge-validation/EXACT_PROJECT npm_live_dir=/path/to/npm/letsencrypt/live/CERTIFICATE_ID tls_dir="${candidate_root}/tls" install -d -m 0700 "${candidate_root}" install -d -m 0700 "${tls_dir}" install -m 0600 "${npm_live_dir}/fullchain.pem" "${tls_dir}/fullchain.pem" install -m 0600 "${npm_live_dir}/privkey.pem" "${tls_dir}/privkey.pem" openssl x509 -in "${tls_dir}/fullchain.pem" -noout -checkend 86400 openssl x509 -in "${tls_dir}/fullchain.pem" -noout -ext subjectAltName test "$(openssl x509 -in "${tls_dir}/fullchain.pem" -pubkey -noout | sha256sum | cut -d' ' -f1)" = \ "$(openssl pkey -in "${tls_dir}/privkey.pem" -pubout | sha256sum | cut -d' ' -f1)" ``` Confirm the SAN contains exactly the intended hostname and the public keys match. Set `MODELFORGE_CANDIDATE_TLS_DIR` to this absolute `tls_dir`, and build the Web image with `VITE_API_BASE_URL=https://modelforge.example.com:18444`. The API's `MODELFORGE_CORS_ORIGINS` must be the same origin. Use the base, production, `docker-compose.rc-ingress.yml`, and `docker-compose.validation-tls.yml` overlays in that order. The `rc` in the historical overlay filename is not a runtime name. The Compose project must use the neutral `modelforge-validation-*` namespace. Before `up`, render the exact Compose JSON and pass it through `scripts/rc_isolation_preflight.py` with `--public-tls-port 18444`, candidate root, protected production ports and resource names. The preflight admits this one public port only for the isolated `tls` service; all other published ports must remain loopback only. Run the observed-resource preflight again after `up`. Scan the exact pinned Caddy image ID with the release Trivy severity policy and retain its report alongside the four application-image scans. Do not start the candidate while the host resource guard is active. Verify the hostname and certificate without `-k` or `--insecure`: ```sh server_ip=SERVER_LAN_IP curl --fail --show-error --resolve "modelforge.example.com:18444:${server_ip}" \ https://modelforge.example.com:18444/api/v1/health/live ``` Verify the Console, its browser API requests, CSP and authentication against the same origin. Record the exact image IDs, source SHA, certificate expiry, and before and after production health. The staged key is per candidate and must be removed with its candidate root only after retained evidence and cleanup checks pass. If NPM renews the certificate while a candidate remains live, copy the new pair and restart only that candidate's `tls` service after checking the SAN and key.