Set up the dedicated proxy VPS that terminates TLS and routes end-user app traffic to worker containers over WireGuard.
The proxy server is a small, dedicated VPS with a public IP address. It runs Traefik, which:
Terminates HTTPS for all user apps (*.route.uday.me and any custom domains)
Issues and renews TLS certificates automatically via Let's Encrypt (no manual cert management)
Forwards each request to the right worker container over WireGuard — no Cloudflare Tunnel required
A wildcard DNS record (*.route.uday.me → proxy public IP) sends all app subdomains to the proxy. Traefik polls the control plane every 5 seconds for the current routing table and updates its config on the fly.
Why a separate proxy server?
Workers run on private hosts that don't need a public IP for app traffic. The proxy is the single public ingress point. This means you can add, remove, or replace workers without touching DNS, and TLS is managed in one place rather than on every worker.
Traefik keeps routing normally. It holds the last successfully fetched config in memory and continues forwarding traffic to all existing apps. The 5-second polls fail silently (logged as errors), but no routes disappear.
What stops working during a DB outage:
New deploys (the scheduler needs the DB)
Domain verifications
The routing table refreshing (but all currently running apps stay up)
The moment the DB recovers, the next poll refreshes everything automatically.
Why not store configs on the proxy?
No disk storage is needed. Traefik's in-memory config is sufficient, and storing a copy on the proxy would create a sync problem — the database is the source of truth, and a stale file could serve dead routes long after an app is stopped. On proxy restart, Traefik re-polls immediately and has a full routing table within 5 seconds.
The proxy server needs to be a WireGuard peer so it can reach worker container ports at 10.50.0.x.
On the proxy server, generate WireGuard keys and write the config:
apt-get install -y wireguard-toolsmkdir -p /etc/wireguard && chmod 700 /etc/wireguardwg genkey | tee /etc/wireguard/proxy_private.key | wg pubkey > /etc/wireguard/proxy_public.keychmod 600 /etc/wireguard/proxy_private.key /etc/wireguard/proxy_public.keycat /etc/wireguard/proxy_public.key # copy this — you'll need it on the CP
Write /etc/wireguard/wg0.conf on the proxy server:
[Interface]Address = 10.50.0.30/24ListenPort = 51821PrivateKey = <contents of /etc/wireguard/proxy_private.key>[Peer]# control planePublicKey = <CP public key from /etc/wireguard/cp_public.key>AllowedIPs = 10.50.0.0/24Endpoint = <CP public IPv4>:51821PersistentKeepalive = 25
AllowedIPs = 10.50.0.0/24 (the whole mesh) lets the proxy reach any worker's WG IP directly.
Bring the tunnel up:
systemctl enable --now wg-quick@wg0ping -c 1 10.50.0.1 # should succeed once the CP side is done
On the control plane, add the proxy as a peer:
sudo tee -a /etc/wireguard/wg0.conf <<EOF[Peer]# proxy server (192.99.14.173)PublicKey = <proxy public key>AllowedIPs = 10.50.0.30/32EOFsudo wg syncconf wg0 <(wg-quick strip wg0)
api: dashboard: trueentryPoints: web: address: ":80" websecure: address: ":443"providers: http: endpoint: "http://10.50.0.1:8081/v1/traefik/proxy-config" pollInterval: "5s" headers: Authorization: "Bearer <your proxy token from config.yaml proxy.token>"certificatesResolvers: letsencrypt: acme: email: admin@example.com # replace with your real email storage: /etc/traefik/acme/acme.json httpChallenge: entryPoint: web
Replace <your proxy token> with the value you set in proxy.token in the control plane's config.yaml (see Configuration).
Keep the proxy token secret
This token gives read access to all app routing config (slugs, custom domains, worker IPs). Treat it like any other credential. Don't commit it to version control.
[Unit]Description=Traefik proxyAfter=network-online.target wg-quick@wg0.serviceWants=network-online.target wg-quick@wg0.serviceDocumentation=https://doc.traefik.io[Service]Type=simpleExecStart=/usr/local/bin/traefik --configFile=/etc/traefik/traefik.ymlRestart=on-failureRestartSec=5sLimitNOFILE=65535# Needed to bind :80 and :443 as a non-root user, or just run as root# AmbientCapabilities=CAP_NET_BIND_SERVICE# User=traefik[Install]WantedBy=multi-user.target
Enable and start:
systemctl daemon-reloadsystemctl enable --now traefiksystemctl status traefik
# On the proxy serversystemctl status traefikjournalctl -u traefik -f# Look for: "Configuration received from provider http"
Deploy a test app from the dashboard, then:
# Replace my-app and route.uday.me with your slug and zonecurl -sI https://my-app.route.uday.me/# Expect: HTTP/2 200 (or your app's response), with a valid Let's Encrypt cert
The first request may take up to 60 seconds while Let's Encrypt issues the certificate.
Once the proxy is running, users can point their own domain at an app:
Add the domain in the WiseHosting dashboard (Settings → Custom Domains → Add domain).
The dashboard generates a TXT verification token. The user adds:
_wisehosting.theirdomain.com TXT <token>
at their DNS provider and clicks Verify in the dashboard.
Once verified, the user adds a CNAME:
theirdomain.com CNAME my-app.route.uday.me
Important: this CNAME must be DNS-only (grey cloud in Cloudflare, not orange proxy). If Cloudflare's proxy is enabled, the Let's Encrypt HTTP-01 challenge is intercepted by Cloudflare and the certificate issuance fails.
Within ~1 minute of DNS propagation, the proxy Traefik picks up the new domain from /v1/traefik/proxy-config and Let's Encrypt issues a certificate automatically.
https://theirdomain.com works.
Cloudflare orange cloud breaks Let's Encrypt
If the user's DNS is on Cloudflare and they leave the proxy (orange cloud) enabled on the CNAME, Let's Encrypt cannot complete the HTTP-01 challenge. The domain will not get a cert and will return a TLS error. The fix is simple: click the orange cloud in Cloudflare's DNS settings to make it grey (DNS-only).
Check Traefik logs on the proxy: journalctl -u traefik -f — look for ACME errors.
Verify port 80 is open. Let's Encrypt HTTP-01 challenges hit :80. Run curl -v http://<proxy-public-ip>/ from outside the proxy to confirm the port is reachable.
Check the CNAME.dig CNAME theirdomain.com should resolve to slug.route.uday.me, and dig A slug.route.uday.me should resolve to the proxy's public IP.
Cloudflare orange cloud. If the custom domain is behind Cloudflare proxy, switch it to DNS-only.
Rate limits. Let's Encrypt has rate limits (5 failures per hour per domain). If you're iterating quickly, check acme.json for existing (possibly invalid) entries and remove them, then restart Traefik.
Is the app running? Check the dashboard — the app status should be "running", not "stopped" or "failed".
Does the worker have wg_ip set? The proxy config only includes apps where the worker has a non-empty wg_ip. Check worker.wg_ip in the worker's config.yaml and restart the worker agent.
Is the proxy polling the control plane?journalctl -u traefik -f should show "Configuration received from provider" every 5 seconds. If not, check the Bearer token in traefik.yml matches proxy.token in the control plane config.
WireGuard connectivity. From the proxy server, ping 10.50.0.<worker-id> should succeed. If it doesn't, the proxy can't reach the worker — check that the worker's [Peer] block exists on the proxy's wg0.conf (or add AllowedIPs = 10.50.0.0/24 on the proxy side to route the whole mesh).
DNS changes can take a few minutes (up to 48 hours in the worst case, though usually under 5 minutes with a low TTL). Use dig or a tool like dnschecker.org to confirm the record is live globally before expecting Let's Encrypt to work.