HTTPS Deployment

Overview

ft-configs-ui is served in production by nginx:alpine inside a single container (see Docker & Deploy). HTTPS is optional and decided automatically at container startup based on whether TLS material is mounted — no HTTPS_ENABLED flag exists anymore.

docker/ops/entrypoint.sh inspects /etc/nginx/ssl/tls.crt and /etc/nginx/ssl/tls.key:

  • Both files present → docker/ops/nginx.conf.template is rendered → nginx listens on 80 and 443 with TLS.

  • Either file missing → docker/ops/nginx-http-only.conf.template is rendered → nginx listens on 80 only.

The entrypoint logs which template it selected. The two templates are identical in every respect except the listen directives and TLS block; locations (/configs-service/, /health, /ready, /nginx_status, SPA fallback, cache rules, source-map block) are shared.

There is no Node.js server in the image and no HTTP monkey-patching. nginx handles TLS termination natively.

Prerequisites

  • A TLS certificate in PEM format (file name tls.crt — see "Why those filenames" below).

  • The matching private key in PEM format (file name tls.key).

  • (Optional) A CA bundle for clients/backends that need to trust corporate or self-signed CAs — mount it wherever your tooling expects; nginx itself does not need it for serving the SPA.

Why tls.crt / tls.key

Those exact names are what entrypoint.sh and the nginx templates look for. They match the keys emitted by Kubernetes Secret resources of type kubernetes.io/tls, so you can mount a secret directly at /etc/nginx/ssl/ without renaming.

Quick Start (Docker)

Run the image with the cert directory bind-mounted into /etc/nginx/ssl:

# Assumes you have tls.crt and tls.key in ./certs/ on the host
docker run --rm \
  -p 80:80 \
  -p 443:443 \
  -v "$PWD/certs:/etc/nginx/ssl:ro" \
  -e BACKEND_URL="http://ft-configs-service:8080" \
  ft-configs-ui:local

Expected stdout (paraphrased):

[entrypoint] TLS material found at /etc/nginx/ssl → using nginx.conf.template
[entrypoint] wrote /usr/share/nginx/html/runtime-config.js

Verify:

curl -k https://localhost/health     # → {"status":"ok"}
curl    http://localhost/health      # still served — nginx listens on 80 as well

For local development without real certificates, use docker/generate-cert.sh to produce a self-signed tls.crt / tls.key pair into ./certs/, then run the command above. Browsers will show a trust warning until you import the cert into your trust store.

HTTP-only mode (K8s with Ingress TLS)

Omit the volume mount entirely — entrypoint.sh will fall back to the HTTP-only template:

docker run --rm \
  -p 80:80 \
  -e BACKEND_URL="http://ft-configs-service:8080" \
  ft-configs-ui:local

Expected stdout:

[entrypoint] No TLS material at /etc/nginx/ssl → using nginx-http-only.conf.template

This is the recommended mode behind Kubernetes Ingress, AWS ALB, or any reverse proxy that terminates TLS upstream.

Docker Compose

services:
  ft-configs-ui:
    image: ft-configs-ui:local
    ports:
      - "80:80"
      - "443:443"
    environment:
      BACKEND_URL: "http://ft-configs-service:8080"
      METRICS_ALLOW_IP: "10.0.0.0/8"
    volumes:
      - ./certs:/etc/nginx/ssl:ro

To run HTTP-only, remove the 443:443 mapping and the volumes: entry.

TLS configuration

Taken from docker/ops/nginx.conf.template:

listen 443 ssl;
ssl_certificate     /etc/nginx/ssl/tls.crt;
ssl_certificate_key /etc/nginx/ssl/tls.key;

ssl_protocols       TLSv1.2 TLSv1.3;
ssl_ciphers         ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers on;

HSTS (Strict-Transport-Security: max-age=31536000; includeSubDomains) is added from the SSL template only. The HTTP-only template deliberately omits HSTS — setting it when TLS is terminated upstream could lock users out if the upstream terminator fails over to plain HTTP.

To change cipher suites or protocol versions, edit the template in docker/ops/ and rebuild the image. There are no runtime flags for this.

Environment variables

| Variable | Default | Purpose | BACKEND_URL | http://ft-configs-service:8080 | proxy_pass target for the /configs-service/ and /ready locations. | METRICS_ALLOW_IP | 0.0.0.0/0 | Extra CIDR/IP whitelisted for /nginx_status on top of the built-in 127.0.0.1, 10.0.0.0/8, 172.16.0.0/12.

See Configuration and Docker & Deploy for the complete env reference.

Certificate file permissions

The nginx:alpine image runs its worker processes as the nginx user, but the master process starts as root (default upstream behavior). Because the master reads the cert/key before dropping privileges, world-readable permissions are not required — owner-read is enough, and private keys should stay strict:

chmod 600 certs/tls.key
chmod 644 certs/tls.crt

Kubernetes

Mount a kubernetes.io/tls Secret directly:

apiVersion: v1
kind: Secret
metadata:
  name: ft-configs-ui-tls
type: kubernetes.io/tls
data:
  tls.crt: <base64>
  tls.key: <base64>
---
spec:
  containers:
    - name: ft-configs-ui
      image: ft-configs-ui:latest
      volumeMounts:
        - name: tls
          mountPath: /etc/nginx/ssl
          readOnly: true
  volumes:
    - name: tls
      secret:
        secretName: ft-configs-ui-tls

When the cluster uses Ingress-level TLS termination, omit the volume entirely and let the image fall through to HTTP-only.

Dockerfile reference

The production Dockerfile (docker/Dockerfile) already declares both ports:

EXPOSE 80 443
ENTRYPOINT ["/entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]

No extra steps are needed to "turn on" HTTPS at build time — the image supports both modes and auto-selects at startup.

Troubleshooting

Symptom Cause Fix

Container runs but only port 80 responds; 443 refused

entrypoint.sh did not find tls.crt and tls.key under /etc/nginx/ssl, so it picked the HTTP-only template

Verify the volume mount path and file names. Check stdout for the using nginx.conf.template / using nginx-http-only.conf.template line. File names must be exactly tls.crt and tls.key.

nginx: [emerg] cannot load certificate "/etc/nginx/ssl/tls.crt"

File is missing, malformed, or not PEM

openssl x509 -in certs/tls.crt -noout -text to validate. Ensure the file is full PEM with -----BEGIN CERTIFICATE----- markers, not DER.

nginx: [emerg] SSL_CTX_use_PrivateKey_file(…​) failed

Key does not match the certificate, or key is encrypted with a passphrase

Compare moduli: openssl x509 -in tls.crt -noout -modulus | openssl md5 vs openssl rsa -in tls.key -noout -modulus | openssl md5. Strip the passphrase with openssl rsa -in tls.key -out tls.key.nopass.

Browser shows NET::ERR_CERT_AUTHORITY_INVALID

Self-signed cert (including output of docker/generate-cert.sh)

Import the cert into the OS/browser trust store, or replace with a cert issued by a trusted CA.

/ready returns 502 / 504

nginx cannot reach the backend at BACKEND_URL, or the backend /actuator/health is down

Exec into the container: wget -qO- $BACKEND_URL/actuator/health. Check Docker/K8s DNS (nginx uses the 127.0.0.11 Docker embedded resolver — the backend service name must resolve from inside the container).

/nginx_status returns 403

Caller IP is not in the allowlist (127.0.0.1, 10.0.0.0/8, 172.16.0.0/12, ${METRICS_ALLOW_IP})

Set METRICS_ALLOW_IP at deploy time to the scraper’s CIDR, or scrape from inside the cluster network.