WiseHosting
Deployment

Proxy server

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.

How it works

There are two completely separate things happening — understanding this saves a lot of debugging time.

Config polling (every 5 seconds)

Traefik on the proxy asks the control plane: "what apps are running right now?"

GET http://10.50.0.1:8081/v1/traefik/proxy-config
Authorization: Bearer <proxy.token>

The control plane queries the database for every app where status = running, port > 0, and the worker has a WireGuard IP set. For each app it builds:

  • Hostnames: the app's slug + subdomain_base (e.g. my-app.route.uday.me) plus any verified custom domains
  • Upstream: http://<worker.wg_ip>:<app.port> — direct WireGuard address of the container

The response is a Traefik dynamic config JSON:

{
  "http": {
    "routers": {
      "83a643d8...": {
        "rule": "Host(`my-app.route.uday.me`) || Host(`custom.domain.com`)",
        "service": "83a643d8...",
        "tls": { "certResolver": "letsencrypt" }
      }
    },
    "services": {
      "83a643d8...": {
        "loadBalancer": { "servers": [{ "url": "http://10.50.0.2:24207" }] }
      }
    }
  }
}

Traefik loads this into memory — nothing is written to disk on the proxy. The routing table is always at most 5 seconds out of date.

Request forwarding (real-time)

When a user visits my-app.route.uday.me, the polling has nothing to do with it:

User → proxy :443 → Traefik matches Host rule in memory
  → HTTP to 10.50.0.2:24207 over WireGuard → container → response

The control plane is never in the path of a live request. Latency is just the network hop to the proxy plus the WireGuard hop to the worker.

What if the database goes down?

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.

Prerequisites

  • A Linux VPS with a public IPv4 address (any provider — 1 vCPU / 512 MB RAM is plenty for routing)
  • Ports 80 and 443 open inbound (required for Let's Encrypt HTTP-01 challenge and HTTPS traffic)
  • Root access
  • The WireGuard mesh already set up on the control plane (sudo ./scripts/wireguard-setup.sh control done)

Step 0 — Add the proxy to the WireGuard mesh

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-tools
mkdir -p /etc/wireguard && chmod 700 /etc/wireguard

wg genkey | tee /etc/wireguard/proxy_private.key | wg pubkey > /etc/wireguard/proxy_public.key
chmod 600 /etc/wireguard/proxy_private.key /etc/wireguard/proxy_public.key

cat /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/24
ListenPort = 51821
PrivateKey = <contents of /etc/wireguard/proxy_private.key>

[Peer]
# control plane
PublicKey = <CP public key from /etc/wireguard/cp_public.key>
AllowedIPs = 10.50.0.0/24
Endpoint = <CP public IPv4>:51821
PersistentKeepalive = 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@wg0
ping -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/32
EOF

sudo wg syncconf wg0 <(wg-quick strip wg0)

Verify from the proxy:

ping -c 1 10.50.0.1          # CP reachable
curl http://10.50.0.1:8081/healthz   # should return {"status":"ok",...}

Step 1 — Install Traefik

Download the latest Traefik binary from GitHub Releases (pick linux_amd64 or linux_arm64 for your VPS):

TRAEFIK_VERSION="v3.3.4"   # check https://github.com/traefik/traefik/releases for latest
curl -L "https://github.com/traefik/traefik/releases/download/${TRAEFIK_VERSION}/traefik_${TRAEFIK_VERSION}_linux_amd64.tar.gz" \
  | tar -xz -C /usr/local/bin traefik
chmod +x /usr/local/bin/traefik
traefik version

Step 2 — Create the Traefik configuration

mkdir -p /etc/traefik

Write /etc/traefik/traefik.yml:

api:
  dashboard: true

entryPoints:
  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.

Step 3 — Create the ACME storage file

Let's Encrypt stores issued certificates in a JSON file. It must exist with strict permissions before Traefik starts:

mkdir -p /etc/traefik/acme
touch /etc/traefik/acme/acme.json
chmod 600 /etc/traefik/acme/acme.json

Step 4 — Create the systemd service

Write /etc/systemd/system/traefik.service:

[Unit]
Description=Traefik proxy
After=network-online.target wg-quick@wg0.service
Wants=network-online.target wg-quick@wg0.service
Documentation=https://doc.traefik.io

[Service]
Type=simple
ExecStart=/usr/local/bin/traefik --configFile=/etc/traefik/traefik.yml
Restart=on-failure
RestartSec=5s
LimitNOFILE=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-reload
systemctl enable --now traefik
systemctl status traefik

Step 5 — Add the proxy section to the control plane config

On the control plane, open /root/hostingbot/config.yaml (or wherever your config lives) and add:

proxy:
  token: "<same token you put in traefik.yml>"   # openssl rand -hex 32
  subdomain_base: "route.uday.me"

Then rebuild and restart the control plane:

go build -o /tmp/wisehosting-cp .
sudo systemctl restart wisehosting

Step 6 — Add the wildcard DNS record

At your DNS provider, add:

TypeNameValueTTL
A*.route.uday.me192.99.14.173 (your proxy public IP)300

Replace route.uday.me with your actual proxy.subdomain_base. This routes all <slug>.route.uday.me requests to the proxy server.

Apex domain not needed

You only need the wildcard record, not the apex (route.uday.me). Apps only get subdomains. The apex can 404 or redirect — it doesn't affect routing.

Step 7 — Verify

Check that Traefik is running and polling:

# On the proxy server
systemctl status traefik
journalctl -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 zone
curl -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.

Custom domain setup (for users)

Once the proxy is running, users can point their own domain at an app:

  1. Add the domain in the WiseHosting dashboard (Settings → Custom Domains → Add domain).
  2. 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.
  3. 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.
  4. 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.
  5. 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).

Troubleshooting

Let's Encrypt certificate isn't being issued

  1. Check Traefik logs on the proxy: journalctl -u traefik -f — look for ACME errors.
  2. 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.
  3. 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.
  4. Cloudflare orange cloud. If the custom domain is behind Cloudflare proxy, switch it to DNS-only.
  5. 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.

App URL returns 404 or "no backend"

  1. Is the app running? Check the dashboard — the app status should be "running", not "stopped" or "failed".
  2. 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.
  3. 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.
  4. 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 not propagated yet

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.

On this page