Ingress Configuration¶
Standard pattern for exposing web applications in Kubernetes using NGINX Ingress Controller.
Prerequisites: - Kubernetes cluster with NGINX Ingress Controller deployed - Understanding of Kubernetes Services - DNS configuration access (OPNsense or /etc/hosts)
See Also: - NGINX Ingress Documentation - NGINX Ingress Controller details - Production Deployment - Complete deployment workflow - App Conventions - Application structure standards
When to Use Ingress¶
✅ Use Ingress for: - HTTP/HTTPS web applications - Apps that can share an IP with hostname-based routing - Standard web services (dashboards, APIs, etc.)
❌ Use LoadBalancer for: - Non-HTTP protocols (PostgreSQL, Redis, custom TCP/UDP) - Infrastructure services requiring dedicated IPs - Apps with complex multi-port requirements
Ingress Manifest Pattern¶
Standard manifest for web applications:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {app}
namespace: {app}
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "false"
nginx.ingress.kubernetes.io/proxy-body-size: "10m"
spec:
ingressClassName: nginx
rules:
- host: {app}.internal
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {app}
port:
number: 80
Key fields:
- metadata.name: Application name (must match namespace convention)
- spec.ingressClassName: Always nginx for our NGINX Ingress Controller
- rules.host: DNS hostname (format: {app}.internal)
- backend.service.name: Service name (typically matches app name)
- backend.service.port.number: Service port (typically 80)
Service Configuration for Ingress¶
When using Ingress, change Service type to ClusterIP (not LoadBalancer):
apiVersion: v1
kind: Service
metadata:
name: {app}
namespace: {app}
spec:
type: ClusterIP # ← Changed from LoadBalancer
selector:
app: {app}
ports:
- name: http
port: 80
targetPort: 3000 # Your container port
protocol: TCP
Why ClusterIP: - Ingress Controller handles external access - Service only needs internal cluster networking - No external IP allocation required - All apps share the Ingress Controller IP (10.89.97.220)
DNS Configuration¶
OPNsense Setup (Recommended):
1. Navigate to: Services → Unbound DNS → Overrides
2. Add Host Override:
- Host: {app}
- Domain: internal
- IP: 10.89.97.220 (NGINX Ingress Controller IP)
Alternative (/etc/hosts for testing):
Result: {app}.internal resolves to the NGINX Ingress Controller, which routes by hostname to the correct service.
Verification¶
# Check Ingress created
kubectl get ingress -n {app}
# Check routing (without DNS)
curl -H "Host: {app}.internal" http://10.89.97.220
# With DNS configured:
curl http://{app}.internal
# Check Ingress details
kubectl describe ingress {app} -n {app}
Expected output:
- Ingress shows ADDRESS: 10.89.97.220
- curl returns your application's response
- No connection errors or 404s
Common Ingress Annotations¶
Path rewriting:
annotations:
# Rewrite paths (e.g., /api/v1/foo → /foo)
nginx.ingress.kubernetes.io/rewrite-target: /
File uploads:
Long-running requests:
annotations:
# Timeouts for streaming or long operations
nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
CORS headers:
annotations:
# Enable CORS
nginx.ingress.kubernetes.io/enable-cors: "true"
nginx.ingress.kubernetes.io/cors-allow-origin: "*"
Complete annotation reference: NGINX Ingress Annotations
Migration from LoadBalancer to Ingress¶
If you have an existing app using LoadBalancer:
Step-by-step migration:
-
Create Ingress manifest (use pattern above)
-
Apply Ingress:
-
Test both routes work (LoadBalancer + Ingress)
-
Change Service to ClusterIP:
-
Configure DNS (OPNsense or /etc/hosts)
-
Test access:
-
Update documentation to reflect new URL
Benefits of Ingress¶
vs. LoadBalancer:
- Fewer IP addresses consumed: All apps share 10.89.97.220 (single Ingress Controller IP)
- Easier SSL management: Single TLS termination point with cert-manager
- Standard HTTP routing patterns: Hostname-based routing, path-based routing
- Professional hostname-based access: home.internal, money.internal vs 10.89.97.241:3000
- Better resource usage: No per-app external IP allocation
Production considerations: - All web apps should use Ingress unless specific requirements prevent it - Reserve LoadBalancer for infrastructure services (databases, message queues) - Use cert-manager for automatic TLS certificate management (future enhancement)
Troubleshooting¶
Issue: Ingress shows no ADDRESS
Cause: Ingress Controller not running Solution: Check NGINX Ingress Controller pods:Issue: 404 Not Found
Cause: Service name mismatch or Service not running Solution: Verify Service exists and matches Ingress backend:Issue: Connection refused
curl http://{app}.internal
# curl: (7) Failed to connect to app.internal port 80: Connection refused
nslookup {app}.internal
- Test with IP and Host header: curl -H "Host: {app}.internal" http://10.89.97.220
- Verify Ingress Controller IP: kubectl get svc -n ingress-nginx
Supabase Storage External Access¶
When browser clients need to access Supabase Storage (e.g., uploaded icons, images), they can't use the internal cluster IP. Instead, expose storage via Ingress.
Use case: Next.js apps where users upload files to Supabase Storage that must be displayed in the browser.
Problem: NEXT_PUBLIC_SUPABASE_URL is typically an internal IP (e.g., http://10.89.97.214:8000) that browsers outside the cluster can't access.
Solution: Create a dedicated storage ingress with a publicly accessible hostname.
Storage Ingress Manifest¶
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: supabase-storage
namespace: supabase
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "50m" # Adjust for max upload size
spec:
ingressClassName: nginx
tls:
- hosts:
- storage.bogocat.com
secretName: wildcard-bogocat-tls
rules:
- host: storage.bogocat.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: kong
port:
number: 8000
Setup Steps¶
-
Copy TLS certificate to supabase namespace:
-
Apply ingress:
-
Add DNS override in OPNsense:
- Host:
storage - Domain:
bogocat.com -
IP:
10.89.97.220(NGINX Ingress Controller) -
Add environment variable in app:
-
Update storage URL usage in components:
Why Separate URLs¶
| URL | Purpose | Accessible From |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL |
API calls (server-side, PostgREST) | Server-side, internal network |
NEXT_PUBLIC_SUPABASE_STORAGE_URL |
Storage URLs for browser display | Client-side, external |
Keep them separate because:
- API calls from Next.js server-side can use internal IPs (faster, no TLS overhead)
- Storage URLs embedded in <img src> must be accessible to the user's browser
- Mixing them could break either API calls (if external) or image loading (if internal)
See Also¶
- NGINX Ingress Documentation - NGINX Ingress Controller setup
- Production Deployment - Kubernetes deployment workflow
- Production App Deployment - Automated deployment scripts
- Troubleshooting - Common issues and solutions