Expose minikube services on the host IP / 0.0.0.0 (replace socat + kubectl port-forward)

Goal

Make the cluster's host-facing services reachable on the host's own interfaces (0.0.0.0 — the LAN IP 192.168.1.142 and localhost) instead of only at the minikube bridge IP (192.168.49.2) or via socat / kubectl port-forward hacks. This is the prerequisite for a second machine on the LAN reaching this cluster (arch 2 in the multi-cluster discussion).

Scope (confirmed with the user): the two existing NodePorts (frontend 30080, registry 30500), MinIO (9000/9001), and the Kubernetes API server (:8443). MinIO + apiserver are included because the two-machine goal needs cross-machine data access (MinIO) and one backend dispatching Jobs to the other machine's cluster (kube-apiserver). A single minikube recreate covers all of them.

Prerequisite — credentials + TLS must land first

docs/plans/self-signed-tls-minio-registry.md is a hard prerequisite of this plan and must be implemented and merged before any of the exposure work here. That plan gives MinIO and the registry (a) authentication — username/password from config.env — and (b) self-signed TLS encryption. Ordering it first is what makes publishing on 0.0.0.0 safe: by the time these services become reachable from off-host, they are never anonymous and never cleartext. This closes the interface-binding concern raised in earlier review — see decision 4.

Decisions (confirmed with the user)

  1. Scope = NodePorts + MinIO + kube-apiserver (fullest option).
  2. Recreate proceeds silently — no CONFIRM prompt. Applying --ports/--apiserver-ips needs a minikube delete + recreate; PVC data (Postgres/MinIO) survives via the host bind-mount and k8s/storage/ static PVs, and in-node docker images are rebuilt by the normal run flow, so the recreate is non-destructive to data and doesn't warrant a prompt.
  3. Frontend is published directly on 30080 (no friendly-port remap). SERVER_URL defaults to http://<host-ip>:30080; the FRONTEND_PORT variable and the entire socat step are removed.
  4. MINIKUBE_LISTEN_ADDRESS defaults to 0.0.0.0 (all interfaces). This is safe because the credentials + TLS prerequisite is in place first — every exposed service requires auth over TLS, so binding a public interface (e.g. on the arch-1 machine) no longer leaks an anonymous/plaintext service. Residual: operators on a public-interface host should still change the default MinIO/ registry credentials away from the shipped defaults (documented in config.env.example).

Current state (confirmed by reading scripts + inspecting the running container)

Port Service Type today Defined in Host bridge today
30080 frontend / nginx NodePort k8s/frontend/service.yaml prod socat 0.0.0.0:${FRONTEND_PORT} → :30080
30500 registry NodePort dev/setup-registry.sh none (reached via ${MINIKUBE_IP}:30500)
9000 MinIO API ClusterIP dev/setup-minio.sh kubectl port-forward … 9000:9000 → localhost only
9001 MinIO console ClusterIP dev/setup-minio.sh (console; localhost only)
8443 kube-apiserver (minikube) minikube published to 127.0.0.1:32774 (random)

docker port minikube confirms 30080/30500/9000 are not published on the host; only 22/2376/8443/32443/5000 are published to 127.0.0.1.

Mechanism

minikube's docker driver (v1.35.0, flags confirmed present): - --ports=<host>:<node> (repeatable) — publishes a node container port on the host via docker's iptables DNAT. Host port may differ from node port, so a NodePort in the 30000–32767 range can be re-published on a friendly host port (e.g. host 9000 → node 30900). - --listen-address=<ip> — host IP the published ports bind to; 0.0.0.0 = all interfaces. - --apiserver-ips=<ip> — adds an IP to the apiserver serving-cert SANs so TLS verification passes when a client connects via the LAN IP rather than the internal 192.168.49.2.

Port publishing and the apiserver cert SAN are fixed at container-creation time, so applying them to the existing container requires minikube delete + recreate (stop/start reuses the container).

