A practical example demonstrating how to expose frontend and backend services through Kubernetes Ingress on bare metal clusters (k3s, kubeadm, etc.).
This repository contains all the Kubernetes manifests and setup instructions needed to deploy a full-stack application with proper routing using an Ingress controller.
The app consists of:
- Frontend: React application served on port 80
- Backend: Go REST API running on port 8081
- Ingress: Single entry point routing
/api/*to backend, everything else to frontend
Using Ingress instead of NodePort services means:
- Single external IP for the entire application
- No CORS configuration needed (all requests appear to come from the same origin)
- Cleaner URLs without port numbers
- Production-ready setup that works the same way cloud providers handle routing
- A Kubernetes cluster (k3s, kubeadm, or any bare metal setup)
kubectlconfigured to access your cluster- An Ingress controller installed (nginx-ingress, Traefik, etc.)
Important: If using minikube, set the driver to none or use minikube tunnel
Replace <ingress-controller> with your controller name (ingress-nginx, traefik)
kubectl -n kube-system get svc <ingress-controller>
Look for the EXTERNAL-IP column. This is what you'll use as your application host.
In ingress.yaml, replace MY_APP_HOST with your external IP or domain:
rules:
- host: 192.168.1.100 # Your EXTERNAL-IP Alternatively, add an entry to /etc/hosts for local testing and use it in ingress.yaml
Apply the manifests in this order:
kubectl apply -f server-secrets.yaml
kubectl apply -f server-service.yaml
kubectl apply -f server-deployment.yaml
kubectl apply -f frontend-service.yaml
kubectl apply -f frontend-deployment.yaml
kubectl apply -f ingress.yamlcheck all pods are running
kubectl get pods
Open your browser and navigate to:
http://YOUR_EXTERNAL_IP
Or if using /etc/hosts:
http://myapp.local
The Ingress acts as a reverse proxy, routing requests based on path:
User Request Ingress Routes To
────────────────────── ────────────────────────
GET / → app-service:80 (frontend)
GET /login → app-service:80 (frontend)
POST /api/signup → server-service:8081 (backend)
POST /api/login → server-service:8081 (backend)
GET /api/profile → server-service:8081 (backend)
Since both frontend and backend are accessed through the same host (via Ingress), the browser sees all requests as same-origin. This eliminates CORS complexity entirely.
The frontend uses a runtime environment variable for the backend URL, set via BACKEND_URL="/api" in the deployment. This is injected at container startup through an entrypoint script, allowing the same image to work across different environments without rebuilding.
Defines routing rules. All /api/* requests go to the backend on port 8081, everything else goes to the frontend on port 80.
Deploys 2 replicas of the React app. Sets BACKEND_URL="/api" so the app knows where to send API requests.
ClusterIP service exposing the frontend pods on port 80. The Ingress routes to this service for non-API requests.
Deploys 2 replicas of the backend API. Key environment variables:
MONGODB_URI,JWT_SECRET: Loaded from secretsWITH_INGRESS="/api": Tells the backend to expect requests at/signupinstead of/api/signupSECURE="false": Disables secure cookie requirements (for HTTP testing; set to"true"in production with HTTPS)
ClusterIP service exposing the backend pods on port 8081. The Ingress routes all /api/* requests here.
Stores sensitive backend configuration
Before deploying to production:
- Enable HTTPS: Set up TLS certificates and change
SECURE="true"in server deployment - Use real secrets: Update
server-secrets.yamlwith actual credentials (don't commit them to git) - Add TLS to Ingress: Configure cert-manager or manual certificates
- Review CORS: If you add additional frontends on different domains, configure CORS appropriately
- Resource limits: Add CPU/memory limits to deployments
- Health checks: Add liveness and readiness probes to deployments
Getting 404 errors? Verify the host header matches what's in your ingress:
curl -H "Host: YOUR_HOST" http://YOUR_EXTERNAL_IP ┌─────────────┐
│ Browser │
└──────┬──────┘
│
│ http://myapp.local
▼
┌─────────────┐
│ Ingress │
│ Controller │
└──────┬──────┘
│
┌─────────────────┴─────────────────┐
│ │
/api/* routes /* routes
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ server-service │ │ app-service │
│ port: 8081 │ │ port: 80 │
└────────┬─────────┘ └────────┬─────────┘
│ │
┌───────────┴───────────┐ ┌──────────┴──────────┐
▼ ▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐
│ Backend │ │ Backend │ │ Frontend │ │ Frontend │
│ Pod 1 │ │ Pod 2 │ │ Pod 1 │ │ Pod 2 │
└────────────┘ └────────────┘ └────────────┘ └────────────┘