7.5 KiB
name, description
| name | description |
|---|---|
| expose-service-subdomain | Expose an internal K3s/container service to a public *.yoresee.cc URL via the edge nginx reverse proxy (DNS→certbot→site rewrite→base_url→verify), and harden a static download site (autoindex off / robots.txt). |
Expose a service on the edge nginx via subdomain
Push an internal K3s/container service to a public *.yoresee.cc URL through the edge nginx on 十堰电信4c8g (125.208.22.116). Targets: a K3s ClusterIP (use svc IP:port) or a container on 襄阳2c4g/other node (use node IP:port).
If the service is not running yet (no image/container/pod), obtain and run it first — see deploy-docker-service.
Steps
-
Confirm DNS first.
getent hosts <domain>must return the edge IP125.208.22.116. If it does not, stop and ask the user to add the A record — do not proceed until it resolves. -
Sign the cert WITHOUT rewriting config. Reuse the existing Let's Encrypt account:
certbot certonly --nginx -d <domain> --non-interactive --agree-tos --keep-until-expiring⚠️ Only
certonlyis safe. Do NOT run plaincertbot --nginx— its auto-configuer rewrites the reverse-proxy site (breaks the 80→443 redirect and the proxy_pass). Preserve the site file by hand. -
Back up the current site config, then rewrite it in the edge pattern (80 redirect + 443 http2 proxy):
server { listen 80; listen [::]:80; server_name <domain>; return 301 https://$host$request_uri; } server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name <domain>; ssl_certificate /etc/letsencrypt/live/<domain>/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/<domain>/privkey.pem; location / { proxy_pass http://<TARGET_IP>:<TARGET_PORT>; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }TARGET_IP: for a K3s service use its ClusterIP (
kubectl get svc -A), for a container on another node use that node reachable IP. For a web UI addclient_max_body_size/ long timeouts as the service needs. -
Match any base_url to the final URL. If the service supports a
server.base_url(e.g. searxng) and you serve it at the root domain, setbase_url: "https://<domain>/"(no subpath). If an earlier step had mounted it under a subpath (/searx/), revert that — remove the trailing subpath from base_url AND delete thelocation /subpath/block in nginx. Mismatched base_url leaves pages loading but static assets pointing at the wrong path. -
Reload and verify each domain.
nginx -tthensystemctl reload nginx. Then:curl -s -m 12 -o /tmp/x.html -w "HTTP %{http_code}\n" https://<domain>/ grep -o '<title>[^<]*' /tmp/x.html | head -1 grep -o 'src="[^"]*"' /tmp/x.html | head -2Confirm the
srcpaths have no leftover/subpath/prefix (should be root/static/...for a root-mounted service). Run a real query through the public URL if it is search-like. If the service requires auth, pull the real credential from the K8s Secret (kubectl get secret <name> -o jsonpath='{.data.<KEY>}' | base64 -d) and use it — never hand-type a placeholder, which yields a spurious 401/bad credentialsand a wasted cycle. -
Record the mapping in the workspace memory so future work does not collide with the domain.
Hardening a static download site (optional)
downloads.yoresee.cc serves /var/www/downloads from the edge host with autoindex on, so its root listed every file and AI crawlers (GPTBot on Azure, ClaudeBot on AWS — check /var/log/nginx/access.log for the UA) walked the listing. To stop enumeration:
- Back up the site file to
/root/backup/, then turn the listing off (autoindex on→autoindex off),nginx -t,systemctl reload nginx. This is the real lock: the root then returns 403. - Write
robots.txtinto the docroot: oneUser-agent: <bot>+Disallow: /block per crawler, then a permissiveUser-agent: *;chownit to the docroot owner. Advisory only — it does not stop an ignoring crawler. - Verify over loopback with the Host header (no DNS round trip):
curl -s -k https://127.0.0.1/ -H "Host: <domain>"→ 403;curl -s -k -r 0-1000 https://127.0.0.1/<file> -H "Host: <domain>"→ 206 for a real file, so existing direct links (e.g. shared into Feishu) keep working.
Pitfalls
-
certbot --nginx(auto) destroys the reverse-proxy site — always usecertonly --nginxand write the site file yourself. -
base_url must equal the public origin (path included) or assets 404 / pages load broken.
-
The edge nginx site files live in
/etc/nginx/sites-enabled/; back up (to/root/backup/) before rewriting so the switch is reversible. -
Some images ship with no default command (e.g.
binwiederhier/ntfy:v2): the entrypoint prints help and exits, so the pod CrashLoopBackOffs right after a successful pull. Fix in the Deployment by adding the server subcommand explicitly, e.g.args: ["serve"](ntfy) or the equivalentserve/serversubcommand for that image. Verify withkubectl logs -l app=<name>: help output = missing command;Listening on= fixed. -
A multi-container app's own front-end nginx (
location /api/ { set $api_upstream http://<svc>:<port>; proxy_pass $api_upstream; }) fails with 502 when<svc>is a short name: nginx's resolver does NOT apply the Kubernetes search domain (unlikenslookup/libc), so coredns returns NXDOMAIN for single-label names. Use the fully-qualified service DNS in the app's nginx.conf and rebuild/redeploy that web image. The FQDN is<svc>.<namespace>.svc.cluster.local:<port>— the namespace segment is a literal part of the name, so a service indefaultisapi.default.svc.cluster.local:8080but the same service deployed in a non-default namespace (e.g.test) isapi.test.svc.cluster.local:8080, NOTdefault. Copying thedefaultexample when the service lives elsewhere will 502. Short names only work for the edge nginx'sproxy_passbecause that resolves at startup via libc, not the resolver. -
certbot certonly --nginxfails if the site config is written withssl_certificate /etc/letsencrypt/live/<domain>/fullchain.pemBEFORE the cert exists — certbot runsnginx -t, which errors "cannot load certificate". Two ways out: (a) create the site file WITHOUT the ssl_certificate lines, sign the cert, then add them; or (b) the simpler route — sign first with webrootcertbot certonly --webroot -w /var/www/html -d <domain> --non-interactive --agree-tos --keep-until-expiring, which needs no live config, then write the full site. If--nginxerrors with a missing-cert message, switch to webroot. -
Do not expose a public download/serve on bare port 80 plaintext HTTP — ISP DPI truncates it. Serving a static file from
http://125.208.22.116:80(no domain/HTTPS) works locally but the public path returns truncated data followed by a synthetic 404. Key diagnostic: the nginx access log records200with a largebody_bytes_sent(e.g. 231393) whilecurlonly downloaded a few KB or got a 404, and a second egress (aliyun) sees the same 404. That mismatch (log says 200, client got 404/partial) is DPI, not a config bug. Fix: serve via HTTPS on 443 for a real domain — the same file over HTTPS downloads fully. For any public serve/download job, go straight to domain + certbot (webroot) + 443 and verify over HTTPS; do not burn cycles testing bare-IP:80.