Phase 1 — dev/setup-minikube.sh: publish ports, apiserver SAN, silent recreate

The single shared minikube start (lines 79-87) is the one place all of this is configured.

  1. Near the top, define (all overridable from config.env): bash # host<:node> specs published on the host. Bare NodePorts map identity; MinIO re-maps its # in-range NodePorts (30900/30901) back to the friendly 9000/9001 on the host. MINIKUBE_EXPOSE_PORTS="${MINIKUBE_EXPOSE_PORTS:-30080 30500 9000:30900 9001:30901}" MINIKUBE_LISTEN_ADDRESS="${MINIKUBE_LISTEN_ADDRESS:-0.0.0.0}" # LAN IP(s) to add to the apiserver cert SAN. Empty = don't expose the apiserver externally. MINIKUBE_APISERVER_IPS="${MINIKUBE_APISERVER_IPS:-}"
  2. Build the flag arrays: bash START_FLAGS=(--listen-address="${MINIKUBE_LISTEN_ADDRESS}") for spec in $MINIKUBE_EXPOSE_PORTS; do case "$spec" in *:*) START_FLAGS+=(--ports="${spec}");; *) START_FLAGS+=(--ports="${spec}:${spec}");; esac done for ip in $MINIKUBE_APISERVER_IPS; do START_FLAGS+=(--apiserver-ips="${ip}"); done Add "${START_FLAGS[@]}" to the minikube start invocation.
  3. Silent recreate detection — a new check alongside the insecure-registry / disk-size checks. When minikube is already running, force a recreate if any desired host port isn't published (or, for the apiserver, if the SAN check is impractical, key the recreate off the ports only and treat the apiserver SAN as applied-on-recreate): bash for spec in $MINIKUBE_EXPOSE_PORTS; do host_port="${spec%%:*}" if ! docker port minikube 2>/dev/null | grep -qE "0\.0\.0\.0:${host_port}(\b|$)"; then echo "Host port ${host_port} not published — recreating minikube (data preserved, images rebuild)" NEEDS_RECREATE=true fi done The recreate path does minikube delete then falls through to the existing start block — no CONFIRM prompt (decision 2). Reuse the disk-size branch's mechanics minus the read CONFIRM.

Verify at implementation: whether --listen-address=0.0.0.0 actually moves the apiserver publish off 127.0.0.1 (it governs published-port bind IP on the docker driver, but minikube manages the 8443 mapping itself). If it does not, the fallback is documenting that the remote kubeconfig uses the LAN IP with the apiserver host port from docker port minikube 8443, and the SAN from --apiserver-ips is what makes that TLS-valid. Do not try to --ports 8443 — it collides with minikube's own management of that port.

Phase 2 — MinIO: NodePort instead of port-forward

2.1 dev/setup-minio.sh

2.2 Remove the now-redundant/ conflicting port-forward machinery

Publishing MinIO on 0.0.0.0:9000 would clash with any lingering port-forward on localhost:9000, so remove them: - dev/restart-minio-portforward.sh — delete the file (or gut to a message pointing at docker publish). - dev/monitor-services.sh — drop the MinIO port-forward check/restart; keep backend/frontend checks. - dev/runall.sh — remove the "ensure MinIO port-forward running" block (~127-132) and adjust the status echoes (~258, ~276). - dev/cleanup-all.sh (~49-62) and dev/stop-all.sh (~17-21) — drop the MinIO port-forward kill logic / comments.

Phase 3 — prod/runall-minikube.sh: drop socat, publish frontend directly

Phase 4 — config + docs

Credentials + TLS — in the prerequisite plan, not here

MinIO/registry authentication (username/password from config.env) and self-signed TLS are handled by the prerequisite docs/plans/self-signed-tls-minio-registry.md (see Prerequisite above), which lands first. This plan is purely port publishing + socat/port-forward removal; it assumes those services already require auth over TLS by the time they're exposed.

Manual verification

Follow-ups / notes