Windows Home Server — Part 7: Multi-Subdomain Ingress & Web App Hosting
When launching products, developers often pay for multiple VPS nodes or cloud instances to host backends, frontends, and dashboards.
By combining free cloud services with a self-hosted Windows server, you can build a hybrid network structure for free. We will keep our main company website hosted on Cloudflare Pages (Free Tier) for static performance and route dynamic operations—like APIs, dashboards, and databases—to specific subdomains running natively on different ports of our Windows home server.
1. The Hybrid Ingress Blueprint
Our traffic routing isolates the static frontend from the dynamic backends:
┌───────────────────────┐
│ User Request │
└──────────┬────────────┘
│
┌────────────────┴────────────────┐
▼ ▼
[Apex: yourdomain.com] [Subdomain: api.yourdomain.com]
(Cloudflare Pages - Free) (Cloudflare Tunnel Agent - Free)
│ │
[Pre-rendered HTML/JS] ▼
[Windows Home Server]
│
(Caddy Reverse Proxy: 80)
│
┌────────────────────┴────────────────────┐
▼ ▼
[Local Port: 5000] [Local Port: 3000]
(Python FastAPI) (Node.js Dashboard)
- yourdomain.com (Apex) & www.yourdomain.com: Hosted on Cloudflare Pages (unlimited bandwidth, edge CDN caching, 100% free).
- api.yourdomain.com: Points to your home server's local Python API (running on port
5000). - dashboard.yourdomain.com: Points to your home server's local Node.js App (running on port
3000).
2. Multi-Subdomain Caddy Configuration
Caddy's reverse-proxy block syntax makes it extremely easy to route multiple domains. Caddy reads the HTTP headers of incoming requests and matches them to the correct local port.
Open C:\Server\caddy\Caddyfile and replace or append the following configuration:
# Global configuration block
{
meta_dir C:\Server\caddy\metadata
cert_dir C:\Server\caddy\certificates
}
# 1. Dynamic REST API (Python/FastAPI listening on port 5000)
api.yourdomain.com {
# CORS headers: Allow your static frontend (Cloudflare Pages) to fetch data
header {
Access-Control-Allow-Origin "https://yourdomain.com"
Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
Access-Control-Allow-Headers "Accept, Content-Type, Authorization"
Access-Control-Allow-Credentials "true"
defer
}
# Handle preflight CORS OPTIONS requests directly in Caddy for speed
@options {
method OPTIONS
}
respond @options 204
reverse_proxy localhost:5000
}
# 2. Dynamic Web Dashboard (Node.js/Express listening on port 3000)
dashboard.yourdomain.com {
reverse_proxy localhost:3000
}
# 3. Microservice / Webhook Receiver (Bun listening on port 4000)
hooks.yourdomain.com {
reverse_proxy localhost:4000
}
Make sure to replace yourdomain.com with your registered domain.
Restart Caddy to load the changes:
nssm restart Caddy
3. Running the Multi-App Stack Natively
Let's write two lightweight dynamic applications to test our reverse-proxy routing: a Python FastAPI backend and a Node.js dashboard.
3.1 Python FastAPI Application (Port 5000)
Create a directory C:\Server\apps\api\ and set up a Python virtual environment:
New-Item -ItemType Directory -Force -Path "C:\Server\apps\api"
cd C:\Server\apps\api
python -m venv venv
.\venv\Scripts\pip install fastapi uvicorn
Create the application code file C:\Server\apps\api\main.py:
from fastapi import FastAPI
app = FastAPI()
@app.get("/status")
def read_status():
return {"status": "online", "hardware": "HP 15s Bare-Metal", "port": 5000}
Create a startup script C:\Server\apps\api\start-api.ps1:
cd C:\Server\apps\api
.\venv\Scripts\uvicorn main:app --host 127.0.0.1 --port 5000
Register the Python API as a service:
nssm install PythonAPI powershell.exe "-ExecutionPolicy Bypass -File C:\Server\apps\api\start-api.ps1"
nssm start PythonAPI
3.2 Node.js Dashboard Application (Port 3000)
Create a directory C:\Server\apps\dashboard\:
New-Item -ItemType Directory -Force -Path "C:\Server\apps\dashboard"
cd C:\Server\apps\dashboard
npm init -y
npm install express
Create C:\Server\apps\dashboard\server.js:
const express = require('express');
const app = express();
const PORT = 3000;
app.get('/', (req, res) => {
res.send(`
<html>
<head><title>Admin Dashboard</title></head>
<body style="font-family: sans-serif; background: #121212; color: #fff; padding: 2rem;">
<h1>Server Administration Dashboard</h1>
<p>Status: <span style="color: #4caf50;">Running</span></p>
<div id="api-status">Loading API status...</div>
<script>
fetch('https://api.yourdomain.com/status')
.then(res => res.json())
.then(data => {
document.getElementById('api-status').innerHTML =
'Connected to API. Hardware: ' + data.hardware;
})
.catch(err => {
document.getElementById('api-status').innerHTML = 'API offline.';
});
</script>
</body>
</html>
`);
});
app.listen(PORT, '127.0.0.1', () => {
console.log(`Dashboard running on port ${PORT}`);
});
Create a startup script C:\Server\apps\dashboard\start-dashboard.ps1:
cd C:\Server\apps\dashboard
node server.js
Register the Node dashboard as a service:
nssm install NodeDashboard powershell.exe "-ExecutionPolicy Bypass -File C:\Server\apps\dashboard\start-dashboard.ps1"
nssm start NodeDashboard
4. Verifying Subdomain Ingress
- Ensure the Cloudflare Tunnel is running (which handles wildcard ingress routing as set up in Part 2).
- Open your browser and navigate to
https://dashboard.yourdomain.com. - The dashboard should load, and the client-side JavaScript will successfully make a cross-origin (CORS) request to fetch the backend status from
https://api.yourdomain.com/status—confirming that our multi-subdomain routing and CORS headers are perfectly configured.
In the next part, we will build a custom Telegram Bot and route its webhooks natively.
Proceed to Part 8: Self-Hosting Telegram Bots, Webhooks, and Event-Driven Services →
Comments
Comments are powered by giscus. Set
PUBLIC_GISCUS_REPO_IDandPUBLIC_GISCUS_CATEGORY_IDin your environment to enable them.