Migrating from Path-Based to Subdomain Routing
Today was a big refactoring day. I migrated all 6 portfolio applications from path-based routing (el-jefe.me/bookmarked) to subdomain routing (bookmarked-k8s.el-jefe.me). Here's why and how.
The Problem with Path-Based Routing
My initial ingress configuration used path prefixes:
rules:
- host: pop-portfolio.el-jefe.me
http:
paths:
- path: /bookmarked
backend:
service:
name: bookmarked-client
- path: /firebook
backend:
service:
name: firebook-client
# ... more appsThis caused several issues:
- Client-side routing conflicts - React Router expected to own
/, not/bookmarked - API path confusion - Frontend calling
/apivs/bookmarked/api - Asset loading - Relative paths broke when served from subpaths
- stripPrefix middleware complexity - Lots of middleware to strip prefixes
The Solution: One Subdomain Per App
Each application now gets its own subdomain:
| App | Old Path | New Subdomain |
|---|---|---|
| Bookmarked | /bookmarked | bookmarked-k8s.el-jefe.me |
| Code Talk | /code-talk | code-talk-k8s.el-jefe.me |
| EducationELLy | /educationelly | educationelly-k8s.el-jefe.me |
| EducationELLy GraphQL | /educationelly-graphql | educationelly-graphql-k8s.el-jefe.me |
| FireBook | /firebook | firebook-k8s.el-jefe.me |
| IntervalAI | /intervalai | intervalai-k8s.el-jefe.me |
Ingress Configuration
Each app now has a dedicated ingress resource. Here's the pattern:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: bookmarked-ingress
annotations:
traefik.ingress.kubernetes.io/router.tls: "true"
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: traefik
tls:
- hosts:
- bookmarked-k8s.el-jefe.me
secretName: bookmarked-tls
rules:
- host: bookmarked-k8s.el-jefe.me
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: bookmarked-client
port:
number: 80
- path: /api
pathType: Prefix
backend:
service:
name: bookmarked-server
port:
number: 3000The API Routing Challenge
The trickiest part was handling API routes. Each full-stack app has:
- Client - React frontend serving static files
- Server - Node.js/Express API backend
With path-based routing, I needed stripPrefix middleware. With subdomains, routing is cleaner:
bookmarked-k8s.el-jefe.me/ → bookmarked-client:80
bookmarked-k8s.el-jefe.me/api/* → bookmarked-server:3000For some apps, I needed to split into separate ingress resources to get path priority right:
# API ingress (higher priority)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: educationelly-api-ingress
annotations:
traefik.ingress.kubernetes.io/router.priority: "100"
spec:
rules:
- host: educationelly-k8s.el-jefe.me
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: educationelly-server
port:
number: 5000
---
# Client ingress (lower priority, catch-all)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: educationelly-client-ingress
spec:
rules:
- host: educationelly-k8s.el-jefe.me
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: educationelly-client
port:
number: 80CORS Configuration
Moving to subdomains meant updating CORS on every backend:
// Before (permissive, bad)
app.use(cors());
// After (explicit allowed origins)
app.use(cors({
origin: [
'https://educationelly-k8s.el-jefe.me',
'http://localhost:3000' // development
],
credentials: true
}));I added ALLOWED_ORIGINS environment variables to each deployment:
env:
- name: ALLOWED_ORIGINS
value: "https://educationelly-k8s.el-jefe.me"Dashboard Updates
The portfolio dashboard needed updates to reflect the new URLs:
const APP_URLS = {
bookmarked: 'https://bookmarked-k8s.el-jefe.me',
codetalk: 'https://code-talk-k8s.el-jefe.me',
educationelly: 'https://educationelly-k8s.el-jefe.me',
educationellyGraphql: 'https://educationelly-graphql-k8s.el-jefe.me',
firebook: 'https://firebook-k8s.el-jefe.me',
intervalai: 'https://intervalai-k8s.el-jefe.me',
};DNS Configuration
In Cloudflare, I added wildcard CNAME records:
*-k8s.el-jefe.me → vmi2951245.contaboserver.netThis means any new *-k8s.el-jefe.me subdomain automatically resolves to my cluster.
Results
After the migration:
- ✅ React Router works correctly (owns
/) - ✅ API calls are straightforward (
/api/*) - ✅ Assets load without path issues
- ✅ Each app is isolated and independently deployable
- ✅ Cleaner ingress configuration
- ✅ Easier debugging (one app per domain)
Lessons Learned
- Plan subdomain structure early - Retrofitting is painful
- Split API and client ingresses - Gives more control over routing priority
- Don't forget CORS - Subdomains are different origins
- Wildcard DNS is your friend - Makes adding new apps trivial
Documenting the evolution of my homelab infrastructure.
