Production edge: Traefik v2.11 (entrypoint TLS), cert-manager LE certs live on both domains; in-place docroot deploys
This commit is contained in:
parent
f33385dacc
commit
fd55ac8837
5 changed files with 79 additions and 54 deletions
73
README.md
73
README.md
|
|
@ -1,38 +1,59 @@
|
||||||
# zai-home-base — revenue projects on djg-oracle-sl
|
# zai-home-base — revenue projects on djg-oracle-sl
|
||||||
|
|
||||||
Workspace for projects built to generate revenue. All sites are served by a
|
Workspace for revenue-generating projects. Cluster edge: **Traefik v2.11**
|
||||||
shared nginx edge DaemonSet in k3s (hostPort 80) reading from `sites/`.
|
(hostNetwork 80/443) + **cert-manager** (Let's Encrypt DNS-01 via Cloudflare).
|
||||||
|
Sites are static nginx Deployments fed by hostPath docroots in `sites/`.
|
||||||
|
|
||||||
|
## Live sites
|
||||||
|
|
||||||
|
- **bestdadjokes.lol** — dad-joke content site (Track 1; ads play via user's AdX/MCM)
|
||||||
|
- **myadhd.dev** — placeholder; ADHD micro-tools product coming (Track 2, Paddle/Stripe later)
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
- `bdj/` — bestdadjokes.lol (dad jokes content site, Track 1)
|
- `bdj/` — bestdadjokes.lol project
|
||||||
- `content/jokes.json` — the dataset (append-only; joke ids = array index)
|
- `content/jokes.json` — dataset, APPEND-ONLY (joke id = array index)
|
||||||
- `build.py` — stdlib-only static site generator (Python 3.6)
|
- `build.py` — stdlib-only static generator (Python 3.6! no walrus, no dict-merge `|`)
|
||||||
- `static/` — css/js/favicon (jokes-data.js is generated, don't hand-edit)
|
- `static/` — css/js/favicon (`jokes-data.js` is generated, don't hand-edit)
|
||||||
- `site/` — build output (gitignored)
|
- `k8s/traefik.yaml` — edge DaemonSet (hostNetwork, RBAC incl. endpointslices+nodes)
|
||||||
- `nginx/conf.d/` — per-site nginx vhosts (mounted into edge pod)
|
- `k8s/letsencrypt-issuer.yaml` — ClusterIssuer, DNS-01 via CF token Secret
|
||||||
- `k8s/nginx-edge.yaml` — shared edge DaemonSet (hostNetwork, port 80)
|
- `k8s/static-sites.yaml` — per-site Deployment+Service+Ingress+Certificate
|
||||||
- `deploy.sh` — build + atomic swap + reload + verify
|
- `deploy.sh` — build + in-place rsync to docroot (no dir swap — see gotchas)
|
||||||
- `sites/` — live docroots served by the edge pod (generated, gitignored)
|
- `adhd/site/index.html` — myadhd.dev placeholder (copied to `sites/myadhd.dev/`)
|
||||||
- `nginx-ingress.yaml` (in /home/opc) — leftover demo manifest, unused
|
- `sites/` — live docroots (generated, gitignored)
|
||||||
|
|
||||||
## Deploy a content change
|
## Deploy content
|
||||||
|
|
||||||
cd bdj && ./deploy.sh
|
cd bdj && ./deploy.sh
|
||||||
|
|
||||||
## Server facts
|
New site = docroot + copy the Deployment/Service/Ingress/Certificate pattern in
|
||||||
|
`bdj/k8s/static-sites.yaml` + a DNS record (Cloudflare API, token in
|
||||||
|
`~/.cloudflare-token`, zones bestdadjokes.lol=0cd16b5205725d310366859b3352a189,
|
||||||
|
myadhd.dev=00fa64c074d7162b7616a4a3dc42ba77).
|
||||||
|
|
||||||
- VM: djg-oracle-sl (Oracle Cloud ARM, 4c/23GB), public IP 144.24.30.131
|
## Server facts & gotchas (hard-won)
|
||||||
- k3s single node (control-plane taint — pods need the toleration in k8s/nginx-edge.yaml)
|
|
||||||
- kubectl: `sudo /usr/local/bin/k3s kubectl` (no standalone kubeconfig for opc)
|
|
||||||
- Local test: `curl -H 'Host: bestdadjokes.lol' http://127.0.0.1/`
|
|
||||||
- Inbound 80/443 must be open in the OCI VCN security list (cloud console side)
|
|
||||||
- DNS: user manages Cloudflare; site goes live with proxied A record → 144.24.30.131
|
|
||||||
|
|
||||||
## Roadmap (bdj)
|
- VM: djg-oracle-sl, Oracle Linux 8.10, **ARM** 4c/23GB, public IP 144.24.30.131
|
||||||
|
- **k3s pinned v1.34.11**: newest kubelet (1.36) refuses cgroup v1; OL8 boots cgroup v1
|
||||||
|
and a reboot to switch would kill the agent session. Revisit only with user OK.
|
||||||
|
- kubectl: `sudo /usr/local/bin/k3s kubectl` (sudo secure_path drops /usr/local/bin)
|
||||||
|
- **firewalld is DISABLED** (it silently rejected pod-forwarded traffic → all pod
|
||||||
|
egress dead, "No route to host"). OCI security list (22/80/443) is the perimeter.
|
||||||
|
- **coredns Corefile patched**: `forward . 1.1.1.1 8.8.8.8` because OCI's
|
||||||
|
169.254.169.254 resolver is link-local and unreachable from pods. A k3s upgrade
|
||||||
|
may revert this — re-patch if pods can't resolve DNS.
|
||||||
|
- **Traefik gotcha**: without `--entrypoints.websecure.http.tls=true`, TLS routers
|
||||||
|
register but never match → every HTTPS request 404s while plaintext works.
|
||||||
|
- **hostPath gotcha**: never atomic-swap docroot dirs (mv breaks the bind by inode);
|
||||||
|
rsync in place. If a pod serves 403/empty after content ops, rollout-restart it.
|
||||||
|
- Local test: `curl -sk --resolve bestdadjokes.lol:443:127.0.0.1 https://bestdadjokes.lol/`
|
||||||
|
- Shell rule: never `cd` into a directory a command might delete (agent shell cwd
|
||||||
|
breaks and needs manual recovery).
|
||||||
|
|
||||||
1. DNS live → submit to Google Search Console (HTML meta verification)
|
## Roadmap
|
||||||
2. Social content (needs user-provided accounts) — daily share-card images
|
|
||||||
3. Ad integration: user's MCM/AdX stack; ad slots are marked `<!-- AD_SLOT_* -->`
|
1. Google Search Console (user creates property; HTML-meta verification token → I add)
|
||||||
4. Expand dataset daily; long-tail topic pages ("dad jokes about coffee")
|
2. Social content pipeline (needs user accounts: X/Pinterest/Bluesky/Reddit)
|
||||||
5. Phase 2 (myadhd.dev): ADHD micro-tools + Paddle/Stripe when user ready
|
3. Ads: site into user's MCM/AdX; ad slots marked `<!-- AD_SLOT_* -->` in build.py
|
||||||
|
4. Daily content growth + long-tail topic pages; then paid-traffic arb experiments
|
||||||
|
5. myadhd.dev build-out (client-side tools, Paddle/Stripe when user ready)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Build sites and sync docroots. Routing/TLS are handled by Traefik + cert-manager
|
# Build sites and sync docroots IN PLACE (no dir swap: hostPath mounts bind to
|
||||||
# in the cluster, so content deploys need no pod reloads or restarts.
|
# the directory inode, so replacing the dir would leave pods serving a stale
|
||||||
|
# path). Routing/TLS are handled by Traefik + cert-manager; no reloads needed.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
BASE=/home/opc/zai-home-base
|
BASE=/home/opc/zai-home-base
|
||||||
|
|
@ -8,16 +9,11 @@ BASE=/home/opc/zai-home-base
|
||||||
echo "==> building site"
|
echo "==> building site"
|
||||||
python3 build.py
|
python3 build.py
|
||||||
|
|
||||||
mkdir -p "$BASE/sites"
|
mkdir -p "$BASE/sites/bestdadjokes"
|
||||||
rm -rf "$BASE/sites/bestdadjokes.new"
|
|
||||||
cp -r site "$BASE/sites/bestdadjokes.new"
|
|
||||||
if command -v getenforce >/dev/null 2>&1 && [ "$(getenforce)" = "Enforcing" ]; then
|
if command -v getenforce >/dev/null 2>&1 && [ "$(getenforce)" = "Enforcing" ]; then
|
||||||
chcon -Rt container_file_t "$BASE/sites/bestdadjokes.new" 2>/dev/null || true
|
chcon -Rt container_file_t "$BASE/sites/bestdadjokes" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
rm -rf "$BASE/sites/bestdadjokes.old"
|
rsync -a --delete site/ "$BASE/sites/bestdadjokes/"
|
||||||
[ -d "$BASE/sites/bestdadjokes" ] && mv "$BASE/sites/bestdadjokes" "$BASE/sites/bestdadjokes.old"
|
|
||||||
mv "$BASE/sites/bestdadjokes.new" "$BASE/sites/bestdadjokes"
|
|
||||||
rm -rf "$BASE/sites/bestdadjokes.old"
|
|
||||||
|
|
||||||
echo "==> verifying through Traefik (origin)"
|
echo "==> verifying through Traefik (origin)"
|
||||||
sleep 1
|
sleep 1
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
# Traefik v3 as the cluster edge: hostNetwork 80/443, global http->https redirect.
|
# Traefik v2.11 LTS as the cluster edge: hostNetwork 80/443, global http->https redirect.
|
||||||
# TLS certificates are provisioned by cert-manager into Ingress secrets.
|
# TLS: entrypoint terminates TLS; certificates provisioned by cert-manager
|
||||||
|
# (Let's Encrypt DNS-01 via Cloudflare) into the Ingress TLS secrets.
|
||||||
|
# NOTE: --entrypoints.websecure.http.tls=true is REQUIRED — without it TLS
|
||||||
|
# routers never engage the mux and every HTTPS request 404s.
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: ServiceAccount
|
kind: ServiceAccount
|
||||||
metadata:
|
metadata:
|
||||||
|
|
@ -12,7 +15,10 @@ metadata:
|
||||||
name: traefik
|
name: traefik
|
||||||
rules:
|
rules:
|
||||||
- apiGroups: [""]
|
- apiGroups: [""]
|
||||||
resources: ["services", "endpoints", "secrets", "namespaces"]
|
resources: ["services", "endpoints", "secrets", "namespaces", "nodes"]
|
||||||
|
verbs: ["get", "list", "watch"]
|
||||||
|
- apiGroups: ["discovery.k8s.io"]
|
||||||
|
resources: ["endpointslices"]
|
||||||
verbs: ["get", "list", "watch"]
|
verbs: ["get", "list", "watch"]
|
||||||
- apiGroups: ["networking.k8s.io"]
|
- apiGroups: ["networking.k8s.io"]
|
||||||
resources: ["ingresses", "ingressclasses"]
|
resources: ["ingresses", "ingressclasses"]
|
||||||
|
|
@ -64,16 +70,16 @@ spec:
|
||||||
effect: NoSchedule
|
effect: NoSchedule
|
||||||
containers:
|
containers:
|
||||||
- name: traefik
|
- name: traefik
|
||||||
image: traefik:v3.3
|
image: traefik:v2.11.2
|
||||||
args:
|
args:
|
||||||
- --providers.kubernetesingress
|
- --providers.kubernetesingress
|
||||||
|
- --providers.kubernetescrd=false
|
||||||
- --entrypoints.web.address=:80
|
- --entrypoints.web.address=:80
|
||||||
- --entrypoints.web.http.redirections.entrypoint.to=websecure
|
- --entrypoints.web.http.redirections.entrypoint.to=websecure
|
||||||
- --entrypoints.web.http.redirections.entrypoint.scheme=https
|
- --entrypoints.web.http.redirections.entrypoint.scheme=https
|
||||||
- --entrypoints.websecure.address=:443
|
- --entrypoints.websecure.address=:443
|
||||||
- --ping=true
|
- --entrypoints.websecure.http.tls=true
|
||||||
- --ping.entrypoint=health
|
- --log.level=WARN
|
||||||
- --entrypoints.health.address=:8082
|
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 80
|
- containerPort: 80
|
||||||
hostPort: 80
|
hostPort: 80
|
||||||
|
|
@ -89,13 +95,19 @@ spec:
|
||||||
memory: 256Mi
|
memory: 256Mi
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /ping
|
path: /
|
||||||
port: 8082
|
port: 80
|
||||||
initialDelaySeconds: 3
|
httpHeaders:
|
||||||
periodSeconds: 10
|
- name: Host
|
||||||
|
value: bestdadjokes.lol
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 15
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /ping
|
path: /
|
||||||
port: 8082
|
port: 80
|
||||||
initialDelaySeconds: 10
|
httpHeaders:
|
||||||
|
- name: Host
|
||||||
|
value: bestdadjokes.lol
|
||||||
|
initialDelaySeconds: 15
|
||||||
periodSeconds: 30
|
periodSeconds: 30
|
||||||
|
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
# Placeholder: the hand-rolled nginx edge was removed in favor of Traefik + cert-manager.
|
|
||||||
# Vhosts now live as Ingress resources in bdj/k8s/static-sites.yaml.
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
# Placeholder: self-signed origin certs were removed in favor of cert-manager
|
|
||||||
# issuing Let's Encrypt certificates into k8s Secrets (see bdj/k8s/letsencrypt-issuer.yaml).
|
|
||||||
Loading…
Reference in a new issue